import { getIamRedis } from "./redis.ts"; /** * Sesiones en Redis (Fase 4): la cookie po_session ya no es un payload * autocontenido firmado con HMAC -- es un id opaco. El estado real vive en * `iam:session:` (hash con TTL) y hay un índice secundario * `iam:user_sessions:` (set de ids) para poder revocar TODAS las * sesiones de un usuario de un golpe (logout real, cambio de password, * bloqueo de tenant) -- algo que el diseño anterior (HMAC stateless) no * podía hacer sin esperar a que expirara la cookie. */ export type SessionRealm = "app" | "platform"; export type SessionData = { userId: number; realm: SessionRealm; tenantId: number | null; issuedAt: number; }; const TTL_SECONDS = 60 * 60 * 24 * 7; // 7 días, igual que el diseño anterior function sessionKey(id: string): string { return `iam:session:${id}`; } function userSessionsKey(userId: number, realm: SessionRealm): string { return `iam:user_sessions:${realm}:${userId}`; } function randomId(): string { const bytes = crypto.getRandomValues(new Uint8Array(32)); return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replaceAll( "=", "", ); } export async function createSession( userId: number, realm: SessionRealm, tenantId: number | null, ): Promise { const redis = await getIamRedis(); const id = randomId(); await redis.hSet(sessionKey(id), { userId: String(userId), realm, tenantId: tenantId == null ? "" : String(tenantId), issuedAt: String(Date.now()), }); await redis.expire(sessionKey(id), TTL_SECONDS); await redis.sAdd(userSessionsKey(userId, realm), id); return id; } export async function getSession(id: string): Promise { if (!id) return null; const redis = await getIamRedis(); const raw = await redis.hGetAll(sessionKey(id)); if (!raw || Object.keys(raw).length === 0) return null; // Refresca el TTL en cada acceso (sesión "deslizante", igual comportamiento // que la cookie de 7 días anterior, que se renovaba en cada login). await redis.expire(sessionKey(id), TTL_SECONDS); return { userId: Number(raw.userId), realm: raw.realm === "platform" ? "platform" : "app", tenantId: raw.tenantId ? Number(raw.tenantId) : null, issuedAt: Number(raw.issuedAt), }; } export async function revokeSession(id: string): Promise { if (!id) return; const redis = await getIamRedis(); const raw = await redis.hGetAll(sessionKey(id)); await redis.del(sessionKey(id)); if (raw?.userId) { const realm: SessionRealm = raw.realm === "platform" ? "platform" : "app"; await redis.sRem(userSessionsKey(Number(raw.userId), realm), id); } } /** Revoca TODAS las sesiones activas de un usuario -- logout real al * cambiar password o al bloquear/suspender su tenant. */ export async function revokeAllSessionsForUser( userId: number, realm: SessionRealm, ): Promise { const redis = await getIamRedis(); const key = userSessionsKey(userId, realm); const ids = await redis.sMembers(key); if (ids.length) { await redis.del(ids.map(sessionKey)); } await redis.del(key); }