ligbox-ops-platform/projects/ops-desk/api/app/domain_console_sandbox_routes.py
Ligbox Spec Hub 038fb8f7ce feat(dns): Spec 037 DNS Viewer — Desk, Console e patches Wizard V4
Entrega read-only de apontamentos DNS (Cloudflare, OpenPanel BIND, público)
no Desk e Console /admin/dominio, com spec, scripts de rollback e patches
VM112 para painel lateral no passo DNS do onboarding (deploy wizard pendente).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:36:25 +00:00

175 lines
5.1 KiB
Python

"""API sandbox — Área Gerente create-only (Spec 035)."""
from __future__ import annotations
import sqlite3
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from app import auth, domain_console_sandbox as sandbox
from app.permissions import can_manage_vm112_domains
router = APIRouter(prefix="/api/v1/domain-console/sandbox", tags=["domain-console-sandbox"])
def _require_sandbox(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser:
if not can_manage_vm112_domains(user.role):
raise HTTPException(403, "Sem permissão sandbox domain console")
return user
def _db() -> sqlite3.Connection:
from app.main import db
return db()
class CreateScenarioBody(BaseModel):
label: str = Field(..., min_length=2, max_length=120)
domain: str | None = None
slug: str | None = Field(None, max_length=40)
plan_mock: str = "Business"
admin_password: str | None = Field(None, min_length=8)
class AddAccountBody(BaseModel):
local_part: str = Field(..., min_length=1, max_length=64)
display_name: str = Field("", max_length=120)
password: str | None = Field(None, min_length=8)
@router.get("/config")
def sandbox_config(_user: auth.DeskUser = Depends(_require_sandbox)):
return {
"enabled": sandbox.SANDBOX_ENABLED,
"mode": "create-only",
"delete_blocked": True,
"domain_prefix": sandbox.SANDBOX_DOMAIN_PREFIX,
"domain_suffix": sandbox.SANDBOX_ALLOWED_SUFFIX,
"suggested_domain": sandbox.suggest_scenario_domain(),
"protected_domains": sorted(sandbox.PROTECTED_DOMAINS),
}
@router.get("/domains")
def list_domains_readonly(
q: str = "",
_user: auth.DeskUser = Depends(_require_sandbox),
):
return sandbox.list_domains_readonly(q)
@router.get("/domains/{domain}")
def get_domain_readonly(
domain: str,
_user: auth.DeskUser = Depends(_require_sandbox),
):
return sandbox.fetch_domain_readonly(domain)
@router.get("/scenarios")
def list_scenarios(
user: auth.DeskUser = Depends(_require_sandbox),
):
from app.domain_console_sandbox_store import init_schema, list_scenarios as ls
conn = _db()
init_schema(conn)
return {"scenarios": ls(conn), "delete_blocked": True}
@router.post("/scenarios")
def create_scenario(
body: CreateScenarioBody,
user: auth.DeskUser = Depends(_require_sandbox),
):
from app.domain_console_sandbox_store import init_schema
conn = _db()
init_schema(conn)
try:
return sandbox.create_scenario(
conn,
label=body.label,
domain=body.domain,
slug=body.slug,
created_by=user.username,
plan_mock=body.plan_mock,
admin_password=body.admin_password,
)
except sandbox.SandboxForbidden as e:
raise HTTPException(403, str(e)) from e
except sandbox.SandboxError as e:
raise HTTPException(400, str(e)) from e
@router.get("/scenarios/{scenario_id}/state")
def scenario_state(
scenario_id: str,
_user: auth.DeskUser = Depends(_require_sandbox),
):
from app.domain_console_sandbox_store import init_schema
conn = _db()
init_schema(conn)
try:
return sandbox.build_console_state(conn, scenario_id)
except sandbox.SandboxError as e:
raise HTTPException(404, str(e)) from e
@router.post("/scenarios/{scenario_id}/accounts")
def add_account(
scenario_id: str,
body: AddAccountBody,
_user: auth.DeskUser = Depends(_require_sandbox),
):
from app.domain_console_sandbox_store import init_schema
conn = _db()
init_schema(conn)
try:
return sandbox.add_account_to_scenario(
conn,
scenario_id=scenario_id,
local_part=body.local_part,
display_name=body.display_name,
password=body.password,
)
except sandbox.SandboxForbidden as e:
raise HTTPException(403, str(e)) from e
except sandbox.SandboxError as e:
raise HTTPException(400, str(e)) from e
@router.delete("/scenarios/{scenario_id}")
def block_delete_scenario(scenario_id: str, _user: auth.DeskUser = Depends(_require_sandbox)):
try:
sandbox.assert_no_delete(f"delete scenario {scenario_id}")
except sandbox.SandboxForbidden as e:
raise HTTPException(403, str(e)) from e
raise HTTPException(403, "delete bloqueado")
@router.delete("/scenarios/{scenario_id}/accounts/{email}")
def block_delete_account(
scenario_id: str,
email: str,
_user: auth.DeskUser = Depends(_require_sandbox),
):
try:
sandbox.assert_no_delete(f"delete account {email}")
except sandbox.SandboxForbidden as e:
raise HTTPException(403, str(e)) from e
raise HTTPException(403, "delete bloqueado")
@router.post("/scenarios/{scenario_id}/purge")
def block_purge_scenario(scenario_id: str, _user: auth.DeskUser = Depends(_require_sandbox)):
try:
sandbox.assert_no_delete(f"purge scenario {scenario_id}")
except sandbox.SandboxForbidden as e:
raise HTTPException(403, str(e)) from e
raise HTTPException(403, "purge bloqueado")