This commit is contained in:
2026-03-15 19:40:35 -05:00
parent 275c73b584
commit dcac4f135d
17 changed files with 659 additions and 318 deletions

View File

@@ -1,6 +1,7 @@
import { prisma } from "./prisma.js";
const SESSION_DURATION_MS = 12 * 60 * 60 * 1000;
const SESSION_RETENTION_DAYS = 30;
export interface AuthSessionContext {
id: string;
@@ -12,6 +13,10 @@ export function getSessionExpiryDate(now = new Date()) {
return new Date(now.getTime() + SESSION_DURATION_MS);
}
export function getSessionRetentionCutoff(now = new Date()) {
return new Date(now.getTime() - SESSION_RETENTION_DAYS * 24 * 60 * 60 * 1000);
}
export async function createAuthSession(input: { userId: string; ipAddress?: string | null; userAgent?: string | null }) {
return prisma.authSession.create({
data: {
@@ -69,3 +74,27 @@ export async function revokeAuthSession(sessionId: string, input: { revokedById?
},
});
}
export async function pruneOldAuthSessions() {
const cutoff = getSessionRetentionCutoff();
const result = await prisma.authSession.deleteMany({
where: {
OR: [
{
revokedAt: {
lt: cutoff,
},
},
{
revokedAt: null,
expiresAt: {
lt: cutoff,
},
},
],
},
});
return result.count;
}

View File

@@ -146,6 +146,10 @@ function mapAuthSession(
lastName: string;
} | null;
},
reviewContext: {
reviewState: "NORMAL" | "REVIEW";
reviewReasons: string[];
},
currentSessionId?: string
): AdminAuthSessionDto {
const now = Date.now();
@@ -157,6 +161,8 @@ function mapAuthSession(
userEmail: record.user.email,
userName: `${record.user.firstName} ${record.user.lastName}`.trim(),
status,
reviewState: reviewContext.reviewState,
reviewReasons: reviewContext.reviewReasons,
isCurrent: record.id === currentSessionId,
createdAt: record.createdAt.toISOString(),
lastSeenAt: record.lastSeenAt.toISOString(),
@@ -404,7 +410,63 @@ export async function listAdminAuthSessions(currentSessionId?: string | null): P
take: 200,
});
return sessions.map((session) => mapAuthSession(session, currentSessionId ?? undefined));
const now = Date.now();
const activeSessionsByUser = new Map<
string,
Array<{
id: string;
ipAddress: string | null;
userAgent: string | null;
lastSeenAt: Date;
}>
>();
for (const session of sessions) {
const isActive = !session.revokedAt && session.expiresAt.getTime() > now;
if (!isActive) {
continue;
}
const existing = activeSessionsByUser.get(session.userId) ?? [];
existing.push({
id: session.id,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
lastSeenAt: session.lastSeenAt,
});
activeSessionsByUser.set(session.userId, existing);
}
return sessions.map((session) => {
const reviewReasons: string[] = [];
const activeUserSessions = activeSessionsByUser.get(session.userId) ?? [];
const isActive = !session.revokedAt && session.expiresAt.getTime() > now;
const staleThresholdMs = 7 * 24 * 60 * 60 * 1000;
if (isActive && activeUserSessions.length > 1) {
reviewReasons.push("Multiple active sessions");
}
if (isActive) {
const distinctIps = new Set(activeUserSessions.map((entry) => entry.ipAddress).filter(Boolean));
if (distinctIps.size > 1) {
reviewReasons.push("Multiple active IP addresses");
}
if (now - session.lastSeenAt.getTime() > staleThresholdMs) {
reviewReasons.push("Stale active session");
}
}
return mapAuthSession(
session,
{
reviewState: reviewReasons.length > 0 ? "REVIEW" : "NORMAL",
reviewReasons,
},
currentSessionId ?? undefined
);
});
}
export async function revokeAdminAuthSession(sessionId: string, actorId?: string | null) {
@@ -596,6 +658,8 @@ export async function updateAdminUser(userId: string, payload: AdminUserInput, a
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
const startupReport = getLatestStartupReport();
const recentSupportLogs = listSupportLogs(50);
const now = new Date();
const reviewSessions = await listAdminAuthSessions();
const [
companyProfile,
userCount,
@@ -624,7 +688,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
where: {
revokedAt: null,
expiresAt: {
gt: new Date(),
gt: now,
},
},
}),
@@ -667,6 +731,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
userCount,
activeUserCount,
activeSessionCount,
reviewSessionCount: reviewSessions.filter((session) => session.reviewState === "REVIEW").length,
roleCount,
permissionCount,
customerCount,

View File

@@ -1,5 +1,6 @@
import { createApp } from "./app.js";
import { env } from "./config/env.js";
import { pruneOldAuthSessions } from "./lib/auth-sessions.js";
import { bootstrapAppData } from "./lib/bootstrap.js";
import { prisma } from "./lib/prisma.js";
import { setLatestStartupReport } from "./lib/startup-state.js";
@@ -8,6 +9,7 @@ import { recordSupportLog } from "./lib/support-log.js";
async function start() {
await bootstrapAppData();
const prunedSessionCount = await pruneOldAuthSessions();
const startupReport = await assertStartupReadiness();
setLatestStartupReport(startupReport);
@@ -21,6 +23,7 @@ async function start() {
passCount: startupReport.passCount,
warnCount: startupReport.warnCount,
failCount: startupReport.failCount,
prunedSessionCount,
},
});