Skip to content

feat(auth): combined magic-link + OTP sign-in email#159

Merged
mortondev merged 5 commits intomainfrom
magic-link-auth
May 6, 2026
Merged

feat(auth): combined magic-link + OTP sign-in email#159
mortondev merged 5 commits intomainfrom
magic-link-auth

Conversation

@mortondev
Copy link
Copy Markdown
Member

Replaces the email-OTP-only sign-in flow with a combined magic link + OTP email. One email carries both a sign-in button and a 6-digit code so users pick whichever fits their context — desktop click, cross-device code entry, or fallback when Outlook Safe Links eats the URL.

Summary

Email

  • New magic-link.tsx template carrying both the click-to-sign-in button and the 6-digit code, with shared styles + email-layout components.
  • Drops the old signin-code.tsx template (OTP-only).
  • packages/email/src/index.ts adds sendMagicLinkEmail(...) covering both delivery modes.

Server

  • lib/server/auth/email-signin.ts — single entry that mints a magic-link token and a paired 6-digit OTP, both pointing at the same Better-Auth session.
  • lib/server/auth/magic-link-mint.ts — token minting + verification helpers.
  • Auth instance build wires the new email-signin path; verifyOtp validates against the same magic-link store.

UI

  • otp-code-step.tsx — new shadcn input-OTP-style 6-digit code entry.
  • use-email-signin.ts + email-signin-types.ts — shared client hook that the portal auth form, the inline form, and the auth dialog all consume.
  • portal-auth-form.tsx, portal-auth-form-inline.tsx, auth-dialog.tsx — rewritten to drive the unified email step.
  • input-otp.tsx — the shadcn input-otp primitive (six characters, auto-advance).

Migration

  • 0052_otp_to_magic_link.sql — data-only migration that flips portalConfig.oauth.email: trueportalConfig.oauth.magicLink: true for any tenant that previously had email OTP enabled. Tenants with email auth disabled are unchanged. (Renumbered from the original 0049 to slot after main's tier-limits migrations 0049/0050/0051.)

Tests

  • apps/web/src/components/auth/__tests__/portal-auth-form.test.tsx — covers the magic-link + OTP UI states.
  • E2E helper renamed get-otp.tsget-magic-link-token.ts.
  • 161 passing tests across settings/functions/auth (was 152 before this branch).

Test plan

  • Code review on email-signin.ts and magic-link-mint.ts (server seam)
  • Eyeball the email render at desktop + Outlook + Gmail mobile
  • Confirm the OTP code step works with a paste-paste-paste flow on mobile
  • Spin up a tenant against this build, enable email auth, sign in via both link and code

mortondev added 3 commits May 1, 2026 10:28
One email carries both a sign-in button and a 6-digit code so users
pick whichever fits their context — desktop click, cross-device code
entry, or fallback when Outlook Safe Links eats the URL.

Migration 0049 retires the standalone Email OTP admin toggle by
flipping oauth.email=true → magicLink=true on existing portals. New
/api/auth/portal-signin mints both tokens via Promise.all. Portal
forms get a segmented 6-cell OTP input that auto-submits on the
sixth digit; the AuthDialog header adapts as the user advances past
the email step. Shared useEmailSignin hook + OtpCodeStep component
dedupe the request/verify/resend flow across the full-page form and
the inline-dialog form.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 5, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77588318c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/server/auth/index.ts Outdated
Comment thread apps/web/src/lib/server/auth/email-signin.ts Outdated
Renumbered 0049_otp_to_magic_link → 0052 to slot after main's
tier-limits migrations (0049_settings_tier_limits, 0050_api_keys_scopes,
0051_ai_usage_log_month_index). 0052 is data-only (no schema change),
so 0052_snapshot.json is a copy of 0051_snapshot.json.
…ilures to /admin/login

Addresses two PR #159 review findings from chatgpt-codex-connector.

P1 — Magic-link sign-in tokens lived for 7 days, contradicting the
sign-in email's "expires in 10 minutes" copy and giving any forwarded
or later-compromised email a week-long passwordless login window.

- Tighten the magicLink plugin's global `expiresIn` from 7 days → 10 min.
- Add `expiresInSeconds` override on `mintMagicLinkUrl` for callers that
  need a longer window (cloud-bootstrap claim URLs only — those are
  multi-day "claim your new workspace" links sent to a single admin).
- `extendVerificationExpiry` pushes the verification row's expires_at
  out post-mint without resigning the token.
- Bootstrap response keeps its 7-day window via the new opt-in.

P2 — Failed admin sign-in verifies (token consumed by an email scanner,
expired, double-clicked) redirected to /auth/login (portal). Portal
auth settings often don't have admin's magic-link option enabled, so
team users were stranded with no way to request a fresh link.

- Derive errorCallbackPath in requestEmailSignin from the success
  callbackURL: `/admin/...` → `/admin/login`, else `/auth/login`.
- Bootstrap (which already explicitly sets `/admin/login`) unaffected.

Tests: 2 new for the redirect logic, 193 total in the relevant suites
(was 161).
@mortondev mortondev merged commit c91ed4f into main May 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants