import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js'; import { isPostgresStorageMode } from './dataPaths.js'; import { decryptJsonPayload, encryptJsonPayload, makeLookupToken, pgQuery, withPgTransaction, } from './postgres.js'; const REQUESTS_FILE = '/data/guest_request_counts.json'; const WINDOW_MS = 24 * 60 * 60 * 1000; const MAX_LOGGED_OUT_REQUESTS = 50; const ipCounters = new Map(); let loaded = false; function requestLookup(ip) { return makeLookupToken('guest-request', ip); } function requestAad(ip) { return `guest-request:${requestLookup(ip)}`; } async function loadGuestCounters() { if (loaded || isPostgresStorageMode()) return; loaded = true; const data = await loadEncryptedJson(REQUESTS_FILE); if (!data) return; for (const [ip, entry] of Object.entries(data)) { ipCounters.set(ip, { count: typeof entry.count === 'number' ? entry.count : 0, resetAt: typeof entry.resetAt === 'number' ? entry.resetAt : Date.now() + WINDOW_MS, }); } } async function loadSqlGuestCounters() { if (loaded || !isPostgresStorageMode()) return; loaded = true; const now = new Date().toISOString(); const { rows } = await pgQuery( 'SELECT payload FROM guest_request_counters WHERE expires_at > $1', [now] ); for (const row of rows) { const payload = decryptJsonPayload(row.payload, `guest-request-row`); if (!payload?.ip) continue; ipCounters.set(payload.ip, { count: typeof payload.count === 'number' ? payload.count : 0, resetAt: typeof payload.resetAt === 'number' ? payload.resetAt : Date.now() + WINDOW_MS, }); } } async function saveGuestCounters() { const data = {}; for (const [ip, entry] of ipCounters) { data[ip] = { count: entry.count, resetAt: entry.resetAt }; } await saveEncryptedJson(REQUESTS_FILE, data); } function cleanupExpired() { const now = Date.now(); for (const [ip, entry] of ipCounters) { if (entry.resetAt <= now) { ipCounters.delete(ip); } } } async function pruneSqlGuestCounters() { await pgQuery('DELETE FROM guest_request_counters WHERE expires_at <= $1', [new Date().toISOString()]); } export async function initGuestRequestLimiter() { if (isPostgresStorageMode()) { await loadSqlGuestCounters().catch((err) => console.error('Failed to load SQL guest request counters:', err)); await pruneSqlGuestCounters().catch((err) => console.error('Failed to prune SQL guest request counters:', err)); cleanupExpired(); setInterval(() => { cleanupExpired(); pruneSqlGuestCounters().catch((err) => console.error('Failed to prune SQL guest request counters:', err)); }, 60 * 60 * 1000); return; } await loadGuestCounters().catch((err) => console.error('Failed to load guest request counters:', err)); cleanupExpired(); setInterval(() => { cleanupExpired(); }, 60 * 60 * 1000); } export async function consumeGuestRequest(ip) { if (isPostgresStorageMode()) { return withPgTransaction(async (client) => { const now = Date.now(); const lookup = requestLookup(ip); const { rows } = await client.query( 'SELECT payload FROM guest_request_counters WHERE key_lookup = $1 FOR UPDATE', [lookup] ); const payload = rows[0] ? decryptJsonPayload(rows[0].payload, 'guest-request-row') : null; let entry = payload && payload.resetAt > now ? { count: payload.count || 0, resetAt: payload.resetAt } : { count: 0, resetAt: now + WINDOW_MS }; if (entry.count >= MAX_LOGGED_OUT_REQUESTS) { ipCounters.set(ip, entry); return false; } entry = { ...entry, count: entry.count + 1 }; ipCounters.set(ip, entry); await client.query( `INSERT INTO guest_request_counters (key_lookup, expires_at, updated_at, payload) VALUES ($1, $2, $3, $4::jsonb) ON CONFLICT (key_lookup) DO UPDATE SET expires_at = EXCLUDED.expires_at, updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`, [ lookup, new Date(entry.resetAt).toISOString(), new Date().toISOString(), JSON.stringify(encryptJsonPayload({ ip, ...entry }, 'guest-request-row')), ] ); return true; }); } await loadGuestCounters(); const now = Date.now(); let entry = ipCounters.get(ip); if (!entry || entry.resetAt <= now) { entry = { count: 0, resetAt: now + WINDOW_MS }; ipCounters.set(ip, entry); } if (entry.count >= MAX_LOGGED_OUT_REQUESTS) { return false; } entry.count += 1; await saveGuestCounters().catch((err) => console.error('Failed to save guest request counters:', err)); return true; } export function getGuestRequestsRemaining(ip) { const now = Date.now(); const entry = ipCounters.get(ip); if (!entry || entry.resetAt <= now) return MAX_LOGGED_OUT_REQUESTS; return Math.max(0, MAX_LOGGED_OUT_REQUESTS - entry.count); }