diff --git a/backend/api/v1/modules/core/auth/routes.py b/backend/api/v1/modules/core/auth/routes.py index 65d35fa8..059d03dc 100644 --- a/backend/api/v1/modules/core/auth/routes.py +++ b/backend/api/v1/modules/core/auth/routes.py @@ -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} diff --git a/backend/api/v1/modules/core/auth/service.py b/backend/api/v1/modules/core/auth/service.py index b64f2eca..6a1ea384 100644 --- a/backend/api/v1/modules/core/auth/service.py +++ b/backend/api/v1/modules/core/auth/service.py @@ -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() +