Skip to content
67 linesCodeBlameRaw

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.

One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1import { ArrowLeft } from "lucide-react";
2import { Form, Link } from "react-router";
3
4import type { AccessToken } from "@g1t/contracts";
5
6import type { Route } from "./+types/token-new";
7import { page } from "../../lib/meta";
8import { EmptyState, ErrorText, SubmitButton } from "../../components/ui";
9import { TokenForm } from "../../components/token-form";
10import { TokenCreated } from "../../components/token-list";
11import { tokenFromForm } from "../../lib/access-tokens";
12import { workspaceRepos } from "../../lib/access-tokens.server";
13import { identity } from "../../lib/services.server";
14import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
15
16// A new token for the workspace: the same form as a person's, owned by the
17// workspace. Owners only.
18
19export function meta({ params, ...args }: Route.MetaArgs) {
20 return page(args, { title: `New access token · ${params.owner} · g1t` });
21}
22
23export async function loader({ params, context }: Route.LoaderArgs) {
24 const viewer = getViewer(context);
25 const slug = params.owner.toLowerCase();
26 const owner = roleIn(viewer, slug) === "owner";
27 return { slug, owner, repos: owner ? await workspaceRepos(viewer, slug) : [] };
28}
29
30type ActionResult = { secret: string; token: AccessToken; error: null } | { secret: null; token: null; error: string };
31
32export async function action({ request, params, context }: Route.ActionArgs): Promise<ActionResult> {
33 assertSameOrigin(request);
34 const user = requireUser(context, request);
35 const slug = params.owner.toLowerCase();
36 const input = tokenFromForm(await request.formData(), { workspaceOwned: true, owner: slug });
37 if (!input.ok) return { secret: null, token: null, error: input.error };
38 const created = await identity.createToken(user, input.value);
39 if (!created.ok) return { secret: null, token: null, error: created.error.message };
40 return { secret: created.value.token, token: created.value.info, error: null };
41}
42
43export default function NewWorkspaceToken({ loaderData, actionData }: Route.ComponentProps) {
44 const { slug, owner, repos } = loaderData;
45 const back = `/${slug}/-/tokens`;
46 return (
47 <section className="max-w-2xl space-y-6">
48 <div>
49 <Link to={back} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
50 <ArrowLeft size={13} /> Access tokens
51 </Link>
52 <h2 className="mt-2 text-lg font-semibold tracking-tight">New token for {slug}</h2>
53 </div>
54 {!owner ? (
55 <EmptyState title="Owners only">Only an owner of {slug} can make its tokens.</EmptyState>
56 ) : actionData?.secret ? (
57 <TokenCreated secret={actionData.secret} token={actionData.token} back={back} />
58 ) : (
59 <Form method="post" className="space-y-6">
60 <TokenForm workspaceOwned slug={slug} repos={repos} preset="ci" />
61 <ErrorText>{actionData?.error}</ErrorText>
62 <SubmitButton pending="Generating…">Generate token</SubmitButton>
63 </Form>
64 )}
65 </section>
66 );
67}