Add rate limiting, sliding sessions, email verification, GDPR self-service, order cancellation/returns, and critical-error alerting
Complements Payload's per-account login lockout with per-IP rate limiting on auth routes; proxy.ts silently refreshes an active customer's session via Payload's built-in refresh-token endpoint instead of a long-lived token. Registration now sends a non-blocking email-verification link (doesn't gate login, since checkout registers and immediately logs in mid-purchase). /konto/profil gets GDPR export/delete; order detail pages get self-service cancel/return-request, backed by a Payload hook that closes a real gap (a customer's JWT could previously PATCH any field of their own order, not just status). Checkout failures now email an alert independent of Payload's own health, since Kuma's uptime checks can't see an order silently failing to persist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Server-only, in-memory sliding-window limiter — deliberately no Redis:
|
||||
// this app runs as a single Coolify container, so a plain in-process Map
|
||||
// is sufficient and needs zero new infra. Counters reset on redeploy/
|
||||
// restart (acceptable — an attacker gets a few extra free attempts right
|
||||
// after a deploy, not a meaningful window) and won't share state if this
|
||||
// ever scales to multiple instances; revisit with a shared store then.
|
||||
//
|
||||
// Complements, doesn't replace, Payload's own built-in per-account login
|
||||
// lockout (Customers collection, maxLoginAttempts: 5 / lockTime: 10min,
|
||||
// Payload defaults) — that stops brute-forcing one account, this stops an
|
||||
// IP hammering registration or spraying attempts across many accounts.
|
||||
const attempts = new Map<string, number[]>();
|
||||
|
||||
// Prevents unbounded growth from IPs that hit a route once and never
|
||||
// return — without this, `attempts` would grow forever on a low-traffic
|
||||
// site that nonetheless gets scanned/crawled periodically.
|
||||
const MAX_TRACKED_KEYS = 10_000;
|
||||
|
||||
export function checkRateLimit(key: string, { limit, windowMs }: { limit: number; windowMs: number }): boolean {
|
||||
const now = Date.now();
|
||||
const windowStart = now - windowMs;
|
||||
const timestamps = (attempts.get(key) ?? []).filter((t) => t > windowStart);
|
||||
|
||||
if (timestamps.length >= limit) {
|
||||
attempts.set(key, timestamps);
|
||||
return false;
|
||||
}
|
||||
|
||||
timestamps.push(now);
|
||||
if (attempts.size >= MAX_TRACKED_KEYS && !attempts.has(key)) {
|
||||
attempts.clear();
|
||||
}
|
||||
attempts.set(key, timestamps);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Caddy sits in front of this app and sets X-Forwarded-For — falls back to
|
||||
// a constant key (effectively a single shared bucket) if that's ever
|
||||
// missing, e.g. local dev, rather than disabling rate limiting outright.
|
||||
export function getClientIp(request: Request): string {
|
||||
const forwarded = request.headers.get("x-forwarded-for");
|
||||
return forwarded?.split(",")[0]?.trim() || "unknown";
|
||||
}
|
||||
Reference in New Issue
Block a user