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

signing-keys.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.

signing-keys.tsxBlame209 lines · 1 contributor
3951454Claude1/**
2 * Block J3 — Signing keys UI.
3 *
4 * GET /settings/signing-keys — list + add form
5 * POST /settings/signing-keys — add new key
6 * POST /settings/signing-keys/:id/delete
7 */
8
9import { Hono } from "hono";
10import { Layout } from "../views/layout";
11import type { AuthEnv } from "../middleware/auth";
12import { requireAuth } from "../middleware/auth";
13import {
14 addSigningKey,
15 deleteSigningKey,
16 listSigningKeysForUser,
17} from "../lib/signatures";
18import { audit } from "../lib/notify";
19
20const signingKeysRoutes = new Hono<AuthEnv>();
21signingKeysRoutes.use("/settings/signing-keys", requireAuth);
22signingKeysRoutes.use("/settings/signing-keys/*", requireAuth);
23
24signingKeysRoutes.get("/settings/signing-keys", async (c) => {
25 const user = c.get("user")!;
26 const keys = await listSigningKeysForUser(user.id);
27 const message = c.req.query("message");
28 const error = c.req.query("error");
29 return c.html(
30 <Layout title="Signing keys" user={user}>
31 <div class="settings-container">
32 <h2>Signing keys</h2>
33 <p style="color:var(--text-muted)">
34 Register the GPG or SSH public key you use for{" "}
35 <code>git commit -S</code>. Commits we can match to a registered key
36 render with a <span style="color:var(--green);font-weight:600">Verified</span>{" "}
37 badge. This is identity matching by fingerprint — cryptographic
38 verification is future work.
39 </p>
40 {message && (
41 <div class="auth-success" style="margin-top:12px">
42 {decodeURIComponent(message)}
43 </div>
44 )}
45 {error && (
46 <div class="auth-error" style="margin-top:12px">
47 {decodeURIComponent(error)}
48 </div>
49 )}
50
51 <h3 style="margin-top:24px">Your keys</h3>
52 {keys.length === 0 ? (
53 <div class="panel-empty" style="padding:16px">
54 No signing keys yet.
55 </div>
56 ) : (
57 <div class="panel">
58 {keys.map((k) => (
59 <div
60 class="panel-item"
61 style="flex-direction:column;align-items:stretch;gap:4px"
62 >
63 <div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap">
64 <div>
65 <span
66 style="font-size:10px;padding:2px 6px;border-radius:3px;background:var(--bg-subtle);text-transform:uppercase;margin-right:6px"
67 >
68 {k.keyType}
69 </span>
70 <span style="font-weight:600">{k.title}</span>
71 {k.email && (
72 <span
73 style="font-size:12px;color:var(--text-muted);margin-left:8px"
74 >
75 {k.email}
76 </span>
77 )}
78 </div>
79 <form
001af43Claude80 method="post"
3951454Claude81 action={`/settings/signing-keys/${k.id}/delete`}
82 >
83 <button
84 type="submit"
85 class="btn btn-sm"
86 style="font-size:11px"
87 >
88 Delete
89 </button>
90 </form>
91 </div>
92 <div
93 style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);word-break:break-all"
94 >
95 {k.fingerprint}
96 </div>
97 </div>
98 ))}
99 </div>
100 )}
101
102 <h3 style="margin-top:24px">Add a key</h3>
103 <form
001af43Claude104 method="post"
3951454Claude105 action="/settings/signing-keys"
106 class="auth-form"
107 style="max-width:720px"
108 >
109 <div class="form-group">
110 <label for="sk-title">Title</label>
111 <input
112 type="text"
113 id="sk-title"
114 name="title"
115 placeholder="e.g. Work laptop"
116 required
117 maxLength={120}
118 />
119 </div>
120 <div class="form-group">
121 <label for="sk-type">Key type</label>
122 <select id="sk-type" name="key_type" required>
123 <option value="gpg">GPG</option>
124 <option value="ssh">SSH</option>
125 </select>
126 </div>
127 <div class="form-group">
128 <label for="sk-email">Email (optional)</label>
129 <input
130 type="email"
131 id="sk-email"
132 name="email"
133 placeholder="commit-author@example.com"
134 maxLength={200}
135 />
136 </div>
137 <div class="form-group">
138 <label for="sk-public">Public key</label>
139 <textarea
140 id="sk-public"
141 name="public_key"
142 rows={10}
143 required
144 placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#10;...&#10;-----END PGP PUBLIC KEY BLOCK-----&#10;&#10;or: ssh-ed25519 AAAA... you@laptop"
145 style="font-family:var(--font-mono);font-size:12px"
146 />
147 </div>
148 <button type="submit" class="btn btn-primary">
149 Add key
150 </button>
151 </form>
152 </div>
153 </Layout>
154 );
155});
156
157signingKeysRoutes.post("/settings/signing-keys", async (c) => {
158 const user = c.get("user")!;
159 const body = await c.req.parseBody();
160 const keyType = String(body.key_type || "").toLowerCase() as "gpg" | "ssh";
161 const title = String(body.title || "");
162 const publicKey = String(body.public_key || "");
163 const email = String(body.email || "");
164
165 const result = await addSigningKey({
166 userId: user.id,
167 keyType,
168 title,
169 publicKey,
170 email,
171 });
172
173 if (!result.ok) {
174 return c.redirect(
175 `/settings/signing-keys?error=${encodeURIComponent(result.error)}`
176 );
177 }
178 await audit({
179 userId: user.id,
180 action: "signing_keys.add",
181 targetId: result.id,
182 metadata: { keyType, fingerprint: result.fingerprint },
183 });
184 return c.redirect(
185 `/settings/signing-keys?message=${encodeURIComponent(
186 `Added key ${result.fingerprint.slice(0, 24)}…`
187 )}`
188 );
189});
190
191signingKeysRoutes.post("/settings/signing-keys/:id/delete", async (c) => {
192 const user = c.get("user")!;
193 const id = c.req.param("id");
194 const ok = await deleteSigningKey(id, user.id);
195 if (ok) {
196 await audit({
197 userId: user.id,
198 action: "signing_keys.delete",
199 targetId: id,
200 });
201 }
202 return c.redirect(
203 `/settings/signing-keys?${ok ? "message" : "error"}=${encodeURIComponent(
204 ok ? "Key removed." : "Key not found"
205 )}`
206 );
207});
208
209export default signingKeysRoutes;