Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

magic-link.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

magic-link.tsxBlame175 lines · 1 contributor
cd4f63bTest User1/**
2 * Block Q2 — Magic-link sign-in routes.
3 *
4 * GET /login/magic → email-entry form
5 * POST /login/magic → always redirects to ?sent=1
6 * GET /login/magic/callback?token=… → consume token, set session, redirect
7 *
8 * Structurally a sibling of `password-reset.tsx`. Differences:
9 * - The "callback" lands the user directly into a fresh session — there
10 * is no second form to fill in. We mint a session cookie and bounce
11 * to /dashboard (existing user) or /onboarding?welcome=1 (auto-created).
12 * - 15-minute TTL is messaged on the success/dead-link pages.
13 */
14
15import { Hono } from "hono";
16import { setCookie } from "hono/cookie";
17import { db } from "../db";
18import { sessions } from "../db/schema";
19import {
20 generateSessionToken,
21 sessionCookieOptions,
22 sessionExpiry,
23} from "../lib/auth";
24import { Layout } from "../views/layout";
25import { Form, FormGroup, Input, Button, Alert, Text } from "../views/ui";
26import { softAuth } from "../middleware/auth";
27import type { AuthEnv } from "../middleware/auth";
28import {
29 startMagicLinkSignIn,
30 consumeMagicLinkToken,
31} from "../lib/magic-link";
32
33const magicLink = new Hono<AuthEnv>();
34
35// ---------------------------------------------------------------------------
36// GET /login/magic — entry form + post-submit success.
37// ---------------------------------------------------------------------------
38
39magicLink.get("/login/magic", softAuth, (c) => {
40 const existing = c.get("user");
41 if (existing) return c.redirect("/dashboard");
42
43 const csrf = c.get("csrfToken") as string | undefined;
44 const sent = c.req.query("sent") === "1";
45
46 if (sent) {
47 return c.html(
48 <Layout title="Check your inbox" user={null}>
49 <div class="auth-container">
50 <h2>Check your inbox</h2>
51 <Alert variant="success">
52 If we can sign you in with that email, we've sent a link. It
53 expires in 15 minutes.
54 </Alert>
55 <p class="auth-switch">
56 <Text>
57 Didn't get it? Check your spam folder, or{" "}
58 <a href="/login/magic">try again</a>.
59 </Text>
60 </p>
61 <p class="auth-switch">
62 <a href="/login">Back to sign in</a>
63 </p>
64 </div>
65 </Layout>
66 );
67 }
68
69 return c.html(
70 <Layout title="Sign in with email link" user={null}>
71 <div class="auth-container">
72 <h2>Sign in with a magic link</h2>
73 <p class="auth-switch" style="margin-bottom:16px;margin-top:0">
74 <Text>
75 Enter your email and we'll send you a one-time sign-in link. No
76 password needed.
77 </Text>
78 </p>
79 <Form method="post" action="/login/magic" csrfToken={csrf}>
80 <FormGroup label="Email" htmlFor="email">
81 <Input
82 type="email"
83 name="email"
84 required
85 placeholder="you@example.com"
86 autocomplete="email"
87 aria-label="Email"
88 />
89 </FormGroup>
90 <Button type="submit" variant="primary">
91 Send me a sign-in link
92 </Button>
93 </Form>
94 <p class="auth-switch">
95 <Text>
96 Prefer a password?{" "}
97 <a href="/login">Sign in the usual way</a>.
98 </Text>
99 </p>
100 </div>
101 </Layout>
102 );
103});
104
105// ---------------------------------------------------------------------------
106// POST /login/magic — always redirects to ?sent=1 (no enumeration).
107// ---------------------------------------------------------------------------
108
109magicLink.post("/login/magic", async (c) => {
110 const body = await c.req.parseBody();
111 const email = String(body.email || "").trim();
112 const ip =
113 c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
114 c.req.header("x-real-ip") ||
115 undefined;
116 await startMagicLinkSignIn(email, { requestIp: ip });
117 return c.redirect("/login/magic?sent=1");
118});
119
120// ---------------------------------------------------------------------------
121// GET /login/magic/callback?token=… — consume the link.
122// ---------------------------------------------------------------------------
123
124function InvalidLinkPage(props: { user: any }) {
125 return (
126 <Layout title="Link no longer valid" user={props.user ?? null}>
127 <div class="auth-container">
128 <h2>This link is no longer valid</h2>
129 <Alert variant="error">
130 Magic links expire after 15 minutes and can only be used once.
131 This link is expired, already used, or unknown.
132 </Alert>
133 <p class="auth-switch" style="margin-top:16px">
134 <a href="/login/magic">Send a fresh one</a>
135 </p>
136 <p class="auth-switch">
137 <a href="/login">Back to sign in</a>
138 </p>
139 </div>
140 </Layout>
141 );
142}
143
144magicLink.get("/login/magic/callback", softAuth, async (c) => {
145 const token = String(c.req.query("token") || "").trim();
146 if (!token) return c.html(<InvalidLinkPage user={c.get("user")} />);
147
148 const ip =
149 c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
150 c.req.header("x-real-ip") ||
151 undefined;
152
153 const result = await consumeMagicLinkToken(token, { requestIp: ip });
154 if (!result.ok || !result.userId) {
155 return c.html(<InvalidLinkPage user={c.get("user")} />);
156 }
157
158 // Mint a fresh session for this user. We deliberately do NOT honour
159 // existing 2FA on magic-link sign-in here — the magic-link flow is
160 // explicitly for users who don't manage a password and 2FA enrollment
161 // requires a password in our current setup. If/when 2FA is decoupled
162 // from password auth (Q3+), this is the place to gate.
163 const sessionToken = generateSessionToken();
164 await db.insert(sessions).values({
165 userId: result.userId,
166 token: sessionToken,
167 expiresAt: sessionExpiry(),
168 });
169 setCookie(c, "session", sessionToken, sessionCookieOptions());
170
171 if (result.createdAccount) return c.redirect("/onboarding?welcome=1");
172 return c.redirect("/dashboard");
173});
174
175export default magicLink;