2026-07-26 03:17:40 +02:00
|
|
|
"""API routes for Backup management."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.core.db import get_db
|
2026-09-18 08:17:57 +02:00
|
|
|
from app.deps import get_current_user, require_admin, require_permission
|
2026-07-26 03:17:40 +02:00
|
|
|
from app.schemas.backup import BackupListResponse, BackupResponse
|
|
|
|
|
from app.services import backup_service
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/v1/backups", tags=["backups"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"",
|
|
|
|
|
response_model=BackupListResponse,
|
|
|
|
|
dependencies=[Depends(require_permission("automation:admin"))],
|
|
|
|
|
)
|
|
|
|
|
async def list_backups(
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""List all backups for the current tenant."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
|
|
|
backups = await backup_service.list_backups(db, tenant_id)
|
|
|
|
|
return BackupListResponse(
|
|
|
|
|
backups=[BackupResponse.model_validate(b) for b in backups],
|
|
|
|
|
total=len(backups),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"",
|
|
|
|
|
response_model=BackupResponse,
|
|
|
|
|
status_code=201,
|
|
|
|
|
dependencies=[Depends(require_permission("automation:admin"))],
|
|
|
|
|
)
|
|
|
|
|
async def create_backup(
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Create a new database backup."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
|
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
|
|
|
backup = await backup_service.create_backup(db, tenant_id, user_id=user_id)
|
|
|
|
|
return backup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/{backup_id}/restore",
|
|
|
|
|
response_model=BackupResponse,
|
2026-09-18 08:17:57 +02:00
|
|
|
dependencies=[Depends(require_admin)],
|
2026-07-26 03:17:40 +02:00
|
|
|
)
|
|
|
|
|
async def restore_backup(
|
|
|
|
|
backup_id: str,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2026-09-18 08:17:57 +02:00
|
|
|
current_user: dict = Depends(require_admin),
|
2026-07-26 03:17:40 +02:00
|
|
|
):
|
|
|
|
|
"""Restore a database backup.
|
|
|
|
|
|
|
|
|
|
WARNING: This is a destructive operation. It drops and recreates the database.
|
2026-09-18 08:17:57 +02:00
|
|
|
|
|
|
|
|
F23 (Astra P1): a full-database restore is a GLOBAL operations action —
|
|
|
|
|
it affects every tenant. A tenant admin (automation:admin) must not be
|
|
|
|
|
able to trigger it: the restore replaces the shared database, not just
|
|
|
|
|
this tenant's rows. Requires a real system admin (is_system_admin or
|
|
|
|
|
*:* via the RBAC system).
|
2026-07-26 03:17:40 +02:00
|
|
|
"""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
|
|
|
try:
|
|
|
|
|
b_id = uuid.UUID(backup_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
raise HTTPException(400, detail={"detail": "Invalid backup_id", "code": "invalid_id"}) from None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
backup = await backup_service.restore_backup(db, tenant_id, b_id)
|
|
|
|
|
return backup
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise HTTPException(400, detail={"detail": str(exc), "code": "invalid_state"}) from exc
|
|
|
|
|
except FileNotFoundError as exc:
|
|
|
|
|
raise HTTPException(404, detail={"detail": str(exc), "code": "file_not_found"}) from exc
|
|
|
|
|
except RuntimeError as exc:
|
|
|
|
|
raise HTTPException(500, detail={"detail": str(exc), "code": "restore_failed"}) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/{backup_id}",
|
|
|
|
|
status_code=204,
|
|
|
|
|
dependencies=[Depends(require_permission("automation:admin"))],
|
|
|
|
|
)
|
|
|
|
|
async def delete_backup(
|
|
|
|
|
backup_id: str,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: dict = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Delete a backup record and its file."""
|
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
|
|
|
try:
|
|
|
|
|
b_id = uuid.UUID(backup_id)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
raise HTTPException(400, detail={"detail": "Invalid backup_id", "code": "invalid_id"}) from None
|
|
|
|
|
|
|
|
|
|
deleted = await backup_service.delete_backup(db, tenant_id, b_id)
|
|
|
|
|
if not deleted:
|
|
|
|
|
raise HTTPException(404, detail={"detail": "Backup not found", "code": "not_found"})
|
|
|
|
|
return None
|