Merge pull request 'fix(invites): logging detallado cuando Hub invite creation falla' (#464) from fix/hub-invite-credentials into development

Reviewed-on: ADUANASOFT/anexo76#464
This commit is contained in:
2026-05-29 21:16:31 +00:00
2 changed files with 32 additions and 15 deletions

View File

@@ -4,13 +4,15 @@ import logging
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, Depends, Request
from fastapi.security import HTTPBearer
from sqlalchemy.orm import Session
from .dto import CreateInviteDTO, InviteResponseDTO
from .service import InviteService
router = APIRouter(prefix="/invites", tags=["Invites"])
_bearer = HTTPBearer()
logger = logging.getLogger(__name__)
@@ -21,12 +23,13 @@ async def create_invite(
request: Request,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
credentials=Depends(_bearer),
):
"""
Genera un token de invitación para que un usuario externo se registre.
Requiere permiso user.create.
Requiere permiso user.create. Usa el token del usuario actual para crear
el invite en el Hub — no requiere credenciales de hub_admin.
"""
# Validar acceso (user.create) y obtener tenant_id
validate_access_to_resource(
db,
data.company_id,
@@ -42,4 +45,5 @@ async def create_invite(
data=data,
created_by=created_by,
tenant_slug=tenant_slug,
user_access_token=credentials.credentials,
)

View File

@@ -45,6 +45,7 @@ class InviteService:
data: CreateInviteDTO,
created_by: str,
tenant_slug: str,
user_access_token: str = "",
) -> InviteResponseDTO:
import httpx
from api.v1.modules.core.tenants.models import Tenant
@@ -75,28 +76,40 @@ class InviteService:
if not company_role:
raise HTTPException(status_code=404, detail="Rol no encontrado")
# Crear invite en el Hub para que el usuario use el form del workspace
# Crear invite en el Hub usando el token del usuario actual.
# El usuario debe tener role='admin' en su tenant dentro del Hub.
# No se requieren credenciales de hub_admin — sin secretos en el .env del cliente.
hub_invite_token: Optional[str] = None
invite_url: str = ""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
login_resp = await client.post(
f"{settings.HUB_URL}api/v1/auth/login",
json={"username": settings.HUB_ADMIN_EMAIL, "password": settings.HUB_ADMIN_PASSWORD},
)
if login_resp.status_code == 200:
svc_token = login_resp.json().get("access_token", "")
if not user_access_token:
logger.error(
"[invite] user_access_token vacío — no se puede crear el invite en el Hub. "
"tenant=%s email=%s",
tenant_slug,
data.email,
)
else:
try:
async with httpx.AsyncClient(timeout=10.0) as client:
hub_resp = await client.post(
f"{settings.HUB_URL}api/v1/hub/invites",
json={"email": str(data.email), "tenant_slug": tenant_slug},
headers={"Authorization": f"Bearer {svc_token}"},
headers={"Authorization": f"Bearer {user_access_token}"},
)
if hub_resp.status_code in (200, 201):
hub_data = hub_resp.json()
hub_invite_token = hub_data.get("invite_token") or _extract_token_from_url(hub_data.get("invite_url", ""))
invite_url = hub_data.get("invite_url", "")
except Exception as exc:
logger.warning("Hub invite creation failed (non-blocking): %s", exc)
else:
logger.error(
"[invite] Hub invite creation falló: status=%s body=%s tenant=%s email=%s",
hub_resp.status_code,
hub_resp.text[:300],
tenant_slug,
data.email,
)
except Exception as exc:
logger.error("[invite] Hub invite creation excepción (non-blocking): %s", exc)
# Fallback: URL del workspace (Hub) si la creación de invitación en Hub falló
if not invite_url: