Add self-service account deletion

Settings → Delete account removes the signed-in account and everything it
owns. DELETE /api/me re-checks the password (guarding an unattended session),
then wipes the user's events, config, sessions and user row in one transaction
and removes their photos/<user_id>/ directory. The client clears the account's
local cache and returns to the login screen.

Bumps the service-worker cache so clients pick up the new UI.

Verified: wrong password is rejected (401, data intact); correct password
returns 204, invalidates the session, drops all rows to zero and removes the
photo dir; the email can be re-registered afterwards. Confirmed end to end in
a headless-browser run of the Settings → delete flow.
This commit is contained in:
Alexander Heldt
2026-07-09 19:20:40 +00:00
parent acf2931fb4
commit 5c016ca49e
7 changed files with 152 additions and 2 deletions
+44
View File
@@ -1522,6 +1522,50 @@
settingsDialog.close();
});
// ---------- delete account ----------
const deleteAccountDialog = document.getElementById("delete-account-dialog");
const deleteAccountPassword = document.getElementById("delete-account-password");
const deleteAccountError = document.getElementById("delete-account-error");
const deleteAccountConfirm = document.getElementById("delete-account-confirm");
document.getElementById("delete-account-btn").addEventListener("click", () => {
settingsDialog.close();
deleteAccountPassword.value = "";
deleteAccountError.hidden = true;
deleteAccountDialog.showModal();
setTimeout(() => deleteAccountPassword.focus(), 50);
});
deleteAccountConfirm.addEventListener("click", async () => {
deleteAccountError.hidden = true;
deleteAccountConfirm.disabled = true;
try {
const res = await fetch("api/me", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: deleteAccountPassword.value }),
});
if (!res.ok) {
const msg = (await res.text()).trim();
throw new Error(res.status === 401 ? "Incorrect password" : (msg || `HTTP ${res.status}`));
}
// Account is gone server-side. Wipe this user's local cache before the
// reload drops us back on the login screen.
try {
localStorage.removeItem(eventsKey());
localStorage.removeItem(configKey());
} catch { /* ignore */ }
clearUser();
deleteAccountDialog.close();
location.reload();
} catch (err) {
deleteAccountError.textContent = err.message || "Something went wrong";
deleteAccountError.hidden = false;
} finally {
deleteAccountConfirm.disabled = false;
}
});
// ---------- wiring ----------
document.querySelectorAll("button.action").forEach(btn => {
btn.addEventListener("click", () => openNoteDialog(btn.dataset.type));
+20
View File
@@ -192,6 +192,26 @@
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="settings-save">Save</button>
</menu>
<hr class="settings-sep" />
<button type="button" id="delete-account-btn" class="danger danger-block">Delete account…</button>
</form>
</dialog>
<dialog id="delete-account-dialog">
<form method="dialog" id="delete-account-form">
<h3>Delete account</h3>
<p class="danger-text">
This permanently deletes your account and <strong>all its data</strong>
every event, photo and your puppy profile. This can't be undone.
</p>
<label>Confirm your password
<input type="password" id="delete-account-password" autocomplete="current-password" />
</label>
<p id="delete-account-error" class="auth-error" hidden></p>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button type="button" id="delete-account-confirm" class="danger">Delete forever</button>
</menu>
</form>
</dialog>
+15
View File
@@ -599,3 +599,18 @@ button.linklike {
cursor: pointer;
}
button.linklike:hover { text-decoration: underline; filter: none; }
/* ---------- delete account ---------- */
.settings-sep {
border: none;
border-top: 1px solid var(--border);
margin: 18px 0 12px;
}
.danger-block { width: 100%; }
.danger-text {
font-size: 0.9rem;
color: var(--muted);
margin: 0 0 14px;
line-height: 1.4;
}
.danger-text strong { color: var(--danger); }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = "puppy-tracker-v6";
const CACHE = "puppy-tracker-v7";
const PHOTO_CACHE = "puppy-tracker-photos-v1";
const ASSETS = [
"./",