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.

118 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 {
16 assertSameOrigin,
17 getViewer,
18 nextPath,
19 startSession,
20} from "../lib/session.server";
21
22export function meta(args: Route.MetaArgs) {
23 return page(args, { title: "Create an account · g1t" });
24}
25
26export async function loader({ request, context }: Route.LoaderArgs) {
27 if (getViewer(context)) throw redirect(nextPath(request));
28 const search = new URL(request.url).searchParams;
29 const code = cleanCode(search.get("invite"));
30 // A good invite is used on its own page, which knows whom it is from and
31 // where it leads; it signs up, joins and lands in one go. The invite
32 // email's proof goes with it, so the address it proves stays confirmed
33 // there. Anyone may sign up, so a code that is no good is just dropped.
34 if (code) {
35 const checked = await identity.checkInvite(code, clientKey(request));
36 if (checked.ok) throw redirect(invitePath(code, cleanProof(search.get("proof"))));
37 }
38 const next = nextPath(request);
39 return {
40 github: await githubSignInEnabled(),
41 githubUrl: next !== "/" ? `/auth/github?${new URLSearchParams({ next })}` : "/auth/github",
42 started: Date.now(),
43 };
44}
45
46export async function action({ request }: Route.ActionArgs) {
47 assertSameOrigin(request);
48 const form = await request.formData();
49 if (looksAutomated(form)) return data({ error: "Something went wrong. Try again." }, { 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="Your team and its agents, working in one place"
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
87 label="Username"
88 hint="Letters, digits and single hyphens. It is how you sign in, and how others see you, in the case you type it."
89 >
90 <Input
91 name="username"
92 autoComplete="username"
93 required
94 autoFocus
95 maxLength={39}
96 pattern={USERNAME_PATTERN}
97 />
98 </Field>
99 <Field label="Email" hint="We send a code to this address to confirm it.">
100 <Input name="email" type="email" autoComplete="email" required />
101 </Field>
102 <Field label="Password" hint="At least 10 characters.">
103 <Input
104 name="password"
105 type="password"
106 autoComplete="new-password"
107 required
108 minLength={10}
109 />
110 </Field>
111 <ErrorText>{error}</ErrorText>
112 <div className="pt-2 *:w-full">
113 <SubmitButton pending="Creating account…">Create account</SubmitButton>
114 </div>
115 </Form>
116 </AuthCard>
117 );
118}