Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,30 +1,15 @@
|
||||
import { Context } from 'hono';
|
||||
import { getRateLimiter } from './stores/rateLimiter.js';
|
||||
|
||||
/**
|
||||
* Simple in-memory rate limiter.
|
||||
* Rate limiting helpers.
|
||||
*
|
||||
* Suitable for a single backend instance (the current deployment model). If the
|
||||
* backend is ever scaled horizontally, replace the in-memory Map with a shared
|
||||
* store (e.g. Redis) so limits are enforced across instances.
|
||||
* The actual counting is delegated to a pluggable rate limiter (in-memory by
|
||||
* default, Redis-backed when REDIS_URL is set) so limits are enforced either
|
||||
* per instance (single-instance deployments) or across all instances
|
||||
* (horizontal scaling). See lib/stores/rateLimiter.ts.
|
||||
*/
|
||||
|
||||
interface Bucket {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
}
|
||||
|
||||
const buckets = new Map<string, Bucket>();
|
||||
|
||||
// Periodically drop expired buckets so the Map does not grow unbounded.
|
||||
const cleanup = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, bucket] of buckets) {
|
||||
if (now > bucket.resetAt) buckets.delete(key);
|
||||
}
|
||||
}, 60_000);
|
||||
// Don't keep the process alive just for cleanup.
|
||||
(cleanup as any).unref?.();
|
||||
|
||||
/** Best-effort client IP extraction (honours common reverse-proxy headers). */
|
||||
export function getClientIp(c: Context): string {
|
||||
const forwarded = c.req.header('x-forwarded-for');
|
||||
@@ -40,20 +25,8 @@ export function consumeRateLimit(
|
||||
key: string,
|
||||
max: number,
|
||||
windowMs: number
|
||||
): { allowed: boolean; retryAfter?: number } {
|
||||
const now = Date.now();
|
||||
const bucket = buckets.get(key);
|
||||
|
||||
if (!bucket || now > bucket.resetAt) {
|
||||
buckets.set(key, { count: 1, resetAt: now + windowMs });
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
bucket.count++;
|
||||
if (bucket.count > max) {
|
||||
return { allowed: false, retryAfter: Math.ceil((bucket.resetAt - now) / 1000) };
|
||||
}
|
||||
return { allowed: true };
|
||||
): Promise<{ allowed: boolean; retryAfter?: number }> {
|
||||
return getRateLimiter().consume(key, max, windowMs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +36,7 @@ export function consumeRateLimit(
|
||||
export function rateLimitMiddleware(opts: { max: number; windowMs: number; prefix: string }) {
|
||||
return async (c: Context, next: () => Promise<void>) => {
|
||||
const ip = getClientIp(c);
|
||||
const result = consumeRateLimit(`${opts.prefix}:${ip}`, opts.max, opts.windowMs);
|
||||
const result = await consumeRateLimit(`${opts.prefix}:${ip}`, opts.max, opts.windowMs);
|
||||
if (!result.allowed) {
|
||||
return c.json(
|
||||
{ error: 'Too many requests. Please try again later.', retryAfter: result.retryAfter },
|
||||
|
||||
Reference in New Issue
Block a user