Add passwordless email auth with pre-checkout signup

Flow: enter email → magic link → verified → Stripe checkout → dashboard

- SessionsController: new/create (send magic link), sent, verify, destroy
- Customer model: generate_magic_token!, magic_token_valid?, verify_email!
- Migration: magic_token, magic_token_expires_at, email_verified_at + unique indexes
- CustomerMailer + mailer layout with magic link email (html + text)
- CheckoutController: GET /checkout/start requires auth, passes customer_id
  as Stripe metadata; webhook finds customer by metadata and updates record
- DashboardController: requires auth, uses current_customer from session
- ApplicationController: current_customer, require_auth, redirect_after_auth
  (stores return_to so verify sends user back to where they were headed)
- Resend gem + initializer; production uses :resend delivery method
- Dev logs magic link URL to Rails logger instead of sending email
- Pricing page: simple link to /checkout/start (no more JS fetch)
- Layout: Sign in / Dashboard / Sign out nav links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew Gundersen 2026-02-27 17:12:23 -05:00
commit a1175ab00d
22 changed files with 284 additions and 51 deletions

View file

@ -1,2 +1,30 @@
class Customer < ApplicationRecord
TOKEN_TTL = 15.minutes
def generate_magic_token!
self.magic_token = SecureRandom.urlsafe_base64(32)
self.magic_token_expires_at = TOKEN_TTL.from_now
save!
magic_token
end
def magic_token_valid?
magic_token.present? && magic_token_expires_at&.future?
end
def verify_email!
update!(
email_verified_at: Time.current,
magic_token: nil,
magic_token_expires_at: nil
)
end
def email_verified?
email_verified_at.present?
end
def subscribed?
stripe_customer_id.present?
end
end