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