Skip to content

g1t/apps/web/app/routes/login.tsx

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