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

password-reset.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.

password-reset.tsxBlame143 lines · 1 contributor
c63b860Claude1/**
2 * Block P1 — Password reset routes.
3 *
4 * GET /forgot-password → email-entry form
5 * POST /forgot-password → always redirects to ?sent=1
6 * GET /reset-password?token=… → new-password form (or invalid-link page)
7 * POST /reset-password → rotate password + redirect to /login
8 */
9
10import { Hono } from "hono";
11import { Layout } from "../views/layout";
12import { Form, FormGroup, Input, Button, Alert, Text } from "../views/ui";
13import { softAuth } from "../middleware/auth";
14import type { AuthEnv } from "../middleware/auth";
15import {
16 createPasswordResetRequest,
17 consumeResetToken,
18 inspectResetToken,
19} from "../lib/password-reset";
20
21const passwordReset = new Hono<AuthEnv>();
22
23passwordReset.get("/forgot-password", softAuth, (c) => {
24 const csrf = c.get("csrfToken") as string | undefined;
25 const sent = c.req.query("sent") === "1";
26
27 if (sent) {
28 return c.html(
29 <Layout title="Reset link sent" user={c.get("user") ?? null}>
30 <div class="auth-container">
31 <h2>Check your inbox</h2>
32 <Alert variant="success">
33 If we have an account for that email, we've sent a reset link.
34 Check your inbox (and spam folder).
35 </Alert>
36 <p class="auth-switch"><Text>The link expires in 1 hour.</Text></p>
37 <p class="auth-switch"><a href="/login">Back to sign in</a></p>
38 </div>
39 </Layout>
40 );
41 }
42
43 return c.html(
44 <Layout title="Forgot password" user={c.get("user") ?? null}>
45 <div class="auth-container">
46 <h2>Reset your password</h2>
47 <p class="auth-switch" style="margin-bottom:16px;margin-top:0">
48 <Text>Enter the email tied to your account and we'll send you a link to set a new password.</Text>
49 </p>
50 <Form method="post" action="/forgot-password" csrfToken={csrf}>
51 <FormGroup label="Email" htmlFor="email">
52 <Input type="email" name="email" required placeholder="you@example.com" autocomplete="email" aria-label="Email" />
53 </FormGroup>
54 <Button type="submit" variant="primary">Send reset link</Button>
55 </Form>
56 <p class="auth-switch"><Text>Remembered it? <a href="/login">Sign in</a></Text></p>
57 </div>
58 </Layout>
59 );
60});
61
62passwordReset.post("/forgot-password", async (c) => {
63 const body = await c.req.parseBody();
64 const email = String(body.email || "").trim();
65 const ip =
66 c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
67 c.req.header("x-real-ip") ||
68 undefined;
69 await createPasswordResetRequest(email, { requestIp: ip });
70 return c.redirect("/forgot-password?sent=1");
71});
72
73function InvalidLinkPage(props: { user: any }) {
74 return (
75 <Layout title="Link no longer valid" user={props.user ?? null}>
76 <div class="auth-container">
77 <h2>This link is no longer valid</h2>
78 <Alert variant="error">
79 Reset links expire after 1 hour and can only be used once. This link is expired, already used, or unknown.
80 </Alert>
81 <p class="auth-switch" style="margin-top:16px"><a href="/forgot-password">Request a new one</a></p>
82 <p class="auth-switch"><a href="/login">Back to sign in</a></p>
83 </div>
84 </Layout>
85 );
86}
87
88passwordReset.get("/reset-password", softAuth, async (c) => {
89 const token = String(c.req.query("token") || "").trim();
90 const csrf = c.get("csrfToken") as string | undefined;
91 const error = c.req.query("error");
92
93 if (!token) return c.html(<InvalidLinkPage user={c.get("user")} />);
94 const check = await inspectResetToken(token);
95 if (!check.valid) return c.html(<InvalidLinkPage user={c.get("user")} />);
96
97 return c.html(
98 <Layout title="Set a new password" user={c.get("user") ?? null}>
99 <div class="auth-container">
100 <h2>Set a new password</h2>
101 {error && <Alert variant="error">{decodeURIComponent(error)}</Alert>}
102 <p class="auth-switch" style="margin-bottom:16px;margin-top:0">
103 <Text>Choose a new password — at least 8 characters. Signing in from other devices will be required afterwards.</Text>
104 </p>
105 <Form method="post" action="/reset-password" csrfToken={csrf}>
106 <input type="hidden" name="token" value={token} />
107 <FormGroup label="New password" htmlFor="password">
108 <Input type="password" name="password" required minLength={8} placeholder="Min 8 characters" autocomplete="new-password" aria-label="New password" />
109 </FormGroup>
110 <FormGroup label="Confirm new password" htmlFor="confirm">
111 <Input type="password" name="confirm" required minLength={8} placeholder="Re-enter the new password" autocomplete="new-password" aria-label="Confirm new password" />
112 </FormGroup>
113 <Button type="submit" variant="primary">Update password</Button>
114 </Form>
115 <p class="auth-switch"><a href="/login">Cancel</a></p>
116 </div>
117 </Layout>
118 );
119});
120
121passwordReset.post("/reset-password", async (c) => {
122 const body = await c.req.parseBody();
123 const token = String(body.token || "").trim();
124 const password = String(body.password || "");
125 const confirm = String(body.confirm || "");
126
127 const back = (msg: string) =>
128 c.redirect(`/reset-password?token=${encodeURIComponent(token)}&error=${encodeURIComponent(msg)}`);
129
130 if (!token) return c.html(<InvalidLinkPage user={null} />);
131 if (!password || password.length < 8) return back("Password must be at least 8 characters");
132 if (password !== confirm) return back("Passwords do not match");
133
134 const result = await consumeResetToken(token, password);
135 if (!result.ok) {
136 if (result.reason === "weak") return back("Password must be at least 8 characters");
137 return c.html(<InvalidLinkPage user={null} />);
138 }
139
140 return c.redirect("/login?success=" + encodeURIComponent("Password updated — please sign in"));
141});
142
143export default passwordReset;