New /api/v1/infra/stack/status probes all stack apps/APIs/SW; Infra UI groups proc-cards by VM; wire vm123 router; menu INFRA COD and Serviços IaaS · Infra as Code labels. Co-authored-by: Cursor <cursoragent@cursor.com>
290 lines
11 KiB
Python
290 lines
11 KiB
Python
"""Rotas VM123 — Spec 027 Fase 3."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app import auth
|
|
from app.permissions import (
|
|
can_access_foss_admin,
|
|
can_create_foss_order,
|
|
can_manage_users,
|
|
can_read_billing,
|
|
)
|
|
from app.vm123.permissions import (
|
|
can_openpanel_autologin,
|
|
can_openpanel_delete,
|
|
can_openpanel_provision,
|
|
)
|
|
from app.platform_role_catalog import catalog_export
|
|
from app.vm123 import foss_client, odoo_client, openpanel_client, openpanel_test, provision, provision_store
|
|
from app.vm123.role_map import PROVISIONABLE_DESK_ROLES
|
|
|
|
router = APIRouter(prefix="/api/v1/vm123", tags=["vm123"])
|
|
|
|
|
|
class FossOrderBody(BaseModel):
|
|
client_id: int | None = None
|
|
domain: str | None = None
|
|
product_id: int | None = None
|
|
note: str | None = None
|
|
|
|
|
|
class ProvisionUserBody(BaseModel):
|
|
desk_username: str = Field(min_length=3)
|
|
desk_role: str | None = None
|
|
|
|
|
|
class OpenPanelProvisionBody(BaseModel):
|
|
username: str | None = None
|
|
password: str = Field(min_length=8)
|
|
email: str = Field(min_length=5)
|
|
domain: str = Field(min_length=3)
|
|
plan_name: str | None = None
|
|
|
|
|
|
class OpenPanelTestAccount(BaseModel):
|
|
username: str = ""
|
|
domain: str = ""
|
|
|
|
|
|
class OpenPanelTestConfirmBody(BaseModel):
|
|
accounts: list[OpenPanelTestAccount] = Field(default_factory=list)
|
|
password: str = Field(default="LbOpenTest805353", min_length=8)
|
|
cleanup: bool = True
|
|
auto_names: bool = True
|
|
check_reference: bool = True
|
|
|
|
|
|
@router.get("/platform/catalog")
|
|
def platform_role_catalog(user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
"""Catálogo mestre função → serviços (padrão Odoo res.groups na plataforma DevOps)."""
|
|
return catalog_export()
|
|
|
|
|
|
@router.get("/health")
|
|
def vm123_health(user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if user.role not in ("super_admin", "devops", "developer"):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
out: dict = {"odoo": {"configured": odoo_client._configured()}}
|
|
try:
|
|
out["odoo"]["role_model_sales_admin"] = odoo_client.list_role_model("sales_admin")
|
|
except Exception as exc:
|
|
out["odoo"]["error"] = str(exc)
|
|
out["foss"] = {"configured": foss_client._configured()}
|
|
out["openpanel"] = openpanel_client.health()
|
|
return out
|
|
|
|
|
|
@router.get("/odoo/role-model/{role}")
|
|
def odoo_role_model(role: str, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_manage_users(user.role) and user.role not in ("devops", "developer"):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
return odoo_client.list_role_model(role)
|
|
except odoo_client.OdooConfigError as exc:
|
|
raise HTTPException(503, str(exc)) from exc
|
|
|
|
|
|
@router.get("/odoo/partner")
|
|
def odoo_partner(email: str = Query(..., min_length=3), user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_read_billing(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
partner = odoo_client.find_partner_by_email(email)
|
|
except odoo_client.OdooConfigError as exc:
|
|
raise HTTPException(503, str(exc)) from exc
|
|
if not partner:
|
|
raise HTTPException(404, "parceiro não encontrado")
|
|
return {
|
|
"partner": partner,
|
|
"login_url": odoo_client.ODOO_PUBLIC_URL,
|
|
}
|
|
|
|
|
|
@router.get("/foss/client/{domain}")
|
|
def foss_client_by_domain(domain: str, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_read_billing(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
client_row = foss_client.find_client_by_domain(domain)
|
|
except foss_client.FossConfigError as exc:
|
|
raise HTTPException(503, str(exc)) from exc
|
|
if not client_row:
|
|
raise HTTPException(404, "cliente FOSS não encontrado")
|
|
return {
|
|
"client": client_row,
|
|
"admin_url": foss_client.FOSS_PUBLIC_ADMIN,
|
|
"can_order": can_create_foss_order(user.role),
|
|
"can_admin": can_access_foss_admin(user.role),
|
|
}
|
|
|
|
|
|
@router.post("/foss/order")
|
|
def foss_create_order(body: FossOrderBody, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_create_foss_order(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
if not body.client_id and not body.domain:
|
|
raise HTTPException(400, "informe client_id ou domain")
|
|
# MVP: delegar criação real à UI FOSS até mapear product_id
|
|
return {
|
|
"accepted": True,
|
|
"message": "Pedido registado — criação FOSS via Admin até product_id estar mapeado",
|
|
"payload": body.model_dump(),
|
|
"foss_admin": foss_client.FOSS_PUBLIC_ADMIN,
|
|
}
|
|
|
|
|
|
@router.post("/openpanel/autologin/{username}")
|
|
def openpanel_autologin(username: str, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_openpanel_autologin(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
return openpanel_client.autologin_payload(username)
|
|
|
|
|
|
@router.get("/openpanel/users")
|
|
def openpanel_list_users(user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_read_billing(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
return openpanel_client.list_users()
|
|
except openpanel_client.OpenPanelBridgeError as exc:
|
|
raise HTTPException(503, str(exc)) from exc
|
|
|
|
|
|
@router.get("/openpanel/users/{username}")
|
|
def openpanel_get_user(username: str, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_read_billing(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
return openpanel_client.get_user(username)
|
|
except openpanel_client.OpenPanelBridgeError as exc:
|
|
raise HTTPException(503, str(exc)) from exc
|
|
|
|
|
|
@router.post("/openpanel/provision")
|
|
def openpanel_provision(body: OpenPanelProvisionBody, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_openpanel_provision(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
return openpanel_client.provision_user(
|
|
username=body.username or "",
|
|
password=body.password,
|
|
email=body.email,
|
|
domain=body.domain,
|
|
plan_name=body.plan_name,
|
|
)
|
|
except openpanel_client.OpenPanelBridgeError as exc:
|
|
raise HTTPException(502, str(exc)) from exc
|
|
|
|
|
|
@router.delete("/openpanel/users/{username}")
|
|
def openpanel_delete_user(username: str, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_openpanel_delete(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
try:
|
|
return openpanel_client.delete_user(username)
|
|
except openpanel_client.OpenPanelBridgeError as exc:
|
|
raise HTTPException(502, str(exc)) from exc
|
|
|
|
|
|
@router.post("/openpanel/test-confirm")
|
|
def openpanel_test_confirm(
|
|
body: OpenPanelTestConfirmBody | None = None,
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
):
|
|
"""Suite de confirmação Spec 028 — multidomínio via Desk API → bridge → opencli."""
|
|
if user.role not in ("super_admin", "devops", "developer"):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
if not openpanel_client.bridge_configured():
|
|
raise HTTPException(503, "OPENPANEL_BRIDGE_TOKEN não configurado")
|
|
payload = body or OpenPanelTestConfirmBody()
|
|
accounts = [a.model_dump() for a in payload.accounts if a.username or a.domain]
|
|
return openpanel_test.run_confirmation_test(
|
|
triggered_by=user.username,
|
|
accounts=accounts or None,
|
|
password=payload.password,
|
|
cleanup=payload.cleanup,
|
|
auto_names=payload.auto_names,
|
|
check_reference=payload.check_reference,
|
|
)
|
|
|
|
|
|
@router.get("/identity/{desk_username}")
|
|
def get_identity_map(desk_username: str, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_manage_users(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
with auth.db() as conn:
|
|
row = provision_store.get_map(conn, desk_username)
|
|
if not row:
|
|
raise HTTPException(404, "sem registo VM123")
|
|
return row
|
|
|
|
|
|
@router.post("/provision/user")
|
|
def provision_user(body: ProvisionUserBody, user: auth.DeskUser = Depends(auth.get_current_user)):
|
|
if not can_manage_users(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
with auth.db() as conn:
|
|
urow = conn.execute(
|
|
"SELECT username, role, display_name, email FROM desk_users WHERE username = ?",
|
|
(body.desk_username.strip().lower(),),
|
|
).fetchone()
|
|
if not urow:
|
|
raise HTTPException(404, "utilizador Desk não encontrado")
|
|
role = body.desk_role or urow["role"]
|
|
if role not in PROVISIONABLE_DESK_ROLES:
|
|
raise HTTPException(400, f"role {role} não provisionável")
|
|
email = urow["email"] or urow["username"]
|
|
result = provision.provision_desk_user(
|
|
conn,
|
|
desk_username=urow["username"],
|
|
desk_role=role,
|
|
display_name=urow["display_name"] or email,
|
|
email=email,
|
|
)
|
|
return result
|
|
|
|
|
|
@router.get("/links/client")
|
|
def client_deep_links(
|
|
domain: str = Query(..., min_length=3),
|
|
email: str = "",
|
|
user: auth.DeskUser = Depends(auth.get_current_user),
|
|
):
|
|
"""Deep-links drawer «Conta do cliente» — Spec 023 + 027."""
|
|
if not can_read_billing(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
links = {
|
|
"domain": domain.strip().lower(),
|
|
"foss": {"url": foss_client.FOSS_PUBLIC_ADMIN, "label": "FOSSBilling Admin"},
|
|
"odoo": {"url": odoo_client.ODOO_PUBLIC_URL, "label": "Odoo ligbox"},
|
|
"openpanel": {"url": openpanel_client.OPENADMIN_URL, "label": "OpenAdmin"},
|
|
}
|
|
out: dict = {"links": links, "role": user.role}
|
|
if foss_client._configured():
|
|
try:
|
|
fc = foss_client.find_client_by_domain(domain)
|
|
if fc:
|
|
out["foss"]["client_id"] = fc.get("id")
|
|
out["foss"]["client_email"] = fc.get("email")
|
|
except Exception:
|
|
pass
|
|
bill_email = (email or "").strip()
|
|
if bill_email and odoo_client._configured():
|
|
try:
|
|
partner = odoo_client.find_partner_by_email(bill_email)
|
|
if partner:
|
|
out["odoo"]["partner_id"] = partner.get("id")
|
|
out["odoo"]["partner_name"] = partner.get("name")
|
|
except Exception:
|
|
pass
|
|
out["permissions"] = {
|
|
"can_order": can_create_foss_order(user.role),
|
|
"can_foss_admin": can_access_foss_admin(user.role),
|
|
"can_openpanel_autologin": can_openpanel_autologin(user.role),
|
|
"can_openpanel_provision": can_openpanel_provision(user.role),
|
|
}
|
|
links["openpanel"]["bridge_ok"] = openpanel_client.health().get("ok", False)
|
|
return out
|