"""System settings routes — get, upsert, backup config (admin only).""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select as sa_select from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.core.jobs import enqueue_job from app.deps import require_permission from app.models.audit import AuditLog from app.schemas.system_settings import SystemSettingsResponse, SystemSettingsUpsert from app.services import system_settings_service router = APIRouter(prefix="/api/v1/system-settings", tags=["system-settings"]) @router.get("", response_model=SystemSettingsResponse) async def get_system_settings( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:read")), ): """Get system settings for the current tenant. Sensitive fields (tax_number, iban, bic) are masked for non-admin users. """ tenant_id = uuid.UUID(current_user["tenant_id"]) result = await system_settings_service.get_system_settings(db, tenant_id) if result is None: return SystemSettingsResponse() # Mask sensitive fields for non-admin users if not current_user.get("is_system_admin"): if hasattr(result, "tax_number") and result.tax_number: result.tax_number = "********" if hasattr(result, "iban") and result.iban: result.iban = "********" if hasattr(result, "bic") and result.bic: result.bic = "********" return result @router.put("", response_model=SystemSettingsResponse) async def upsert_system_settings( body: SystemSettingsUpsert, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:write")), ): """Upsert system settings. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) data = body.model_dump() return await system_settings_service.upsert_system_settings(db, tenant_id, user_id, data) # ── Backup configuration endpoints ─────────────────────────────────────────── @router.get("/backup-config") async def get_backup_config( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:read")), ): """Get backup configuration for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) result = await system_settings_service.get_system_settings(db, tenant_id) if result is None: return { "backup_enabled": False, "backup_interval": "24h", "backup_retention_days": 7, "backup_destination": "local", } return { "backup_enabled": result.get("backup_enabled", False), "backup_interval": result.get("backup_interval", "24h"), "backup_retention_days": result.get("backup_retention_days", 7), "backup_destination": result.get("backup_destination", "local"), } @router.put("/backup-config") async def update_backup_config( body: dict, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:write")), ): """Update backup configuration. Admin only. Accepts: backup_enabled (bool), backup_interval (str), backup_retention_days (int), backup_destination (str). """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) # Get existing settings existing = await system_settings_service.get_system_settings(db, tenant_id) if existing is None: raise HTTPException(404, detail={"detail": "System settings not found. Please configure company settings first.", "code": "settings_not_found"}) # Merge backup fields into existing data data = dict(existing) if "backup_enabled" in body: data["backup_enabled"] = bool(body["backup_enabled"]) if "backup_interval" in body: data["backup_interval"] = str(body["backup_interval"]) if "backup_retention_days" in body: data["backup_retention_days"] = int(body["backup_retention_days"]) if "backup_destination" in body: data["backup_destination"] = str(body["backup_destination"]) return await system_settings_service.upsert_system_settings(db, tenant_id, user_id, data) @router.post("/backup-now") async def trigger_backup_now( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:write")), ): """Trigger an immediate backup via ARQ job. Admin only.""" job_id = await enqueue_job("run_backup") if job_id is None: raise HTTPException(503, detail={"detail": "Failed to enqueue backup job. Worker may not be running.", "code": "enqueue_failed"}) return {"message": "Backup job enqueued", "job_id": job_id} @router.get("/backup-history") async def get_backup_history( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("settings:read")), ): """Get last 10 backup results from audit log.""" tenant_id = uuid.UUID(current_user["tenant_id"]) stmt = ( sa_select(AuditLog) .where( AuditLog.tenant_id == tenant_id, AuditLog.entity_type == "backup", AuditLog.action.in_(["backup_success", "backup_failed"]), ) .order_by(AuditLog.timestamp.desc()) .limit(10) ) result = await db.execute(stmt) entries = result.scalars().all() history = [] for entry in entries: changes = entry.changes or {} history.append({ "id": str(entry.id), "timestamp": entry.timestamp.isoformat() if entry.timestamp else None, "action": entry.action, "success": changes.get("success", entry.action == "backup_success"), "destination": changes.get("destination", "local"), "error": changes.get("error", ""), }) return {"history": history}