Skip to content

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

109 linesCodeBlameRaw
1import { Plus } from "lucide-react";
2import { Link, redirect } from "react-router";
3
4import type { Route } from "./+types/tokens";
5import { page } from "../../lib/meta";
6import { useAddresses } from "../../lib/addresses";
7import { ButtonLink, CopyLine, EmptyState } from "../../components/ui";
8import { Card } from "../../components/ui/card";
9import { TokenList } from "../../components/token-list";
10import { currentTokensPath } from "../../lib/access-tokens";
11import { identity } from "../../lib/services.server";
12import { getViewer, roleIn, unwrap } from "../../lib/session.server";
13
14// A workspace's own access tokens: they belong to the workspace, act as it,
15// and keep working when the member who made one leaves. The same tokens as
16// a person's (components/token-form.tsx), owned by the workspace.
17
18export function meta({ params, ...args }: Route.MetaArgs) {
19 return page(args, { title: `Access tokens · ${params.owner} · g1t` });
20}
21
22export async function loader({ request, params, context }: Route.LoaderArgs) {
23 const slug = params.owner.toLowerCase();
24 const moved = currentTokensPath(`/${slug}/-/tokens`, new URL(request.url).searchParams);
25 if (moved) throw redirect(moved, 301);
26 const viewer = getViewer(context);
27 return {
28 slug,
29 role: roleIn(viewer, params.owner),
30 tokens: unwrap(await identity.listWorkspaceTokens(params.owner, viewer)),
31 };
32}
33
34export default function WorkspaceTokens({ loaderData }: Route.ComponentProps) {
35 const { slug, role, tokens } = loaderData;
36 const { site, api } = useAddresses();
37 // With git, the token is the password in the clone address, after the scheme.
38 const [scheme, rest] = site.split("://");
39 const owner = role === "owner";
40 return (
41 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
42 <div className="min-w-0 space-y-5">
43 <div className="flex flex-wrap items-start justify-between gap-3">
44 <p className="max-w-lg text-sm text-muted">
45 Each token has permissions, a level for each resource, and reaches all of {slug}'s repositories or the ones chosen.
46 </p>
47 {owner && (
48 <ButtonLink to={`/${slug}/-/tokens/new`}>
49 <Plus size={15} />
50 New token
51 </ButtonLink>
52 )}
53 </div>
54 {tokens.length === 0 ? (
55 <EmptyState title="No access tokens yet">
56 {owner ? "Make one and give it to whatever needs to act for this workspace." : "An owner can make one."}
57 </EmptyState>
58 ) : (
59 <TokenList tokens={tokens} href={(token) => `/${slug}/-/tokens/${token.id}`} />
60 )}
61 {!owner && <p className="text-sm text-muted">Only owners can make, change or delete a workspace's tokens.</p>}
62 </div>
63
64 <aside className="space-y-5 text-sm">
65 <Card asChild className="p-5">
66 <section>
67 <h3 className="font-medium">What a token can do</h3>
68 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
69 <li>
70 What its permissions allow, in this workspace only, with Write on its repositories, as a member: push, open and merge pull requests, manage work
71 items. Admin only with Repositories: admin.
72 </li>
73 <li>
74 It acts as <span className="font-mono text-fg">{slug}</span>, so what it does is shown as the workspace's doing.
75 </li>
76 <li>It keeps working when the person who made it leaves.</li>
77 <li>It cannot manage people, tokens or other workspaces.</li>
78 </ul>
79 </section>
80 </Card>
81 {owner && (
82 <Card asChild tone="plain" className="p-5">
83 <section>
84 <h3 className="font-medium">Your members' own tokens</h3>
85 <p className="mt-2 text-muted">
86 Which personal tokens may reach {slug}, how long they may last, and approving the ones made for it:{" "}
87 <Link to={`/${slug}/-/personal-access-tokens`} className="text-fg underline underline-offset-4">
88 Personal access tokens
89 </Link>
90 .
91 </p>
92 </section>
93 </Card>
94 )}
95 <section>
96 <h3 className="font-medium">Using one</h3>
97 <p className="mt-2 text-muted">With git, as the password:</p>
98 <div className="mt-2">
99 <CopyLine prompt text={`git clone ${scheme}://${slug}:$G1T_TOKEN@${rest}/${slug}/<repo>.git`} />
100 </div>
101 <p className="mt-4 text-muted">With the API and the MCP server:</p>
102 <div className="mt-2">
103 <CopyLine prompt text={`curl -H "Authorization: Bearer $G1T_TOKEN" ${api}/user`} />
104 </div>
105 </section>
106 </aside>
107 </div>
108 );
109}