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

180 lines6,314 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.

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