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

90 lines3,457 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";
2import { Form, Link } from "react-router";
3
4import type { Route } from "./+types/tokens";
5import { page } from "../../lib/meta";
6import { Button, Field, Input, TimeAgo } from "../../components/ui";
7import { DeleteButton } from "../../components/account-settings";
8import { assertSameOrigin, requireUser } from "../../lib/session.server";
9
10export function meta(args: Route.MetaArgs) {
11 return page(args, { title: "Access tokens · Settings · g1t" });
12}
13
14export async function loader({ request, context }: Route.LoaderArgs) {
15 const user = requireUser(context, request);
16 return { user, tokens: await identity.listAccessTokens(user) };
17}
18
19export async function action({ request, context }: Route.ActionArgs) {
20 assertSameOrigin(request);
21 const user = requireUser(context, request);
22 const form = await request.formData();
23 switch (form.get("intent")) {
24 case "add-token": {
25 const created = await identity.createAccessToken(user, String(form.get("label") ?? ""));
26 return { newToken: created.token };
27 }
28 case "delete-token":
29 await identity.removeAccessToken(user, String(form.get("id") ?? ""));
30 return null;
31 }
32 return null;
33}
34
35export default function TokenSettings({ loaderData, actionData }: Route.ComponentProps) {
36 const { user, tokens } = loaderData;
37 return (
38 <section id="tokens" className="scroll-mt-20">
39 {(user.workspaces ?? []).length > 0 && (
40 <p className="text-sm text-muted">
41 For CI and integrations that work for a team, use a workspace's own tokens instead:{" "}
42 {(user.workspaces ?? []).map((membership, i) => (
43 <span key={membership.slug}>
44 {i > 0 && ", "}
45 <Link to={`/${membership.slug}/-/tokens`} className="font-mono text-fg underline underline-offset-4">
46 {membership.slug}
47 </Link>
48 </span>
49 ))}
50 .
51 </p>
52 )}
53 {actionData?.newToken && (
54 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
55 <p className="text-sm">Copy it now. It will not be shown again.</p>
56 <pre className="mt-2 overflow-x-auto font-mono text-sm text-accent">{actionData.newToken}</pre>
57 </div>
58 )}
59 <ul className="mt-4 divide-y divide-line rounded-md border border-line empty:hidden">
60 {tokens.map((token) => (
61 <li key={token.id} className="flex items-center gap-4 px-4 py-3">
62 <div className="min-w-0">
63 <p className="truncate text-sm">{token.name}</p>
64 <p className="text-xs text-faint">
65 Created <TimeAgo at={token.createdAt} /> ·{" "}
66 {token.lastUsedAt ? (
67 <>
68 last used <TimeAgo at={token.lastUsedAt} />
69 </>
70 ) : (
71 "never used"
72 )}
73 </p>
74 </div>
75 <DeleteButton intent="delete-token" id={token.id} />
76 </li>
77 ))}
78 </ul>
79 <Form method="post" className="mt-4 flex items-end gap-3">
80 <input type="hidden" name="intent" value="add-token" />
81 <div className="grow">
82 <Field label="Name">
83 <Input name="label" maxLength={100} placeholder="laptop" />
84 </Field>
85 </div>
86 <Button type="submit">Create token</Button>
87 </Form>
88 </section>
89 );
90}