feat(profile): add Danger zone account deletion UI
This commit is contained in:
@@ -231,6 +231,17 @@ button:disabled {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.danger-zone-confirm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.danger-zone-confirm p {
|
||||
margin: 0;
|
||||
color: #9aa0ab;
|
||||
}
|
||||
|
||||
.filter-pills {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -106,6 +106,10 @@ export const api = {
|
||||
getProfile: () => request<Profile>("/api/profile"),
|
||||
updateProfile: (profile: Profile) =>
|
||||
request<Profile>("/api/profile", { method: "PUT", body: JSON.stringify(profile) }),
|
||||
// Permanently deletes the signed-in user's entire account. The caller is
|
||||
// responsible for following a successful call with a real logout
|
||||
// navigation -- this does not touch the session cookie (see Profile.tsx).
|
||||
deleteProfile: () => request<void>("/api/profile", { method: "DELETE" }),
|
||||
|
||||
// Review queue -- cursor-paginated: pass the previous page's next_cursor
|
||||
// as `before` to fetch the next one. Fetching every activity's laps and
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user