[Privacy] Account deletion flow #117
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context / Motivation
There is no way to delete a user account — self-service or admin. Users are keyed on
(oidc_issuer, oidc_sub)(webapp/backend/app/models/user.py:26-29, unique constraintuq_users_oidc_sub_issuer). For a privacy-respecting self-hosted app this is table stakes.Spec
Entry points
DELETE /api/users/me.webapp/backend/app/routers/users.py); same cascade, audit-logged with the admin as actor viaaudit_service.log_event(services/audit_service.py:12).Cascade — decided per table (from the
Userrelationships atmodels/user.py:57-81plus non-relationship FKs; implementer must sweep for any FK added since):campaign_memberships(CampaignMember)votes(Vote)attendance_records(SessionAttendance)session_notes(SessionNote)platform_links(PlatformLink)transcript_feedbackSession.created_by,Session.audio_trashed_by_idmodels/user.py:61-63, 79-81)lore_entry_owners/unlinked_lore_entry_owner)audit_logs.actor_idmodels/audit_log.py:27)Tombstone: a well-known "Deleted user" rendering — either a reserved user row or
NULLFK + display fallback. Pick one, apply consistently, and make the frontend render "Deleted user" wherever a display name would appear.OIDC: deletion is local only — the identity at the provider is untouched (document this in the modal). After deletion, logging in again with the same OIDC sub creates a new, empty user row (the old row is gone;
get_or_createby(sub, issuer)naturally recreates). This is the decided behavior — test it explicitly.Grace period: none — immediate deletion, but the confirm modal prompts "Download your data first" linking each campaign's export (
GET /campaigns/{id}/export,routers/campaigns.py:2580) where the user is GM.Audit: write
user.deletedviaaudit_service.log_event(actor = self or admin), context includes anonymization counts.Out of scope
Acceptance criteria
References
webapp/backend/app/models/user.py:26-29(OIDC keying),:57-81(relationships)webapp/backend/app/models/audit_log.py,services/audit_service.py:12(log_event)webapp/backend/app/routers/users.py(admin user surface + existing audit call sites at:217)webapp/backend/app/routers/campaigns.py:2580(export_campaignfor the "download first" prompt)Filed from the July 2026 full-project review.
Done — merged in PR #203 (backend
62f2af7+ frontend). CI green. This completes the v3.8.0 Privacy & Data Lifecycle milestone.Shipped: reserved "Deleted user" tombstone (migration
a3b4c5d6e7f8);delete_user_accountblocks sole-GM deletion first (409 + campaign list, nothing mutated), then sweeps all 26users.idFKs — deletes personal data, anonymizes authored content to the tombstone (collision-guarded), leavesaudit_logs.actor_id; one actor-attributeduser.deletedaudit.DELETE /api/me(self) +DELETE /api/admin/users/{id}(admin; 400 on the system user). OIDC-local-only — re-login recreates a fresh empty user (tested). Frontend: Profile danger zone (type-to-confirm + export links + OIDC note → logout) and admin per-row delete.Verification caught three real issues (fixed): a UUID-cast bug in the migration INSERT, and two ORM identity-map staleness cases from the bulk
synchronize_session=Falsecascade.Tests: backend 575 (+14, incl. no-orphan HTTP reads + OIDC re-login); frontend 307 (+11). All acceptance criteria met. Closing.