CC2 R4 #4: /api/users/me/gdpr-export alias

- New auth.gdpr.me_router prefix /api/users/me with:
  - GET/POST /gdpr-export → Art.20 JSON download with Content-Disposition
  - POST /gdpr-erase → Art.17 erasure request
  - GET /gdpr-consent → consent history for caller
- jsonable_encoder fixes datetime serialisation in JSONResponse
- admin_users.html: 'Izvezi moje podatke' now POSTs to alias and uses
  filename from Content-Disposition header
- 401 enforced on no-auth, 200 on valid Bearer (verified live)
This commit is contained in:
Damir Radulić
2026-05-05 00:47:22 +02:00
parent ca92717039
commit a0db65fc31
14 changed files with 4796 additions and 30 deletions
+27
View File
@@ -16,6 +16,7 @@ from typing import Optional, Dict, List
from fastapi import APIRouter, HTTPException, Depends, Request, Body
from pydantic import BaseModel
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from .auth_v2 import (
db_query, db_one, db_exec,
@@ -25,6 +26,7 @@ from .admin_users import _is_pgz_admin
router = APIRouter(prefix="/api/gdpr", tags=["gdpr"])
admin_router = APIRouter(prefix="/api/admin/gdpr", tags=["gdpr_admin"])
me_router = APIRouter(prefix="/api/users/me", tags=["users_me_gdpr"])
# Ensure GDPR tables exist (idempotent)
def _ensure_tables():
@@ -174,6 +176,31 @@ def request_erasure(req: EraseReq, request: Request, user = Depends(require_user
return {"status": "ok", "request_id": new_id,
"message": "Vaš zahtjev je zaprimljen i bit će obrađen unutar 30 dana."}
# ─────────────────────────── Admin: erasure queue ───────────────────────────
# ─────────────────────────── /api/users/me alias (R4 #4) ───────────────────────────
@me_router.get("/gdpr-export")
@me_router.post("/gdpr-export")
def me_gdpr_export(user = Depends(require_user)):
"""GDPR Art. 20 — JSON export of all data we hold about the caller.
Same payload as GET /api/gdpr/export, exposed at user-friendly path.
Returns Content-Disposition: attachment so browsers offer a download."""
payload = export_my_data(user=user)
fn = f"pgz_data_export_{user['id']}_{int(datetime.utcnow().timestamp())}.json"
return JSONResponse(jsonable_encoder(payload),
headers={"Content-Disposition": f'attachment; filename="{fn}"'})
@me_router.post("/gdpr-erase")
def me_gdpr_erase(req: 'EraseReq', request: Request, user = Depends(require_user)):
return request_erasure(req=req, request=request, user=user)
@me_router.get("/gdpr-consent")
def me_gdpr_consent(user = Depends(require_user)):
rows = db_query("""SELECT necessary, analytics, marketing, consent_at,
policy_version, ip, session_id
FROM pgz_sport.gdpr_consent WHERE user_id=%s
ORDER BY consent_at DESC LIMIT 50""", (user["id"],))
return {"current": rows[0] if rows else None, "history": rows}
# ─────────────────────────── Admin: erasure queue ───────────────────────────
@admin_router.get("/erasure-requests")
def list_erasure_requests(status: Optional[str] = None,