From 2896eea4f2ac26f372fcfaedd607700ce4efd6aa Mon Sep 17 00:00:00 2001 From: Horux Dev Date: Tue, 28 Apr 2026 04:24:16 +0000 Subject: [PATCH] fix: allow platform_admin/platform_ti login without active tenant memberships - login(): check platformRoles before membership validation; fallback to first active tenant for superset admins - refreshTokens(): allow refresh for superset admins even without memberships - tenantMiddleware: skip tenantPool resolution when tenantId is empty for superset admins --- apps/api/src/middlewares/tenant.middleware.ts | 11 +++ apps/api/src/services/auth.service.ts | 77 +++++++++++++++---- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/apps/api/src/middlewares/tenant.middleware.ts b/apps/api/src/middlewares/tenant.middleware.ts index 8ed5c81..0aa7208 100644 --- a/apps/api/src/middlewares/tenant.middleware.ts +++ b/apps/api/src/middlewares/tenant.middleware.ts @@ -2,6 +2,7 @@ import type { Request, Response, NextFunction } from 'express'; import type { Pool } from 'pg'; import { prisma, tenantDb } from '../config/database.js'; import { isGlobalAdmin } from '../utils/global-admin.js'; +import { hasAnyPlatformRole } from '../utils/platform-admin.js'; import { decryptAesGcm, deriveAesKey } from '@horux/core'; import { env } from '../config/env.js'; @@ -68,6 +69,16 @@ export async function tenantMiddleware(req: Request, res: Response, next: NextFu let tenantId = req.user.tenantId; + // Si el tenantId está vacío o no es válido, verificar si es superset admin + // (platform_admin o platform_ti). En ese caso, permitir continuar sin tenantPool. + if (!tenantId || tenantId === '') { + const isSuperset = await hasAnyPlatformRole(req.user.userId, 'platform_admin', 'platform_ti'); + if (isSuperset) { + return next(); + } + return res.status(404).json({ message: 'Tenant no encontrado' }); + } + // Admin impersonation via X-View-Tenant header (global admin only) const viewTenantHeader = req.headers['x-view-tenant'] as string; if (viewTenantHeader) { diff --git a/apps/api/src/services/auth.service.ts b/apps/api/src/services/auth.service.ts index 4e14d68..c1f984f 100644 --- a/apps/api/src/services/auth.service.ts +++ b/apps/api/src/services/auth.service.ts @@ -130,15 +130,42 @@ export async function login(data: LoginRequest): Promise { include: { tenant: true, rol: true }, orderBy: { joinedAt: 'asc' }, }); - if (allMemberships.length === 0) { + + const [platformRoles, tenants] = await Promise.all([ + getPlatformRoles(user.id), + getUserTenants(user.id), + ]); + + const isSupersetAdmin = platformRoles.some(r => r === 'platform_admin' || r === 'platform_ti'); + + if (allMemberships.length === 0 && !isSupersetAdmin) { throw new AppError(401, 'No tienes acceso a ninguna empresa activa'); } + const preferred = user.lastTenantId ? allMemberships.find(m => m.tenantId === user.lastTenantId) : null; - const activeMembership = preferred ?? allMemberships[0]; - const activeTenant = activeMembership.tenant; - const activeRole = activeMembership.rol.nombre as Role; + let activeMembership = preferred ?? allMemberships[0]; + + // Si no hay membership pero es superset admin, buscar un tenant activo como contexto + let activeTenant = activeMembership?.tenant; + let activeRole = activeMembership?.rol.nombre as Role; + + if (!activeTenant && isSupersetAdmin) { + const fallbackTenant = await prisma.tenant.findFirst({ + where: { active: true }, + orderBy: { createdAt: 'asc' }, + }); + if (fallbackTenant) { + activeTenant = fallbackTenant; + activeRole = 'owner'; + } + } + + // Si aún no hay tenant y no es superset, error + if (!activeTenant) { + throw new AppError(401, 'No tienes acceso a ninguna empresa activa'); + } await prisma.user.update({ where: { id: user.id }, @@ -152,11 +179,6 @@ export async function login(data: LoginRequest): Promise { metadata: { email: user.email, tenantRfc: activeTenant.rfc }, }); - const [platformRoles, tenants] = await Promise.all([ - getPlatformRoles(user.id), - getUserTenants(user.id), - ]); - const tokenPayload = { userId: user.id, email: user.email, @@ -235,20 +257,47 @@ export async function refreshTokens(token: string): Promise<{ accessToken: strin orderBy: { joinedAt: 'asc' }, }); } - if (!activeMembership) { + + const platformRoles = await getPlatformRoles(user.id); + const isSupersetAdmin = platformRoles.some(r => r === 'platform_admin' || r === 'platform_ti'); + + // Si no hay membership pero es superset admin, mantener o buscar contexto + let activeTenantId = activeMembership?.tenantId ?? payload.tenantId; + let activeRole = activeMembership?.rol.nombre as Role; + + if (!activeMembership && isSupersetAdmin) { + const tenantExists = await tx.tenant.findUnique({ + where: { id: payload.tenantId }, + select: { id: true, active: true }, + }); + if (tenantExists?.active) { + activeTenantId = payload.tenantId; + activeRole = payload.role as Role; + } else { + const fallbackTenant = await tx.tenant.findFirst({ + where: { active: true }, + orderBy: { createdAt: 'asc' }, + select: { id: true }, + }); + if (fallbackTenant) { + activeTenantId = fallbackTenant.id; + activeRole = 'owner'; + } + } + } + + if (!activeMembership && !isSupersetAdmin) { throw new AppError(401, 'No tienes acceso a ninguna empresa activa'); } // Use deleteMany to avoid error if already deleted (race condition) await tx.refreshToken.deleteMany({ where: { id: storedToken.id } }); - const platformRoles = await getPlatformRoles(user.id); - const newTokenPayload = { userId: user.id, email: user.email, - role: activeMembership.rol.nombre as Role, - tenantId: activeMembership.tenantId, + role: activeRole, + tenantId: activeTenantId, platformRoles, tokenVersion: user.tokenVersion, };