diff --git a/backend/api/v1/modules/core/invites/routes.py b/backend/api/v1/modules/core/invites/routes.py index 9a9225fd..a134440a 100644 --- a/backend/api/v1/modules/core/invites/routes.py +++ b/backend/api/v1/modules/core/invites/routes.py @@ -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, ) diff --git a/backend/api/v1/modules/core/invites/service.py b/backend/api/v1/modules/core/invites/service.py index 447ad72d..9a138f70 100644 --- a/backend/api/v1/modules/core/invites/service.py +++ b/backend/api/v1/modules/core/invites/service.py @@ -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: