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
This commit is contained in:
Horux Dev
2026-04-28 04:24:16 +00:00
parent 9e9e874dc5
commit 2896eea4f2
2 changed files with 74 additions and 14 deletions

View File

@@ -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) {

View File

@@ -130,15 +130,42 @@ export async function login(data: LoginRequest): Promise<LoginResponse> {
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<LoginResponse> {
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,
};