obsidian-vault/ligbox-ops-platform/projects/wizard/backend/app/routers/internal_provision.py
Ligbox Obsidian Vault e5564c15b9 Obsidian: Spec 043 mail-bundle + FOSS ligbox-mail-business
Sync contratos, runbooks VM112/VM123, código wizard e VM112.md com deploy validado.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 16:54:41 +00:00

75 lines
2.8 KiB
Python

"""Rotas internas VM112 — Desk/FOSS (Spec 035/043). Auth: X-Ops-Internal-Token."""
from __future__ import annotations
import os
from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel, Field
from app.services import activity_log, mail_bundle
from app.services.carbonio import CarbonioError
from app.services.domain_format import invalid_domain_http_detail, normalize_domain, validate_primary_domain
router = APIRouter(prefix="/internal/provision", tags=["internal-provision"])
OPS_INTERNAL_TOKEN = os.getenv("OPS_INTERNAL_TOKEN", "")
def _require_internal(x_ops_internal_token: str | None = Header(default=None, alias="X-Ops-Internal-Token")):
if not OPS_INTERNAL_TOKEN:
raise HTTPException(503, "OPS_INTERNAL_TOKEN não configurado no wizard")
if x_ops_internal_token != OPS_INTERNAL_TOKEN:
raise HTTPException(401, "token interno inválido")
return True
class MailBundleRequest(BaseModel):
domain: str = Field(..., min_length=3, max_length=253)
admin_email: str = Field(..., min_length=5)
admin_name: str | None = Field(None, max_length=120)
seats: int = Field(25, ge=1, le=500)
mail_gb_per_seat: int = Field(30, ge=1, le=500)
files_gb_per_seat: int = Field(200, ge=1, le=2000)
foss_order_id: str | int | None = None
@router.post("/mail-bundle", dependencies=[Depends(_require_internal)])
def post_mail_bundle(body: MailBundleRequest):
"""
Provisiona domínio + conta gerente Carbonio com quotas do bundle mail.
Chamado pelo Desk (Spec 043) após FOSS/OpenPanel/Odoo.
"""
domain = normalize_domain(body.domain)
try:
validate_primary_domain(domain)
except ValueError as e:
raise HTTPException(400, detail=invalid_domain_http_detail()) from e
try:
result = mail_bundle.provision_mail_bundle(
domain=domain,
admin_email=body.admin_email.strip().lower(),
admin_name=body.admin_name,
seats=body.seats,
mail_gb_per_seat=body.mail_gb_per_seat,
files_gb_per_seat=body.files_gb_per_seat,
foss_order_id=body.foss_order_id,
)
safe = {k: v for k, v in result.items() if k not in ("password",)}
return {"ok": True, **safe}
except ValueError as e:
raise HTTPException(400, str(e)) from e
except CarbonioError as e:
activity_log.error(f"mail-bundle falhou: {e}", source="internal")
raise HTTPException(502, str(e)) from e
@router.get("/mail-bundle/{domain}", dependencies=[Depends(_require_internal)])
def get_mail_bundle(domain: str):
"""Estado do bundle (sem senhas)."""
domain = normalize_domain(domain)
data = mail_bundle.load_bundle(domain)
if not data:
raise HTTPException(404, "bundle não provisionado")
return {"ok": True, **{k: v for k, v in data.items() if k != "password"}}