feat(auth): combined magic-link + OTP sign-in email#159
Merged
Conversation
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.
There was a problem hiding this comment.
💡 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".
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
magic-link.tsxtemplate carrying both the click-to-sign-in button and the 6-digit code, with shared styles + email-layout components.signin-code.tsxtemplate (OTP-only).packages/email/src/index.tsaddssendMagicLinkEmail(...)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.verifyOtpvalidates 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
portalConfig.oauth.email: true→portalConfig.oauth.magicLink: truefor any tenant that previously had email OTP enabled. Tenants with email auth disabled are unchanged. (Renumbered from the original 0049 to slot aftermain'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.get-otp.ts→get-magic-link-token.ts.Test plan
email-signin.tsandmagic-link-mint.ts(server seam)