g1t/apps/web/app/routes/settings/tokens.tsx

264 lines8,225 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1import { identity } from "../../lib/services.server";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step2import { Form, Link, redirect, useSearchParams } from "react-router";
3
4import { presetScopes, type AccessToken } from "@g1t/contracts";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas5
6import type { Route } from "./+types/tokens";
7import { page } from "../../lib/meta";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step8import {
Merge branch 'worktree-agent-a985c01f43538af01'9 SubmitButton,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step10 ButtonLink,
11 ErrorText,
12 Field,
13 Input,
14 TimeAgo,
15} from "../../components/ui";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas16import { DeleteButton } from "../../components/account-settings";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step17import {
18 AccessSummary,
19 ExpiryField,
20 ScopeChecklist,
21} from "../../components/token-scopes";
22import {
23 describeExpiry,
24 expiryTtl,
25 grantFromForm,
26} from "../../lib/token-scopes";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas27import { assertSameOrigin, requireUser } from "../../lib/session.server";
28
29export function meta(args: Route.MetaArgs) {
30 return page(args, { title: "Access tokens · Settings · g1t" });
31}
32
33export async function loader({ request, context }: Route.LoaderArgs) {
34 const user = requireUser(context, request);
35 return { user, tokens: await identity.listAccessTokens(user) };
36}
37
38export async function action({ request, context }: Route.ActionArgs) {
39 assertSameOrigin(request);
40 const user = requireUser(context, request);
41 const form = await request.formData();
42 switch (form.get("intent")) {
43 case "add-token": {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step44 const grant = grantFromForm(form);
45 if (!grant.ok)
46 return {
47 newToken: null,
48 created: null,
49 error: grant.error,
50 editing: null,
51 };
52 const created = await identity.createAccessToken(
53 user,
54 String(form.get("label") ?? ""),
55 expiryTtl(form.get("expires")),
56 { ...grant.value, listed: true },
57 );
58 return {
59 newToken: created.token,
60 created: created.info,
61 error: null,
62 editing: null,
63 };
64 }
65 case "update-token": {
66 const id = String(form.get("id") ?? "");
67 const grant = grantFromForm(form);
68 if (!grant.ok)
69 return {
70 newToken: null,
71 created: null,
72 error: grant.error,
73 editing: id,
74 };
75 const updated = await identity.updateAccessToken(user, id, grant.value);
76 if (!updated.ok)
77 return {
78 newToken: null,
79 created: null,
80 error: updated.error.message,
81 editing: id,
82 };
83 throw redirect("/settings/tokens");
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas84 }
85 case "delete-token":
86 await identity.removeAccessToken(user, String(form.get("id") ?? ""));
87 return null;
88 }
89 return null;
90}
91
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step92export default function TokenSettings({
93 loaderData,
94 actionData,
95}: Route.ComponentProps) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas96 const { user, tokens } = loaderData;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step97 const [params] = useSearchParams();
98 const editing = actionData?.editing ?? params.get("edit");
99 const workspaces = (user.workspaces ?? []).map(
100 (membership) => membership.slug,
101 );
102 const created = actionData?.created;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas103 return (
104 <section id="tokens" className="scroll-mt-20">
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step105 {workspaces.length > 0 && (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas106 <p className="text-sm text-muted">
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step107 For CI and integrations that work for a team, use a workspace's own
108 tokens instead:{" "}
109 {workspaces.map((slug, i) => (
110 <span key={slug}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas111 {i > 0 && ", "}
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step112 <Link
113 to={`/${slug}/-/tokens`}
114 className="font-mono text-fg underline underline-offset-4"
115 >
116 {slug}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas117 </Link>
118 </span>
119 ))}
120 .
121 </p>
122 )}
123 {actionData?.newToken && (
124 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step125 <p className="text-sm">
126 {created ? (
127 <span className="font-medium">{created.name}</span>
128 ) : (
129 "Your token"
130 )}{" "}
131 is ready. Copy it now. It will not be shown again.
132 </p>
133 <pre className="mt-2 font-mono text-sm break-all whitespace-pre-wrap text-accent">
134 {actionData.newToken}
135 </pre>
136 {created && (
137 <>
138 <AccessSummary holder={created} className="mt-3" />
139 <p className="mt-1.5 text-xs text-faint">
140 {describeExpiry(created.expiresAt)}
141 </p>
142 </>
143 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas144 </div>
145 )}
146 <ul className="mt-4 divide-y divide-line rounded-md border border-line empty:hidden">
147 {tokens.map((token) => (
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step148 <TokenRow
149 key={token.id}
150 token={token}
151 editing={editing === token.id}
152 error={actionData?.editing === token.id ? actionData.error : null}
153 />
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas154 ))}
155 </ul>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step156
157 {!editing && (
Merge branch 'worktree-agent-a985c01f43538af01'158 // Keyed on the token just made, so the fields start over for the next.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step159 <Form
Merge branch 'worktree-agent-a985c01f43538af01'160 key={created?.id ?? "new"}
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step161 method="post"
162 className="mt-8 space-y-5 rounded-md border border-line p-4 sm:p-5"
163 >
164 <input type="hidden" name="intent" value="add-token" />
165 <h2 className="font-medium">New token</h2>
166 <div className="grid gap-4 sm:grid-cols-[1fr_11rem]">
167 <Field label="Name" hint="Name it after what will use it.">
168 <Input
169 name="label"
170 maxLength={100}
171 placeholder="laptop"
172 required
173 />
174 </Field>
175 <ExpiryField />
176 </div>
177 <ScopeChecklist initial={presetScopes("agent")} />
178 {!actionData?.editing && <ErrorText>{actionData?.error}</ErrorText>}
Merge branch 'worktree-agent-a985c01f43538af01'179 <SubmitButton pending="Creating…" match={{ intent: "add-token" }}>
180 Create token
181 </SubmitButton>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step182 </Form>
183 )}
184 </section>
185 );
186}
187
188function TokenRow({
189 token,
190 editing,
191 error,
192}: {
193 token: AccessToken;
194 editing: boolean;
195 error: string | null | undefined;
196}) {
197 const legacy = token.legacy && token.scopes === null;
198 const expiry = describeExpiry(token.expiresAt);
199 return (
200 <li className="px-4 py-3">
201 <div className="flex flex-wrap items-start gap-x-4 gap-y-2">
202 <div className="min-w-0 grow basis-60">
203 <p className="truncate text-sm font-medium">{token.name}</p>
204 <p className="text-xs text-faint">
205 Created <TimeAgo at={token.createdAt} /> ·{" "}
206 {token.lastUsedAt ? (
207 <>
208 last used <TimeAgo at={token.lastUsedAt} />
209 </>
210 ) : (
211 "never used"
212 )}{" "}
213 ·{" "}
214 <span className={expiry === "Expired" ? "text-danger" : undefined}>
215 {expiry}
216 </span>
217 </p>
218 <AccessSummary holder={token} />
219 {legacy && (
220 <p className="mt-1.5 text-xs text-warn">
221 Made before tokens had scopes, so it can do everything you can.
222 Narrow it to what it needs.
223 </p>
224 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas225 </div>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step226 <div className="ml-auto flex shrink-0 items-center gap-2">
227 {!editing && (
228 <ButtonLink
229 variant="quiet"
230 to={`?edit=${token.id}`}
231 preventScrollReset
232 >
233 {legacy ? "Narrow this token" : "Edit access"}
234 </ButtonLink>
235 )}
236 <DeleteButton intent="delete-token" id={token.id} />
237 </div>
238 </div>
239 {editing && (
240 <Form
241 method="post"
242 className="mt-4 space-y-5 border-t border-line pt-4"
243 >
244 <input type="hidden" name="intent" value="update-token" />
245 <input type="hidden" name="id" value={token.id} />
246 <p className="text-sm text-muted">
247 The token stays the same; only what it may do changes, from its next
248 request.
249 </p>
250 <ScopeChecklist initial={token.scopes} />
251 <ErrorText>{error}</ErrorText>
252 <div className="flex gap-2">
Merge branch 'worktree-agent-a985c01f43538af01'253 <SubmitButton pending="Saving…" match={{ intent: "update-token", id: token.id }}>
254 Save access
255 </SubmitButton>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step256 <ButtonLink variant="quiet" to="." preventScrollReset>
257 Cancel
258 </ButtonLink>
259 </div>
260 </Form>
261 )}
262 </li>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas263 );
264}

This file's history is long; its oldest lines are credited to the oldest commit read.