diff --git a/README.md b/README.md index 460a5e8..05c3229 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,9 @@ CAPTCHA_PRIVATE_KEY = 'your-secret-key' Import the decorator and apply it to any Django view: ```python -from django_hoptcha.decorators import hoptcha_required +from django_hoptcha.decorators import hoptcha_protected -@hoptcha_required() +@hoptcha_protected() def my_view(request): return JsonResponse({'success': True}) ``` @@ -156,7 +156,7 @@ def ip_and_student_id(request): Apply it like this: ```python -@hoptcha_required(threshold=5, timeout=300, key_func=ip_and_student_id) +@hoptcha_protected(threshold=5, timeout=300, key=ip_and_student_id) def send_reset_code(request): ... ``` @@ -169,15 +169,15 @@ This package works seamlessly with [`django-ratelimit`](https://pypi.org/project ```python from ratelimit.decorators import ratelimit -from django_hoptcha.decorators import hoptcha_required +from django_hoptcha.decorators import hoptcha_protected @ratelimit(key='ip', rate='15/m', method='POST', block=True) -@hoptcha_required(threshold=5, timeout=300) +@hoptcha_protected(threshold=5, timeout=300) def secure_view(request): ... ``` -The `@ratelimit` decorator enforces strict rate limits while `@hoptcha_required` provides a soft CAPTCHA fallback. +The `@ratelimit` decorator enforces strict rate limits while `@hoptcha_protected` provides a soft CAPTCHA fallback. ### Custom CAPTCHA UI Rendering @@ -195,11 +195,11 @@ hoptchaPost('/endpoint', payload, onSuccess, onError, function renderCustom(url, ## 🔧 Customization -| Parameter | Description | Default | -| ----------- | ----------------------------------------------------- | ------------- | -| `threshold` | Number of allowed attempts before CAPTCHA is required | `5` | -| `timeout` | Time in seconds to reset attempt count | `300` (5 min) | -| `key_func` | Function to identify requestor (IP, user ID, etc.) | IP address | +| Parameter | Description | Default | +|-------------|--------------------------------------------------------------|---------------| +| `threshold` | Number of allowed attempts before CAPTCHA is required | `5` | +| `timeout` | Time in seconds to reset attempt count | `300` (5 min) | +| `key` | Function or string to identify requestor (IP, user ID, etc.) | `ip` | --- diff --git a/django_hoptcha/decorators.py b/django_hoptcha/decorators.py index c7ec2c0..0208a31 100644 --- a/django_hoptcha/decorators.py +++ b/django_hoptcha/decorators.py @@ -28,20 +28,26 @@ from django.http import JsonResponse from .validators import verify_token from .settings import GENERATE_URL, PUBLIC_KEY -def default_key_func(request): - return request.META.get('REMOTE_ADDR', 'unknown-ip') +def get_builtin_key_func(name): + if name == 'ip': + return lambda request: request.META.get('REMOTE_ADDR', 'unknown-ip') + raise ValueError(f"Unknown built-in key: '{name}'") -def hoptcha_required(threshold=5, timeout=300, key_func=default_key_func): +def hoptcha_protected(threshold=5, timeout=300, key='ip'): """ Decorator that applies CAPTCHA verification after `threshold` failed attempts, - tracked via cache using a customizable `key_func`. + tracked via cache using a customizable key. The `key` can be: + - a string like 'ip' to use a built-in method + - a callable that accepts a Django request and returns a string """ + key_func = get_builtin_key_func(key) if isinstance(key, str) else key + def decorator(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): - key = key_func(request) - cache_key = f"hoptcha-attempts:{key}" + key_val = key_func(request) + cache_key = f"hoptcha-attempts:{key_val}" attempts = cache.get(cache_key, 0) if attempts >= threshold: @@ -53,7 +59,6 @@ def hoptcha_required(threshold=5, timeout=300, key_func=default_key_func): "key": PUBLIC_KEY }, status=400) - # Increment and store attempt count unless the view says otherwise cache.set(cache_key, attempts + 1, timeout=timeout) return view_func(request, *args, **kwargs) return _wrapped_view diff --git a/example/demoapp/demoapp/views.py b/example/demoapp/demoapp/views.py index c262c04..ffed32e 100644 --- a/example/demoapp/demoapp/views.py +++ b/example/demoapp/demoapp/views.py @@ -25,10 +25,10 @@ SOFTWARE. from django.http import JsonResponse from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt -from django_hoptcha.decorators import hoptcha_required +from django_hoptcha.decorators import hoptcha_protected @csrf_exempt -@hoptcha_required(threshold=3, timeout=300) +@hoptcha_protected(threshold=3, timeout=300) def protected_form_submit(request): name = request.POST.get("name") if not name: