fix: harden booking and meeting flows
This commit is contained in:
@@ -13,16 +13,17 @@ const MAX_TRACKED_KEYS = 5000;
|
||||
|
||||
function sweepExpired(now: number) {
|
||||
for (const [key, bucket] of buckets) {
|
||||
if (bucket.resetAt < now) buckets.delete(key);
|
||||
if (bucket.resetAt <= now) buckets.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkRateLimit(key: string, opts: { max: number; windowMs: number }): boolean {
|
||||
const now = Date.now();
|
||||
if (buckets.size > MAX_TRACKED_KEYS) sweepExpired(now);
|
||||
if (buckets.size >= MAX_TRACKED_KEYS) sweepExpired(now);
|
||||
|
||||
const bucket = buckets.get(key);
|
||||
if (!bucket || bucket.resetAt < now) {
|
||||
if (!bucket || bucket.resetAt <= now) {
|
||||
if (!bucket && buckets.size >= MAX_TRACKED_KEYS) return false;
|
||||
buckets.set(key, { count: 1, resetAt: now + opts.windowMs });
|
||||
return true;
|
||||
}
|
||||
@@ -31,7 +32,7 @@ export function checkRateLimit(key: string, opts: { max: number; windowMs: numbe
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getClientIp(request: Request): string {
|
||||
export function getClientIp(request: { headers: Headers }): string {
|
||||
// Deployment is always behind exactly one trusted reverse proxy (see
|
||||
// DEPLOYMENT.md — Nginx Proxy Manager; the app port isn't published
|
||||
// directly to the host). nginx's default `$proxy_add_x_forwarded_for`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Only origin-relative application paths may be used after login. */
|
||||
export function safeLoginRedirect(value: string | null): string {
|
||||
if (!value || !value.startsWith("/") || value.startsWith("//") || /[\\\u0000-\u0020]/.test(value)) return "/konto";
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { checkRateLimit, getClientIp } from "./rateLimit";
|
||||
|
||||
/** Shared budgets for registration, email changes and resend (REST and GraphQL). */
|
||||
export function allowVerificationEmail(request: { headers: Headers }, email: string, customerId?: string | number): boolean {
|
||||
const windowMs = 60 * 60 * 1000;
|
||||
return checkRateLimit(`verification:ip:${getClientIp(request)}`, { max: 10, windowMs }) &&
|
||||
(customerId === undefined || checkRateLimit(`verification:customer:${customerId}`, { max: 3, windowMs })) &&
|
||||
checkRateLimit(`verification:email:${email.trim().toLowerCase()}`, { max: 3, windowMs });
|
||||
}
|
||||
Reference in New Issue
Block a user