Files
leocrm/app/routes/system_settings.py
T

166 lines
6.0 KiB
Python

"""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}