All files / src/pages/api contact.utils.ts

100% Statements 26/26
100% Branches 25/25
100% Functions 3/3
100% Lines 21/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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  2x 2x 2x     43x 43x 43x 23x 23x   20x 20x       123x 85x 83x       29x 28x 28x 24x 24x 24x 24x 20x    
/** Rate limit: max requests per IP in the time window */
export const RATE_LIMIT = 3;
export const RATE_WINDOW_MS = 15 * 60 * 1000; // 15 min
export const hits = new Map<string, { count: number; reset: number }>();
 
export function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const entry = hits.get(ip);
  if (!entry || now > entry.reset) {
    hits.set(ip, { count: 1, reset: now + RATE_WINDOW_MS });
    return false;
  }
  entry.count++;
  return entry.count > RATE_LIMIT;
}
 
export function sanitize(value: unknown): string {
  if (value === null || value === undefined) return "";
  if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return "";
  return String(value).trim().slice(0, 2000);
}
 
export function isValidEmail(email: string): boolean {
  if (email.length > 254) return false;
  const at = email.indexOf("@");
  if (at <= 0 || at !== email.lastIndexOf("@")) return false;
  const local = email.slice(0, at);
  const domain = email.slice(at + 1);
  const dot = domain.lastIndexOf(".");
  if (dot <= 0 || dot === domain.length - 1) return false;
  return /^[^\s@]+$/.test(local) && /^[^\s@]+$/.test(domain);
}