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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
password-reset.tsx5.8 KB · 143 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
 * Block P1 — Password reset routes.
 *
 * GET  /forgot-password         → email-entry form
 * POST /forgot-password         → always redirects to ?sent=1
 * GET  /reset-password?token=…  → new-password form (or invalid-link page)
 * POST /reset-password          → rotate password + redirect to /login
 */

import { Hono } from "hono";
import { Layout } from "../views/layout";
import { Form, FormGroup, Input, Button, Alert, Text } from "../views/ui";
import { softAuth } from "../middleware/auth";
import type { AuthEnv } from "../middleware/auth";
import {
  createPasswordResetRequest,
  consumeResetToken,
  inspectResetToken,
} from "../lib/password-reset";

const passwordReset = new Hono<AuthEnv>();

passwordReset.get("/forgot-password", softAuth, (c) => {
  const csrf = c.get("csrfToken") as string | undefined;
  const sent = c.req.query("sent") === "1";

  if (sent) {
    return c.html(
      <Layout title="Reset link sent" user={c.get("user") ?? null}>
        <div class="auth-container">
          <h2>Check your inbox</h2>
          <Alert variant="success">
            If we have an account for that email, we've sent a reset link.
            Check your inbox (and spam folder).
          </Alert>
          <p class="auth-switch"><Text>The link expires in 1 hour.</Text></p>
          <p class="auth-switch"><a href="/login">Back to sign in</a></p>
        </div>
      </Layout>
    );
  }

  return c.html(
    <Layout title="Forgot password" user={c.get("user") ?? null}>
      <div class="auth-container">
        <h2>Reset your password</h2>
        <p class="auth-switch" style="margin-bottom:16px;margin-top:0">
          <Text>Enter the email tied to your account and we'll send you a link to set a new password.</Text>
        </p>
        <Form method="post" action="/forgot-password" csrfToken={csrf}>
          <FormGroup label="Email" htmlFor="email">
            <Input type="email" name="email" required placeholder="you@example.com" autocomplete="email" aria-label="Email" />
          </FormGroup>
          <Button type="submit" variant="primary">Send reset link</Button>
        </Form>
        <p class="auth-switch"><Text>Remembered it? <a href="/login">Sign in</a></Text></p>
      </div>
    </Layout>
  );
});

passwordReset.post("/forgot-password", async (c) => {
  const body = await c.req.parseBody();
  const email = String(body.email || "").trim();
  const ip =
    c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
    c.req.header("x-real-ip") ||
    undefined;
  await createPasswordResetRequest(email, { requestIp: ip });
  return c.redirect("/forgot-password?sent=1");
});

function InvalidLinkPage(props: { user: any }) {
  return (
    <Layout title="Link no longer valid" user={props.user ?? null}>
      <div class="auth-container">
        <h2>This link is no longer valid</h2>
        <Alert variant="error">
          Reset links expire after 1 hour and can only be used once. This link is expired, already used, or unknown.
        </Alert>
        <p class="auth-switch" style="margin-top:16px"><a href="/forgot-password">Request a new one</a></p>
        <p class="auth-switch"><a href="/login">Back to sign in</a></p>
      </div>
    </Layout>
  );
}

passwordReset.get("/reset-password", softAuth, async (c) => {
  const token = String(c.req.query("token") || "").trim();
  const csrf = c.get("csrfToken") as string | undefined;
  const error = c.req.query("error");

  if (!token) return c.html(<InvalidLinkPage user={c.get("user")} />);
  const check = await inspectResetToken(token);
  if (!check.valid) return c.html(<InvalidLinkPage user={c.get("user")} />);

  return c.html(
    <Layout title="Set a new password" user={c.get("user") ?? null}>
      <div class="auth-container">
        <h2>Set a new password</h2>
        {error && <Alert variant="error">{decodeURIComponent(error)}</Alert>}
        <p class="auth-switch" style="margin-bottom:16px;margin-top:0">
          <Text>Choose a new password — at least 8 characters. Signing in from other devices will be required afterwards.</Text>
        </p>
        <Form method="post" action="/reset-password" csrfToken={csrf}>
          <input type="hidden" name="token" value={token} />
          <FormGroup label="New password" htmlFor="password">
            <Input type="password" name="password" required minLength={8} placeholder="Min 8 characters" autocomplete="new-password" aria-label="New password" />
          </FormGroup>
          <FormGroup label="Confirm new password" htmlFor="confirm">
            <Input type="password" name="confirm" required minLength={8} placeholder="Re-enter the new password" autocomplete="new-password" aria-label="Confirm new password" />
          </FormGroup>
          <Button type="submit" variant="primary">Update password</Button>
        </Form>
        <p class="auth-switch"><a href="/login">Cancel</a></p>
      </div>
    </Layout>
  );
});

passwordReset.post("/reset-password", async (c) => {
  const body = await c.req.parseBody();
  const token = String(body.token || "").trim();
  const password = String(body.password || "");
  const confirm = String(body.confirm || "");

  const back = (msg: string) =>
    c.redirect(`/reset-password?token=${encodeURIComponent(token)}&error=${encodeURIComponent(msg)}`);

  if (!token) return c.html(<InvalidLinkPage user={null} />);
  if (!password || password.length < 8) return back("Password must be at least 8 characters");
  if (password !== confirm) return back("Passwords do not match");

  const result = await consumeResetToken(token, password);
  if (!result.ok) {
    if (result.reason === "weak") return back("Password must be at least 8 characters");
    return c.html(<InvalidLinkPage user={null} />);
  }

  return c.redirect("/login?success=" + encodeURIComponent("Password updated — please sign in"));
});

export default passwordReset;