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:
Michilis
2026-06-25 07:12:59 +00:00
co-authored by Cursor
parent f0e2de2834
commit 613bd7be1d
75 changed files with 7702 additions and 5580 deletions
+9 -36
View File
@@ -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 },