pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/routes/auth-github-username.tsx

94 lines3,738 bytesCodeBlame
1import { Form, Link, redirect, useNavigation } from "react-router";
2
3import type { Route } from "./+types/auth-github-username";
4import { AuthCard } from "../components/auth-card";
5import { GithubMark } from "../components/github";
6import { Button, ErrorText, Field, Input } from "../components/ui";
7import { PENDING_COOKIE, cookie, readCookie } from "../lib/github";
8import { githubSignIn } from "../lib/github.server";
9import { page } from "../lib/meta";
10import { safeNext } from "../lib/next";
11import { assertSameOrigin, startSession } from "../lib/session.server";
12
13export function meta(args: Route.MetaArgs) {
14 return page(args, { title: "Choose a username · g1t" });
15}
16
17/** A new account from GitHub, once its username (and invite) are settled. */
18export async function loader({ request }: Route.LoaderArgs) {
19 const pending = readCookie(request.headers.get("cookie"), PENDING_COOKIE);
20 if (!pending) throw redirect("/register");
21 const found = await githubSignIn.pending(pending);
22 if (!found.ok || found.value.kind !== "username") {
23 throw redirect("/register", { headers: { "set-cookie": cookie(PENDING_COOKIE, "", 0) } });
24 }
25 return found.value;
26}
27
28export async function action({ request }: Route.ActionArgs) {
29 assertSameOrigin(request);
30 const pending = readCookie(request.headers.get("cookie"), PENDING_COOKIE);
31 if (!pending) throw redirect("/register");
32 const form = await request.formData();
33 // Where to go afterwards, read before the pending sign-in is used up.
34 const found = await githubSignIn.pending(pending).catch(() => null);
35 const result = await githubSignIn.signUp(
36 pending,
37 String(form.get("username") ?? ""),
38 String(form.get("invite") ?? "").trim() || null,
39 );
40 if (!result.ok) return { error: result.error.message };
41 const headers = new Headers({ "set-cookie": startSession(result.value.sessionToken) });
42 headers.append("set-cookie", cookie(PENDING_COOKIE, "", 0));
43 throw redirect(safeNext(found?.ok ? found.value.next : "/"), { headers });
44}
45
46export default function GithubUsername({ loaderData, actionData }: Route.ComponentProps) {
47 const busy = useNavigation().state === "submitting";
48 return (
49 <AuthCard
50 title="Create your account"
51 subtitle={loaderData.inviteRequired ? "g1t is invite-only for now" : "Choose your username"}
52 footer={
53 <>
54 Have an account already?{" "}
55 <Link to="/login?github=link" className="text-fg underline underline-offset-4">
56 Sign in to link GitHub
57 </Link>
58 </>
59 }
60 >
61 <p className="mb-6 flex items-center gap-2 text-sm text-muted">
62 <GithubMark />
63 Signed in to GitHub as <span className="font-mono text-fg">@{loaderData.login}</span>
64 </p>
65 <Form method="post" className="space-y-4">
66 <Field
67 label="Username"
68 hint="Lowercase letters, digits and hyphens. It is how others see you on g1t."
69 >
70 <Input
71 name="username"
72 autoComplete="username"
73 required
74 autoFocus
75 maxLength={39}
76 defaultValue={loaderData.suggestion ?? ""}
77 pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*"
78 />
79 </Field>
80 {loaderData.inviteRequired && (
81 <Field label="Invite code" hint="g1t is invite-only. Ask someone on g1t for one of their invites.">
82 <Input name="invite" required autoComplete="off" spellCheck={false} />
83 </Field>
84 )}
85 <ErrorText>{actionData?.error}</ErrorText>
86 <div className="pt-2 *:w-full">
87 <Button type="submit" disabled={busy}>
88 Create account
89 </Button>
90 </div>
91 </Form>
92 </AuthCard>
93 );
94}