feat: implement rate limiting for API routes and enhance global error handling

This commit is contained in:
2026-03-01 02:07:27 +01:00
parent 670aa7810c
commit 263670dcae
2 changed files with 83 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { building } from '$app/environment';
import { auth } from '$lib/server/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
@@ -7,6 +7,7 @@ import { db } from '$lib/server/db';
import { user as userTable } from '$lib/server/db/auth.schema';
import { eq } from 'drizzle-orm';
import { ensureRootAdminRole } from '$lib/server/permissions';
import { checkRateLimit } from '$lib/server/rate-limit';
// ── Initialize the IRC DM handler once on server startup ──
// This runs when the module is first imported (server boot).
@@ -15,9 +16,30 @@ if (!building) {
import('$lib/server/bancho/dm-handler')
.then(({ initDMHandler }) => initDMHandler())
.catch((err) => console.warn('[Hooks] DM handler init skipped:', err.message));
process.on('unhandledRejection', (reason) => {
console.error('[Unhandled Rejection]', reason);
});
process.on('uncaughtException', (err) => {
console.error('[Uncaught Exception]', err);
});
}
const handleBetterAuth: Handle = async ({ event, resolve }) => {
// ── Rate limiting for API routes ──
// 200 req/min per IP — high enough for legit use, stops bots/scrapers.
if (event.url.pathname.startsWith('/api/')) {
const ip = event.getClientAddress();
const { ok, retryAfter } = checkRateLimit(`api:${ip}`, 200, 60_000);
if (!ok) {
return new Response('Too Many Requests', {
status: 429,
headers: { 'Retry-After': String(retryAfter ?? 60), 'Content-Type': 'text/plain' }
});
}
}
const session = await auth.api.getSession({ headers: event.request.headers });
if (session) {
@@ -49,3 +71,17 @@ const handleBetterAuth: Handle = async ({ event, resolve }) => {
};
export const handle: Handle = handleBetterAuth;
// ── Global server error handler ──
// Catches unhandled errors from load functions, API routes, etc.
// Logs context for debugging; returns a safe message to the client.
export const handleError: HandleServerError = ({ error, event }) => {
const err = error as any;
console.error('[Server Error]', {
message: err?.message ?? String(error),
path: event.url.pathname,
method: event.request.method,
stack: err?.stack
});
return { message: 'An unexpected error occurred.' };
};

View File

@@ -0,0 +1,46 @@
// src/lib/server/rate-limit.ts
// Simple fixed-window in-memory rate limiter.
// Good enough for a single-server deployment — swap for Redis if you ever scale horizontally.
interface Entry {
count: number;
resetAt: number;
}
const store = new Map<string, Entry>();
// Prune expired entries every 5 minutes to avoid unbounded memory growth
setInterval(
() => {
const now = Date.now();
for (const [key, entry] of store) {
if (now > entry.resetAt) store.delete(key);
}
},
5 * 60 * 1000
).unref?.();
/**
* Returns `{ ok: true }` if the request is within the limit,
* or `{ ok: false, retryAfter }` (seconds) if it's exceeded.
*/
export function checkRateLimit(
key: string,
limit: number,
windowMs: number
): { ok: boolean; retryAfter?: number } {
const now = Date.now();
const entry = store.get(key);
if (!entry || now > entry.resetAt) {
store.set(key, { count: 1, resetAt: now + windowMs });
return { ok: true };
}
if (entry.count >= limit) {
return { ok: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
}
entry.count++;
return { ok: true };
}