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.

100 linesCodeBlameRaw
1import { Form, Link, redirect } from "react-router";
2
3import type { Route } from "./+types/login";
4import { page } from "../lib/meta";
5import { AuthCard } from "../components/auth-card";
6import { ContinueWithGithub, GithubMark, OrDivider } from "../components/github";
7import { PENDING_COOKIE, TWO_FACTOR_COOKIE, TWO_FACTOR_SECONDS, cookie, readCookie } from "../lib/github";
8import { githubSignIn, githubSignInEnabled } from "../lib/github.server";
9import { ErrorText, Field, Input, SubmitButton } from "../components/ui";
10import { identity } from "../lib/services.server";
11import { assertSameOrigin, getViewer, nextPath, clientOf, startSession } from "../lib/session.server";
12
13export function meta(args: Route.MetaArgs) {
14 return page(args, { title: "Sign in · g1t" });
15}
16
17export async function loader({ request, context }: Route.LoaderArgs) {
18 if (getViewer(context)) throw redirect(nextPath(request));
19 // A GitHub sign-in whose email belongs to an account: signing in to it
20 // links GitHub, and nothing is linked before then.
21 const pending = readCookie(request.headers.get("cookie"), PENDING_COOKIE);
22 const held = pending ? await githubSignIn.pending(pending).catch(() => null) : null;
23 return {
24 next: nextPath(request),
25 github: await githubSignInEnabled(),
26 linking: held?.ok ? held.value.login : null,
27 };
28}
29
30export async function action({ request }: Route.ActionArgs) {
31 assertSameOrigin(request);
32 const form = await request.formData();
33 const result = await identity.signIn(String(form.get("username") ?? "").trim(), String(form.get("password") ?? ""), clientOf(request));
34 if (!result.ok) return { error: result.error.message };
35 // Two-factor authentication on: no session yet, a code first.
36 if (result.value.twoFactorChallenge) {
37 const next = nextPath(request);
38 throw redirect(`/login/two-factor${next === "/" ? "" : `?next=${encodeURIComponent(next)}`}`, {
39 headers: { "set-cookie": cookie(TWO_FACTOR_COOKIE, result.value.twoFactorChallenge, TWO_FACTOR_SECONDS) },
40 });
41 }
42 const headers = new Headers({ "set-cookie": startSession(result.value.sessionToken) });
43 const pending = readCookie(request.headers.get("cookie"), PENDING_COOKIE);
44 if (pending) {
45 await githubSignIn.claim(pending, result.value.user).catch(() => null);
46 headers.append("set-cookie", cookie(PENDING_COOKIE, "", 0));
47 }
48 throw redirect(nextPath(request), { headers });
49}
50
51export default function Login({ loaderData, actionData }: Route.ComponentProps) {
52 const registerUrl = loaderData.next === "/" ? "/register" : `/register?next=${encodeURIComponent(loaderData.next)}`;
53 return (
54 <AuthCard
55 title="Sign in to g1t"
56 subtitle="Welcome back"
57 footer={
58 <>
59 New to g1t?{" "}
60 <Link to={registerUrl} className="text-fg underline underline-offset-4">
61 Create an account
62 </Link>
63 </>
64 }
65 >
66 {loaderData.linking ? (
67 <p className="mb-6 flex items-start gap-2 rounded-md border border-accent/40 bg-surface p-3 text-sm" role="status">
68 <GithubMark className="mt-0.5 size-4 shrink-0" />
69 <span>
70 An account with this email exists. Sign in to link GitHub (<span className="font-mono">@{loaderData.linking}</span>) to it.
71 </span>
72 </p>
73 ) : (
74 loaderData.github && (
75 <>
76 <ContinueWithGithub href={loaderData.next === "/" ? "/auth/github" : `/auth/github?next=${encodeURIComponent(loaderData.next)}`} />
77 <OrDivider />
78 </>
79 )
80 )}
81 <Form method="post" className="space-y-4">
82 <Field label="Username or email">
83 <Input name="username" autoComplete="username" required autoFocus />
84 </Field>
85 <Field label="Password">
86 <Input name="password" type="password" autoComplete="current-password" required />
87 </Field>
88 <ErrorText>{actionData?.error}</ErrorText>
89 <div className="pt-2 *:w-full">
90 <SubmitButton pending="Signing in…">Sign in</SubmitButton>
91 </div>
92 <p className="text-center text-sm">
93 <Link to="/forgot" className="text-muted hover:text-fg">
94 Forgot your password?
95 </Link>
96 </p>
97 </Form>
98 </AuthCard>
99 );
100}