Skip to content
106 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 { TokenList } from "../../components/token-list";
9import { currentTokensPath } from "../../lib/access-tokens";
10import { identity } from "../../lib/services.server";
11import { getViewer, roleIn, unwrap } from "../../lib/session.server";
12
13// A workspace's own access tokens: they belong to the workspace, act as it,
14// and keep working when the member who made one leaves. The same tokens as
15// a person's (components/token-form.tsx), owned by the workspace.
16
17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Access tokens · ${params.owner} · g1t` });
19}
20
21export async function loader({ request, params, context }: Route.LoaderArgs) {
22 const slug = params.owner.toLowerCase();
23 const moved = currentTokensPath(`/${slug}/-/tokens`, new URL(request.url).searchParams);
24 if (moved) throw redirect(moved, 301);
25 const viewer = getViewer(context);
26 return {
27 slug,
28 role: roleIn(viewer, params.owner),
29 tokens: unwrap(await identity.listWorkspaceTokens(params.owner, viewer)),
30 };
31}
32
33export default function WorkspaceTokens({ loaderData }: Route.ComponentProps) {
34 const { slug, role, tokens } = loaderData;
35 const { site, api } = useAddresses();
36 // With git, the token is the password in the clone address, after the scheme.
37 const [scheme, rest] = site.split("://");
38 const owner = role === "owner";
39 return (
40 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
41 <div className="min-w-0 space-y-5">
42 <div className="flex flex-wrap items-start justify-between gap-3">
43 <p className="max-w-lg text-sm text-muted">
44 Each token has permissions, a level for each resource, and reaches all of {slug}'s repositories or the ones
45 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 <section className="rounded-xl border border-line bg-surface p-5">
66 <h3 className="font-medium">What a token can do</h3>
67 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
68 <li>
69 What its permissions allow, in this workspace only, with Write on its repositories, as a member: push, open
70 and merge pull requests, manage issues. Admin only with Repositories: admin.
71 </li>
72 <li>
73 It acts as <span className="font-mono text-fg">{slug}</span>, so what it does is shown as the workspace's
74 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 {owner && (
81 <section className="rounded-xl border border-line p-5">
82 <h3 className="font-medium">Your members' own tokens</h3>
83 <p className="mt-2 text-muted">
84 Which personal tokens may reach {slug}, how long they may last, and approving the ones made for it:{" "}
85 <Link to={`/${slug}/-/personal-access-tokens`} className="text-fg underline underline-offset-4">
86 Personal access tokens
87 </Link>
88 .
89 </p>
90 </section>
91 )}
92 <section>
93 <h3 className="font-medium">Using one</h3>
94 <p className="mt-2 text-muted">With git, as the password:</p>
95 <div className="mt-2">
96 <CopyLine prompt text={`git clone ${scheme}://${slug}:$G1T_TOKEN@${rest}/${slug}/<repo>.git`} />
97 </div>
98 <p className="mt-4 text-muted">With the API and the MCP server:</p>
99 <div className="mt-2">
100 <CopyLine prompt text={`curl -H "Authorization: Bearer $G1T_TOKEN" ${api}/user`} />
101 </div>
102 </section>
103 </aside>
104 </div>
105 );
106}