| """ |
| Simple rate limiter with Valkey (optional) + in-process fallback. |
| |
| This is used for lightweight protection endpoints such as registration code sending. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import time |
| from typing import Dict, Optional, Tuple |
|
|
| from ..services.cache_service import get_cache_service |
|
|
| _lock = asyncio.Lock() |
| _memory_buckets: Dict[str, Tuple[int, float]] = {} |
|
|
|
|
| async def hit(key: str, limit: int, window_seconds: int) -> Tuple[bool, int, Optional[int]]: |
| """ |
| Increment a rate-limit bucket. |
| |
| Returns (allowed, remaining, reset_in_seconds). |
| reset_in_seconds is best-effort (None when unknown). |
| """ |
| cache = await get_cache_service() |
| if getattr(cache, "_connected", False): |
| value = await cache.incr_with_ttl(key, window_seconds) |
| if value is None: |
| |
| pass |
| else: |
| allowed = value <= limit |
| remaining = max(0, limit - value) |
| return allowed, remaining, None |
|
|
| now = time.time() |
| async with _lock: |
| current, reset_at = _memory_buckets.get(key, (0, now + window_seconds)) |
| if now >= reset_at: |
| current, reset_at = 0, now + window_seconds |
| current += 1 |
| _memory_buckets[key] = (current, reset_at) |
|
|
| allowed = current <= limit |
| remaining = max(0, limit - current) |
| reset_in = max(0, int(reset_at - now)) |
| return allowed, remaining, reset_in |
|
|
|
|