Self-service account deletion + personal data export (GDPR-style "delete my account" / "download my data") #266

Closed
opened 2026-06-27 15:58:31 +00:00 by spikerj · 2 comments
Owner

Motivation

Give a signed-in user the ability to (a) permanently delete their own account and all associated data, and (b) export all of their data on request — the way Facebook/Instagram offer "Download your information" and "Delete account."

Primary near-term driver: I need to delete and re-register test users repeatedly to fully exercise the registration flow (registration-stepper) until I'm confident in it. Today there is no adult self-service delete, so a deleted/abandoned test identity leaves residue that blocks re-registering with the same email/username/phone. (The only existing Keycloak delete is the parent→child unlink in UnlinkChildAccountCommandHandler.)

Critical requirement: deletion must fully unblock re-registration

After a successful self-delete, re-registering with the same email, username, and phone must work end-to-end. That means clearing every re-registration blocker:

  • Keycloak user (email/username conflicts → 409 in KeycloakAdminService).
  • user-profiles (unique index on KeycloakUserId).
  • registrations (pending pre-registration blocks matching email/username/phone until expiry).
  • twillio-sms-opt-ins (SMS consent keyed by UserId / email / username / phone / PreRegistrationId).
  • child-account-requests + parental-approval-tokens (block reused child usernames / pending workflows).

Scope

1. Delete my account (hard delete)

Scoped to the authenticated Keycloak user (sub / ClaimTypes.NameIdentifier). Remove the Keycloak user via IKeycloakAdminService.DeleteUserAsync and purge all user-owned data. Known user-linked stores (from SpikerDbContext) to cover:

  • user-profiles, registrations, twillio-sms-opt-ins
  • Learning: user-submitted-code, userLessonProgress, offlineLessonAttempts, user-knowledge-progress, geography-knowledge-progress, earned-skills
  • Reading/books: reading-progress, reading-preferences, reading-profiles, user-bookmarks, user-notes, user-comments, book-generated-quizzes, book-generated-quiz-attempts
  • Activity/notifications: user-activities, keycloak-events, file-upload-scans, sensitive-data-audit
  • Social/sponsorship: donations, fundraisers, vault-items, calendar-events, contact-messages
  • Games: game-keybindings, hex-tower-defence-matches, chess-games
  • Parent/child: child-account-requests, parental-approval-tokens
  • Non-Mongo stores: GridFS blobs (profile images, avatars, vault files, travel docs), Redis-backed chat/message state (ChatHub), and the provisioned Gitea student git repository (GiteaStudentGitRepositoryProvisioner).

Let's centralize the collection/store list in one place so it can't silently drift as new user-scoped collections are added.

2. Export my data (on request)

GDPR data-portability: aggregate everything above into a single downloadable archive — a ZIP of JSON documents per collection plus the binary assets (images/vault/travel docs). FB/IG do this asynchronously and notify when ready; a synchronous endpoint is fine for v1 given our data volumes, but note the async option if archives get large.

Suggested implementation

  • Backend: new endpoints on ProfileController (or a dedicated AccountController) in SpikerSoft.Api:
    • GET /api/account/export → streams the ZIP archive.
    • DELETE /api/account → hard-deletes the authenticated user (export-then-delete optional).
    • MediatR commands/queries under SpikerSoft.Business/Domain/Account/... (ExportAccountDataQueryHandler, DeleteAccountCommandHandler) that fan out across the stores + Keycloak + GridFS + Redis + Gitea.
  • Frontend: a "Danger zone" / "Your data" section in the existing profile page (profile.component.ts, route /profile) with Download my data and Delete account actions.

Safety / UX

  • Destructive action → require explicit confirmation (type username to confirm and/or recent re-auth).
  • For the test-user workflow we want immediate hard delete (no soft-delete grace period that would impede rapid re-registration). FB/IG-style grace period is explicitly not wanted here, but note it as a future option if real users get this.
  • Audit the deletion (without retaining PII) so we have a record that an account was removed.

Open questions

  1. Child accounts: if a parent self-deletes, do we cascade-delete their managed child accounts, block deletion until children are unlinked, or reassign? Needs a decision before implementing.
  2. Content others depend on: donations/fundraisers/contact-messages and chess games reference other users — hard-delete vs anonymize the deleted user's references?
  3. Scope guard: should self-delete be gated behind a feature flag / restricted to non-prod initially, given the near-term goal is test-user churn?

Acceptance criteria

  • A signed-in user can delete their own account; afterward they can immediately re-register with the same email/username/phone with no leftover conflicts.
  • A signed-in user can download a complete archive of their data (JSON + binaries).
  • Deletion removes the Keycloak user and all user-scoped Mongo collections, GridFS assets, Redis chat state, and the Gitea student repo.
  • Destructive delete is confirmation-gated and audited.
  • Child-account behavior (open question #1) is resolved and covered by tests.
## Motivation Give a signed-in user the ability to (a) **permanently delete their own account and all associated data**, and (b) **export all of their data** on request — the way Facebook/Instagram offer "Download your information" and "Delete account." **Primary near-term driver:** I need to delete and re-register test users repeatedly to fully exercise the registration flow (`registration-stepper`) until I'm confident in it. Today there is **no adult self-service delete**, so a deleted/abandoned test identity leaves residue that blocks re-registering with the same email/username/phone. (The only existing Keycloak delete is the parent→child unlink in `UnlinkChildAccountCommandHandler`.) ## Critical requirement: deletion must fully unblock re-registration After a successful self-delete, re-registering with the **same email, username, and phone** must work end-to-end. That means clearing every re-registration blocker: - **Keycloak** user (email/username conflicts → 409 in `KeycloakAdminService`). - `user-profiles` (unique index on `KeycloakUserId`). - `registrations` (pending pre-registration blocks matching email/username/phone until expiry). - `twillio-sms-opt-ins` (SMS consent keyed by `UserId` / email / username / phone / `PreRegistrationId`). - `child-account-requests` + `parental-approval-tokens` (block reused child usernames / pending workflows). ## Scope ### 1. Delete my account (hard delete) Scoped to the authenticated Keycloak user (`sub` / `ClaimTypes.NameIdentifier`). Remove the Keycloak user via `IKeycloakAdminService.DeleteUserAsync` and purge all user-owned data. Known user-linked stores (from `SpikerDbContext`) to cover: - `user-profiles`, `registrations`, `twillio-sms-opt-ins` - Learning: `user-submitted-code`, `userLessonProgress`, `offlineLessonAttempts`, `user-knowledge-progress`, `geography-knowledge-progress`, `earned-skills` - Reading/books: `reading-progress`, `reading-preferences`, `reading-profiles`, `user-bookmarks`, `user-notes`, `user-comments`, `book-generated-quizzes`, `book-generated-quiz-attempts` - Activity/notifications: `user-activities`, `keycloak-events`, `file-upload-scans`, `sensitive-data-audit` - Social/sponsorship: `donations`, `fundraisers`, `vault-items`, `calendar-events`, `contact-messages` - Games: `game-keybindings`, `hex-tower-defence-matches`, `chess-games` - Parent/child: `child-account-requests`, `parental-approval-tokens` - **Non-Mongo stores:** GridFS blobs (profile images, avatars, vault files, travel docs), Redis-backed chat/message state (`ChatHub`), and the provisioned **Gitea student git repository** (`GiteaStudentGitRepositoryProvisioner`). Let's centralize the collection/store list in one place so it can't silently drift as new user-scoped collections are added. ### 2. Export my data (on request) GDPR data-portability: aggregate everything above into a single downloadable archive — a ZIP of JSON documents per collection plus the binary assets (images/vault/travel docs). FB/IG do this asynchronously and notify when ready; a synchronous endpoint is fine for v1 given our data volumes, but note the async option if archives get large. ## Suggested implementation - **Backend:** new endpoints on `ProfileController` (or a dedicated `AccountController`) in `SpikerSoft.Api`: - `GET /api/account/export` → streams the ZIP archive. - `DELETE /api/account` → hard-deletes the authenticated user (export-then-delete optional). - MediatR commands/queries under `SpikerSoft.Business/Domain/Account/...` (`ExportAccountDataQueryHandler`, `DeleteAccountCommandHandler`) that fan out across the stores + Keycloak + GridFS + Redis + Gitea. - **Frontend:** a "Danger zone" / "Your data" section in the existing profile page (`profile.component.ts`, route `/profile`) with **Download my data** and **Delete account** actions. ## Safety / UX - Destructive action → require explicit confirmation (type username to confirm and/or recent re-auth). - For the test-user workflow we want **immediate hard delete** (no soft-delete grace period that would impede rapid re-registration). FB/IG-style grace period is explicitly *not* wanted here, but note it as a future option if real users get this. - Audit the deletion (without retaining PII) so we have a record that an account was removed. ## Open questions 1. **Child accounts:** if a parent self-deletes, do we cascade-delete their managed child accounts, block deletion until children are unlinked, or reassign? Needs a decision before implementing. 2. **Content others depend on:** donations/fundraisers/contact-messages and chess games reference other users — hard-delete vs anonymize the deleted user's references? 3. **Scope guard:** should self-delete be gated behind a feature flag / restricted to non-prod initially, given the near-term goal is test-user churn? ## Acceptance criteria - A signed-in user can delete their own account; afterward they can immediately re-register with the same email/username/phone with no leftover conflicts. - A signed-in user can download a complete archive of their data (JSON + binaries). - Deletion removes the Keycloak user and all user-scoped Mongo collections, GridFS assets, Redis chat state, and the Gitea student repo. - Destructive delete is confirmation-gated and audited. - Child-account behavior (open question #1) is resolved and covered by tests.
spikerj added the enhancement label 2026-06-27 15:58:31 +00:00
Author
Owner

Implementation opened across two PRs (not yet merged):

  • Backend: spikersoft-backend PR #24AccountDataService (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, AccountController (GET /api/account/export, DELETE /api/account).
  • Frontend: spikersoft-angular PR #81 — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" with typed-confirmation dialog, then logout for re-registration.

Decisions applied: child accounts are cascade-deleted, available everywhere (no flag/env gating). Financial records (donations, sale-receipts) are intentionally retained. Known follow-ups: orphaned GridFS blobs and Redis chat scrollback. Will close this ticket once both PRs merge to master.

Implementation opened across two PRs (not yet merged): - Backend: spikersoft-backend PR #24 — `AccountDataService` (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, `AccountController` (`GET /api/account/export`, `DELETE /api/account`). - Frontend: spikersoft-angular PR #81 — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" with typed-confirmation dialog, then logout for re-registration. Decisions applied: child accounts are cascade-deleted, available everywhere (no flag/env gating). Financial records (donations, sale-receipts) are intentionally retained. Known follow-ups: orphaned GridFS blobs and Redis chat scrollback. Will close this ticket once both PRs merge to `master`.
Author
Owner

Resolved. Both PRs merged to master:

  • spikersoft-backend PR #24 (merged 2026-06-27) — AccountDataService (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, AccountController (GET /api/account/export, DELETE /api/account).
  • spikersoft-angular PR #81 (merged 2026-06-27) — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" (typed-confirmation dialog), then logout for re-registration.

Delivered as specified: child accounts cascade-delete, available everywhere. Financial records (donations, sale-receipts) intentionally retained. Closing. Known follow-ups remain (orphaned GridFS blobs, Redis chat scrollback) — file separately if desired.

Resolved. Both PRs merged to `master`: - spikersoft-backend PR #24 (merged 2026-06-27) — `AccountDataService` (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, `AccountController` (`GET /api/account/export`, `DELETE /api/account`). - spikersoft-angular PR #81 (merged 2026-06-27) — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" (typed-confirmation dialog), then logout for re-registration. Delivered as specified: child accounts cascade-delete, available everywhere. Financial records (donations, sale-receipts) intentionally retained. Closing. Known follow-ups remain (orphaned GridFS blobs, Redis chat scrollback) — file separately if desired.
Sign in to join this conversation.