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

258 lines7,931 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 {
9 Button,
10 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 && (
158 <Form
159 method="post"
160 className="mt-8 space-y-5 rounded-md border border-line p-4 sm:p-5"
161 >
162 <input type="hidden" name="intent" value="add-token" />
163 <h2 className="font-medium">New token</h2>
164 <div className="grid gap-4 sm:grid-cols-[1fr_11rem]">
165 <Field label="Name" hint="Name it after what will use it.">
166 <Input
167 name="label"
168 maxLength={100}
169 placeholder="laptop"
170 required
171 />
172 </Field>
173 <ExpiryField />
174 </div>
175 <ScopeChecklist initial={presetScopes("agent")} />
176 {!actionData?.editing && <ErrorText>{actionData?.error}</ErrorText>}
177 <Button type="submit">Create token</Button>
178 </Form>
179 )}
180 </section>
181 );
182}
183
184function TokenRow({
185 token,
186 editing,
187 error,
188}: {
189 token: AccessToken;
190 editing: boolean;
191 error: string | null | undefined;
192}) {
193 const legacy = token.legacy && token.scopes === null;
194 const expiry = describeExpiry(token.expiresAt);
195 return (
196 <li className="px-4 py-3">
197 <div className="flex flex-wrap items-start gap-x-4 gap-y-2">
198 <div className="min-w-0 grow basis-60">
199 <p className="truncate text-sm font-medium">{token.name}</p>
200 <p className="text-xs text-faint">
201 Created <TimeAgo at={token.createdAt} /> ·{" "}
202 {token.lastUsedAt ? (
203 <>
204 last used <TimeAgo at={token.lastUsedAt} />
205 </>
206 ) : (
207 "never used"
208 )}{" "}
209 ·{" "}
210 <span className={expiry === "Expired" ? "text-danger" : undefined}>
211 {expiry}
212 </span>
213 </p>
214 <AccessSummary holder={token} />
215 {legacy && (
216 <p className="mt-1.5 text-xs text-warn">
217 Made before tokens had scopes, so it can do everything you can.
218 Narrow it to what it needs.
219 </p>
220 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas221 </div>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step222 <div className="ml-auto flex shrink-0 items-center gap-2">
223 {!editing && (
224 <ButtonLink
225 variant="quiet"
226 to={`?edit=${token.id}`}
227 preventScrollReset
228 >
229 {legacy ? "Narrow this token" : "Edit access"}
230 </ButtonLink>
231 )}
232 <DeleteButton intent="delete-token" id={token.id} />
233 </div>
234 </div>
235 {editing && (
236 <Form
237 method="post"
238 className="mt-4 space-y-5 border-t border-line pt-4"
239 >
240 <input type="hidden" name="intent" value="update-token" />
241 <input type="hidden" name="id" value={token.id} />
242 <p className="text-sm text-muted">
243 The token stays the same; only what it may do changes, from its next
244 request.
245 </p>
246 <ScopeChecklist initial={token.scopes} />
247 <ErrorText>{error}</ErrorText>
248 <div className="flex gap-2">
249 <Button type="submit">Save access</Button>
250 <ButtonLink variant="quiet" to="." preventScrollReset>
251 Cancel
252 </ButtonLink>
253 </div>
254 </Form>
255 )}
256 </li>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas257 );
258}