Skip to content

Instantly share code, notes, and snippets.

@matedemorphy
Last active September 18, 2026 15:14
Show Gist options
  • Select an option

  • Save matedemorphy/fca8ea3af79d59eb1b94e47d07a934df to your computer and use it in GitHub Desktop.

Select an option

Save matedemorphy/fca8ea3af79d59eb1b94e47d07a934df to your computer and use it in GitHub Desktop.
clerk-backend-auth
# frozen_string_literal: true
class ApplicationController < ActionController::API
before_action :authenticate!
before_action :current_user
include Clerk::Authenticatable
private
def paginate_records(set)
@pagy, @records = pagy(:keyset, set)
@paginated_records = { links: @pagy.urls_hash, data: @records }
end
def domain
Rails.env.development? ? "http://lvh.me:3000" : "https://linker.chat"
end
def authenticate!
head :unauthorized unless clerk.user?
end
def current_user
return nil unless clerk.user?
@current_user ||= Auth::SyncClerkUser.call(clerk.user)
end
end
# frozen_string_literal: true
module Api
module V1
module Webhooks
class ClerkController < ActionController::API
OTP_TEMPLATE_SLUG = "verification_code"
def receive
result = Auth::VerifyClerkWebhook.call(raw_body: request.raw_post, headers: request.headers)
return head :unauthorized unless result.valid?
event = result.payload
# Guarda el payload crudo, tal cual llegó, ANTES de intentar
# extraer nada de él. Así, aunque OTP_TEMPLATE_SLUG o los campos
# de abajo estén mal adivinados, siempre tienes el JSON real
Auth::DebugClerkWebhook.store(event)
handle_email_created(event) if event.dig(:type) == "email.created"
head :ok
end
private
def handle_email_created(event)
data = event.dig(:data)
return unless data.dig(:slug) == OTP_TEMPLATE_SLUG
recipient = data.dig(:to_email_address)
otp_code = data.dig(:data, :otp_code)
if recipient.blank? || otp_code.blank?
Rails.logger.error("[ClerkWebhook] No se pudo extraer recipient/otp_code del payload: #{data.inspect}")
return
end
Utils::SendEmailService.call(
mailer: :otp,
method: :code,
email: recipient,
params: { otp_code: otp_code }
)
end
end
end
end
end
module Auth
class OtpMailer < ApplicationMailer
def code(email:, otp_code:)
@otp_code = otp_code
mail(to: email, subject: "Tu código de verificación")
end
end
end
# frozen_string_literal: true
module Utils
class SendEmailService < ApplicationService
MAILERS = {
otp: Auth::OtpMailer
}.freeze
def initialize(mailer:, method:, email:, params: {})
@mailer = mailer.to_sym
@method = method.to_sym
@email = email
@params = params
end
def call
mailer_class
.public_send(@method, **mailer_params)
.deliver_later!
end
private
def mailer_class
MAILERS.fetch(@mailer) do
raise ArgumentError, "Unknown mailer: #{@mailer}"
end
end
def mailer_params
@params.merge(email: @email)
end
end
end
# frozen_string_literal: true
module Auth
class SyncClerkUser
class << self
def call(clerk_user)
new(clerk_user).call
end
end
def initialize(clerk_user)
@clerk_user = clerk_user
end
def call
user = User.find_or_initialize_by(uid: clerk_user_id)
user.assign_attributes(
email: primary_email
)
user.save!
user
end
private
attr_reader :clerk_user
def clerk_user_id
clerk_user.respond_to?(:id) ? clerk_user.id : clerk_user[:id]
end
def primary_email
if clerk_user.respond_to?(:email_addresses)
# Buscar explícitamente el email marcado como primario en la respuesta de Clerk
primary_id = clerk_user.primary_email_address_id
primary_obj = clerk_user.email_addresses.find { |e| e.id == primary_id }
primary_obj&.email_address || clerk_user.email_addresses.first&.email_address
else
# Si clerk_user viene como Hash (ej. payloads de Webhooks)
emails = clerk_user[:email_addresses] || []
primary_id = clerk_user[:primary_email_address_id]
primary = emails.find { |e| e[:id] == primary_id } || emails.first
primary&.dig(:email_address)
end
end
end
end
module Auth
class VerifyClerkWebhook
Result = Struct.new(:valid, :payload, :error, keyword_init: true) do
def valid? = valid
end
def self.call(raw_body:, headers:)
new(raw_body: raw_body, headers: headers).call
end
def initialize(raw_body:, headers:)
@raw_body = raw_body
@headers = headers
end
def call
Svix::Webhook.new(signing_secret).verify(@raw_body, svix_headers)
payload = JSON.parse(@raw_body, symbolize_names: true)
Result.new(valid: true, payload: payload)
rescue Svix::WebhookVerificationError => e
Rails.logger.warn("[clerk-webhook] firma inválida: #{e.message}")
Result.new(valid: false, payload: nil, error: e.message)
rescue JSON::ParserError => e
Rails.logger.error("[clerk-webhook] JSON inválido tras verificar firma: #{e.message}")
Result.new(valid: false, payload: nil, error: e.message)
end
private
def svix_headers
{
"svix-id" => @headers["svix-id"],
"svix-timestamp" => @headers["svix-timestamp"],
"svix-signature" => @headers["svix-signature"]
}
end
def signing_secret
Rails.application.credentials.dig(:clerk, :webhook_signing_secret) ||
raise("Missing webhook signing secret key")
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment