Rewrite to vanilla JS

This commit is contained in:
Ivan Nikolskiy 2025-06-09 17:37:19 +02:00
parent a53fada7e1
commit 0759b43efc
4 changed files with 96 additions and 45 deletions

View File

@ -23,6 +23,8 @@ SOFTWARE.
""" """
import time import time
import json
from urllib.parse import urlencode from urllib.parse import urlencode
from functools import wraps from functools import wraps
@ -75,10 +77,13 @@ def hoptcha_protected(
if isinstance(key, str): if isinstance(key, str):
key_func = BUILTIN_KEYS.get(key) key_func = BUILTIN_KEYS.get(key)
if not key_func: if not key_func:
raise ValueError(f"Unknown key: {key}") raise ValueError(f"Unknown key: {key}")
elif callable(key): elif callable(key):
key_func = key key_func = key
else: else:
raise TypeError("key must be a string or callable") raise TypeError("key must be a string or callable")
@ -101,7 +106,19 @@ def hoptcha_protected(
attempts = cache.get(cache_key, 0) attempts = cache.get(cache_key, 0)
if attempts >= threshold: if attempts >= threshold:
token = request.POST.get("captcha_token") or request.GET.get("captcha_token") token = (
request.POST.get("captcha_token") or
request.GET.get("captcha_token")
)
# Try extracting from JSON body if not found yet
if not token and request.content_type == "application/json":
try:
body = json.loads(request.body)
token = body.get("captcha_token")
except (json.JSONDecodeError, TypeError):
pass # Malformed or empty JSON
if not token or not verify_token(token): if not token or not verify_token(token):
return response(request) if response else JsonResponse({ return response(request) if response else JsonResponse({
"error": "CAPTCHA", "error": "CAPTCHA",

View File

@ -1,11 +1,6 @@
// ==================== HOPTCHA CLIENT ==================== // ==================== HOPTCHA CLIENT (Vanilla JS) ====================
(function(window, $) {
if (!$) {
console.error("Hoptcha requires jQuery. Please include it before this script.");
return;
}
(function (window) {
/** /**
* Handle messages from the iframe. * Handle messages from the iframe.
*/ */
@ -23,14 +18,17 @@
* Renders the Hoptcha iframe inside #hoptcha-container. * Renders the Hoptcha iframe inside #hoptcha-container.
* @param {string} url - The Hoptcha URL endpoint. * @param {string} url - The Hoptcha URL endpoint.
*/ */
window.renderCaptchaStep = function(url) { window.renderCaptchaStep = function (url) {
$('#hoptcha-container').html(` const container = document.getElementById("hoptcha-container");
<iframe if (container) {
id="captcha-iframe" container.innerHTML = `
src=${url} <iframe
style="width: 100%; height: 250px; border: none; border-radius: 12px;" id="captcha-iframe"
></iframe> src="${url}"
`); style="width: 100%; height: 250px; border: none; border-radius: 12px;"
></iframe>
`;
}
}; };
/** /**
@ -39,13 +37,30 @@
* @param {object} payload - Payload to send, must include `captcha_token`. * @param {object} payload - Payload to send, must include `captcha_token`.
* @param {function} onSuccess - Success handler. * @param {function} onSuccess - Success handler.
* @param {function} [onError] - Error fallback. * @param {function} [onError] - Error fallback.
* @param {function} [onCaptcha] - Optional custom render callback.
*/ */
window.hoptchaPost = function(url, payload, onSuccess, onError, onCaptcha) { window.hoptchaPost = function (url, payload, onSuccess, onError, onCaptcha) {
$.post(url, payload, function(data) { fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload)
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw { status: response.status, data };
});
}
return response.json();
})
.then(data => {
if (onSuccess) onSuccess(data); if (onSuccess) onSuccess(data);
}).fail(xhr => { })
const error = xhr.responseJSON ? xhr.responseJSON.error : 'Something went wrong.'; .catch(err => {
const captcha_url = xhr.responseJSON?.url; const error = err.data?.error || 'Something went wrong.';
const captcha_url = err.data?.url;
if (!captcha_url) { if (!captcha_url) {
if (onError) onError(error); if (onError) onError(error);
@ -54,7 +69,7 @@
} }
// Register callback // Register callback
window._captchaSuccessCallback = function(token) { window._captchaSuccessCallback = function (token) {
payload.captcha_token = token; payload.captcha_token = token;
window.hoptchaPost(url, payload, onSuccess, onError, onCaptcha); window.hoptchaPost(url, payload, onSuccess, onError, onCaptcha);
}; };
@ -63,5 +78,4 @@
render(captcha_url); render(captcha_url);
}); });
}; };
})(window);
})(window, window.jQuery);

View File

@ -4,7 +4,6 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Hoptcha Demo</title> <title>Hoptcha Demo</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="{% static 'django_hoptcha/hoptcha.js' %}"></script> <script src="{% static 'django_hoptcha/hoptcha.js' %}"></script>
<style> <style>
body { body {
@ -73,26 +72,39 @@
<script> <script>
function closeCaptcha() { function closeCaptcha() {
$('#custom-captcha-modal').hide(); const modal = document.getElementById("custom-captcha-modal");
if (modal) modal.style.display = "none";
} }
$("#demo-form").submit(function(e) { const demoForm = document.getElementById("demo-form");
e.preventDefault();
const formData = $(this).serializeArray();
let payload = {};
formData.forEach(item => payload[item.name] = item.value);
payload["captcha_token"] = "init";
hoptchaPost('/submit/', payload, function(data) { if (demoForm) {
closeCaptcha(); demoForm.addEventListener("submit", function (e) {
alert(data.success); e.preventDefault();
}, function(error) {
alert(error); const formData = new FormData(demoForm);
}, function renderCustomCaptcha(url) { const payload = {};
$('#custom-captcha-frame').attr('src', url);
$('#custom-captcha-modal').show(); for (const [key, value] of formData.entries()) {
payload[key] = value;
}
payload["captcha_token"] = "init";
hoptchaPost('/submit/', payload, function (data) {
closeCaptcha();
alert(data.success);
}, function (error) {
alert(error);
}, function renderCustomCaptcha(url) {
const frame = document.getElementById("custom-captcha-frame");
const modal = document.getElementById("custom-captcha-modal");
if (frame) frame.src = url;
if (modal) modal.style.display = "block";
});
}); });
}); }
</script> </script>
</body> </body>
</html> </html>

View File

@ -22,6 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
""" """
import json
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -30,10 +32,16 @@ from django_hoptcha.decorators import hoptcha_protected
@csrf_exempt @csrf_exempt
@hoptcha_protected(threshold=3, timeout=300) @hoptcha_protected(threshold=3, timeout=300)
def protected_form_submit(request): def protected_form_submit(request):
name = request.POST.get("name") try:
if not name: data = json.loads(request.body)
return JsonResponse({"error": "Name is required."}, status=400) name = data.get('name', '')
return JsonResponse({"success": f"Hello, {name}!"})
if not name:
return JsonResponse({"error": "Name is required."}, status=400)
return JsonResponse({"success": f"Hello, {name}!"})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
def form_index(request): def form_index(request):
return render(request, "templates/form.html") return render(request, "templates/form.html")