flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

179 lines6,256 bytesCodeBlame
1import { Form } from "react-router";
2
3import type { Route } from "./+types/tokens";
4import {
5 Button,
6 CopyLine,
7 EmptyState,
8 ErrorText,
9 Field,
10 Input,
11 TimeAgo,
12} from "../../components/ui";
13import { identity } from "../../lib/services.server";
14import {
15 assertSameOrigin,
16 getViewer,
17 requireUser,
18 roleIn,
19 unwrap,
20} from "../../lib/session.server";
21
22export function meta({ params }: Route.MetaArgs) {
23 return [{ title: `Access tokens · ${params.owner} · g1t` }];
24}
25
26export async function loader({ params, context }: Route.LoaderArgs) {
27 const viewer = getViewer(context);
28 return {
29 slug: params.owner.toLowerCase(),
30 role: roleIn(viewer, params.owner),
31 tokens: unwrap(await identity.listWorkspaceTokens(params.owner, viewer)),
32 };
33}
34
35export async function action({ request, params, context }: Route.ActionArgs) {
36 assertSameOrigin(request);
37 const user = requireUser(context, request);
38 const form = await request.formData();
39 if (form.get("action") === "delete") {
40 const removed = await identity.removeWorkspaceToken(
41 user,
42 params.owner,
43 String(form.get("id") ?? ""),
44 );
45 return { token: null, error: removed.ok ? null : removed.error.message };
46 }
47 const created = await identity.createWorkspaceToken(
48 user,
49 params.owner,
50 String(form.get("label") ?? ""),
51 );
52 return created.ok
53 ? { token: created.value, error: null }
54 : { token: null, error: created.error.message };
55}
56
57export default function WorkspaceTokens({ loaderData, actionData }: Route.ComponentProps) {
58 const { slug, role, tokens } = loaderData;
59 const created = actionData?.token;
60 return (
61 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
62 <div className="min-w-0">
63
64 {created && (
65 <div className="mt-5 rounded-xl border border-accent/40 bg-surface p-4">
66 <p className="text-sm">
67 <span className="font-medium">{created.info.name}</span> is ready. Copy
68 it now; it will not be shown again.
69 </p>
70 <div className="mt-3">
71 <CopyLine text={created.token} />
72 </div>
73 </div>
74 )}
75
76 <div className="mt-5">
77 {tokens.length === 0 ? (
78 <EmptyState title="No access tokens yet">
79 {role === "owner"
80 ? "Create one below and give it to whatever needs to act for this workspace."
81 : "An owner can create one."}
82 </EmptyState>
83 ) : (
84 <ul className="divide-y divide-line rounded-xl border border-line">
85 {tokens.map((token) => (
86 <li key={token.id} className="flex items-center gap-4 px-4 py-3">
87 <div className="min-w-0 grow">
88 <p className="truncate text-sm font-medium">{token.name}</p>
89 <p className="mt-0.5 text-xs text-faint">
90 Created <TimeAgo at={token.createdAt} />
91 {token.createdBy ? (
92 <>
93 {" "}
94 by <span className="font-mono">{token.createdBy}</span>
95 </>
96 ) : (
97 " by someone who has since left g1t"
98 )}{" "}
99 ·{" "}
100 {token.lastUsedAt ? (
101 <>
102 last used <TimeAgo at={token.lastUsedAt} />
103 </>
104 ) : (
105 "never used"
106 )}
107 </p>
108 </div>
109 {role === "owner" && (
110 <Form method="post">
111 <input type="hidden" name="action" value="delete" />
112 <input type="hidden" name="id" value={token.id} />
113 <Button variant="quiet" type="submit">
114 Delete
115 </Button>
116 </Form>
117 )}
118 </li>
119 ))}
120 </ul>
121 )}
122 </div>
123
124 {role === "owner" ? (
125 <Form method="post" className="mt-6 flex items-end gap-3">
126 <div className="grow">
127 <Field label="New token" hint="Name it after what will use it.">
128 <Input name="label" required maxLength={100} placeholder="deploy pipeline" />
129 </Field>
130 </div>
131 <div className="pb-6">
132 <Button type="submit">Create token</Button>
133 </div>
134 </Form>
135 ) : (
136 <p className="mt-4 text-sm text-muted">
137 Only owners can create or delete a workspace's tokens.
138 </p>
139 )}
140 <ErrorText>{actionData?.error}</ErrorText>
141 </div>
142
143 <aside className="space-y-5 text-sm">
144 <section className="rounded-xl border border-line bg-surface p-5">
145 <h3 className="font-medium">What a token can do</h3>
146 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
147 <li>
148 Everything a member can, in this workspace only: push, open and
149 merge pull requests, manage issues.
150 </li>
151 <li>
152 It acts as <span className="font-mono text-fg">{slug}</span>, so
153 what it does is shown as the workspace's doing.
154 </li>
155 <li>It keeps working when the person who made it leaves.</li>
156 <li>It cannot manage people, tokens or other workspaces.</li>
157 </ul>
158 </section>
159 <section>
160 <h3 className="font-medium">Using one</h3>
161 <p className="mt-2 text-muted">With git, as the password:</p>
162 <div className="mt-2">
163 <CopyLine
164 prompt
165 text={`git clone https://${slug}:$G1T_TOKEN@g1t.sh/${slug}/<repo>.git`}
166 />
167 </div>
168 <p className="mt-4 text-muted">With the API and the MCP server:</p>
169 <div className="mt-2">
170 <CopyLine
171 prompt
172 text={'curl -H "Authorization: Bearer $G1T_TOKEN" https://api.g1t.sh/user'}
173 />
174 </div>
175 </section>
176 </aside>
177 </div>
178 );
179}