feat(auth): passwordless email OTP behind console_auth_redesign (#3178)#3186
Conversation
Adds an email-OTP sign-in flow behind the existing console_auth_redesign
flag, replacing the Log in / Sign up tabs with one entry screen
(email + OAuth) and a verification screen.
- AuthPage shrinks to a flag switch between AuthPageLegacy (today's
body, extracted verbatim) and AuthPagePasswordlessClient.
- AuthPagePasswordless is a small orchestrator; OAuthRow, EmailCodeStart
and EmailCodeVerify own their own mutations, errors and (for verify)
resend + cooldown.
- withPersistedPasswordlessFlow HoC owns sessionStorage hydration and
persistence; the route imports the orchestrator via
dynamic({ ssr: false }) so the first paint already reflects the
persisted screen without a hydration mismatch.
- New /api/auth/email-code-start and /api/auth/email-code-verify routes
translate to Auth0's passwordless OTP endpoints; verify sets the
session and calls register-user.
- setSession is DI'd via the server container, so all three auth route
handlers consume services.setSession instead of importing the
module-level function.
- /login-v2 page renders the orchestrator unconditionally for the
Playwright e2e, which exercises the real OTP path via Mailsac.
📝 WalkthroughWalkthroughThis PR adds a feature-flagged passwordless email-code auth flow alongside the legacy password flow: new client components (EmailCodeStart/Verify, OAuthRow, AuthPagePasswordless), persisted client flow HOC, API endpoints (/email-code-start, /email-code-verify), SessionService/AuthService methods for email-code flows with Auth0 error mapping and retry handling, DI registration for setSession, tests, and Playwright E2E coverage including OTP extraction. ChangesPasswordless Email-Code Auth System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3186 +/- ##
==========================================
- Coverage 63.69% 63.23% -0.47%
==========================================
Files 1089 1059 -30
Lines 26433 25657 -776
Branches 6408 6302 -106
==========================================
- Hits 16837 16224 -613
+ Misses 8400 8247 -153
+ Partials 1196 1186 -10
*This pull request uses carry forward flags. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/deploy-web/src/components/auth/AuthPageLegacy/AuthPageLegacy.tsx`:
- Around line 90-100: The initial activeView (const activeView =
searchParams.get("tab") || "login") can be an invalid tab value; normalize it
the same way setActiveView does by mapping only "login", "signup", or
"forgot-password" to themselves and defaulting everything else to "login" before
using it as the Tabs state (and optionally update the URL to the canonical tab).
Update the initialization of activeView to use that sanitizer logic (reusing the
same allowed-values check), and if the initial value was normalized, call
resetMutations() and router.replace(`?tab=${sanitized}`,... ) to ensure URL +
state stay canonical (referencing activeView, setActiveView, searchParams,
resetMutations, and router.replace).
In `@apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx`:
- Around line 77-80: Resend can be triggered while verifyMutation is pending;
update the resend-disabled logic to also block when verify is in flight by
including verifyMutation.isPending in isResendDisabled (and any other places
around lines 98-99 that compute resend availability), so isResendDisabled
becomes true if resendCooldownSec > 0 OR resendMutation.isPending OR
verifyMutation.isPending; keep isAnyMutationPending/activeError semantics intact
(they can still derive from verifyMutation.isPending ||
resendMutation.isPending) but ensure the UI disables the resend button whenever
verifyMutation.isPending to prevent overlapping verify/resend calls and racey
mutation resets.
In `@apps/deploy-web/src/services/session/session.service.ts`:
- Around line 217-293: The verifyEmailCode function contains fragile
string-matching on tokenResponse.data.error_description
(description.includes("expired")) to return an "expired_code" error, but Auth0
typically returns a generic "Wrong email or verification code." making the
expired branch unreachable; remove the description.includes("expired") branch
and consolidate handling of tokenResponse.data?.error === "invalid_grant" to
always return the "invalid_code" Err (or, if you confirm Auth0 provides a
reliable discriminant, replace the string check with that explicit field),
updating any tests that expected "expired_code" accordingly and keeping
extractResponseDetails(tokenResponse) as the cause for diagnostics.
In `@apps/deploy-web/tests/ui/passwordless-login.spec.ts`:
- Around line 10-13: The afterEach cleanup currently swallows all errors when
calling auth0.deleteUser for testUserId; update the test.afterEach handler to
await auth0.deleteUser(testUserId) but only ignore the expected "user not found"
case (inspect the thrown error's status code or message from the Auth0 client)
and rethrow or surface any other errors so outages are visible; reference the
test.afterEach block, auth0.deleteUser, and testUserId when making the change.
In
`@apps/deploy-web/tests/ui/services/email-verification/mailsac-code.strategy.ts`:
- Around line 49-55: The timeout hint text still mentions “matching subject”
even though the polling logic in mailsac-code.strategy.ts looks for the code in
message bodies (see CODE_NEAR_KEYWORD, fetchMessageBody, lastBodyPreview,
codeNotFoundInBody); update the timeout/failure hint generation to reflect
body-based matching (e.g., mention “code not found in message body” or include
lastBodyPreview) and remove or replace any references to “matching subject” (and
any use of lastSubject in the hint) so the message accurately describes the
current logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6c58918f-8896-4291-801d-a447373fd9ba
📒 Files selected for processing (32)
.husky/commit-msgapps/deploy-web/src/components/auth/AuthLayoutV2/AuthLayoutV2.tsxapps/deploy-web/src/components/auth/AuthPage/AuthPage.spec.tsxapps/deploy-web/src/components/auth/AuthPage/AuthPage.tsxapps/deploy-web/src/components/auth/AuthPageLegacy/AuthPageLegacy.spec.tsxapps/deploy-web/src/components/auth/AuthPageLegacy/AuthPageLegacy.tsxapps/deploy-web/src/components/auth/AuthPagePasswordless/AuthPagePasswordless.spec.tsxapps/deploy-web/src/components/auth/AuthPagePasswordless/AuthPagePasswordless.tsxapps/deploy-web/src/components/auth/AuthPagePasswordless/withPersistedPasswordlessFlow.spec.tsxapps/deploy-web/src/components/auth/AuthPagePasswordless/withPersistedPasswordlessFlow.tsxapps/deploy-web/src/components/auth/EmailCodeStart/EmailCodeStart.spec.tsxapps/deploy-web/src/components/auth/EmailCodeStart/EmailCodeStart.tsxapps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.spec.tsxapps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsxapps/deploy-web/src/components/auth/OAuthRow/OAuthRow.spec.tsxapps/deploy-web/src/components/auth/OAuthRow/OAuthRow.tsxapps/deploy-web/src/pages/api/auth/email-code-start.spec.tsapps/deploy-web/src/pages/api/auth/email-code-start.tsapps/deploy-web/src/pages/api/auth/email-code-verify.spec.tsapps/deploy-web/src/pages/api/auth/email-code-verify.tsapps/deploy-web/src/pages/api/auth/password-login.tsapps/deploy-web/src/pages/api/auth/password-signup.tsapps/deploy-web/src/pages/login-v2/index.tsxapps/deploy-web/src/services/app-di-container/server-di-container.service.tsapps/deploy-web/src/services/auth/auth/auth.service.spec.tsapps/deploy-web/src/services/auth/auth/auth.service.tsapps/deploy-web/src/services/session/session.service.spec.tsapps/deploy-web/src/services/session/session.service.tsapps/deploy-web/tests/ui/fixture/onboarding-test.tsapps/deploy-web/tests/ui/pages/AuthPagePasswordless.tsapps/deploy-web/tests/ui/passwordless-login.spec.tsapps/deploy-web/tests/ui/services/email-verification/mailsac-code.strategy.ts
Resend button only gated on cooldown + resendMutation.isPending. Once cooldown reached zero, clicking Resend during an in-flight verify could trigger overlapping calls and mutation-reset thrash. Hoist isAnyMutationPending and fold it into isResendDisabled so the button also blocks while verify is pending.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/deploy-web/src/services/session/session.service.ts`:
- Around line 255-259: The `invalid_grant` branch returns message:
tokenResponse.data.error_description which can be undefined; update the Err
construction in the invalid_grant mapping to provide a fallback string (e.g.,
"Invalid authorization code" or similar) when
tokenResponse.data.error_description is falsy, keeping the rest of the object
(code: "invalid_code", cause: extractResponseDetails(tokenResponse)) unchanged
so the message field always satisfies the string contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fc5adcf8-6137-4603-bf99-3a868274c961
📒 Files selected for processing (6)
apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsxapps/deploy-web/src/pages/api/auth/email-code-verify.spec.tsapps/deploy-web/src/services/session/session.service.spec.tsapps/deploy-web/src/services/session/session.service.tsapps/deploy-web/tests/ui/passwordless-login.spec.tsapps/deploy-web/tests/ui/services/email-verification/mailsac-code.strategy.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/deploy-web/tests/ui/services/email-verification/mailsac-code.strategy.ts
- apps/deploy-web/tests/ui/passwordless-login.spec.ts
- apps/deploy-web/src/pages/api/auth/email-code-verify.spec.ts
Why
Closes CON-300.
Today the login surface is a Log in / Sign up tabbed form that requires a password. CON-300 collapses that into a single passwordless flow: enter your email and click Continue (or pick OAuth), then verify a 6-digit code emailed by Auth0. New accounts and returning users share the same path; the OTP also marks the email as verified, so the post-signup verification step disappears for this entry point.
What
console_auth_redesignflag (shared with CON-299). Flag-off keeps today's tabbed body intact viaAuthPageLegacy.AuthPagePasswordlessis a thin orchestrator;OAuthRow,EmailCodeStart, andEmailCodeVerifyown their own mutations and errors.withPersistedPasswordlessFlowHoC handles sessionStorage hydration via a lazyuseStateinitializer; the route loads the wrapped orchestrator viadynamic({ ssr: false })to avoid SSR hydration mismatch.POST /api/auth/email-code-startandPOST /api/auth/email-code-verifytranslate to Auth0's passwordless OTP endpoints; verify mints the session and idempotently calls/v1/register-user.setSessionis DI'd via the server container; all three auth routes (password-login,password-signup,email-code-verify) consumeservices.setSession./login-v2route renders the orchestrator unconditionally for the Playwright e2e, which exercises the real OTP path through Mailsac.Coordination
auth0-actionsmakesemail-dedupexit early whenevent.connection.strategy === "email"so first-time passwordless logins for existing emails are not blocked by the dedup gate. Either PR can land first — the flag stays off until both deploy.Screen.Recording.2026-05-14.at.19.09.29.mov
Summary by CodeRabbit
New Features
Refactor
Tests