feat(profile): add Danger zone account deletion UI

This commit is contained in:
2026-07-26 10:20:56 +02:00
parent ab8cdea214
commit e2533bd1a8
3 changed files with 84 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { api } from "../api/client";
import { api, BASE_URL } from "../api/client";
import { ColorField } from "../components/ColorField";
import { GarminConnection } from "../components/GarminConnection";
import { NullableNumberField } from "../components/NullableNumberField";
@@ -47,6 +47,9 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
const profileRef = useRef<ProfileType | null>(null);
profileRef.current = profile;
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleteConfirmText, setDeleteConfirmText] = useState("");
const [deleting, setDeleting] = useState(false);
useEffect(() => {
api.getProfile().then(setProfile).catch((e) => setError(String(e)));
@@ -100,6 +103,26 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
}
}
async function deleteAccount() {
setDeleting(true);
setError(null);
try {
await api.deleteProfile();
// Deletion doesn't clear the session cookie -- follow with a real
// logout navigation (must be a POST, same as the header's Log out
// button in App.tsx) so Keycloak's own SSO session ends too, not just
// geniusrun's local one.
const form = document.createElement("form");
form.method = "post";
form.action = `${BASE_URL}/api/session/logout`;
document.body.appendChild(form);
form.submit();
} catch (e) {
setError(String(e));
setDeleting(false);
}
}
if (!profile) {
return (
<div className="page">
@@ -240,6 +263,51 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
</fieldset>
<TrainingTypesCard />
<fieldset className="kind-editor danger-zone">
<legend>Danger zone</legend>
{!deleteConfirmOpen ? (
<button type="button" className="button-danger" onClick={() => setDeleteConfirmOpen(true)}>
Delete profile
</button>
) : (
<div className="danger-zone-confirm">
<p>
This permanently deletes your account -- Garmin credentials, every custom rule, and every synced
activity -- and logs you out. This cannot be undone.
</p>
<label>
Type DELETE to confirm
<input
type="text"
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
disabled={deleting}
/>
</label>
<div className="controls">
<button
type="button"
disabled={deleting}
onClick={() => {
setDeleteConfirmOpen(false);
setDeleteConfirmText("");
}}
>
Cancel
</button>
<button
type="button"
className="button-danger"
disabled={deleting || deleteConfirmText !== "DELETE"}
onClick={deleteAccount}
>
Permanently delete
</button>
</div>
</div>
)}
</fieldset>
</div>
);
}