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

tokens.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.

tokens.tsxBlame209 lines · 1 contributor
c81ab7aClaude1/**
2 * API tokens — personal access tokens for automation.
3 */
4
5import { Hono } from "hono";
6import { eq } from "drizzle-orm";
7import { db } from "../db";
8import { apiTokens } from "../db/schema";
9import { Layout } from "../views/layout";
10import { softAuth, requireAuth } from "../middleware/auth";
11import type { AuthEnv } from "../middleware/auth";
12
13const tokens = new Hono<AuthEnv>();
14
15tokens.use("/settings/tokens*", softAuth, requireAuth);
16tokens.use("/api/user/tokens*", softAuth, requireAuth);
17
18function generateToken(): string {
19 const bytes = crypto.getRandomValues(new Uint8Array(32));
20 return (
21 "glc_" +
22 Array.from(bytes)
23 .map((b) => b.toString(16).padStart(2, "0"))
24 .join("")
25 );
26}
27
28async function hashToken(token: string): Promise<string> {
29 const data = new TextEncoder().encode(token);
30 const hash = await crypto.subtle.digest("SHA-256", data);
31 return Array.from(new Uint8Array(hash))
32 .map((b) => b.toString(16).padStart(2, "0"))
33 .join("");
34}
35
36// Token settings page
37tokens.get("/settings/tokens", async (c) => {
38 const user = c.get("user")!;
39 const success = c.req.query("success");
40 const newToken = c.req.query("new_token");
41
42 const userTokens = await db
43 .select()
44 .from(apiTokens)
45 .where(eq(apiTokens.userId, user.id));
46
47 return c.html(
48 <Layout title="API Tokens" user={user}>
49 <div class="settings-container">
50 <h2>Personal access tokens</h2>
51 {success && (
52 <div class="auth-success" style="margin-top: 12px">
53 {decodeURIComponent(success)}
54 </div>
55 )}
56 {newToken && (
57 <div
58 class="auth-success"
59 style="margin-top: 12px; font-family: var(--font-mono); word-break: break-all"
60 >
61 New token (copy now — it won't be shown again):{" "}
62 <strong>{decodeURIComponent(newToken)}</strong>
63 </div>
64 )}
65 <div style="margin-top: 16px">
66 {userTokens.length === 0 ? (
67 <p style="color: var(--text-muted)">No tokens yet.</p>
68 ) : (
69 userTokens.map((token) => (
70 <div class="ssh-key-item">
71 <div>
72 <strong>{token.name}</strong>
73 <div class="ssh-key-meta">
74 <code>{token.tokenPrefix}...</code>
75 <span style="margin-left: 8px">
76 Scopes: {token.scopes}
77 </span>
78 {token.lastUsedAt && (
79 <span>
80 {" "}
81 | Last used{" "}
82 {new Date(token.lastUsedAt).toLocaleDateString()}
83 </span>
84 )}
85 </div>
86 </div>
87 <form
88 method="POST"
89 action={`/settings/tokens/${token.id}/delete`}
90 >
91 <button type="submit" class="btn btn-danger btn-sm">
92 Revoke
93 </button>
94 </form>
95 </div>
96 ))
97 )}
98 </div>
99
100 <h3 style="margin-top: 24px; margin-bottom: 12px">
101 Generate new token
102 </h3>
103 <form method="POST" action="/settings/tokens">
104 <div class="form-group">
105 <label for="name">Token name</label>
106 <input
107 type="text"
108 id="name"
109 name="name"
110 required
111 placeholder="e.g. CI/CD pipeline"
112 />
113 </div>
114 <div class="form-group">
115 <label>Scopes</label>
116 <div style="display: flex; gap: 16px; flex-wrap: wrap">
117 {["repo", "user", "admin"].map((scope) => (
118 <label style="display: flex; align-items: center; gap: 4px; font-size: 14px; cursor: pointer">
119 <input
120 type="checkbox"
121 name="scopes"
122 value={scope}
123 checked={scope === "repo"}
124 />{" "}
125 {scope}
126 </label>
127 ))}
128 </div>
129 </div>
130 <button type="submit" class="btn btn-primary">
131 Generate token
132 </button>
133 </form>
134 </div>
135 </Layout>
136 );
137});
138
139// Create token
140tokens.post("/settings/tokens", async (c) => {
141 const user = c.get("user")!;
142 const body = await c.req.parseBody();
143 const name = String(body.name || "").trim();
144
145 let scopes: string;
146 const rawScopes = body.scopes;
147 if (Array.isArray(rawScopes)) {
148 scopes = rawScopes.join(",");
149 } else {
150 scopes = String(rawScopes || "repo");
151 }
152
153 if (!name) {
154 return c.redirect("/settings/tokens?error=Name+is+required");
155 }
156
157 const token = generateToken();
158 const tokenH = await hashToken(token);
159
160 await db.insert(apiTokens).values({
161 userId: user.id,
162 name,
163 tokenHash: tokenH,
164 tokenPrefix: token.slice(0, 12),
165 scopes,
166 });
167
168 return c.redirect(
169 `/settings/tokens?new_token=${encodeURIComponent(token)}`
170 );
171});
172
173// Delete token
174tokens.post("/settings/tokens/:id/delete", async (c) => {
175 const user = c.get("user")!;
176 const tokenId = c.req.param("id");
177
178 const [token] = await db
179 .select()
180 .from(apiTokens)
181 .where(eq(apiTokens.id, tokenId))
182 .limit(1);
183
184 if (!token || token.userId !== user.id) {
185 return c.redirect("/settings/tokens");
186 }
187
188 await db.delete(apiTokens).where(eq(apiTokens.id, tokenId));
189 return c.redirect("/settings/tokens?success=Token+revoked");
190});
191
192// API endpoint
193tokens.get("/api/user/tokens", async (c) => {
194 const user = c.get("user")!;
195 const userTokens = await db
196 .select({
197 id: apiTokens.id,
198 name: apiTokens.name,
199 tokenPrefix: apiTokens.tokenPrefix,
200 scopes: apiTokens.scopes,
201 lastUsedAt: apiTokens.lastUsedAt,
202 createdAt: apiTokens.createdAt,
203 })
204 .from(apiTokens)
205 .where(eq(apiTokens.userId, user.id));
206 return c.json(userTokens);
207});
208
209export default tokens;