File size: 1,290 Bytes
30cc31a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// In-memory magic-link code store. Replace with Redis / DB for multi-instance deployments.

export const pendingCodes = new Map<
  string,
  { code: string; expiresAt: number }
>();

export const CODE_TTL_MS = 10 * 60 * 1000; // 10 minutes

/** Per-email rate limit: max requests within the window. */
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 min
const RATE_LIMIT_MAX = 3;
const rateLimitMap = new Map<string, { count: number; windowStart: number }>();

/** Returns true if the email has exceeded the rate limit. */
export function isRateLimited(email: string): boolean {
  const now = Date.now();
  const entry = rateLimitMap.get(email);
  if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
    rateLimitMap.set(email, { count: 1, windowStart: now });
    return false;
  }
  entry.count++;
  return entry.count > RATE_LIMIT_MAX;
}

/** Purge expired codes and stale rate-limit entries. Call periodically. */
export function purgeExpired(): void {
  const now = Date.now();
  Array.from(pendingCodes.entries()).forEach(([email, entry]) => {
    if (now > entry.expiresAt) pendingCodes.delete(email);
  });
  Array.from(rateLimitMap.entries()).forEach(([email, entry]) => {
    if (now - entry.windowStart > RATE_LIMIT_WINDOW_MS) rateLimitMap.delete(email);
  });
}