Files
VacatureRadar/apps/core/rate_limit.py
Jens 7caea3a1ee
deploy / deploy (push) Canceled after 0s
demo mode
2026-07-27 23:38:37 +02:00

99 lines
3.0 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from django.core.cache import cache
from django.http import HttpRequest
from django.utils import timezone
from apps.core.network import client_ip
def _identity_for_user(request: HttpRequest) -> str:
user = getattr(request, "user", None)
if user and getattr(user, "is_authenticated", False):
return f"user:{user.pk}"
username = (
(request.POST.get("username", "") if request.method == "POST" else "").strip().lower()
)
return f"anon:{username or client_ip(request)}"
@dataclass(frozen=True)
class RateLimitState:
is_blocked: bool
remaining_seconds: int
attempts: int
def _attempts_key(namespace: str, identity: str) -> str:
return f"core-rate-limit:{namespace}:{identity}:attempts"
def _block_key(namespace: str, identity: str) -> str:
return f"core-rate-limit:{namespace}:{identity}:block"
def is_rate_limited(
request: HttpRequest,
*,
namespace: str,
max_attempts: int,
window_seconds: int,
block_seconds: int,
) -> RateLimitState:
now = timezone.now().timestamp()
identity = _identity_for_user(request)
block_until = cache.get(_block_key(namespace, identity))
if block_until and isinstance(block_until, int | float) and block_until > now:
return RateLimitState(True, max(0, int(block_until - now)), 0)
if cache.get(_attempts_key(namespace, identity), 0) >= max_attempts:
cache.set(
_block_key(namespace, identity),
now + max(block_seconds, 1),
timeout=block_seconds,
)
cache.delete(_attempts_key(namespace, identity))
return RateLimitState(True, max(block_seconds, 1), 0)
return RateLimitState(False, 0, int(cache.get(_attempts_key(namespace, identity), 0)))
def register_rate_limit_failure(
request: HttpRequest,
*,
namespace: str,
max_attempts: int,
window_seconds: int,
block_seconds: int,
) -> RateLimitState:
remaining = is_rate_limited(
request,
namespace=namespace,
max_attempts=max_attempts,
window_seconds=window_seconds,
block_seconds=block_seconds,
)
if remaining.is_blocked:
return remaining
identity = _identity_for_user(request)
attempts = int(cache.get(_attempts_key(namespace, identity), 0)) + 1
if attempts >= max_attempts:
now = timezone.now().timestamp()
cache.set(
_block_key(namespace, identity), now + max(block_seconds, 1), timeout=block_seconds
)
cache.delete(_attempts_key(namespace, identity))
return RateLimitState(True, max(block_seconds, 1), attempts)
cache.set(_attempts_key(namespace, identity), attempts, timeout=max(window_seconds, 1))
return RateLimitState(False, 0, attempts)
def clear_rate_limit(request: HttpRequest, *, namespace: str) -> None:
identity = _identity_for_user(request)
cache.delete(_attempts_key(namespace, identity))
cache.delete(_block_key(namespace, identity))