Cria produto FOSS (product_id=3), endpoint mail-bundle no wizard VM112 com contrato documentado, e runbook de deploy para fechar o ciclo de activação. Co-authored-by: Cursor <cursoragent@cursor.com>
75 lines
2.8 KiB
Python
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"}}
|