feat(account): GDPR self-service account + data deletion (5d)
Add DELETE /api/me/account: permanently erases the signed-in account and all its personal data — OAuth tokens, subscriptions, tags, video states, playlists, notifications all cascade on the users row (FK ON DELETE CASCADE); quota-audit events are kept but anonymised (SET NULL); the email's invite/whitelist row is removed too. Guards: the shared demo account can't be deleted, and the last remaining admin can't delete itself. Frontend: a Danger zone in Settings -> Account (non-demo) with a danger-confirm; on success the session clears and the app lands on the welcome page. EN/HU/DE. Verified via curl: self-delete + session clear + demo 403.
This commit is contained in:
parent
08b3c47876
commit
84b55c0567
6 changed files with 90 additions and 3 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth import current_user, has_read_scope, has_write_scope, is_allowed
|
||||
|
|
@ -85,6 +85,43 @@ def get_me(
|
|||
}
|
||||
|
||||
|
||||
@router.delete("/account")
|
||||
def delete_my_account(
|
||||
request: Request,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""GDPR self-service erasure: permanently delete the signed-in account and ALL its personal
|
||||
data — OAuth tokens, subscriptions, tags, watch/save/hide states, playlists, notifications —
|
||||
which all cascade on the users row. Quota-audit events are kept but anonymised (FK SET NULL).
|
||||
The shared demo account can't be deleted, and the last remaining admin can't delete itself
|
||||
(that would lock everyone out of the admin surfaces)."""
|
||||
if user.is_demo:
|
||||
raise HTTPException(status_code=403, detail="The shared demo account can't be deleted.")
|
||||
if user.role == "admin":
|
||||
admins = db.scalar(select(func.count()).select_from(User).where(User.role == "admin")) or 0
|
||||
if admins <= 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You're the only admin — promote another admin before deleting your account.",
|
||||
)
|
||||
email = user.email.lower()
|
||||
user_id = user.id
|
||||
# Raw delete so Postgres applies the ON DELETE CASCADE / SET NULL on every dependent table.
|
||||
db.execute(delete(User).where(User.id == user_id))
|
||||
# Erase the access-request/whitelist row for this email too (full erasure; they'd re-request).
|
||||
db.execute(delete(Invite).where(Invite.email == email))
|
||||
db.commit()
|
||||
# Drop this account from the browser session; switch to another signed-in account if any.
|
||||
remaining = [a for a in (request.session.get("account_ids") or []) if a != user_id]
|
||||
if remaining:
|
||||
request.session["account_ids"] = remaining
|
||||
request.session["user_id"] = remaining[-1]
|
||||
else:
|
||||
request.session.clear()
|
||||
return {"deleted": True}
|
||||
|
||||
|
||||
@router.put("/preferences")
|
||||
def update_preferences(
|
||||
preferences: dict,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue