ligbox-ops-platform/projects/ops-desk/api/app/vm112_domains_routes.py
Ligbox Spec Hub 0ee4845818 Fix purge 500 when SQLite database is locked (Spec 017).
Enable WAL/busy_timeout, retry writes, reject duplicate running jobs with HTTP 409,
use bcrypt directly instead of broken passlib 1.7.4 + bcrypt 4.x, and improve UI errors.
2026-06-19 22:06:06 +00:00

171 lines
5.2 KiB
Python

"""Rotas Desk — domínios VM112 (Spec 017)."""
from __future__ import annotations
import sqlite3
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app import auth, vm112_domains
from app.permissions import can_manage_vm112_domains
from app.vm112_purge_stream import purge_sse_generator
from app.vm112_purge_jobs import (
PurgeJobConflictError,
get_job_public,
list_jobs,
recover_job,
start_job,
)
router = APIRouter(prefix="/api/v1/vm112", tags=["vm112-domains"])
class DomainPurgeRequest(BaseModel):
confirm_domain: str = Field(..., min_length=3)
root_password: str = Field(..., min_length=1)
def _require_admin(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser:
if not can_manage_vm112_domains(user.role):
raise HTTPException(403, "Apenas perfis Admin (super_admin, ops_lead)")
return user
def _validate_purge_request(domain: str, body: DomainPurgeRequest) -> str:
domain = domain.lower().strip()
if domain in vm112_domains.PURGE_BLOCKLIST:
raise HTTPException(400, f"Domínio {domain} está protegido contra purge")
if body.confirm_domain.lower().strip() != domain:
raise HTTPException(400, "Confirmação do domínio não coincide")
return domain
@router.get("/domains")
def list_vm112_domains(
q: str = "",
user: auth.DeskUser = Depends(_require_admin),
):
try:
return vm112_domains.list_domains(q)
except Exception as e:
raise HTTPException(502, f"VM112 indisponível: {e}") from e
@router.get("/domains/{domain}")
def get_vm112_domain(
domain: str,
user: auth.DeskUser = Depends(_require_admin),
):
try:
return vm112_domains.get_domain(domain)
except Exception as e:
raise HTTPException(502, f"VM112: {e}") from e
@router.post("/domains/{domain}/purge")
def purge_vm112_domain(
domain: str,
body: DomainPurgeRequest,
user: auth.DeskUser = Depends(_require_admin),
):
domain = _validate_purge_request(domain, body)
conn = auth.db()
try:
if not vm112_domains.verify_root_password(conn, body.root_password):
raise HTTPException(403, "Senha Root incorrecta")
finally:
conn.close()
try:
vm112_result = vm112_domains.purge_vm112(domain)
except Exception as e:
raise HTTPException(502, f"Purge VM112 falhou: {e}") from e
conn = auth.db()
try:
desk_counts, desk_timeline = vm112_domains.purge_desk_timeline(conn, domain)
finally:
conn.close()
timeline = vm112_domains.build_purge_timeline(vm112_result, desk_counts, desk_timeline)
return {
"ok": True,
"domain": domain,
"vm112": vm112_result,
"desk": desk_counts,
"timeline": timeline,
"by": user.username,
}
@router.post("/domains/{domain}/purge/stream")
def purge_vm112_domain_stream(
domain: str,
body: DomainPurgeRequest,
user: auth.DeskUser = Depends(_require_admin),
):
"""SSE — progresso purge em tempo real (Fase 2 Spec 017)."""
domain = _validate_purge_request(domain, body)
return StreamingResponse(
purge_sse_generator(domain, body.root_password, user.username),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@router.post("/domains/{domain}/purge/jobs")
def start_purge_job(
domain: str,
body: DomainPurgeRequest,
user: auth.DeskUser = Depends(_require_admin),
):
"""Inicia purge em background; consultar GET /purge/jobs/{id} (recomendado via Traefik)."""
domain = _validate_purge_request(domain, body)
try:
job_id = start_job(domain, body.root_password, user.username)
except PurgeJobConflictError as exc:
raise HTTPException(409, str(exc)) from exc
except sqlite3.OperationalError as exc:
if "locked" in str(exc).lower():
raise HTTPException(
503,
"Base de dados ocupada — aguarde o purge em curso ou tente novamente em alguns segundos",
) from exc
raise HTTPException(500, f"Erro SQLite ao iniciar purge: {exc}") from exc
return {"ok": True, "job_id": job_id, "domain": domain, "status": "running"}
@router.get("/purge/jobs")
def list_purge_jobs(
limit: int = 100,
offset: int = 0,
user: auth.DeskUser = Depends(_require_admin),
):
return list_jobs(limit=limit, offset=offset)
@router.get("/purge/jobs/{job_id}")
def get_purge_job_status(
job_id: str,
user: auth.DeskUser = Depends(_require_admin),
):
job = get_job_public(job_id)
if not job:
raise HTTPException(404, "Job purge não encontrado")
return job
@router.post("/purge/jobs/{job_id}/recover")
def recover_purge_job(
job_id: str,
domain: str = "",
user: auth.DeskUser = Depends(_require_admin),
):
"""Recupera purge quando job sumiu da memória mas VM112 já removeu o domínio."""
job = recover_job(job_id, domain or None)
if not job:
raise HTTPException(404, "Não foi possível recuperar o job purge")
return job