Merge pull request 'fix/redirect-invitation-users' (#391) from fix/redirect-invitation-users into development
Reviewed-on: ADUANASOFT/anexo76#391
This commit is contained in:
@@ -220,6 +220,11 @@ async def lazy_link(
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
claims = service._decode_kc_user_from_token(credentials.credentials)
|
||||
service._backfill_company_roles(claims.get("sub", ""))
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
@@ -146,6 +146,16 @@ class AuthService:
|
||||
except Exception as exc:
|
||||
logger.warning("Lazy-link invite check failed (non-blocking): %s", exc)
|
||||
|
||||
# Backfill: crear UserCompanyRole faltantes para usuarios ya registrados
|
||||
try:
|
||||
login_sub = data.get("sub") or data.get("user_id")
|
||||
if not login_sub:
|
||||
claims = self._decode_kc_user_from_token(data.get("access_token", ""))
|
||||
login_sub = claims.get("sub")
|
||||
self._backfill_company_roles(login_sub)
|
||||
except Exception as exc:
|
||||
logger.warning("backfill_company_roles failed on login (non-blocking): %s", exc)
|
||||
|
||||
# Sync de perfil/avatar desde Workspace usando el mismo bearer.
|
||||
# No bloquea login si Workspace no responde.
|
||||
access_token = data.get("access_token")
|
||||
@@ -470,7 +480,7 @@ class AuthService:
|
||||
logger.error("Hub admin create error during invite flow: %s", exc)
|
||||
raise HTTPException(status_code=503, detail="Error al crear usuario en el sistema de autenticación")
|
||||
|
||||
# 4. Crear fila UserTenant local
|
||||
# 4. Crear fila UserTenant y UserCompanyRole local
|
||||
if hub_user_id and invite_result.company_id:
|
||||
try:
|
||||
ut = UserTenant(
|
||||
@@ -483,11 +493,47 @@ class AuthService:
|
||||
last_name=register_data.last_name,
|
||||
)
|
||||
self.db.add(ut)
|
||||
self.db.commit()
|
||||
self.db.flush()
|
||||
except Exception as exc:
|
||||
logger.warning("Could not create UserTenant (may already exist): %s", exc)
|
||||
self.db.rollback()
|
||||
|
||||
# Asignar UserCompanyRole para que el usuario tenga permisos resueltos
|
||||
try:
|
||||
from api.v1.modules.core.permissions.models import CompanyRole, UserCompanyRole
|
||||
company_role_obj = (
|
||||
self.db.query(CompanyRole)
|
||||
.filter(
|
||||
CompanyRole.code == invite_result.role,
|
||||
CompanyRole.company_id == invite_result.company_id,
|
||||
CompanyRole.is_active == True,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if company_role_obj:
|
||||
existing_ucr = (
|
||||
self.db.query(UserCompanyRole)
|
||||
.filter(
|
||||
UserCompanyRole.user_id == hub_user_id,
|
||||
UserCompanyRole.company_role_id == company_role_obj.id,
|
||||
UserCompanyRole.company_id == invite_result.company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not existing_ucr:
|
||||
ucr = UserCompanyRole(
|
||||
user_id=hub_user_id,
|
||||
company_role_id=company_role_obj.id,
|
||||
company_id=invite_result.company_id,
|
||||
tenant_id=tenant.id,
|
||||
is_active=True,
|
||||
)
|
||||
self.db.add(ucr)
|
||||
self.db.commit()
|
||||
except Exception as exc:
|
||||
logger.warning("Could not create UserCompanyRole for invited user: %s", exc)
|
||||
self.db.rollback()
|
||||
|
||||
# 5. Consumir invite token
|
||||
invite_service.consume_by_id(invite_result.invite_id)
|
||||
|
||||
@@ -509,7 +555,19 @@ class AuthService:
|
||||
json=exchange_data.model_dump()
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return TokenResponseDTO(**response.json())
|
||||
data = response.json()
|
||||
# Lazy-link: crear UserTenant si hay invite pendiente
|
||||
try:
|
||||
await self._link_pending_invite("", access_token=data.get("access_token", ""))
|
||||
except Exception as exc:
|
||||
logger.warning("exchange_code lazy-link failed (non-blocking): %s", exc)
|
||||
# Backfill: crear UserCompanyRole faltantes para usuarios ya registrados
|
||||
try:
|
||||
ec_claims = self._decode_kc_user_from_token(data.get("access_token", ""))
|
||||
self._backfill_company_roles(ec_claims.get("sub") or data.get("sub"))
|
||||
except Exception as exc:
|
||||
logger.warning("backfill_company_roles failed on exchange_code (non-blocking): %s", exc)
|
||||
return TokenResponseDTO(**data)
|
||||
raise HTTPException(status_code=response.status_code, detail="Code exchange failed")
|
||||
except Exception as e:
|
||||
logger.error(f"Exchange code error: {str(e)}")
|
||||
@@ -545,6 +603,17 @@ class AuthService:
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# Lazy-link: crear UserTenant si hay invite pendiente (usuario registrado vía workspace)
|
||||
try:
|
||||
await self._link_pending_invite("", access_token=data.get("access_token", ""))
|
||||
except Exception as exc:
|
||||
logger.warning("sso_exchange lazy-link failed (non-blocking): %s", exc)
|
||||
# Backfill: crear UserCompanyRole faltantes para usuarios ya registrados
|
||||
try:
|
||||
sso_claims = self._decode_kc_user_from_token(data.get("access_token", ""))
|
||||
self._backfill_company_roles(sso_claims.get("sub") or data.get("sub"))
|
||||
except Exception as exc:
|
||||
logger.warning("backfill_company_roles failed on sso_exchange (non-blocking): %s", exc)
|
||||
return TokenResponseDTO(
|
||||
access_token=data["access_token"],
|
||||
refresh_token=data["refresh_token"],
|
||||
@@ -580,11 +649,10 @@ class AuthService:
|
||||
hub_user_id = None
|
||||
user_email = username_or_email
|
||||
|
||||
# Si tenemos el access_token, extraer info del JWT directamente
|
||||
# Si tenemos el access_token, extraer info del JWT directamente (sin red)
|
||||
if access_token:
|
||||
try:
|
||||
from core.security import verify_token
|
||||
claims = await verify_token(access_token)
|
||||
claims = self._decode_kc_user_from_token(access_token)
|
||||
hub_user_id = claims.get("sub")
|
||||
user_email = claims.get("email") or username_or_email
|
||||
except Exception as exc:
|
||||
@@ -592,6 +660,8 @@ class AuthService:
|
||||
|
||||
# Sin access_token: buscar usuario en el Hub vía service account
|
||||
if not hub_user_id:
|
||||
if not user_email:
|
||||
return # Sin email ni hub_user_id no podemos buscar el invite
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
login_resp = await client.post(
|
||||
@@ -669,8 +739,7 @@ class AuthService:
|
||||
is_active=True,
|
||||
)
|
||||
self.db.add(ut)
|
||||
pending.used_at = now
|
||||
self.db.commit()
|
||||
self.db.flush()
|
||||
logger.info(
|
||||
"Lazy-link: UserTenant created for user=%s tenant=%s role=%s",
|
||||
hub_user_id,
|
||||
@@ -681,3 +750,108 @@ class AuthService:
|
||||
logger.warning("_link_pending_invite: could not create UserTenant: %s", exc)
|
||||
self.db.rollback()
|
||||
|
||||
# Asignar UserCompanyRole para que el usuario tenga permisos resueltos
|
||||
if pending.company_id:
|
||||
try:
|
||||
from api.v1.modules.core.permissions.models import CompanyRole, UserCompanyRole
|
||||
company_role_obj = (
|
||||
self.db.query(CompanyRole)
|
||||
.filter(
|
||||
CompanyRole.code == pending.role,
|
||||
CompanyRole.company_id == pending.company_id,
|
||||
CompanyRole.is_active == True,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if company_role_obj:
|
||||
existing_ucr = (
|
||||
self.db.query(UserCompanyRole)
|
||||
.filter(
|
||||
UserCompanyRole.user_id == hub_user_id,
|
||||
UserCompanyRole.company_role_id == company_role_obj.id,
|
||||
UserCompanyRole.company_id == pending.company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not existing_ucr:
|
||||
ucr = UserCompanyRole(
|
||||
user_id=hub_user_id,
|
||||
company_role_id=company_role_obj.id,
|
||||
company_id=pending.company_id,
|
||||
tenant_id=tenant.id,
|
||||
is_active=True,
|
||||
)
|
||||
self.db.add(ucr)
|
||||
except Exception as exc:
|
||||
logger.warning("_link_pending_invite: could not create UserCompanyRole: %s", exc)
|
||||
|
||||
pending.used_at = now
|
||||
self.db.commit()
|
||||
|
||||
def _backfill_company_roles(self, hub_user_id: str) -> None:
|
||||
"""
|
||||
Self-healing: para usuarios ya registrados vía invitación que tienen UserTenant
|
||||
pero no UserCompanyRole (creados antes del fix del flujo de invitación).
|
||||
Por cada UserTenant activo con role y company_id busca el CompanyRole y crea
|
||||
el UserCompanyRole si no existe. Non-blocking.
|
||||
"""
|
||||
if not hub_user_id:
|
||||
return
|
||||
try:
|
||||
from api.v1.modules.core.user_tenant.models import UserTenant
|
||||
from api.v1.modules.core.permissions.models import CompanyRole, UserCompanyRole
|
||||
|
||||
user_tenants = (
|
||||
self.db.query(UserTenant)
|
||||
.filter(
|
||||
UserTenant.keycloak_user_id == hub_user_id,
|
||||
UserTenant.is_active == True,
|
||||
UserTenant.company_id.isnot(None),
|
||||
UserTenant.role.isnot(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
changed = False
|
||||
for ut in user_tenants:
|
||||
company_role_obj = (
|
||||
self.db.query(CompanyRole)
|
||||
.filter(
|
||||
CompanyRole.code == ut.role,
|
||||
CompanyRole.company_id == ut.company_id,
|
||||
CompanyRole.is_active == True,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not company_role_obj:
|
||||
continue
|
||||
|
||||
existing = (
|
||||
self.db.query(UserCompanyRole)
|
||||
.filter(
|
||||
UserCompanyRole.user_id == hub_user_id,
|
||||
UserCompanyRole.company_role_id == company_role_obj.id,
|
||||
UserCompanyRole.company_id == ut.company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not existing:
|
||||
self.db.add(UserCompanyRole(
|
||||
user_id=hub_user_id,
|
||||
company_role_id=company_role_obj.id,
|
||||
company_id=ut.company_id,
|
||||
tenant_id=ut.tenant_id,
|
||||
is_active=True,
|
||||
))
|
||||
changed = True
|
||||
logger.info(
|
||||
"backfill: UserCompanyRole created for user=%s company=%s role=%s",
|
||||
hub_user_id, ut.company_id, ut.role,
|
||||
)
|
||||
|
||||
if changed:
|
||||
self.db.commit()
|
||||
except Exception as exc:
|
||||
logger.warning("_backfill_company_roles failed (non-blocking): %s", exc)
|
||||
self.db.rollback()
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import logging
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from core.config import settings
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -37,12 +36,10 @@ async def create_invite(
|
||||
|
||||
tenant_slug: str = current_user.get("tenant_slug") or ""
|
||||
created_by: str = current_user.get("sub") or ""
|
||||
base_url = settings.APP_PUBLIC_URL.rstrip("/")
|
||||
|
||||
service = InviteService(db)
|
||||
return await service.create_invite(
|
||||
data=data,
|
||||
created_by=created_by,
|
||||
tenant_slug=tenant_slug,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
@@ -45,7 +45,6 @@ class InviteService:
|
||||
data: CreateInviteDTO,
|
||||
created_by: str,
|
||||
tenant_slug: str,
|
||||
base_url: str,
|
||||
) -> InviteResponseDTO:
|
||||
import httpx
|
||||
from api.v1.modules.core.tenants.models import Tenant
|
||||
@@ -99,10 +98,11 @@ class InviteService:
|
||||
except Exception as exc:
|
||||
logger.warning("Hub invite creation failed (non-blocking): %s", exc)
|
||||
|
||||
# Fallback: URL local si el Hub falló
|
||||
# Fallback: URL del workspace (Hub) si la creación de invitación en Hub falló
|
||||
if not invite_url:
|
||||
hub_base = settings.HUB_URL.rstrip("/")
|
||||
invite_url = (
|
||||
f"{base_url}/register"
|
||||
f"{hub_base}/register"
|
||||
f"?invite_token={token_plain}"
|
||||
f"&tenant={tenant_slug}"
|
||||
f"&email={data.email}"
|
||||
|
||||
Reference in New Issue
Block a user