Commit graph

10 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
d042606955 Add 6-digit code sign-in as primary, magic link as fast-path fallback
Same email now contains a 6-digit code and a magic link, both backed by a
single verificationToken row. After submitting their email, the user
lands on /auth/verify-code with a single-input form (inputmode=numeric,
autocomplete=one-time-code, auto-submit on 6 digits or paste) and can
either type the code or use the link from the email. Either path
consumes the same row — single-use, replace-on-resend.

This is the structural fix for the silent-quarantine pattern observed
with Atkins and Sustainable Building UK: corporate gateways are happier
with short transactional content than long opaque token URLs, and a
code can't be broken by SafeLinks-style URL rewriting. The link path is
preserved so users whose email gets through unmangled keep one-click UX.

Security:
  - Codes are 6-digit, crypto.randomInt-generated, stored as sha256
    hashed against NEXTAUTH_SECRET on the same row as the link token
  - 5-attempt lockout per code (attempts column); 6th attempt with the
    correct code still fails
  - Per-email send rate limit: 5/hour fixed window (authRateLimits
    table); 6th send returns an error
  - Code + link share a 10-minute window (maxAge dropped from 1h)
  - Resending replaces any prior token rows for the identifier so only
    the latest send is ever live

Implementation:
  - verificationCode.ts holds generateCode + hashCode + the pure
    evaluateCodeAttempt decision tree; 9 unit tests cover every branch
    of the verification outcome (no-such-row, expired, locked-out, ok,
    wrong-with-newAttempts, locked-out-still-rejects-correct-code)
  - sendVerificationRequest now hashes the URL token the same way
    /verify/[token]/page.tsx does, applies the rate limit + records the
    code + replaces older rows in two transactions
  - CredentialsProvider (id: "email-code") calls evaluateCodeAttempt
    inside a transaction, handles all 5 outcomes, creates the user on
    first successful code (parity with the magic-link callback path)
  - oauthId backfill in the signIn callback is now guarded on
    account.type === "oauth" so the credentials flow doesn't pollute
    oauthProvider with "email-code"
  - Migration is additive: code_hash nullable, attempts default 0; new
    authRateLimits table is independent. In-flight tokens at deploy time
    keep working via the link path.

Vercel preview deployment is the test surface; a Mailpit + Cypress E2E
loop is intentionally deferred per the lean-setup plan in docs/wip/
auth-email-code-fallback-plan.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:16:47 +00:00
Khalim Conn-Kowlessar
a264686552 onboarding working 2025-10-15 19:07:10 +00:00
Khalim Conn-Kowlessar
cf28e5d9fc onboarding wip 2025-10-13 20:27:17 +00:00
Khalim Conn-Kowlessar
7d51fcc4dc working on sign in and onboarding 2025-10-13 08:52:34 +00:00
29c57b64fa new user gets added woo hoo 2025-09-08 13:56:33 +00:00
Khalim Conn-Kowlessar
abad7ad893 modified all int columns to big serial 2023-08-01 16:03:14 +01:00
Khalim Conn-Kowlessar
d1e9d8949d implemented user sign in against database 2023-07-11 10:24:46 +01:00
Khalim Conn-Kowlessar
a1a578c97f Added create_user and also making users table schema changes 2023-07-10 19:14:06 +01:00
Khalim Conn-Kowlessar
39695232c6 Got drizzle orm working 2023-07-10 17:48:37 +01:00
Khalim Conn-Kowlessar
f405c0174f Setting up db connection 2023-07-10 16:22:51 +01:00