Last active
September 18, 2026 15:14
-
-
Save matedemorphy/fca8ea3af79d59eb1b94e47d07a934df to your computer and use it in GitHub Desktop.
clerk-backend-auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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