Skip to content
233 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge main (membership, two-factor, GitHub repo roles) into tokens1import { KeyRound, ShieldAlert, ShieldCheck, Smartphone } from "lucide-react";
2import { useState } from "react";
3import { Form } from "react-router";
4
5import type { Route } from "./+types/two-factor";
6import { ConfirmItIsYou } from "../../components/emails-section";
7import { QrCode } from "../../components/qr-code";
8import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui";
9import { Badge } from "../../components/ui/badge";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.10import { Button } from "../../components/ui/button";
11import { Card } from "../../components/ui/card";
Merge main (membership, two-factor, GitHub repo roles) into tokens12import { githubSignIn } from "../../lib/github.server";
13import { page } from "../../lib/meta";
14import { accounts } from "../../lib/services.server";
15import { assertSameOrigin, requireUser } from "../../lib/session.server";
16import { TWO_FACTOR_INTENTS, twoFactorAction } from "../../lib/two-factor.server";
17
18export function meta(args: Route.MetaArgs) {
19 return page(args, { title: "Two-factor authentication · Settings · g1t" });
20}
21
22export async function loader({ request, context }: Route.LoaderArgs) {
23 const user = requireUser(context, request);
24 const [status, github] = await Promise.all([
25 accounts.twoFactorStatus(user).catch(() => null),
26 githubSignIn.account(user).catch(() => null),
27 ]);
28 return {
29 status: status?.ok ? status.value : null,
30 hasPassword: github?.hasPassword ?? true,
31 };
32}
33
34export async function action({ request, context }: Route.ActionArgs) {
35 assertSameOrigin(request);
36 const user = requireUser(context, request);
37 const form = await request.formData();
38 if ((TWO_FACTOR_INTENTS as readonly string[]).includes(String(form.get("intent")))) {
39 return (await twoFactorAction(user, form, request)) ?? null;
40 }
41 return null;
42}
43
44/** A base32 secret in groups of four, as people type it. */
45function grouped(secret: string): string {
46 return secret.match(/.{1,4}/g)?.join(" ") ?? secret;
47}
48
49/** The six digits an app shows, typed. */
50function CodeInput({ label = "Code from your app", recovery = false }: { label?: string; recovery?: boolean }) {
51 return (
52 <Field label={label} hint={recovery ? "Or a recovery code, such as k7m2q-9xw4d." : undefined}>
53 <Input
54 name="code"
55 required
56 autoComplete="one-time-code"
57 inputMode={recovery ? "text" : "numeric"}
58 pattern={recovery ? undefined : "[0-9 ]{6,7}"}
59 maxLength={recovery ? 20 : 7}
60 placeholder={recovery ? "123456" : "123 456"}
61 />
62 </Field>
63 );
64}
65
66/** Recovery codes, shown once, with a way to keep them. */
67function RecoveryCodes({ codes }: { codes: string[] }) {
68 const [copied, setCopied] = useState(false);
69 return (
70 <section className="rounded-xl border border-warn/40 bg-surface p-4" aria-labelledby="recovery-codes">
71 <h2 id="recovery-codes" className="flex items-center gap-2 text-sm font-medium">
72 <KeyRound size={16} className="text-warn" /> Save your recovery codes
73 </h2>
74 <p className="mt-1 text-sm text-muted">
75 Each one signs you in once if you lose your phone. They are shown only now: keep them in your password manager.
76 </p>
77 <ul className="mt-4 grid grid-cols-2 gap-2 font-mono text-sm sm:grid-cols-5">
78 {codes.map((code) => (
79 <li key={code} className="rounded-md border border-line bg-bg px-2 py-1.5 text-center">
80 {code}
81 </li>
82 ))}
83 </ul>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.84 <Button
85 type="button" variant="link" size="inline" className="mt-4 text-sm underline" onClick={() => {
Merge main (membership, two-factor, GitHub repo roles) into tokens86 void navigator.clipboard.writeText(codes.join("\n"));
87 setCopied(true);
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.88 }}>
Merge main (membership, two-factor, GitHub repo roles) into tokens89 {copied ? "Copied" : "Copy all"}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.90 </Button>
Merge main (membership, two-factor, GitHub repo roles) into tokens91 </section>
92 );
93}
94
95export default function TwoFactorSettings({ loaderData, actionData }: Route.ComponentProps) {
96 const { status, hasPassword } = loaderData;
97 const setup = actionData?.setup;
98 if (!status) {
99 return <p className="text-sm text-muted">Two-factor authentication could not be loaded just now. Try again in a moment.</p>;
100 }
101 return (
102 <div className="space-y-6">
103 {actionData?.reauth && <ConfirmItIsYou pending={actionData.reauth} hasPassword={hasPassword} />}
104 {actionData?.codes && <RecoveryCodes codes={actionData.codes} />}
105 {actionData?.notice && !actionData.codes && (
106 <p role="status" className="text-sm text-muted">
107 {actionData.notice}
108 </p>
109 )}
110
111 {status.required_by.length > 0 && !status.enabled && (
112 <div role="alert" className="flex gap-3 rounded-xl border border-warn/40 bg-surface p-4 text-sm">
113 <ShieldAlert size={16} className="mt-0.5 shrink-0 text-warn" />
114 <p>
115 <span className="font-medium">{status.required_by.join(", ")}</span>{" "}
116 {status.required_by.length === 1 ? "requires" : "require"} two-factor authentication. You cannot use{" "}
117 {status.required_by.length === 1 ? "it" : "them"} until you turn it on here.
118 </p>
119 </div>
120 )}
121
122 {status.enabled ? (
123 <>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.124 <Card asChild className="p-4">
125 <section>
126 <div className="flex flex-wrap items-center gap-3">
127 <ShieldCheck size={18} className="text-success" />
128 <h2 className="grow text-sm font-medium">Authenticator app</h2>
129 <Badge tone="success">On</Badge>
130 </div>
131 <p className="mt-2 text-sm text-muted">
132 Signing in with your password asks for a code from your app too. Git over HTTPS takes an access token,
133 never your password.
134 {status.enabled_at && (
135 <>
136 {" "}
137 Turned on <TimeAgo at={status.enabled_at} />.
138 </>
139 )}
140 </p>
141 <p className="mt-2 text-sm text-muted">
142 {status.recovery_codes_left} of 10 recovery codes left.
143 </p>
144 <Form method="post" className="mt-4">
145 <input type="hidden" name="intent" value="two-factor-codes" />
146 <SubmitButton variant="outline" match={{ intent: "two-factor-codes" }} pending="Making codes…">
147 Make new recovery codes
148 </SubmitButton>
149 </Form>
150 </section>
151 </Card>
152
153 <Card asChild tone="plain" className="p-4">
154 <section>
155 <h2 className="text-sm font-medium">Turn off two-factor authentication</h2>
156 <p className="mt-1 text-sm text-muted">
157 {status.required_by.length > 0
158 ? `You would lose access to ${status.required_by.join(", ")} until you turn it on again.`
159 : "Your account is safer with it on."}
160 </p>
161 <Form method="post" className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
162 <input type="hidden" name="intent" value="two-factor-disable" />
163 <div className="grow sm:max-w-56">
164 <CodeInput recovery />
165 </div>
166 <SubmitButton variant="destructive" match={{ intent: "two-factor-disable" }} pending="Turning off…">
167 Turn off
168 </SubmitButton>
169 </Form>
170 <ErrorText>{actionData?.error}</ErrorText>
171 </section>
172 </Card>
173 </>
174 ) : setup ? (
175 <Card asChild className="p-4">
176 <section>
177 <h2 className="flex items-center gap-2 text-sm font-medium">
178 <Smartphone size={16} className="text-accent" /> Set up your authenticator app
179 </h2>
180 <ol className="mt-4 space-y-5 text-sm">
181 <li>
182 <p className="text-muted">1. Scan this with an authenticator app, such as 1Password, Google Authenticator or Authy.</p>
183 <div className="mt-3 inline-block rounded-lg bg-white p-2">
184 <QrCode value={setup.uri} label="QR code for your authenticator app" />
185 </div>
186 <p className="mt-3 text-muted">Or enter this key by hand:</p>
187 <div className="mt-2 max-w-sm">
188 <CopyLine text={grouped(setup.secret)} />
189 </div>
190 </li>
191 <li>
192 <p className="text-muted">2. Enter the six-digit code your app shows.</p>
193 <Form method="post" className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-end">
194 <input type="hidden" name="intent" value="two-factor-enable" />
195 <input type="hidden" name="secret" value={setup.secret} />
196 <input type="hidden" name="uri" value={setup.uri} />
197 <div className="grow sm:max-w-56">
198 <CodeInput />
199 </div>
200 <SubmitButton match={{ intent: "two-factor-enable" }} pending="Checking…">
201 Turn on
202 </SubmitButton>
203 </Form>
204 <ErrorText>{actionData?.error}</ErrorText>
205 </li>
206 </ol>
207 </section>
208 </Card>
209 ) : (
210 <Card asChild className="p-4">
211 <section>
Merge main (membership, two-factor, GitHub repo roles) into tokens212 <div className="flex flex-wrap items-center gap-3">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.213 <Smartphone size={18} className="text-faint" />
Merge main (membership, two-factor, GitHub repo roles) into tokens214 <h2 className="grow text-sm font-medium">Authenticator app</h2>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.215 <Badge>Off</Badge>
Merge main (membership, two-factor, GitHub repo roles) into tokens216 </div>
217 <p className="mt-2 text-sm text-muted">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.218 A code from an app on your phone, asked for each time you sign in with your password, so a stolen password
219 is not enough.
Merge main (membership, two-factor, GitHub repo roles) into tokens220 </p>
221 <Form method="post" className="mt-4">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.222 <input type="hidden" name="intent" value="two-factor-start" />
223 <SubmitButton match={{ intent: "two-factor-start" }} pending="Starting…">
224 Set up
Merge main (membership, two-factor, GitHub repo roles) into tokens225 </SubmitButton>
226 </Form>
227 <ErrorText>{actionData?.error}</ErrorText>
228 </section>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.229 </Card>
Merge main (membership, two-factor, GitHub repo roles) into tokens230 )}
231 </div>
232 );
233}

This file's history is long; its oldest lines are credited to the oldest commit read.