# backend/app/services/export_service.py — per-user 데이터 ZIP 번들(소유권) + 민감 필드 redact import io import json import zipfile from datetime import date, datetime from sqlmodel import Session, select from ..models import ( ConnectorAccount, Email, InboxItem, Notification, Person, Task, Trip, ) _REDACT_FIELDS = {"token_enc", "password_hash", "token_hash", "code_verifier", "scopes"} def _ser(v): if isinstance(v, (datetime, date)): return v.isoformat() return v def _dump(rows, user_id: str) -> list[dict]: out = [] for r in rows: d = r.model_dump() if d.get("user_id") not in (None, user_id) and "user_id" in d: continue for k in list(d.keys()): if k in _REDACT_FIELDS and d[k]: d[k] = "[redacted]" else: d[k] = _ser(d[k]) out.append(d) return out def _scoped(session: Session, model, user_id: str): stmt = select(model) if hasattr(model, "user_id"): stmt = stmt.where(model.user_id == user_id) return session.exec(stmt).all() def build_export(session: Session, user: Person) -> bytes: """사용자 전 데이터 ZIP(profile/tasks/inbox/emails/notifications/trips/connectors).""" buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: profile = {"id": user.id, "name": user.name, "initial": user.initial, "email": user.email} z.writestr("profile.json", json.dumps(profile, ensure_ascii=False, indent=2)) bundles = { "tasks.json": _dump(_scoped(session, Task, user.id), user.id), "inbox.json": _dump(_scoped(session, InboxItem, user.id), user.id), "emails.json": _dump(_scoped(session, Email, user.id), user.id), "notifications.json": _dump(_scoped(session, Notification, user.id), user.id), "trips.json": _dump(_scoped(session, Trip, user.id), user.id), "connectors.json": _dump(_scoped(session, ConnectorAccount, user.id), user.id), } for name, data in bundles.items(): z.writestr(name, json.dumps(data, ensure_ascii=False, indent=2)) return buf.getvalue()