From b6c15f35d570d4f54901b811654991847502ca82 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 1 Sep 2026 11:06:47 +0200 Subject: feat(hub): reCAPTCHA v2 on Register and Password Reset pages Server-side verification module, CaptchaConfig in hub.toml, captcha_site_key exposed via /v1/hub/info, useCaptcha() hook in the SPA with stable DOM rendering (strength bar always present to avoid Preact re-ordering the captcha widget). Native clients (auth_key path) skip captcha. All 10 locales updated. Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-hub/src/meshbay_hub/captcha.py | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/captcha.py (limited to 'packages/meshbay-hub/src/meshbay_hub/captcha.py') diff --git a/packages/meshbay-hub/src/meshbay_hub/captcha.py b/packages/meshbay-hub/src/meshbay_hub/captcha.py new file mode 100644 index 0000000..faf7907 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/captcha.py @@ -0,0 +1,28 @@ +"""reCAPTCHA v2 server-side verification.""" + +import logging + +import httpx + +log = logging.getLogger(__name__) + +VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify" + + +async def verify_captcha( + secret_key: str, token: str, remote_ip: str | None = None, +) -> bool: + payload: dict[str, str] = {"secret": secret_key, "response": token} + if remote_ip: + payload["remoteip"] = remote_ip + try: + async with httpx.AsyncClient(timeout=5) as client: + resp = await client.post(VERIFY_URL, data=payload) + resp.raise_for_status() + result = resp.json() + if not result.get("success"): + log.info("captcha rejected: %s", result.get("error-codes", [])) + return result.get("success", False) + except Exception: + log.exception("captcha verification request failed") + return False -- cgit v1.2.3