Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

96 linesCodeBlameRaw
1import { Link, redirect } from "react-router";
2
3import type { Route } from "./+types/account";
4import { DeleteAccountAction } from "../../components/delete-account";
5import { DangerZone } from "../../components/danger-zone";
6import { DeveloperModeSwitch } from "../../components/developer-mode-switch";
7import { ThemeSwitch } from "../../components/theme-switch";
8import { githubSignIn } from "../../lib/github.server";
9import { page } from "../../lib/meta";
10import { accounts } from "../../lib/services.server";
11import { assertSameOrigin, clientOf, endSession, requireUser, sessionTokenOf } from "../../lib/session.server";
12
13export function meta(args: Route.MetaArgs) {
14 return page(args, { title: "Account · Settings · g1t" });
15}
16
17export async function loader({ request, context }: Route.LoaderArgs) {
18 const user = requireUser(context, request);
19 // What deleting it would take, and anything in the way, shown before
20 // anyone types: workspaces you own alone, each with what billing needs.
21 const [deletion, github] = await Promise.all([
22 accounts.checkAccountDeletion(user).catch(() => null),
23 githubSignIn.account(user).catch(() => null),
24 ]);
25 return {
26 username: user.username,
27 deletion: deletion?.ok ? deletion.value : null,
28 hasPassword: github?.hasPassword ?? true,
29 };
30}
31
32/**
33 * Deleting the account: identity checks the person, the typed username,
34 * the proof that it is them, protection and the workspaces they own alone.
35 * Once it is deleted, this browser is signed out and sent home.
36 */
37export async function action({ request, context }: Route.ActionArgs) {
38 assertSameOrigin(request);
39 const user = requireUser(context, request);
40 const form = await request.formData();
41 if (form.get("intent") !== "delete-account") return null;
42 const password = String(form.get("password") ?? "");
43 const result = await accounts.deleteAccount(user, String(form.get("confirm") ?? ""), {
44 sessionToken: sessionTokenOf(request),
45 password: password || null,
46 client: clientOf(request),
47 });
48 if (!result.ok) return { deleteError: result.error.message, reauth: result.error.code === "reauth_required" };
49 throw redirect("/", { headers: { "set-cookie": await endSession(request) } });
50}
51
52export default function AccountSettings({ loaderData, actionData }: Route.ComponentProps) {
53 const { username, deletion, hasPassword } = loaderData;
54 return (
55 <div className="space-y-8">
56 <section aria-labelledby="username-heading">
57 <h2 id="username-heading" className="font-medium">
58 Username
59 </h2>
60 <p className="mt-1 text-sm text-muted">
61 You are <span className="font-mono text-fg">{username}</span>. Your profile is at{" "}
62 <Link to={`/${username}`} className="text-accent underline-offset-4 hover:underline">
63 g1t.sh/{username}
64 </Link>
65 .
66 </p>
67 </section>
68 <section aria-labelledby="appearance-heading">
69 <h2 id="appearance-heading" className="font-medium">
70 Appearance
71 </h2>
72 <p className="mt-1 text-sm text-muted">
73 Light, dark, or Auto to follow your system. This browser remembers it.
74 </p>
75 <ThemeSwitch className="mt-3 w-full max-w-xs" />
76 </section>
77 <section id="developer" aria-labelledby="developer-heading" className="scroll-mt-20">
78 <h2 id="developer-heading" className="font-medium">
79 Developer
80 </h2>
81 <p className="mt-1 text-sm text-muted">For reporting what went wrong when something breaks.</p>
82 <div className="mt-3">
83 <DeveloperModeSwitch />
84 </div>
85 </section>
86 <DangerZone>
87 <DeleteAccountAction
88 username={username}
89 deletion={deletion}
90 hasPassword={hasPassword}
91 error={actionData?.deleteError}
92 />
93 </DangerZone>
94 </div>
95 );
96}