156 lines
5.3 KiB
JavaScript
156 lines
5.3 KiB
JavaScript
// packages/core/middlewares/src/tenantContext.mjs
|
|
|
|
const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; // schema seguro
|
|
const UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
function redact(obj) {
|
|
// Evita loggear datos sensibles; muestra sólo lo útil para diagnóstico
|
|
if (!obj || typeof obj !== 'object') return obj;
|
|
const out = {};
|
|
for (const k of Object.keys(obj)) {
|
|
if (['token', 'access_token', 'id_token', 'refresh_token'].includes(k)) {
|
|
out[k] = '[redacted]';
|
|
} else if (k === 'sub' || k === 'email' || k === 'name') {
|
|
out[k] = obj[k];
|
|
} else if (k === 'tenant') {
|
|
const t = obj[k] || {};
|
|
out[k] = { id: t.id ?? null, schema: t.schema ?? null };
|
|
} else if (k === 'user') {
|
|
const u = obj[k] || {};
|
|
out[k] = {
|
|
sub: u.sub ?? null,
|
|
email: u.email ?? null,
|
|
default_tenant: u.default_tenant ?? u.defaultTenant ?? null,
|
|
memberships: Array.isArray(u.memberships) ? `[${u.memberships.length}]` : null,
|
|
};
|
|
} else {
|
|
// no inundar el log; deja constancia de que existe
|
|
out[k] = '[present]';
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function tenantContext(opts = {}) {
|
|
const {
|
|
requireUser = true,
|
|
debug = false,
|
|
log = console, // podés inyectar tu logger
|
|
autoDeriveFromDefault = true,
|
|
// callback opcional para buscar tenant (p.ej., en CORE) si no está en sesión
|
|
// Debe devolver { id: uuid, schema: string } o null
|
|
resolveTenant = null,
|
|
schemaPrefixes = [
|
|
process.env.TENANT_SCHEMA_PREFIX || 'empresa_',
|
|
].filter(Boolean),
|
|
} = opts;
|
|
|
|
const diag = (msg, data) => {
|
|
if (!debug) return;
|
|
try { log.debug?.(`[tenantContext] ${msg}`, data !== undefined ? redact(data) : ''); }
|
|
catch { /* noop */ }
|
|
};
|
|
const setDiagHeader = (res, kv) => {
|
|
if (!debug) return;
|
|
const cur = res.getHeader('X-Tenant-Diag');
|
|
const base = typeof cur === 'string' ? String(cur) + '; ' : '';
|
|
res.setHeader('X-Tenant-Diag', base + kv);
|
|
};
|
|
|
|
return async (req, res, next) => {
|
|
try {
|
|
diag('incoming', { sid: req.sessionID, headers: { accept: req.headers.accept } });
|
|
|
|
const sess = req.session;
|
|
if (!sess) {
|
|
setDiagHeader(res, 'no-session');
|
|
return res.status(401).json({ error: 'unauthenticated' });
|
|
}
|
|
diag('session.present', { keys: Object.keys(sess) });
|
|
|
|
if (requireUser && !sess.user?.sub) {
|
|
diag('user.missing', { session: sess });
|
|
setDiagHeader(res, 'no-user');
|
|
return res.status(401).json({ error: 'unauthenticated' });
|
|
}
|
|
if (requireUser) diag('user.ok', sess.user);
|
|
|
|
// 1) Leer tenant desde sesión
|
|
let t = sess.tenant ?? null;
|
|
diag('session.tenant', t);
|
|
|
|
// 2) Derivar automáticamente si falta
|
|
if ((!t?.id || !t?.schema) && autoDeriveFromDefault) {
|
|
const fallbackId =
|
|
sess.user?.tenant?.id ||
|
|
sess.user?.default_tenant ||
|
|
sess.user?.defaultTenant ||
|
|
null;
|
|
|
|
if (fallbackId && UUID_RX.test(String(fallbackId))) {
|
|
const prefix = String(schemaPrefixes[0] || 'empresa_');
|
|
const schema = `${prefix}${String(fallbackId).replace(/-/g, '').toLowerCase()}`;
|
|
t = { id: String(fallbackId), schema };
|
|
sess.tenant = t; // persistir para siguientes requests
|
|
diag('derived.fromDefault', t);
|
|
setDiagHeader(res, 'derived-default');
|
|
} else {
|
|
diag('derived.fromDefault.skipped', { fallbackId });
|
|
}
|
|
}
|
|
|
|
// 3) Resolver con callback si aún falta
|
|
if ((!t?.id || !t?.schema) && typeof resolveTenant === 'function') {
|
|
try {
|
|
t = await resolveTenant(req, sess);
|
|
if (t) {
|
|
sess.tenant = t;
|
|
diag('derived.fromResolver', t);
|
|
setDiagHeader(res, 'derived-resolver');
|
|
} else {
|
|
diag('resolver.returned-null');
|
|
}
|
|
} catch (e) {
|
|
diag('resolver.error', { message: e?.message });
|
|
}
|
|
}
|
|
|
|
// 4) Validaciones
|
|
if (!t?.id || !t?.schema) {
|
|
diag('missing-tenant.final');
|
|
setDiagHeader(res, 'missing-tenant');
|
|
return res.status(401).json({ error: 'Sesión inválida o tenant no seleccionado' });
|
|
}
|
|
if (!UUID_RX.test(String(t.id))) {
|
|
diag('invalid-tenant-id', t);
|
|
setDiagHeader(res, 'bad-tenant-id');
|
|
return res.status(400).json({ error: 'TenantID inválido' });
|
|
}
|
|
if (!VALID_IDENT.test(t.schema)) {
|
|
diag('invalid-schema', t);
|
|
setDiagHeader(res, 'bad-schema');
|
|
return res.status(400).json({ error: 'Schema inválido' });
|
|
}
|
|
const okPrefix = schemaPrefixes.some(p =>
|
|
t.schema.toLowerCase().startsWith(String(p).toLowerCase()),
|
|
);
|
|
if (!okPrefix) {
|
|
diag('schema-prefix.rejected', { schema: t.schema, schemaPrefixes });
|
|
setDiagHeader(res, 'schema-prefix-rejected');
|
|
return res.status(400).json({ error: 'Schema no permitido' });
|
|
}
|
|
|
|
// 5) OK
|
|
req.tenant = { id: String(t.id), schema: String(t.schema) };
|
|
res.locals.tenant = req.tenant;
|
|
setDiagHeader(res, `ok schema=${req.tenant.schema}`);
|
|
diag('attach.req.tenant', req.tenant);
|
|
|
|
return next();
|
|
} catch (err) {
|
|
diag('exception', { message: err?.message });
|
|
return next(err);
|
|
}
|
|
};
|
|
}
|