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

90 lines3,457 bytesCodeBlame
1import { 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}