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

114 lines4,113 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, cookie, readCookie } from "../lib/github";
9import { githubSignIn, githubSignInEnabled } from "../lib/github.server";
10import { Button, ErrorText, Field, Input } 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 const headers = new Headers({ "set-cookie": startSession(result.value.sessionToken) });
47 const pending = readCookie(request.headers.get("cookie"), PENDING_COOKIE);
48 if (pending) {
49 await githubSignIn.claim(pending, result.value.user).catch(() => null);
50 headers.append("set-cookie", cookie(PENDING_COOKIE, "", 0));
51 }
52 throw redirect(nextPath(request), { headers });
53}
54
55export default function Login({ loaderData, actionData }: Route.ComponentProps) {
56 const inviteOnly = useInviteOnly();
57 const registerUrl =
58 loaderData.next === "/"
59 ? "/register"
60 : `/register?next=${encodeURIComponent(loaderData.next)}`;
61 return (
62 <AuthCard
63 title="Welcome back"
64 subtitle="Sign in to g1t"
65 footer={
66 <>
67 New to g1t?{" "}
68 <Link to={registerUrl} className="text-fg underline underline-offset-4">
69 {inviteOnly ? "Request access or use an invite" : "Create an account"}
70 </Link>
71 </>
72 }
73 >
74 {loaderData.linking ? (
75 <p className="mb-6 flex items-start gap-2 rounded-md border border-accent/40 bg-surface p-3 text-sm" role="status">
76 <GithubMark className="mt-0.5 size-4 shrink-0" />
77 <span>
78 An account with this email exists. Sign in to link GitHub (
79 <span className="font-mono">@{loaderData.linking}</span>) to it.
80 </span>
81 </p>
82 ) : (
83 loaderData.github && (
84 <>
85 <ContinueWithGithub href={loaderData.next === "/" ? "/auth/github" : `/auth/github?next=${encodeURIComponent(loaderData.next)}`} />
86 <OrDivider />
87 </>
88 )
89 )}
90 <Form method="post" className="space-y-4">
91 <Field label="Username or email">
92 <Input name="username" autoComplete="username" required autoFocus />
93 </Field>
94 <Field label="Password">
95 <Input
96 name="password"
97 type="password"
98 autoComplete="current-password"
99 required
100 />
101 </Field>
102 <ErrorText>{actionData?.error}</ErrorText>
103 <div className="pt-2 *:w-full">
104 <Button type="submit">Sign in</Button>
105 </div>
106 <p className="text-center text-sm">
107 <Link to="/forgot" className="text-muted hover:text-fg">
108 Forgot your password?
109 </Link>
110 </p>
111 </Form>
112 </AuthCard>
113 );
114}