feat(invites): usar token del usuario actual para crear invite en Hub

Elimina la dependencia de HUB_ADMIN_EMAIL/HUB_ADMIN_PASSWORD del flujo
de invitaciones. Ahora se usa el Bearer token del usuario autenticado,
que debe tener role='admin' en su tenant dentro del Hub.

Esto hace el flujo compatible con instalaciones self-hosted donde el
cliente no debe tener acceso a credenciales del Workspace Hub.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:51:31 -05:00
parent 02a5ac07ed
commit 73aa55646b
2 changed files with 30 additions and 36 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,49 +76,38 @@ 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 = ""
if not settings.HUB_ADMIN_EMAIL or not settings.HUB_ADMIN_PASSWORD:
if not user_access_token:
logger.error(
"[invite] HUB_ADMIN_EMAIL o HUB_ADMIN_PASSWORD no configurados — "
"el invite NO se creará en el Hub y se usará el fallback local. "
"Configura HUB_ADMIN_EMAIL y HUB_ADMIN_PASSWORD en .env"
"[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:
login_resp = await client.post(
f"{settings.HUB_URL}api/v1/auth/login",
json={"username": settings.HUB_ADMIN_EMAIL, "password": settings.HUB_ADMIN_PASSWORD},
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 {user_access_token}"},
)
if login_resp.status_code != 200:
logger.error(
"[invite] Hub login falló: status=%s body=%s"
"verifica HUB_ADMIN_EMAIL=%s y HUB_ADMIN_PASSWORD en .env",
login_resp.status_code,
login_resp.text[:200],
settings.HUB_ADMIN_EMAIL,
)
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", "")
else:
svc_token = login_resp.json().get("access_token", "")
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}"},
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,
)
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", "")
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)