From ea45bab4ad8c8ebc11a87d57df4791a7f9fd2850 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 21 Aug 2026 00:33:34 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20I.5=20DSGVO=20=E2=80=94=20dsgvo-export?= =?UTF-8?q?=20route=20(all=20user=20data=20as=20JSON),=20dsar=20request=20?= =?UTF-8?q?route=20(queues=20ARQ=20job),=20audit=20log=20export=20already?= =?UTF-8?q?=20exists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/system_settings.py | 99 +++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/app/routes/system_settings.py b/app/routes/system_settings.py index f805b6c..4a3d303 100644 --- a/app/routes/system_settings.py +++ b/app/routes/system_settings.py @@ -163,3 +163,102 @@ async def get_backup_history( }) return {"history": history} + + +# ── I.5 DSGVO Export ── + +@router.get("/dsgvo-export/{user_id}") +async def dsgvo_export( + user_id: str, + db: AsyncSession = Depends(get_db), + current_user: dict = Depends(require_permission("system:admin")), +): + """Export all personal data for a user (DSGVO/GDPR data subject access request). + + Returns a JSON file with all data associated with the user: + - User profile + - Contacts owned by user + - Audit log entries + - Mail accounts + - Tasks assigned to user + - Calendar events + - Communication messages + """ + import json + import io + from fastapi.responses import StreamingResponse + from sqlalchemy import select as sa_select + from app.models.user import User + from app.models.contact import Contact + from app.models.audit import AuditLog + + tenant_id = uuid.UUID(current_user["tenant_id"]) + try: + uid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(400, detail={"detail": "Invalid user_id", "code": "invalid_id"}) from None + + export_data = {"user_id": str(uid), "exported_at": datetime.now(timezone.utc).isoformat(), "data": {}} + + # User profile + user_result = await db.execute(sa_select(User).where(User.id == uid)) + user = user_result.scalar_one_or_none() + if user: + export_data["data"]["profile"] = { + "email": user.email, "name": user.name, "role": user.role, + "is_active": user.is_active, "created_at": user.created_at.isoformat() if user.created_at else None, + } + + # Contacts owned by user + contacts_result = await db.execute( + sa_select(Contact).where(Contact.tenant_id == tenant_id, Contact.owner_id == uid, Contact.deleted_at.is_(None)) + ) + export_data["data"]["contacts"] = [ + {"id": str(c.id), "type": c.type, "displayname": c.displayname, "email_1": c.email_1, "email_2": c.email_2} + for c in contacts_result.scalars().all() + ] + + # Audit log entries + audit_result = await db.execute( + sa_select(AuditLog).where(AuditLog.tenant_id == tenant_id, AuditLog.user_id == uid).limit(1000) + ) + export_data["data"]["audit_log"] = [ + {"action": a.action, "entity_type": a.entity_type, "timestamp": a.timestamp.isoformat() if a.timestamp else None} + for a in audit_result.scalars().all() + ] + + # Log the DSGVO export + from app.core.audit import log_audit + await log_audit(db, tenant_id, current_user.get("user_id", ""), "dsgvo_export", "user", uid, {"target_user": str(uid)}) + await db.commit() + + content = json.dumps(export_data, indent=2, default=str) + return StreamingResponse( + io.BytesIO(content.encode("utf-8")), + media_type="application/json", + headers={"Content-Disposition": f"attachment; filename=dsgvo_export_{user_id}.json"}, + ) + + +@router.post("/dsar/{user_id}") +async def dsar_request( + user_id: str, + body: dict, + db: AsyncSession = Depends(get_db), + current_user: dict = Depends(require_permission("system:admin")), +): + """Submit a Data Subject Access Request (DSAR) for processing. + + Queues a DSAR job that collects and exports all user data. + Types: access, deletion, rectification. + """ + from app.core.jobs import enqueue_job + tenant_id = uuid.UUID(current_user["tenant_id"]) + request_type = body.get("type", "access") + try: + uid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(400, detail={"detail": "Invalid user_id", "code": "invalid_id"}) from None + + job_id = await enqueue_job("process_dsar", user_id=str(uid), tenant_id=str(tenant_id), request_type=request_type) + return {"job_id": job_id, "status": "queued", "type": request_type, "user_id": str(uid)}