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
+111
View File
@@ -0,0 +1,111 @@
// Distributed lock abstraction with two implementations:
// - memory: per-process key set with TTL (only meaningful within one instance)
// - redis: SET key token NX PX ttl, released with a compare-and-delete Lua
// script so only the holder can release it
//
// Use acquire/release for long-lived ownership (e.g. a background poller) and
// withLock for a one-shot critical section. Selection is based on REDIS_URL.
import { randomUUID } from 'crypto';
import { getRedis, isRedisEnabled } from '../redis.js';
export interface Lock {
readonly backend: 'memory' | 'redis';
// Returns a token when the lock was acquired, or null when already held.
acquire(key: string, ttlMs: number): Promise<string | null>;
release(key: string, token: string): Promise<void>;
// Runs fn while holding the lock; returns fn's result, or null if not acquired.
withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null>;
}
// ==================== Memory implementation ====================
class MemoryLock implements Lock {
readonly backend = 'memory' as const;
private held = new Map<string, { token: string; expiresAt: number }>();
async acquire(key: string, ttlMs: number): Promise<string | null> {
const existing = this.held.get(key);
const now = Date.now();
if (existing && existing.expiresAt > now) {
return null;
}
const token = randomUUID();
this.held.set(key, { token, expiresAt: now + ttlMs });
return token;
}
async release(key: string, token: string): Promise<void> {
const existing = this.held.get(key);
if (existing && existing.token === token) {
this.held.delete(key);
}
}
async withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null> {
const token = await this.acquire(key, ttlMs);
if (!token) return null;
try {
return await fn();
} finally {
await this.release(key, token);
}
}
}
// ==================== Redis implementation ====================
const RELEASE_SCRIPT =
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end';
class RedisLock implements Lock {
readonly backend = 'redis' as const;
async acquire(key: string, ttlMs: number): Promise<string | null> {
const redis = getRedis();
if (!redis) {
// Redis configured but unavailable: do not block critical sections.
return randomUUID();
}
const token = randomUUID();
try {
const result = await redis.set(`lock:${key}`, token, 'PX', ttlMs, 'NX');
return result === 'OK' ? token : null;
} catch (err: any) {
console.error('[lock] redis acquire error, proceeding without lock:', err?.message || err);
// Fail open so a Redis outage does not deadlock startup or jobs.
return randomUUID();
}
}
async release(key: string, token: string): Promise<void> {
const redis = getRedis();
if (!redis) return;
try {
await redis.eval(RELEASE_SCRIPT, 1, `lock:${key}`, token);
} catch (err: any) {
console.error('[lock] redis release error:', err?.message || err);
}
}
async withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null> {
const token = await this.acquire(key, ttlMs);
if (!token) return null;
try {
return await fn();
} finally {
await this.release(key, token);
}
}
}
// ==================== Selection ====================
let instance: Lock | null = null;
export function getLock(): Lock {
if (!instance) {
instance = isRedisEnabled() ? new RedisLock() : new MemoryLock();
}
return instance;
}