Skip to content

feat(auth): passwordless email OTP behind console_auth_redesign (#3178)#3186

Merged
ygrishajev merged 2 commits into
mainfrom
feat/auth-passwordless
May 18, 2026
Merged

feat(auth): passwordless email OTP behind console_auth_redesign (#3178)#3186
ygrishajev merged 2 commits into
mainfrom
feat/auth-passwordless

Conversation

@ygrishajev
Copy link
Copy Markdown
Contributor

@ygrishajev ygrishajev commented May 15, 2026

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

  • One-screen entry (email + OAuth) and a verify screen (6-digit OTP, "Wrong email? Edit", 30s resend cooldown). Behind the existing console_auth_redesign flag (shared with CON-299). Flag-off keeps today's tabbed body intact via AuthPageLegacy.
  • AuthPagePasswordless is a thin orchestrator; OAuthRow, EmailCodeStart, and EmailCodeVerify own their own mutations and errors. withPersistedPasswordlessFlow HoC handles sessionStorage hydration via a lazy useState initializer; the route loads the wrapped orchestrator via dynamic({ ssr: false }) to avoid SSR hydration mismatch.
  • New routes POST /api/auth/email-code-start and POST /api/auth/email-code-verify translate to Auth0's passwordless OTP endpoints; verify mints the session and idempotently calls /v1/register-user.
  • setSession is DI'd via the server container; all three auth routes (password-login, password-signup, email-code-verify) consume services.setSession.
  • New /login-v2 route renders the orchestrator unconditionally for the Playwright e2e, which exercises the real OTP path through Mailsac.

Coordination

  • Companion PR in auth0-actions makes email-dedup exit early when event.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.
  • Auth0 tenant needs the Email passwordless connection enabled on this Application plus the Passwordless OTP grant. Email template + subject suggested in the design doc are Akash-branded; defaults work for v1.
Screen.Recording.2026-05-14.at.19.09.29.mov

Summary by CodeRabbit

  • New Features

    • Passwordless email-code sign-in (6‑digit OTP) with new /login-v2 page, resend cooldown, CAPTCHA protection, and social OAuth buttons.
  • Refactor

    • Split legacy password-based UI from the new passwordless flow and improved auth layout with explicit light/dark backgrounds.
    • Server session handling unified via shared session service.
  • Tests

    • Extensive unit, integration, and UI tests covering passwordless flows, API endpoints, and end-to-end OTP verification.

Review Change Stack

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Passwordless Email-Code Auth System

Layer / File(s) Summary
AuthLayoutV2 background tweak
apps/deploy-web/src/components/auth/AuthLayoutV2/AuthLayoutV2.tsx
Children wrapper adds explicit light/dark background classes while preserving layout/overflow and z-index.
Feature-flag Auth router and page
apps/deploy-web/src/components/auth/AuthPage/AuthPage.tsx, apps/deploy-web/src/components/auth/AuthPage/AuthPage.spec.tsx, apps/deploy-web/src/pages/login-v2/index.tsx
AuthPage now chooses between passwordless and legacy implementations via console_auth_redesign. Adds /login-v2 page rendering the passwordless client and server-side props validating returnTo and redirecting authenticated users.
Legacy password-based auth
apps/deploy-web/src/components/auth/AuthPageLegacy/*
New standalone AuthPageLegacy component: query-driven tabs, password sign-in/sign-up/forgot-password flows using Turnstile, social auth area, and tests exercising flows and error handling.
Passwordless orchestrator & persisted flow
apps/deploy-web/src/components/auth/AuthPagePasswordless/*
AuthPagePasswordless orchestrates entry↔verify screens, supplies captcha token getters, invokes checkSession and navigateBack on success; withPersistedPasswordlessFlow HOC persists {email,screen} to sessionStorage and is covered by tests. Includes SSR-disabled client wrapper.
Email-code entry / verification components
apps/deploy-web/src/components/auth/EmailCodeStart/*, apps/deploy-web/src/components/auth/EmailCodeVerify/*
EmailCodeStart: email input + captcha + startEmailCode. EmailCodeVerify: 6-digit input, verify/resend mutations, 30s resend cooldown, error handling and UI states. Both include unit tests.
OAuthRow
apps/deploy-web/src/components/auth/OAuthRow/*
Renders Google/GitHub OAuth buttons, tracks analytics, and calls authService.loginViaOauth with resolved returnTo; tested for analytics and correct provider.
Email-code API handlers
apps/deploy-web/src/pages/api/auth/email-code-start.ts, apps/deploy-web/src/pages/api/auth/email-code-verify.ts
POST endpoints validate input with Zod, verify captcha, delegate to sessionService, create local user and set session on verify success, and map rate_limited → 429 (with Retry-After). Tests cover success and error mappings.
Session & Auth service contracts
apps/deploy-web/src/services/session/session.service.ts, apps/deploy-web/src/services/auth/auth/auth.service.ts, tests
SessionService adds startEmailCode and verifyEmailCode with typed Result error mappings and retryAfter header parsing helper; AuthService adds client wrappers. Tests added/updated to assert request shapes and error mapping behavior.
DI & session injection
apps/deploy-web/src/services/app-di-container/server-di-container.service.ts, apps/deploy-web/src/pages/api/auth/password-login.ts, apps/deploy-web/src/pages/api/auth/password-signup.ts
Registers setSession in server DI and updates password login/signup handlers to call services.setSession instead of importing helper directly.
UI E2E & OTP extraction
apps/deploy-web/tests/ui/*
Playwright fixture widened to intercept email-code endpoints, new AuthPagePasswordless page object, E2E passwordless test drives full OTP flow using Mailsac strategy which extracts 6-digit code near the word “code”.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • stalniy
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth-passwordless

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 94.16058% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.23%. Comparing base (e1d2bba) to head (d75ca3a).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
apps/deploy-web/src/pages/login-v2/index.tsx 0.00% 6 Missing and 1 partial ⚠️
...auth/AuthPagePasswordless/AuthPagePasswordless.tsx 93.02% 3 Missing ⚠️
.../components/auth/AuthPageLegacy/AuthPageLegacy.tsx 96.42% 2 Missing ⚠️
...PagePasswordless/withPersistedPasswordlessFlow.tsx 93.10% 2 Missing ⚠️
...ps/deploy-web/src/pages/api/auth/password-login.ts 0.00% 1 Missing ⚠️
...s/deploy-web/src/pages/api/auth/password-signup.ts 0.00% 1 Missing ⚠️
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     
Flag Coverage Δ *Carryforward flag
api 84.26% <ø> (-0.03%) ⬇️
deploy-web 47.37% <94.16%> (+0.68%) ⬆️
log-collector ?
notifications 91.06% <ø> (ø) Carriedforward from 061df3c
provider-console 81.48% <ø> (ø) Carriedforward from 061df3c
provider-inventory 82.13% <ø> (+0.77%) ⬆️
provider-proxy 86.08% <ø> (ø) Carriedforward from 061df3c
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
.../src/components/auth/AuthLayoutV2/AuthLayoutV2.tsx 83.33% <ø> (ø)
...ploy-web/src/components/auth/AuthPage/AuthPage.tsx 100.00% <100.00%> (+3.27%) ⬆️
.../components/auth/EmailCodeStart/EmailCodeStart.tsx 100.00% <100.00%> (ø)
...omponents/auth/EmailCodeVerify/EmailCodeVerify.tsx 100.00% <100.00%> (ø)
...ploy-web/src/components/auth/OAuthRow/OAuthRow.tsx 100.00% <100.00%> (ø)
.../deploy-web/src/pages/api/auth/email-code-start.ts 100.00% <100.00%> (ø)
...deploy-web/src/pages/api/auth/email-code-verify.ts 100.00% <100.00%> (ø)
...es/app-di-container/server-di-container.service.ts 100.00% <100.00%> (ø)
.../deploy-web/src/services/auth/auth/auth.service.ts 75.00% <100.00%> (+5.00%) ⬆️
...deploy-web/src/services/session/session.service.ts 86.17% <100.00%> (+8.04%) ⬆️
... and 6 more

... and 51 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e1d2bba and 061df3c.

📒 Files selected for processing (32)
  • .husky/commit-msg
  • apps/deploy-web/src/components/auth/AuthLayoutV2/AuthLayoutV2.tsx
  • apps/deploy-web/src/components/auth/AuthPage/AuthPage.spec.tsx
  • apps/deploy-web/src/components/auth/AuthPage/AuthPage.tsx
  • apps/deploy-web/src/components/auth/AuthPageLegacy/AuthPageLegacy.spec.tsx
  • apps/deploy-web/src/components/auth/AuthPageLegacy/AuthPageLegacy.tsx
  • apps/deploy-web/src/components/auth/AuthPagePasswordless/AuthPagePasswordless.spec.tsx
  • apps/deploy-web/src/components/auth/AuthPagePasswordless/AuthPagePasswordless.tsx
  • apps/deploy-web/src/components/auth/AuthPagePasswordless/withPersistedPasswordlessFlow.spec.tsx
  • apps/deploy-web/src/components/auth/AuthPagePasswordless/withPersistedPasswordlessFlow.tsx
  • apps/deploy-web/src/components/auth/EmailCodeStart/EmailCodeStart.spec.tsx
  • apps/deploy-web/src/components/auth/EmailCodeStart/EmailCodeStart.tsx
  • apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.spec.tsx
  • apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx
  • apps/deploy-web/src/components/auth/OAuthRow/OAuthRow.spec.tsx
  • apps/deploy-web/src/components/auth/OAuthRow/OAuthRow.tsx
  • apps/deploy-web/src/pages/api/auth/email-code-start.spec.ts
  • apps/deploy-web/src/pages/api/auth/email-code-start.ts
  • apps/deploy-web/src/pages/api/auth/email-code-verify.spec.ts
  • apps/deploy-web/src/pages/api/auth/email-code-verify.ts
  • apps/deploy-web/src/pages/api/auth/password-login.ts
  • apps/deploy-web/src/pages/api/auth/password-signup.ts
  • apps/deploy-web/src/pages/login-v2/index.tsx
  • apps/deploy-web/src/services/app-di-container/server-di-container.service.ts
  • apps/deploy-web/src/services/auth/auth/auth.service.spec.ts
  • apps/deploy-web/src/services/auth/auth/auth.service.ts
  • apps/deploy-web/src/services/session/session.service.spec.ts
  • apps/deploy-web/src/services/session/session.service.ts
  • apps/deploy-web/tests/ui/fixture/onboarding-test.ts
  • apps/deploy-web/tests/ui/pages/AuthPagePasswordless.ts
  • apps/deploy-web/tests/ui/passwordless-login.spec.ts
  • apps/deploy-web/tests/ui/services/email-verification/mailsac-code.strategy.ts

Comment thread apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx Outdated
Comment thread apps/deploy-web/src/services/session/session.service.ts
Comment thread apps/deploy-web/tests/ui/passwordless-login.spec.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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 061df3c and d75ca3a.

📒 Files selected for processing (6)
  • apps/deploy-web/src/components/auth/EmailCodeVerify/EmailCodeVerify.tsx
  • apps/deploy-web/src/pages/api/auth/email-code-verify.spec.ts
  • apps/deploy-web/src/services/session/session.service.spec.ts
  • apps/deploy-web/src/services/session/session.service.ts
  • apps/deploy-web/tests/ui/passwordless-login.spec.ts
  • apps/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

Comment thread apps/deploy-web/src/services/session/session.service.ts
@ygrishajev ygrishajev added this pull request to the merge queue May 18, 2026
Merged via the queue into main with commit 024d582 May 18, 2026
95 of 97 checks passed
@ygrishajev ygrishajev deleted the feat/auth-passwordless branch May 18, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants