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:
@@ -2,6 +2,7 @@ import type { Request, Response, NextFunction } from 'express';
|
|||||||
import type { Pool } from 'pg';
|
import type { Pool } from 'pg';
|
||||||
import { prisma, tenantDb } from '../config/database.js';
|
import { prisma, tenantDb } from '../config/database.js';
|
||||||
import { isGlobalAdmin } from '../utils/global-admin.js';
|
import { isGlobalAdmin } from '../utils/global-admin.js';
|
||||||
|
import { hasAnyPlatformRole } from '../utils/platform-admin.js';
|
||||||
import { decryptAesGcm, deriveAesKey } from '@horux/core';
|
import { decryptAesGcm, deriveAesKey } from '@horux/core';
|
||||||
import { env } from '../config/env.js';
|
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;
|
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)
|
// Admin impersonation via X-View-Tenant header (global admin only)
|
||||||
const viewTenantHeader = req.headers['x-view-tenant'] as string;
|
const viewTenantHeader = req.headers['x-view-tenant'] as string;
|
||||||
if (viewTenantHeader) {
|
if (viewTenantHeader) {
|
||||||
|
|||||||
@@ -130,15 +130,42 @@ export async function login(data: LoginRequest): Promise<LoginResponse> {
|
|||||||
include: { tenant: true, rol: true },
|
include: { tenant: true, rol: true },
|
||||||
orderBy: { joinedAt: 'asc' },
|
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');
|
throw new AppError(401, 'No tienes acceso a ninguna empresa activa');
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferred = user.lastTenantId
|
const preferred = user.lastTenantId
|
||||||
? allMemberships.find(m => m.tenantId === user.lastTenantId)
|
? allMemberships.find(m => m.tenantId === user.lastTenantId)
|
||||||
: null;
|
: null;
|
||||||
const activeMembership = preferred ?? allMemberships[0];
|
let activeMembership = preferred ?? allMemberships[0];
|
||||||
const activeTenant = activeMembership.tenant;
|
|
||||||
const activeRole = activeMembership.rol.nombre as Role;
|
// 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({
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
@@ -152,11 +179,6 @@ export async function login(data: LoginRequest): Promise<LoginResponse> {
|
|||||||
metadata: { email: user.email, tenantRfc: activeTenant.rfc },
|
metadata: { email: user.email, tenantRfc: activeTenant.rfc },
|
||||||
});
|
});
|
||||||
|
|
||||||
const [platformRoles, tenants] = await Promise.all([
|
|
||||||
getPlatformRoles(user.id),
|
|
||||||
getUserTenants(user.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tokenPayload = {
|
const tokenPayload = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@@ -235,20 +257,47 @@ export async function refreshTokens(token: string): Promise<{ accessToken: strin
|
|||||||
orderBy: { joinedAt: 'asc' },
|
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');
|
throw new AppError(401, 'No tienes acceso a ninguna empresa activa');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use deleteMany to avoid error if already deleted (race condition)
|
// Use deleteMany to avoid error if already deleted (race condition)
|
||||||
await tx.refreshToken.deleteMany({ where: { id: storedToken.id } });
|
await tx.refreshToken.deleteMany({ where: { id: storedToken.id } });
|
||||||
|
|
||||||
const platformRoles = await getPlatformRoles(user.id);
|
|
||||||
|
|
||||||
const newTokenPayload = {
|
const newTokenPayload = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: activeMembership.rol.nombre as Role,
|
role: activeRole,
|
||||||
tenantId: activeMembership.tenantId,
|
tenantId: activeTenantId,
|
||||||
platformRoles,
|
platformRoles,
|
||||||
tokenVersion: user.tokenVersion,
|
tokenVersion: user.tokenVersion,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user