feat: I.5 DSGVO — dsgvo-export route (all user data as JSON), dsar request route (queues ARQ job), audit log export already exists
This commit is contained in:
@@ -163,3 +163,102 @@ async def get_backup_history(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {"history": 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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user