Skip to content
111 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
46 chosen.
47 </p>
48 {owner && (
49 <ButtonLink to={`/${slug}/-/tokens/new`}>
50 <Plus size={15} />
51 New token
52 </ButtonLink>
53 )}
54 </div>
55 {tokens.length === 0 ? (
56 <EmptyState title="No access tokens yet">
57 {owner ? "Make one and give it to whatever needs to act for this workspace." : "An owner can make one."}
58 </EmptyState>
59 ) : (
60 <TokenList tokens={tokens} href={(token) => `/${slug}/-/tokens/${token.id}`} />
61 )}
62 {!owner && <p className="text-sm text-muted">Only owners can make, change or delete a workspace's tokens.</p>}
63 </div>
64
65 <aside className="space-y-5 text-sm">
66 <Card asChild className="p-5">
67 <section>
68 <h3 className="font-medium">What a token can do</h3>
69 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
70 <li>
71 What its permissions allow, in this workspace only, with Write on its repositories, as a member: push, open
72 and merge pull requests, manage issues. Admin only with Repositories: admin.
73 </li>
74 <li>
75 It acts as <span className="font-mono text-fg">{slug}</span>, so what it does is shown as the workspace's
76 doing.
77 </li>
78 <li>It keeps working when the person who made it leaves.</li>
79 <li>It cannot manage people, tokens or other workspaces.</li>
80 </ul>
81 </section>
82 </Card>
83 {owner && (
84 <Card asChild tone="plain" className="p-5">
85 <section>
86 <h3 className="font-medium">Your members' own tokens</h3>
87 <p className="mt-2 text-muted">
88 Which personal tokens may reach {slug}, how long they may last, and approving the ones made for it:{" "}
89 <Link to={`/${slug}/-/personal-access-tokens`} className="text-fg underline underline-offset-4">
90 Personal access tokens
91 </Link>
92 .
93 </p>
94 </section>
95 </Card>
96 )}
97 <section>
98 <h3 className="font-medium">Using one</h3>
99 <p className="mt-2 text-muted">With git, as the password:</p>
100 <div className="mt-2">
101 <CopyLine prompt text={`git clone ${scheme}://${slug}:$G1T_TOKEN@${rest}/${slug}/<repo>.git`} />
102 </div>
103 <p className="mt-4 text-muted">With the API and the MCP server:</p>
104 <div className="mt-2">
105 <CopyLine prompt text={`curl -H "Authorization: Bearer $G1T_TOKEN" ${api}/user`} />
106 </div>
107 </section>
108 </aside>
109 </div>
110 );
111}