feat(backup): add enterprise backup & restore foundation
This commit is contained in:
parent
7835d3ca75
commit
6c917df515
30 changed files with 1538 additions and 61 deletions
|
|
@ -66,6 +66,8 @@ def can_read_activity(action: str, permissions: set[str]) -> bool:
|
|||
return "lexware.read" in permissions
|
||||
if action.startswith("accounting."):
|
||||
return "lexware.read" in permissions
|
||||
if action.startswith("backups."):
|
||||
return "backup.read" in permissions
|
||||
if action.startswith("audit_logs."):
|
||||
return "audit_logs.read" in permissions
|
||||
if action.startswith("auth."):
|
||||
|
|
|
|||
150
backend/hermes/app/api/backups.py
Normal file
150
backend/hermes/app/api/backups.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.core.rbac import require_permission
|
||||
from app.db.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.api_response import ApiSuccess
|
||||
from app.schemas.backup import BackupRestoreRequest
|
||||
from app.services.audit_service import write_audit_log
|
||||
from app.services.backup_service import BackupService
|
||||
|
||||
router = APIRouter(prefix="/backups", tags=["Backups"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiSuccess)
|
||||
def list_backups(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("backup.read")),
|
||||
):
|
||||
return ApiSuccess(data=BackupService.list_backups(), message="Backups geladen")
|
||||
|
||||
|
||||
@router.post("/create", response_model=ApiSuccess, status_code=status.HTTP_201_CREATED)
|
||||
def create_backup(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("backup.create")),
|
||||
):
|
||||
backup = BackupService.create_backup(actor=current_user)
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.create",
|
||||
entity_type="backup",
|
||||
entity_label=backup.filename,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={
|
||||
"filename": backup.filename,
|
||||
"size_bytes": backup.size_bytes,
|
||||
"app_version": backup.app_version,
|
||||
},
|
||||
)
|
||||
return ApiSuccess(data=backup, message="Backup erstellt")
|
||||
|
||||
|
||||
@router.get("/{filename}/download")
|
||||
def download_backup(
|
||||
filename: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("backup.download")),
|
||||
):
|
||||
path = BackupService.resolve_backup_path(filename)
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.download",
|
||||
entity_type="backup",
|
||||
entity_label=path.name,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={"filename": path.name, "size_bytes": path.stat().st_size},
|
||||
)
|
||||
return FileResponse(path=path, media_type="application/zip", filename=path.name)
|
||||
|
||||
|
||||
@router.delete("/{filename}", response_model=ApiSuccess)
|
||||
def delete_backup(
|
||||
filename: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("backup.delete")),
|
||||
):
|
||||
path = BackupService.resolve_backup_path(filename)
|
||||
size_bytes = path.stat().st_size
|
||||
BackupService.delete_backup(filename)
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.delete",
|
||||
entity_type="backup",
|
||||
entity_label=path.name,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={"filename": path.name, "size_bytes": size_bytes},
|
||||
)
|
||||
return ApiSuccess(message="Backup geloescht")
|
||||
|
||||
|
||||
@router.post("/{filename}/restore/validate", response_model=ApiSuccess)
|
||||
def validate_backup_restore(
|
||||
filename: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("backup.restore")),
|
||||
):
|
||||
validation = BackupService.validate_backup(filename)
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.validate",
|
||||
entity_type="backup",
|
||||
entity_label=filename,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={"filename": filename, "valid": validation.valid, "issues": validation.issues},
|
||||
)
|
||||
return ApiSuccess(data=validation, message=validation.message)
|
||||
|
||||
|
||||
@router.post("/{filename}/restore", response_model=ApiSuccess)
|
||||
def restore_backup(
|
||||
filename: str,
|
||||
payload: BackupRestoreRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_permission("backup.restore")),
|
||||
):
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.restore_started",
|
||||
entity_type="backup",
|
||||
entity_label=filename,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={"filename": filename},
|
||||
)
|
||||
try:
|
||||
validation = BackupService.restore_backup(filename, confirm_text=payload.confirm_text)
|
||||
except Exception:
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.restore_failed",
|
||||
entity_type="backup",
|
||||
entity_label=filename,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={"filename": filename},
|
||||
)
|
||||
raise
|
||||
|
||||
write_audit_log(
|
||||
db,
|
||||
action="backups.restore_completed",
|
||||
entity_type="backup",
|
||||
entity_label=filename,
|
||||
actor=current_user,
|
||||
request=request,
|
||||
metadata={"filename": filename},
|
||||
)
|
||||
return ApiSuccess(data=validation, message="Restore abgeschlossen")
|
||||
|
|
@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
|
|||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.rbac import get_user_permission_names, require_permission
|
||||
from app.db.database import get_db
|
||||
from app.models.rbac import Role
|
||||
|
|
@ -16,6 +17,7 @@ from app.repositories.repair_repository import RepairRepository
|
|||
from app.repositories.repair_estimate_repository import RepairEstimateRepository
|
||||
from app.repositories.user_repository import UserRepository
|
||||
from app.schemas.dashboard import DashboardSummary, EmptyWidget, MetricCard, SystemStatusItem
|
||||
from app.services.backup_service import BackupService
|
||||
from app.services.system_settings_service import SystemSettingsService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -162,6 +164,27 @@ def get_dashboard_summary(
|
|||
),
|
||||
]
|
||||
|
||||
if "backup.read" in permissions:
|
||||
backup_stats = BackupService.get_backup_stats()
|
||||
repairs.append(MetricCard(label="Backups", value=backup_stats.total_count))
|
||||
system_status.extend([
|
||||
SystemStatusItem(
|
||||
label="Letztes Backup",
|
||||
value=backup_stats.latest_backup_at.isoformat() if backup_stats.latest_backup_at else "Noch kein Backup",
|
||||
status="ok" if backup_stats.latest_backup_at else "warning",
|
||||
),
|
||||
SystemStatusItem(
|
||||
label="Backup-Speicher",
|
||||
value=f"{backup_stats.total_count} Backup(s), {backup_stats.total_size_bytes} Bytes",
|
||||
status="ok" if backup_stats.total_count else "warning",
|
||||
),
|
||||
SystemStatusItem(
|
||||
label="Hermes-Version",
|
||||
value=settings.app_version,
|
||||
status="info",
|
||||
),
|
||||
])
|
||||
|
||||
logger.info("dashboard.summary", extra={"actor_user_id": current_user.id})
|
||||
|
||||
return DashboardSummary(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue