chat-dev / server /guestRequestLimiter.js
incognitolm
Migration to PostgreSQL
bff1056
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);
}