download backup admin possibility

This commit is contained in:
JiriUhlir
2026-05-29 10:56:20 +02:00
parent 6e5d9b3120
commit e3019bd76e
+44 -3
View File
@@ -1,11 +1,12 @@
import html import html
from pathlib import Path from pathlib import Path
from urllib.parse import quote
from fastapi import APIRouter, Depends, Form, Request from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from ..auth import require_user from ..auth import require_user
from ..backups import is_backup_path, list_backups from ..backups import backup_dir, is_backup_path, list_backups
from ..config import BACKUP_SCRIPT from ..config import BACKUP_SCRIPT
from ..db.audit import log_audit_event from ..db.audit import log_audit_event
from ..shell import run_command from ..shell import run_command
@@ -14,14 +15,25 @@ from ..templates.layout import page, render_result
router = APIRouter() router = APIRouter()
def is_admin(user) -> bool:
return (user.get("role") or "").lower() == "admin"
@router.get("/backups", response_class=HTMLResponse) @router.get("/backups", response_class=HTMLResponse)
def backups_page(request: Request, user=Depends(require_user)): def backups_page(request: Request, user=Depends(require_user)):
rows = "" rows = ""
can_download = is_admin(user)
for path in list_backups(): for path in list_backups():
size_mb = path.stat().st_size / 1024 / 1024 size_mb = path.stat().st_size / 1024 / 1024
backup_name = html.escape(path.name) backup_name = html.escape(path.name)
backup_path = html.escape(str(path), quote=True) backup_path = html.escape(str(path), quote=True)
backup_url_name = quote(path.name, safe="")
download_action = (
f'<p><a class="btn btn-secondary" href="/portal/backups/download/{backup_url_name}">Stáhnout</a></p>'
if can_download
else ""
)
restore_cmd = html.escape( restore_cmd = html.escape(
f"sudo /home/jiri/workspace/appfactory-tools/scripts/restore-appfactory.sh --force {path}", f"sudo /home/jiri/workspace/appfactory-tools/scripts/restore-appfactory.sh --force {path}",
@@ -48,6 +60,7 @@ def backups_page(request: Request, user=Depends(require_user)):
<div class="muted">Obnova je nebezpečná a musí se spustit ručně přes SSH.</div> <div class="muted">Obnova je nebezpečná a musí se spustit ručně přes SSH.</div>
</td> </td>
<td class="actions-cell"> <td class="actions-cell">
{download_action}
<form method="post" action="/portal/backups/delete" onsubmit="return confirm('Smazat zálohu {backup_name}?');"> <form method="post" action="/portal/backups/delete" onsubmit="return confirm('Smazat zálohu {backup_name}?');">
<input type="hidden" name="backup_path" value="{backup_path}"> <input type="hidden" name="backup_path" value="{backup_path}">
<button type="submit" class="danger">Smazat</button> <button type="submit" class="danger">Smazat</button>
@@ -92,6 +105,34 @@ def backups_page(request: Request, user=Depends(require_user)):
) )
@router.get("/backups/download/{backup_name}")
def download_backup(backup_name: str, user=Depends(require_user)):
if not is_admin(user):
raise HTTPException(status_code=403, detail="Only admins can download backups")
target = backup_dir() / backup_name
if not is_backup_path(target):
raise HTTPException(status_code=400, detail="Invalid backup path")
target = target.resolve()
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail="Backup not found")
log_audit_event(
user,
action="backup_download",
target_type="backup",
target_id=target.name,
metadata={"backup_path": str(target)},
)
return FileResponse(
path=target,
filename=target.name,
media_type="application/gzip",
)
@router.post("/backups/create", response_class=HTMLResponse) @router.post("/backups/create", response_class=HTMLResponse)
def create_backup(user=Depends(require_user)): def create_backup(user=Depends(require_user)):
result = run_command([BACKUP_SCRIPT]) result = run_command([BACKUP_SCRIPT])