feat: account deletion flow (#117) #203

Merged
claude-bot merged 2 commits from feat/117-account-deletion into main 2026-07-18 09:00:57 +00:00
Contributor

Closes #117. Final issue of v3.8.0 Privacy & Data Lifecycle — with this, the milestone is complete.

Self-service and admin account deletion with an orphan-free cascade.

Backend (62f2af7)

  • Reserved "Deleted user" tombstone (fixed UUID, is_system flag); migration a3b4c5d6e7f8 seeds it + excludes is_system from user listings and the first-user-admin bootstrap count. Round-trips.
  • delete_user_account: sole-GM BLOCK first (nothing mutates on 409, body lists blocking campaigns); then a full 26-FK users.id sweep — delete personal data (memberships, platform links, feedback, availability/absences, attendance proposals, private notes), anonymize authored content to the tombstone (votes, attendance, session authorship, lore ownership/versions/drafts, loot/XP/gold ledger, milestones, highlights, shared notes — collision-guarded on unique constraints); audit_logs.actor_id left as-is. One user.deleted audit attributed to the actor. Single transaction.
  • Endpoints: DELETE /api/me (self, 409 sole-GM), DELETE /api/admin/users/{id} (admin; 403/404/400-system/409). OIDC deletion is local-only — re-login with the same sub yields a fresh empty user (tested).
  • Loot/gold FKs repoint to the tombstone rather than SET NULL (NULL = "party pool" there).

Frontend (<head>)

  • Profile "Danger zone": GM-campaign export links ("download your data first"), local-only/OIDC note, type-to-confirm delete → redirect to /auth/logout; 409 shows the sole-GM blocking list.
  • Admin Users tab: per-row type-to-confirm delete (hidden for own row).
  • ApiError now carries the parsed .body so the 409 {campaigns} list is available to the UI.

Tests

  • Backend: +14 (test_account_deletion.py) — sole-GM block+proceed, full-cascade no-orphans + remaining-member HTTP reads, tombstone→"Deleted user", OIDC re-login, actor-attributed audit, admin authz. Full suite 575 pass; migration round-trip verified.
  • Frontend: +11. 307 pass, eslint clean, vite build green.

Notes (fixes found during authoritative verification)

  • Migration bug fixed: the tombstone INSERT bound the UUID id as a string against a uuid column (DatatypeMismatchError) — added an explicit CAST.
  • ORM-cache fixes: the bulk cascade uses synchronize_session=False; expunged the deleted user + adjusted the shared-session HTTP-read test (production is unaffected — per-request sessions read fresh).
  • Route is DELETE /api/me (not /api/users/me) — matches the existing updateMe sibling.

🤖 Generated with Claude Code

Closes #117. **Final issue of v3.8.0 Privacy & Data Lifecycle** — with this, the milestone is complete. Self-service and admin account deletion with an orphan-free cascade. ## Backend (`62f2af7`) - Reserved **"Deleted user" tombstone** (fixed UUID, `is_system` flag); migration `a3b4c5d6e7f8` seeds it + excludes `is_system` from user listings and the first-user-admin bootstrap count. Round-trips. - `delete_user_account`: **sole-GM BLOCK first** (nothing mutates on 409, body lists blocking campaigns); then a full **26-FK `users.id` sweep** — delete personal data (memberships, platform links, feedback, availability/absences, attendance proposals, private notes), anonymize authored content to the tombstone (votes, attendance, session authorship, lore ownership/versions/drafts, loot/XP/gold ledger, milestones, highlights, shared notes — collision-guarded on unique constraints); `audit_logs.actor_id` left as-is. One `user.deleted` audit **attributed to the actor**. Single transaction. - Endpoints: `DELETE /api/me` (self, 409 sole-GM), `DELETE /api/admin/users/{id}` (admin; 403/404/400-system/409). OIDC deletion is **local-only** — re-login with the same sub yields a fresh empty user (tested). - Loot/gold FKs repoint to the tombstone rather than SET NULL (NULL = "party pool" there). ## Frontend (`<head>`) - Profile "Danger zone": GM-campaign export links ("download your data first"), local-only/OIDC note, **type-to-confirm** delete → redirect to `/auth/logout`; 409 shows the sole-GM blocking list. - Admin Users tab: per-row type-to-confirm delete (hidden for own row). - `ApiError` now carries the parsed `.body` so the 409 `{campaigns}` list is available to the UI. ## Tests - Backend: +14 (`test_account_deletion.py`) — sole-GM block+proceed, full-cascade no-orphans + remaining-member HTTP reads, tombstone→"Deleted user", OIDC re-login, actor-attributed audit, admin authz. Full suite **575 pass**; migration round-trip verified. - Frontend: +11. **307 pass**, eslint clean, vite build green. ## Notes (fixes found during authoritative verification) - **Migration bug fixed:** the tombstone INSERT bound the UUID id as a string against a `uuid` column (`DatatypeMismatchError`) — added an explicit `CAST`. - **ORM-cache fixes:** the bulk cascade uses `synchronize_session=False`; expunged the deleted user + adjusted the shared-session HTTP-read test (production is unaffected — per-request sessions read fresh). - Route is `DELETE /api/me` (not `/api/users/me`) — matches the existing `updateMe` sibling. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Self-service and admin account deletion with an orphan-free cascade.

- Reserved "Deleted user" tombstone (fixed UUID, is_system flag); migration
  a3b4c5d6e7f8 seeds it + excludes is_system from user listings + the
  first-user-admin bootstrap count. (round-trips)
- account_deletion_service.delete_user_account: sole-GM BLOCK first (nothing
  mutates on 409, body lists blocking campaigns); then a full users.id FK
  sweep (26 refs) — delete personal rows (memberships, platform links,
  feedback, availability/absences, attendance proposals, private notes),
  anonymize authored content to the tombstone (votes, attendance, session
  authorship, lore ownership/versions/drafts, loot/XP/gold ledger, milestones,
  highlights, shared notes — collision-guarded on unique constraints);
  audit_logs.actor_id left as-is. user.deleted audit attributed to the actor
  (self or admin). One transaction.
- Endpoints: DELETE /users/me (self, 409 sole-GM) and DELETE /admin/users/{id}
  (admin; 403/404/400-system/409). OIDC deletion is local-only — re-login with
  the same sub creates a fresh empty user (tested).

Loot/gold FKs repoint to the tombstone rather than SET NULL (NULL means
"party pool" there, so nulling would silently reassign a deleted member's
items).

Tests: +14 (test_account_deletion.py) — sole-GM block+proceed, full cascade
no-orphans + remaining-member HTTP reads, tombstone→"Deleted user", OIDC
re-login, actor-attributed audit, admin authz. Full suite 575 pass; migration
round-trip verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(frontend): account deletion — self-service + admin (#117)
All checks were successful
CI / Docker image build (pull_request) Successful in 21s
CI / Backend lint (ruff) (pull_request) Successful in 2m47s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m23s
CI / Bot tests and audit (pull_request) Successful in 5m20s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m8s
82481150f9
- api/client.js: ApiError now carries the parsed .body so callers can read
  the 409 sole-GM {campaigns} list.
- api/auth.js: deleteMe (DELETE /api/me) + adminDeleteUser (DELETE
  /api/admin/users/{id}).
- Profile: "Danger zone" — GM-campaign export links ("download your data
  first"), a local-only/OIDC-re-login note, and a "Delete account" button →
  type-to-confirm modal (type your display name) → deleteMe → full-page
  redirect to /auth/logout on success; 409 shows the sole-GM blocking list.
- Admin Users tab: per-row "Delete" (hidden for own row) → type-to-confirm →
  adminDeleteUser → removes the row; 409 shows the sole-GM message.

Tests: +11 (auth wrappers 2, Profile 5, Admin 4). 307 pass, eslint clean,
vite build succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/117-account-deletion 2026-07-18 09:00:58 +00:00
Sign in to join this conversation.
No description provided.