File size: 1,707 Bytes
b324c71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"""
Shared checks for the public, no-sign-in forms (/bsa and /bff): a simple rate
limiter and the contact-detail validators. Rate limits are in-memory, per
process (same trade-off as routes/help.py).
"""

import re
import threading
import time
from collections import defaultdict

from fastapi import HTTPException

EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
# Digits with an optional leading +, allowing the spaces, dashes, dots and brackets people type.
PHONE_RE = re.compile(r"^\+?[\d\s().-]+$")


class RateLimiter:
    """Allows `count` hits per `period` seconds for each key."""

    def __init__(self):
        self._lock = threading.Lock()
        self._hits: dict[str, list[float]] = defaultdict(list)

    def allow(self, key: str, count: int, period: int) -> bool:
        now = time.time()
        with self._lock:
            recent = [t for t in self._hits[key] if now - t < period]
            allowed = len(recent) < count
            if allowed:
                recent.append(now)
            self._hits[key] = recent
            return allowed

    def reset(self):
        with self._lock:
            self._hits.clear()


def clean_email(raw: str) -> str:
    email = raw.strip().lower()
    if not EMAIL_RE.match(email):
        raise HTTPException(status_code=400, detail="Enter a valid email address.")
    return email


def clean_phone(raw: str) -> str:
    """Trimmed phone number, kept as typed apart from spacing. 7 to 15 digits."""
    phone = " ".join(raw.split())
    digits = re.sub(r"\D", "", phone)
    if not PHONE_RE.match(phone) or not 7 <= len(digits) <= 15:
        raise HTTPException(status_code=400, detail="Enter a valid phone number.")
    return phone