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.

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