g1t/apps/web/app/routes/settings/keys.tsx

70 lines2,650 bytesCodeBlame
1import { identity } from "../../lib/services.server";
2import { Form } from "react-router";
3
4import type { Route } from "./+types/keys";
5import { page } from "../../lib/meta";
6import { ErrorText, Field, Input, SubmitButton } from "../../components/ui";
7import { DeleteButton } from "../../components/account-settings";
8import { assertSameOrigin, requireUser } from "../../lib/session.server";
9
10export function meta(args: Route.MetaArgs) {
11 return page(args, { title: "SSH keys · Settings · g1t" });
12}
13
14export async function loader({ request, context }: Route.LoaderArgs) {
15 const user = requireUser(context, request);
16 return { keys: await identity.listSshKeys(user) };
17}
18
19export async function action({ request, context }: Route.ActionArgs) {
20 assertSameOrigin(request);
21 const user = requireUser(context, request);
22 const form = await request.formData();
23 switch (form.get("intent")) {
24 case "add-key": {
25 const result = await identity.addSshKey(
26 user,
27 String(form.get("title") ?? ""),
28 String(form.get("key") ?? ""),
29 );
30 return result.ok ? null : { keyError: result.error.message };
31 }
32 case "delete-key":
33 await identity.removeSshKey(user, String(form.get("id") ?? ""));
34 return null;
35 }
36 return null;
37}
38
39export default function SshKeySettings({ loaderData, actionData }: Route.ComponentProps) {
40 const { keys } = loaderData;
41 return (
42 <section id="ssh-keys" className="scroll-mt-20">
43 <ul className="divide-y divide-line rounded-md border border-line empty:hidden">
44 {keys.map((key) => (
45 <li key={key.id} className="flex items-center gap-4 px-4 py-3">
46 <div className="min-w-0">
47 <p className="text-sm">{key.title}</p>
48 <p className="truncate font-mono text-xs text-muted">{key.fingerprint}</p>
49 </div>
50 <DeleteButton intent="delete-key" id={key.id} />
51 </li>
52 ))}
53 </ul>
54 {/* Keyed on the list, so a key once added leaves the fields empty for the next. */}
55 <Form key={keys.length} method="post" className={`space-y-3 ${keys.length > 0 ? "mt-4" : ""}`}>
56 <input type="hidden" name="intent" value="add-key" />
57 <Field label="Title (optional)">
58 <Input name="title" maxLength={100} />
59 </Field>
60 <Field label="Public key">
61 <Input name="key" placeholder="ssh-ed25519 AAAA…" required />
62 </Field>
63 <ErrorText>{actionData?.keyError}</ErrorText>
64 <SubmitButton pending="Adding…" match={{ intent: "add-key" }}>
65 Add SSH key
66 </SubmitButton>
67 </Form>
68 </section>
69 );
70}