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>
175 lines
5.1 KiB
Python
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")
|