import { redis } from '../redis.js'; import { prisma } from '../db.js'; export interface SecurityEvent { type: 'login_failure' | 'login_success' | 'password_change' | 'password_reset' | 'account_locked' | 'account_unlocked' | 'session_invalidated' | '2fa_enabled' | '2fa_disabled' | '2fa_failure' | 'csrf_failure' | 'rate_limited' | 'suspicious_activity'; userId?: string | null; ip: string; userAgent?: string | null; metadata?: Record; } const SECURITY_EVENT_TTL = 7 * 24 * 60 * 60; function eventKey(ip: string, type: string, windowSeconds: number): string { return `sec:${type}:${ip}`; } export async function logSecurityEvent(event: SecurityEvent): Promise { const payload = JSON.stringify({ ...event, ts: Date.now(), }); await Promise.all([ redis.lpush(`seclog:${event.type}`, payload), redis.ltrim(`seclog:${event.type}`, 0, 999), redis.expire(`seclog:${event.type}`, SECURITY_EVENT_TTL), event.userId ? redis.lpush(`seclog:user:${event.userId}`, payload).then(() => { redis.ltrim(`seclog:user:${event.userId}`, 0, 199); redis.expire(`seclog:user:${event.userId}`, SECURITY_EVENT_TTL); }) : Promise.resolve(), prisma.activityLog.create({ data: { userId: event.userId ?? null, action: `[SECURITY] ${event.type}`, target: event.ip, type: 'security', }, }).catch(() => undefined), ]); } export async function getRecentSecurityEvents( type?: string, userId?: string, limit = 50, ): Promise { const key = userId ? `seclog:user:${userId}` : type ? `seclog:${type}` : null; if (!key) return []; const raw = await redis.lrange(key, 0, limit - 1); return raw.map((r: string) => { try { return JSON.parse(r) as SecurityEvent; } catch { return null; } }).filter((e): e is SecurityEvent => e !== null); } export async function getSecurityEventCount( type: string, ip: string, windowSeconds: number, ): Promise { const key = eventKey(ip, type, windowSeconds); const count = await redis.incr(key); if (count === 1) await redis.expire(key, windowSeconds); return count; } export async function checkAccountLock(userId: string): Promise { const key = `lock:${userId}`; const locked = await redis.get(key); return locked === '1'; } export async function lockAccount(userId: string, durationSeconds: number = 30 * 60): Promise { const key = `lock:${userId}`; await redis.set(key, '1', 'EX', durationSeconds); await logSecurityEvent({ type: 'account_locked', userId, ip: 'system', metadata: { durationSeconds }, }); } export async function unlockAccount(userId: string): Promise { const key = `lock:${userId}`; await redis.del(key); await logSecurityEvent({ type: 'account_unlocked', userId, ip: 'system', }); } export async function recordLoginFailure(ip: string, userId: string | null, userAgent?: string): Promise<{ locked: boolean; failCount: number }> { const key = `loginfail:${ip}`; const failCount = await redis.incr(key); if (failCount === 1) await redis.expire(key, 15 * 60); await logSecurityEvent({ type: 'login_failure', userId, ip, userAgent, metadata: { failCount }, }); if (failCount >= 20 && userId) { await lockAccount(userId, 30 * 60); return { locked: true, failCount }; } return { locked: false, failCount }; }