"""A tiny in-process sliding-window rate limiter for the login endpoint. The portal runs as a single uvicorn process, so an in-memory counter is enough to blunt online password guessing without adding a dependency or a shared store. It is keyed by client IP; only failed attempts are counted, and a successful login clears the key. This deliberately does NOT lock accounts (which would let anyone lock out a user by name) — it throttles the source of the guessing instead. """ import time from collections import defaultdict, deque from threading import Lock class SlidingWindowLimiter: def __init__(self, max_attempts: int, window_seconds: float): self.max_attempts = max_attempts self.window = window_seconds self._hits: dict[str, deque] = defaultdict(deque) self._lock = Lock() def _prune(self, key: str, now: float) -> deque: dq = self._hits[key] cutoff = now - self.window while dq and dq[0] <= cutoff: dq.popleft() if not dq: self._hits.pop(key, None) return dq def retry_after(self, key: str) -> float: """Seconds until `key` may try again, or 0.0 if it is under the limit right now.""" now = time.monotonic() with self._lock: dq = self._prune(key, now) if len(dq) < self.max_attempts: return 0.0 return self.window - (now - dq[0]) def record_failure(self, key: str) -> None: now = time.monotonic() with self._lock: self._hits[key].append(now) def reset(self, key: str) -> None: with self._lock: self._hits.pop(key, None)