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.tsxBlame226 lines · 2 contributors
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";
3e8f8e8Claude12import {
13 Container,
14 PageHeader,
15 Section,
16 Alert,
17 EmptyState,
18 ListItem,
19 Flex,
20 Form,
21 FormGroup,
22 Input,
23 Button,
24 InlineCode,
25 Text,
26} from "../views/ui";
c81ab7aClaude27
28const tokens = new Hono<AuthEnv>();
29
30tokens.use("/settings/tokens*", softAuth, requireAuth);
31tokens.use("/api/user/tokens*", softAuth, requireAuth);
32
33function generateToken(): string {
34 const bytes = crypto.getRandomValues(new Uint8Array(32));
35 return (
36 "glc_" +
37 Array.from(bytes)
38 .map((b) => b.toString(16).padStart(2, "0"))
39 .join("")
40 );
41}
42
43async function hashToken(token: string): Promise<string> {
44 const data = new TextEncoder().encode(token);
45 const hash = await crypto.subtle.digest("SHA-256", data);
46 return Array.from(new Uint8Array(hash))
47 .map((b) => b.toString(16).padStart(2, "0"))
48 .join("");
49}
50
51// Token settings page
52tokens.get("/settings/tokens", async (c) => {
53 const user = c.get("user")!;
54 const success = c.req.query("success");
55 const newToken = c.req.query("new_token");
56
57 const userTokens = await db
58 .select()
59 .from(apiTokens)
60 .where(eq(apiTokens.userId, user.id));
61
62 return c.html(
63 <Layout title="API Tokens" user={user}>
3e8f8e8Claude64 <Container class="settings-container">
65 <PageHeader title="Personal access tokens" />
c81ab7aClaude66 {success && (
3e8f8e8Claude67 <Alert variant="success">
c81ab7aClaude68 {decodeURIComponent(success)}
3e8f8e8Claude69 </Alert>
c81ab7aClaude70 )}
71 {newToken && (
3e8f8e8Claude72 <Alert variant="success">
73 <span style="font-family: var(--font-mono); word-break: break-all">
74 New token (copy now — it won't be shown again):{" "}
75 <strong>{decodeURIComponent(newToken)}</strong>
76 </span>
77 </Alert>
c81ab7aClaude78 )}
79 <div style="margin-top: 16px">
80 {userTokens.length === 0 ? (
3e8f8e8Claude81 <EmptyState>
82 <Text muted>No tokens yet.</Text>
83 </EmptyState>
c81ab7aClaude84 ) : (
85 userTokens.map((token) => (
3e8f8e8Claude86 <ListItem>
c81ab7aClaude87 <div>
88 <strong>{token.name}</strong>
89 <div class="ssh-key-meta">
3e8f8e8Claude90 <InlineCode>{token.tokenPrefix}...</InlineCode>
c81ab7aClaude91 <span style="margin-left: 8px">
92 Scopes: {token.scopes}
93 </span>
94 {token.lastUsedAt && (
95 <span>
96 {" "}
97 | Last used{" "}
98 {new Date(token.lastUsedAt).toLocaleDateString()}
99 </span>
100 )}
101 </div>
102 </div>
103 <form
001af43Claude104 method="post"
c81ab7aClaude105 action={`/settings/tokens/${token.id}/delete`}
106 >
3e8f8e8Claude107 <Button type="submit" variant="danger" size="sm">
c81ab7aClaude108 Revoke
3e8f8e8Claude109 </Button>
0316dbbClaude110 </form>
3e8f8e8Claude111 </ListItem>
c81ab7aClaude112 ))
113 )}
114 </div>
115
116 <h3 style="margin-top: 24px; margin-bottom: 12px">
117 Generate new token
118 </h3>
001af43Claude119 <form method="post" action="/settings/tokens">
c81ab7aClaude120 <div class="form-group">
121 <label for="name">Token name</label>
122 <input
123 type="text"
124 id="name"
125 name="name"
126 required
127 placeholder="e.g. CI/CD pipeline"
128 />
129 </div>
130 <div class="form-group">
131 <label>Scopes</label>
132 <div style="display: flex; gap: 16px; flex-wrap: wrap">
133 {["repo", "user", "admin"].map((scope) => (
134 <label style="display: flex; align-items: center; gap: 4px; font-size: 14px; cursor: pointer">
135 <input
136 type="checkbox"
137 name="scopes"
138 value={scope}
139 checked={scope === "repo"}
2c3ba6ecopilot-swe-agent[bot]140 aria-label={scope}
c81ab7aClaude141 />{" "}
142 {scope}
143 </label>
144 ))}
145 </div>
146 </div>
147 <button type="submit" class="btn btn-primary">
148 Generate token
149 </button>
150 </form>
0316dbbClaude151 </Container>
c81ab7aClaude152 </Layout>
153 );
154});
155
156// Create token
157tokens.post("/settings/tokens", async (c) => {
158 const user = c.get("user")!;
159 const body = await c.req.parseBody();
160 const name = String(body.name || "").trim();
161
162 let scopes: string;
163 const rawScopes = body.scopes;
164 if (Array.isArray(rawScopes)) {
165 scopes = rawScopes.join(",");
166 } else {
167 scopes = String(rawScopes || "repo");
168 }
169
170 if (!name) {
171 return c.redirect("/settings/tokens?error=Name+is+required");
172 }
173
174 const token = generateToken();
175 const tokenH = await hashToken(token);
176
177 await db.insert(apiTokens).values({
178 userId: user.id,
179 name,
180 tokenHash: tokenH,
181 tokenPrefix: token.slice(0, 12),
182 scopes,
183 });
184
185 return c.redirect(
186 `/settings/tokens?new_token=${encodeURIComponent(token)}`
187 );
188});
189
190// Delete token
191tokens.post("/settings/tokens/:id/delete", async (c) => {
192 const user = c.get("user")!;
193 const tokenId = c.req.param("id");
194
195 const [token] = await db
196 .select()
197 .from(apiTokens)
198 .where(eq(apiTokens.id, tokenId))
199 .limit(1);
200
201 if (!token || token.userId !== user.id) {
202 return c.redirect("/settings/tokens");
203 }
204
205 await db.delete(apiTokens).where(eq(apiTokens.id, tokenId));
206 return c.redirect("/settings/tokens?success=Token+revoked");
207});
208
209// API endpoint
210tokens.get("/api/user/tokens", async (c) => {
211 const user = c.get("user")!;
212 const userTokens = await db
213 .select({
214 id: apiTokens.id,
215 name: apiTokens.name,
216 tokenPrefix: apiTokens.tokenPrefix,
217 scopes: apiTokens.scopes,
218 lastUsedAt: apiTokens.lastUsedAt,
219 createdAt: apiTokens.createdAt,
220 })
221 .from(apiTokens)
222 .where(eq(apiTokens.userId, user.id));
223 return c.json(userTokens);
224});
225
226export default tokens;