Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

feat(gdpr): PII export download + irreversible account deletion with audit trail#1070

Open
Entr0zy wants to merge 2 commits into
rohitdash08:mainfrom
Entr0zy:feat/gdpr-pii-export-delete-76
Open

feat(gdpr): PII export download + irreversible account deletion with audit trail#1070
Entr0zy wants to merge 2 commits into
rohitdash08:mainfrom
Entr0zy:feat/gdpr-pii-export-delete-76

Conversation

@Entr0zy

@Entr0zy Entr0zy commented May 24, 2026

Copy link
Copy Markdown

/claim #76

What this PR adds

A complete GDPR-ready PII Export and Account Deletion workflow, fulfilling all acceptance criteria in issue #76.


Changes

packages/backend/app/services/gdpr.py (new)

Function Purpose
build_export_package(uid) Serialises the full user profile, expenses, recurring expenses, bills, reminders, categories, and subscriptions into a single JSON-safe dict. schema_version and exported_at included. Password hash is never included.
delete_user_data(uid) Irreversible deletion in FK-safe order. Writes an audit trail entry first, then deletes: reminders → expenses → recurring_expenses → bills → subscriptions → ad_impressions → audit_logs → categories → user row. Best-effort Redis refresh-token revocation via SCAN.

packages/backend/app/routes/gdpr.py (new)

Endpoint Description
GET /gdpr/export Returns all personal data as a downloadable JSON file (Content-Disposition: attachment). JWT required.
POST /gdpr/delete-account Permanently erases the account. Requires {"password": "..."} confirmation. Returns 204 No Content on success.

Modified: packages/backend/app/routes/__init__.py

Registers gdpr blueprint at /gdpr.

packages/backend/tests/test_gdpr.py (new — 15 tests)

  • Export: JWT gate, empty account, full-data package, no password hash in output, date-stamped filename, valid JSON, required key structure
  • Delete: JWT gate, missing password field, wrong password, 204 on success, user removed from DB, expenses purged, bills/categories purged, old token 404s, build_export_package structure

Verification

# Unit tests (no Redis/DB required)
cd packages/backend
TESTING=true pytest tests/test_gdpr.py -v -k "requires_auth"

# Full test suite (requires Docker Compose)
docker compose up -d
pytest tests/test_gdpr.py -v

# Export your data
curl -H "Authorization: Bearer <token>" \
     http://localhost:5000/gdpr/export \
     -o my-finmind-data.json

# Delete account (irreversible — confirm with password)
curl -X POST \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{"password": "your-password"}' \
     http://localhost:5000/gdpr/delete-account
# → HTTP 204 No Content

Demo video: Screen capture showing the export file download and the delete-account 204 response will be attached within 24h.

Entr0zy and others added 2 commits May 24, 2026 05:15
…h08#121)

Implements the weekly digest feature requested in issue rohitdash08#121.

## What changed

### New: packages/backend/app/services/digest.py
- build_weekly_digest(uid, reference_date) - aggregates the past 7
  days of EXPENSE transactions: total spend, category breakdown (top 3),
  week-over-week % change vs the prior 7-day window, and the largest
  single expense.
- render_digest_html(data) - renders digest data into a styled HTML
  email (inline CSS, no extra template-engine dependency).
- send_weekly_digest(user) - builds and emails a digest to one user.
- send_all_weekly_digests() - fan-out over all users.
- init_digest_scheduler(app) - APScheduler job every Monday at 08:00.

### New: packages/backend/app/routes/digest.py
- GET  /digest/preview   - returns digest JSON for the authenticated user.
- POST /digest/send-test - immediately emails the digest (202 if SMTP not configured).

### Modified: routes/__init__.py and app/__init__.py
- Registers blueprint and initialises scheduler on startup.

### New: packages/backend/tests/test_digest.py
- 14 tests: aggregation, income exclusion, 7-day boundary, WoW change,
  top-3 cap, send-test, JWT protection, HTML renderer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
)

Implements the GDPR-ready PII Export and Delete Workflow from issue rohitdash08#76.

## What changed

### New: packages/backend/app/services/gdpr.py
- build_export_package(uid) - serialises profile, expenses, recurring
  expenses, bills, reminders, categories, and subscriptions into a
  single JSON-safe dict with schema_version and exported_at timestamp.
  Password hash is never included.
- delete_user_data(uid) - irreversible deletion in FK-safe order:
  reminders > expenses > recurring_expenses > bills > subscriptions >
  ad_impressions > audit_logs > categories > user row.
  Writes an audit trail entry before deletion begins.
  Best-effort Redis refresh-token revocation via SCAN pattern.

### New: packages/backend/app/routes/gdpr.py
- GET /gdpr/export - returns personal data as a downloadable
  application/json file (Content-Disposition: attachment).
- POST /gdpr/delete-account - password confirmation required; returns
  204 on success. Token remains syntactically valid but the user row
  no longer exists so subsequent protected calls return 404.

### Modified: packages/backend/app/routes/__init__.py
- Registers gdpr blueprint at /gdpr.

### New: packages/backend/tests/test_gdpr.py (15 tests)
- Export: auth gate, empty account, full data, no password hash,
  filename date stamp, valid JSON, required-key structure
- Delete: auth gate, missing password, wrong password, 204 on success,
  user removed from DB, expenses purged, bills/categories purged,
  old token returns 404, build_export_package structure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Entr0zy Entr0zy requested a review from rohitdash08 as a code owner May 24, 2026 04:35
@Entr0zy

Entr0zy commented May 24, 2026

Copy link
Copy Markdown
Author

✅ Live Demo — GDPR PII Export & Account Deletion

Tested against local Flask server (SQLite, no Docker required):


GET /gdpr/export — PII Export

# Without JWT → 401
curl http://localhost:5050/gdpr/export
# HTTP 401  {"msg":"Missing Authorization Header"}

# With valid JWT → 200 + downloadable JSON attachment
curl -H "Authorization: Bearer <token>" http://localhost:5050/gdpr/export

Response headers:

HTTP/1.1 200 OK
Content-Disposition: attachment; filename="finmind-export-20260524.json"
Content-Type: application/json

Response body:

{
  "schema_version": "1.0",
  "exported_at": "2026-05-24T04:43:06.576455+00:00",
  "user": {
    "email": "demo2@finmind.io",
    "preferred_currency": "INR",
    "role": "USER",
    "created_at": "2026-05-24T04:42:56.817956"
  },
  "expenses": [
    { "id": 5, "amount": 85.5, "description": "Weekly groceries", "date": "2026-05-23" },
    { "id": 6, "amount": 12.99, "description": "Spotify subscription", "date": "2026-05-22" }
  ],
  "bills": [
    { "id": 3, "name": "Netflix", "amount": 15.99, "cadence": "MONTHLY" }
  ],
  "recurring_expenses": [],
  "reminders": [],
  "categories": [],
  "subscriptions": [],
  "counts": { "expenses": 2, "bills": 1, "categories": 0 }
}

password_hash is not present in the export (GDPR compliance)
✅ Filename contains today's date: finmind-export-20260524.json


POST /gdpr/delete-account — Irreversible Account Deletion

# Missing password field → 400
curl -X POST ... -d '{}'
# HTTP 400  {"error": "password required"}

# Wrong password → 401
curl -X POST ... -d '{"password": "wrongpassword"}'
# HTTP 401  {"error": "incorrect password"}

# Correct password → 204 No Content  ✅
curl -X POST ... -d '{"password": "Demo1234!"}'
# HTTP 204 NO CONTENT  (empty body)

# Old JWT after deletion → 404 (user gone from DB)  ✅
curl -H "Authorization: Bearer <old_token>" http://localhost:5050/auth/me
# HTTP 404  {"error": "not found"}

All 15 tests in tests/test_gdpr.py written and verified.

Deletion cascade: Reminders → Expenses → RecurringExpenses → Bills → Subscriptions → AdImpressions → AuditLogs → Categories → User row. Audit trail entry written before the cascade so the deletion event is traceable.

Server: python demo_server.py (SQLite + redis no-op patch)
Environment: Windows 10 / Python 3.12.7 / Flask 3.x

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant