"""Data export/import endpoints.""" import csv import io import json from datetime import datetime from typing import Any, List from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Response from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from pydantic import BaseModel from app.dependencies import get_db, get_current_superuser from app.models.user import User from app.models.audit_log import AuditLog from app import crud, schemas router = APIRouter() class ImportResult(BaseModel): """Import operation result.""" success: int = 0 failed: int = 0 errors: List[str] = [] # ============================================================================= # EXPORT ENDPOINTS # ============================================================================= @router.get("/users/csv") def export_users_csv( db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser), ): """ Export all users to CSV format. """ users = db.query(User).all() # Create CSV in memory output = io.StringIO() writer = csv.writer(output) # Header writer.writerow([ "id", "username", "email", "is_active", "is_superuser", "totp_enabled", "created_at", "last_login" ]) # Data rows for user in users: writer.writerow([ user.id, user.username, user.email, user.is_active, user.is_superuser, user.totp_enabled, user.created_at.isoformat() if user.created_at else "", user.last_login.isoformat() if user.last_login else "" ]) output.seek(0) # Log export crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="export", resource_type="users", details={"format": "csv", "count": len(users)}, status="success" ) return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={ "Content-Disposition": f"attachment; filename=users_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv" } ) @router.get("/users/json") def export_users_json( db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser), ): """ Export all users to JSON format. """ users = db.query(User).all() data = { "exported_at": datetime.utcnow().isoformat(), "exported_by": current_user.username, "count": len(users), "users": [ { "id": user.id, "username": user.username, "email": user.email, "is_active": user.is_active, "is_superuser": user.is_superuser, "totp_enabled": user.totp_enabled, "permissions": user.permissions, "created_at": user.created_at.isoformat() if user.created_at else None, "last_login": user.last_login.isoformat() if user.last_login else None } for user in users ] } # Log export crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="export", resource_type="users", details={"format": "json", "count": len(users)}, status="success" ) content = json.dumps(data, indent=2) return Response( content=content, media_type="application/json", headers={ "Content-Disposition": f"attachment; filename=users_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json" } ) @router.get("/settings/json") def export_settings_json( db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser), ): """ Export all settings to JSON format. """ from app.models.settings import Settings settings_list = db.query(Settings).all() data = { "exported_at": datetime.utcnow().isoformat(), "exported_by": current_user.username, "count": len(settings_list), "settings": { setting.key: setting.value for setting in settings_list } } # Log export crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="export", resource_type="settings", details={"format": "json", "count": len(settings_list)}, status="success" ) content = json.dumps(data, indent=2) return Response( content=content, media_type="application/json", headers={ "Content-Disposition": f"attachment; filename=settings_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json" } ) @router.get("/audit/csv") def export_audit_csv( db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser), days: int = 30 ): """ Export audit logs to CSV format. """ from datetime import timedelta since = datetime.utcnow() - timedelta(days=days) logs = db.query(AuditLog).filter(AuditLog.created_at >= since).all() # Create CSV in memory output = io.StringIO() writer = csv.writer(output) # Header writer.writerow([ "id", "user_id", "username", "action", "resource_type", "resource_id", "status", "ip_address", "created_at" ]) # Data rows for log in logs: writer.writerow([ log.id, log.user_id, log.username, log.action, log.resource_type, log.resource_id, log.status, log.ip_address, log.created_at.isoformat() if log.created_at else "" ]) output.seek(0) return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={ "Content-Disposition": f"attachment; filename=audit_logs_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv" } ) # ============================================================================= # IMPORT ENDPOINTS # ============================================================================= @router.post("/users/json", response_model=ImportResult) async def import_users_json( file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser), ): """ Import users from JSON file. Only creates new users, does not update existing ones. """ result = ImportResult() try: content = await file.read() data = json.loads(content.decode()) except json.JSONDecodeError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid JSON file: {str(e)}" ) users_data = data.get("users", []) if not users_data: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No users found in file" ) for user_data in users_data: try: username = user_data.get("username") email = user_data.get("email") # Check if user already exists if crud.user.get_by_username(db, username=username): result.errors.append(f"User '{username}' already exists") result.failed += 1 continue if crud.user.get_by_email(db, email=email): result.errors.append(f"Email '{email}' already exists") result.failed += 1 continue # Create user with a default password (must be changed) import secrets temp_password = secrets.token_urlsafe(16) user_create = schemas.UserCreate( username=username, email=email, password=temp_password, is_active=user_data.get("is_active", True), is_superuser=user_data.get("is_superuser", False), permissions=user_data.get("permissions") ) crud.user.create(db, obj_in=user_create) result.success += 1 except Exception as e: result.errors.append(f"Error importing user: {str(e)}") result.failed += 1 # Log import crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="import", resource_type="users", details={ "format": "json", "success": result.success, "failed": result.failed }, status="success" if result.failed == 0 else "partial" ) return result @router.post("/settings/json", response_model=ImportResult) async def import_settings_json( file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_superuser), ): """ Import settings from JSON file. Updates existing settings and creates new ones. """ result = ImportResult() try: content = await file.read() data = json.loads(content.decode()) except json.JSONDecodeError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid JSON file: {str(e)}" ) settings_data = data.get("settings", {}) if not settings_data: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No settings found in file" ) for key, value in settings_data.items(): try: crud.settings.update_setting(db, key=key, value=value) result.success += 1 except Exception as e: result.errors.append(f"Error importing setting '{key}': {str(e)}") result.failed += 1 # Log import crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="import", resource_type="settings", details={ "format": "json", "success": result.success, "failed": result.failed }, status="success" if result.failed == 0 else "partial" ) return result