Spaces:
Runtime error
Runtime error
| 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); | |
| } | |