diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32ed2c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +test + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so + +# Virtual environments +env/ +venv/ +ENV/ +*.egg-info/ +.eggs/ + +# Django migrations +**/migrations/ +!*/migrations/__init__.py + +# SQLite database +*.sqlite3 +*.db + +# IDEs and editors +.vscode/ +.idea/ +*.swp +*~ + +# OS files +.DS_Store +Thumbs.db + +# Static/media cache (optional, uncomment if needed) +# staticfiles/ +# media/ + +# Logs +*.log + +# Coverage & testing +htmlcov/ +coverage.xml +.tox/ +.nox/ +.pytest_cache/ + +# dotenv or secrets +.env +.env.* + +# Redis dump or cache (if using) +*.rdb + +# Node (if using JS build tools) +node_modules/ + + + +# Docker +*.pid +*.tar +*.sock +docker-compose.override.yml diff --git a/README.md b/README.md index 0120265..bc8f285 100644 --- a/README.md +++ b/README.md @@ -130,11 +130,10 @@ function startPasswordReset() { You may optionally pass a custom onCaptcha renderer if you want to override the default iframe design: ```javascript -hoptchaPost('/users/send-reset/', payload, onSuccess, onError, function customCaptchaRenderer(url, key, callback) { +hoptchaPost('/users/send-reset/', payload, onSuccess, onError, function customCaptchaRenderer(url) { // Your custom UI logic here showCustomModal(); - renderMyCustomCaptchaIframe(url, key); - window._captchaSuccessCallback = callback; + renderMyCustomCaptchaIframe(url); }); ``` @@ -184,10 +183,9 @@ The `@ratelimit` decorator enforces strict rate limits while `@hoptcha_protected You can override the default iframe and style using the optional onCaptcha parameter in the hoptchaPost() JavaScript function. This is useful if you want to match your app’s branding or use modals. ```javascript -hoptchaPost('/endpoint', payload, onSuccess, onError, function renderCustom(url, key, cb) { +hoptchaPost('/endpoint', payload, onSuccess, onError, function renderCustom(url, key) { // Replace container with your custom implementation $('#myCustomCaptchaArea').html(``); - window._captchaSuccessCallback = cb; }); ``` diff --git a/django_hoptcha/decorators.py b/django_hoptcha/decorators.py index 953ced1..abf94bb 100644 --- a/django_hoptcha/decorators.py +++ b/django_hoptcha/decorators.py @@ -22,6 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import time +from urllib.parse import urlencode + from functools import wraps from django.core.cache import cache from django.http import JsonResponse @@ -92,11 +95,11 @@ def hoptcha_protected( if not token or not verify_token(token): return response(request) if response else JsonResponse({ "error": "CAPTCHA", - "url": GENERATE_URL, - "key": PUBLIC_KEY + "url": f"{GENERATE_URL}?{urlencode({'client_key': PUBLIC_KEY, 'timestamp': int(time.time() * 1000)})}" }, status=400) else: cache.delete(cache_key) # reset counter if passed + return view_func(request, *args, **kwargs) timeout_val = timeout * (2 ** (attempts - threshold)) if backoff and attempts >= threshold else timeout cache.set(cache_key, attempts + 1, timeout=timeout_val) diff --git a/django_hoptcha/static/django_hoptcha/hoptcha.js b/django_hoptcha/static/django_hoptcha/hoptcha.js index 665d118..fd5f2b5 100644 --- a/django_hoptcha/static/django_hoptcha/hoptcha.js +++ b/django_hoptcha/static/django_hoptcha/hoptcha.js @@ -21,21 +21,16 @@ /** * Renders the Hoptcha iframe inside #hoptcha-container. - * @param {string} captcha_url - The Hoptcha URL endpoint. - * @param {string} client_key - The client public key. - * @param {function} onSuccessCallback - Called with token when CAPTCHA is solved. + * @param {string} url - The Hoptcha URL endpoint. */ - window.renderCaptchaStep = function(captcha_url, client_key, onSuccessCallback) { + window.renderCaptchaStep = function(url) { $('#hoptcha-container').html(` `); - - // Register callback - window._captchaSuccessCallback = onSuccessCallback; }; /** @@ -51,19 +46,21 @@ }).fail(xhr => { const error = xhr.responseJSON ? xhr.responseJSON.error : 'Something went wrong.'; const captcha_url = xhr.responseJSON?.url; - const client_key = xhr.responseJSON?.key; - if (!captcha_url || !client_key) { + if (!captcha_url) { if (onError) onError(error); else console.error(error); return; } - const render = onCaptcha || window.renderCaptchaStep; - render(captcha_url, client_key, function(token) { + // Register callback + window._captchaSuccessCallback = function(token) { payload.captcha_token = token; window.hoptchaPost(url, payload, onSuccess, onError, onCaptcha); - }); + }; + + const render = onCaptcha || window.renderCaptchaStep; + render(captcha_url); }); }; diff --git a/django_hoptcha/templates/django_hoptcha/captcha_iframe.html b/django_hoptcha/templates/django_hoptcha/captcha_iframe.html index 983d891..160d958 100644 --- a/django_hoptcha/templates/django_hoptcha/captcha_iframe.html +++ b/django_hoptcha/templates/django_hoptcha/captcha_iframe.html @@ -4,4 +4,3 @@ style="width: 100%; height: 250px; border: none; border-radius: 12px;"> -