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.

108 linesCodeBlameRaw
1import { Form, Link, data, redirect } from "react-router";
2
3import { USERNAME_PATTERN } from "@g1t/contracts";
4
5import type { Route } from "./+types/register";
6import { page } from "../lib/meta";
7import { AuthCard } from "../components/auth-card";
8import { ContinueWithGithub, OrDivider } from "../components/github";
9import { Honeypot } from "../components/honeypot";
10import { githubSignInEnabled } from "../lib/github.server";
11import { ErrorText, Field, Input, SubmitButton } from "../components/ui";
12import { identity } from "../lib/services.server";
13import { cleanCode, cleanProof, invitePath, looksAutomated } from "../lib/invites";
14import { clientKey } from "../lib/registration.server";
15import { TURNSTILE_FAILED, TURNSTILE_SCRIPT } from "../lib/turnstile";
16import { turnstile, turnstileOk } from "../lib/turnstile.server";
17import { assertSameOrigin, getViewer, nextPath, startSession } from "../lib/session.server";
18
19export function meta(args: Route.MetaArgs) {
20 return page(args, { title: "Create an account · g1t" });
21}
22
23export async function loader({ request, context }: Route.LoaderArgs) {
24 if (getViewer(context)) throw redirect(nextPath(request));
25 const search = new URL(request.url).searchParams;
26 const code = cleanCode(search.get("invite"));
27 // A good invite is used on its own page, which knows whom it is from and
28 // where it leads; it signs up, joins and lands in one go. The invite
29 // email's proof goes with it, so the address it proves stays confirmed
30 // there. Anyone may sign up, so a code that is no good is just dropped.
31 if (code) {
32 const checked = await identity.checkInvite(code, clientKey(request));
33 if (checked.ok) throw redirect(invitePath(code, cleanProof(search.get("proof"))));
34 }
35 const next = nextPath(request);
36 return {
37 github: await githubSignInEnabled(),
38 githubUrl: next !== "/" ? `/auth/github?${new URLSearchParams({ next })}` : "/auth/github",
39 started: Date.now(),
40 // Turnstile's public site key when it is on (lib/turnstile.ts).
41 turnstile: turnstile()?.siteKey ?? null,
42 };
43}
44
45export async function action({ request }: Route.ActionArgs) {
46 assertSameOrigin(request);
47 const form = await request.formData();
48 if (looksAutomated(form)) return data({ error: "Something went wrong. Try again." }, { status: 422 });
49 if (!(await turnstileOk(form, clientKey(request)))) return data({ error: TURNSTILE_FAILED }, { status: 422 });
50 const result = await identity.register(
51 String(form.get("username") ?? ""),
52 String(form.get("email") ?? ""),
53 String(form.get("password") ?? ""),
54 null,
55 clientKey(request),
56 );
57 if (!result.ok) return data({ error: result.error.message }, { status: 422 });
58 throw redirect(nextPath(request), {
59 headers: { "set-cookie": startSession(result.value.sessionToken) },
60 });
61}
62
63export default function Register({ loaderData, actionData }: Route.ComponentProps) {
64 const error = actionData && "error" in actionData ? actionData.error : undefined;
65 return (
66 <AuthCard
67 title="Create your account"
68 subtitle="Where your team and its coding agents ship code"
69 footer={
70 <>
71 Already have an account?{" "}
72 <Link to="/login" className="text-fg underline underline-offset-4">
73 Sign in
74 </Link>
75 </>
76 }
77 >
78 {loaderData.github && (
79 <>
80 <ContinueWithGithub href={loaderData.githubUrl} />
81 <OrDivider />
82 </>
83 )}
84 <Form method="post" className="relative space-y-4">
85 <Honeypot started={loaderData.started} />
86 <Field label="Username" hint="Letters, digits and single hyphens. It is how you sign in, and how others see you, in the case you type it.">
87 <Input name="username" autoComplete="username" required autoFocus maxLength={39} pattern={USERNAME_PATTERN} />
88 </Field>
89 <Field label="Email" hint="We send a code to this address to confirm it.">
90 <Input name="email" type="email" autoComplete="email" required />
91 </Field>
92 <Field label="Password" hint="At least 10 characters.">
93 <Input name="password" type="password" autoComplete="new-password" required minLength={10} />
94 </Field>
95 {loaderData.turnstile && (
96 <>
97 <script src={TURNSTILE_SCRIPT} async defer />
98 <div className="cf-turnstile" data-sitekey={loaderData.turnstile} data-theme="auto" data-size="flexible" />
99 </>
100 )}
101 <ErrorText>{error}</ErrorText>
102 <div className="pt-2 *:w-full">
103 <SubmitButton pending="Creating account…">Create account</SubmitButton>
104 </div>
105 </Form>
106 </AuthCard>
107 );
108}