Skip to main content

Magic links (retired)

Retired

This page has been retired. Magic links are now managed inside System settings, in the Magic Links tab.

  • Route: /admin/magic-links
  • Guard: /admin/* layout (isAdminTier()); the magic-links API enforces requireSuperAdmin — deliberately stricter than the cross-client admin guard, since a magic link is an impersonation credential.
  • Redirects to: /admin/settings?tab=magic-links — the screenshot below shows that destination. At capture: 16 tokens listed, metadata only (no plaintext links; the audit run's own tokens appear as "Used").

Admin: Magic Links redirect target

What this route is now​

src/app/admin/magic-links/page.tsx is an unconditional redirect: the standalone page was folded into System Settings as the Magic Links tab, where MagicLinksList renders with embedded=true (System supplies the page header). The old sidebar entry was removed from src/lib/nav.ts.

  • Token list — GET /api/admin/magic-links returns every row in the Supabase magic_link_tokens table: user email, label, redirect target, created at/by, expiry, consumed at, last-consumed at + IP, consume count, revoked at/by. Only metadata is shown — the token column stays hashed.
  • Status derivation — each row renders one of Revoked / Used / Expired / Active / "Lifetime · Active", computed client-side from the row fields (revoked wins; a consumed non-lifetime token is Used; past expiresAt is Expired).
  • Filters + search — filter modes all | lifetime | active | used | revoked, plus free-text search over email, label, redirect, and id.
  • Mint — POST /api/admin/magic-links with targetEmail, lifetime flag, optional label, and ttlMinutes (default 30) for non-lifetime tokens. Lifetime tokens require a label (enforced in the UI for the audit trail). The response is the only time the plaintext URL/token exists client-side — it is shown once with a copy affordance, then never again.
  • Revoke — POST /api/admin/magic-links/{id}/revoke; the sole kill switch for a leaked lifetime credential.

Security model (why nothing needed redaction in the screenshot)​

src/lib/magic-link.ts persists only sha256(token) — a DB dump cannot be redeemed, and the admin list can never display a live link after mint time. The consume flow lives at /api/magic-link/consume: the route hashes the presented token, matches it against magic_link_tokens, enforces expiry/revocation, and signs the user in (recording consume count and IP). Lifetime tokens are multi-use until revoked; standard tokens are single-use. scripts/mint-magic-link-demo.mjs is the CLI equivalent of the mint form (used to authenticate the capture run itself, per the manifest).

Behavior summary​

AspectValue
HTTP behaviorServer redirect to /admin/settings?tab=magic-links
Query paramsNot preserved
Who can use itList/mint/revoke: super_admin only (API-enforced)
Token storagesha256 hash only; plaintext shown once at mint
Rendering modeforce-dynamic

Key files​

File / endpointRole
src/app/admin/magic-links/page.tsxThe redirect (whole page)
src/components/admin/MagicLinksList.tsxTab UI (list, filters, mint, revoke)
src/app/admin/settings/SettingsView.tsxHost tab (?tab=magic-links)
src/lib/magic-link.tsToken mint/hash/consume/summary logic
GET/POST /api/admin/magic-linksList + mint (super_admin)
POST /api/admin/magic-links/{id}/revokeRevoke
/api/magic-link/consumePublic consume/sign-in endpoint
scripts/mint-magic-link-demo.mjsCLI mint helper
Supabase magic_link_tokensBacking table