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