Skip to content
91 linesCodeBlameRaw
1import { identity } from "../../lib/services.server";
2import { Form, Link } from "react-router";
3
4import type { Route } from "./+types/keys";
5import { page } from "../../lib/meta";
6import { ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui";
7import { Card } from "../../components/ui/card";
8import { DeleteButton } from "../../components/account-settings";
9import { assertSameOrigin, requireUser } from "../../lib/session.server";
10
11export function meta(args: Route.MetaArgs) {
12 return page(args, { title: "SSH keys · Settings · g1t" });
13}
14
15export async function loader({ request, context }: Route.LoaderArgs) {
16 const user = requireUser(context, request);
17 return { keys: await identity.listSshKeys(user) };
18}
19
20export async function action({ request, context }: Route.ActionArgs) {
21 assertSameOrigin(request);
22 const user = requireUser(context, request);
23 const form = await request.formData();
24 switch (form.get("intent")) {
25 case "add-key": {
26 const result = await identity.addSshKey(
27 user,
28 String(form.get("title") ?? ""),
29 String(form.get("key") ?? ""),
30 );
31 return result.ok ? null : { keyError: result.error.message };
32 }
33 case "delete-key":
34 await identity.removeSshKey(user, String(form.get("id") ?? ""));
35 return null;
36 }
37 return null;
38}
39
40export default function SshKeySettings({ loaderData, actionData }: Route.ComponentProps) {
41 const { keys } = loaderData;
42 return (
43 <section id="ssh-keys" className="scroll-mt-20">
44 <Card asChild tone="plain" radius="lg" className="mb-4 border-dashed p-3 text-xs text-muted">
45 <p>
46 Git over SSH is not on yet: it is waiting on inbound TCP on Cloudflare, which g1t has applied for. Keys you
47 add now will work as soon as it is. Until then, clone and push over HTTPS with an{" "}
48 <Link to="/settings/tokens" className="text-fg underline underline-offset-4">
49 access token
50 </Link>
51 .
52 </p>
53 </Card>
54 <ul className="divide-y divide-line rounded-md border border-line empty:hidden">
55 {keys.map((key) => (
56 <li key={key.id} className="flex items-center gap-4 px-4 py-3">
57 <div className="min-w-0">
58 <p className="text-sm">{key.title}</p>
59 <p className="truncate font-mono text-xs text-muted">{key.fingerprint}</p>
60 <p className="mt-0.5 text-xs text-faint">
61 Added <TimeAgo at={key.createdAt} /> ·{" "}
62 {key.lastUsedAt ? (
63 <>
64 Last used <TimeAgo at={key.lastUsedAt} />
65 </>
66 ) : (
67 "Never used"
68 )}
69 </p>
70 </div>
71 <DeleteButton intent="delete-key" id={key.id} />
72 </li>
73 ))}
74 </ul>
75 {/* Keyed on the list, so a key once added leaves the fields empty for the next. */}
76 <Form key={keys.length} method="post" className={`space-y-3 ${keys.length > 0 ? "mt-4" : ""}`}>
77 <input type="hidden" name="intent" value="add-key" />
78 <Field label="Title (optional)">
79 <Input name="title" maxLength={100} />
80 </Field>
81 <Field label="Public key">
82 <Input name="key" placeholder="ssh-ed25519 AAAA…" required />
83 </Field>
84 <ErrorText>{actionData?.keyError}</ErrorText>
85 <SubmitButton pending="Adding…" match={{ intent: "add-key" }}>
86 Add SSH key
87 </SubmitButton>
88 </Form>
89 </section>
90 );
91}