Add hCaptcha support alongside Turnstile and reCAPTCHA
- Add hcaptcha_site_key and hcaptcha to captcha_provider options in admin settings - Update captcha.ts server-side verification for hCaptcha (new endpoint + secret key) - Add hCaptcha widget rendering in register-form.tsx - All TypeScript and Biome checks pass
This commit is contained in:
1 parent
1bca9657fa
commit
0f35bc8529
3 files changed
+34
-8
No files matched your search
@@ -269,7 +269,7 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
{
|
{
|
||||||
key: "captcha_provider",
|
key: "captcha_provider",
|
||||||
label: "Captcha provider",
|
label: "Captcha provider",
|
||||||
description: "none | turnstile | recaptcha",
|
description: "none | turnstile | recaptcha | hcaptcha",
|
||||||
type: "text",
|
type: "text",
|
||||||
defaultValue: "none",
|
defaultValue: "none",
|
||||||
},
|
},
|
||||||
@@ -278,6 +278,11 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
label: "Turnstile site key",
|
label: "Turnstile site key",
|
||||||
type: "text",
|
type: "text",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "hcaptcha_site_key",
|
||||||
|
label: "hCaptcha site key",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "recaptcha_site_key",
|
key: "recaptcha_site_key",
|
||||||
label: "reCAPTCHA site key",
|
label: "reCAPTCHA site key",
|
||||||
|
|||||||
@@ -322,6 +322,9 @@ export function RegisterForm({
|
|||||||
{showCaptcha && captcha.provider === "recaptcha" && (
|
{showCaptcha && captcha.provider === "recaptcha" && (
|
||||||
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
|
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
|
||||||
)}
|
)}
|
||||||
|
{showCaptcha && captcha.provider === "hcaptcha" && (
|
||||||
|
<div className="h-captcha" data-sitekey={captcha.siteKey} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { siteSettings } from "@/lib/services/site-settings";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Server-side CAPTCHA verification, driven by website_settings so staff pick the
|
* Server-side CAPTCHA verification, driven by website_settings so staff pick the
|
||||||
* provider in housekeeping. Supports Cloudflare Turnstile and Google reCAPTCHA
|
* provider in housekeeping. Supports Cloudflare Turnstile, Google reCAPTCHA,
|
||||||
* (the two AtomCMS offers, mutually exclusive).
|
* and hCaptcha (three AtomCMS offers, mutually exclusive).
|
||||||
*
|
*
|
||||||
* Behaviour:
|
* Behaviour:
|
||||||
* - provider "none" (or unset) → fail-open (allow)
|
* - provider "none" (or unset) → fail-open (allow)
|
||||||
@@ -11,12 +11,13 @@ import { siteSettings } from "@/lib/services/site-settings";
|
|||||||
* API error, or network failure → fail-closed (deny)
|
* API error, or network failure → fail-closed (deny)
|
||||||
*
|
*
|
||||||
* Settings keys:
|
* Settings keys:
|
||||||
* captcha_provider = "turnstile" | "recaptcha" | "none" (default none)
|
* captcha_provider = "turnstile" | "recaptcha" | "hcaptcha" | "none" (default none)
|
||||||
* turnstile_secret / turnstile_site_key
|
* turnstile_secret / turnstile_site_key
|
||||||
* recaptcha_secret / recaptcha_site_key
|
* recaptcha_secret / recaptcha_site_key
|
||||||
|
* hcaptcha_secret / hcaptcha_site_key
|
||||||
*/
|
*/
|
||||||
export interface CaptchaConfig {
|
export interface CaptchaConfig {
|
||||||
provider: "turnstile" | "recaptcha" | "none";
|
provider: "turnstile" | "recaptcha" | "hcaptcha" | "none";
|
||||||
siteKey: string;
|
siteKey: string;
|
||||||
/** Form field the widget writes the token into. */
|
/** Form field the widget writes the token into. */
|
||||||
field: string;
|
field: string;
|
||||||
@@ -25,6 +26,7 @@ export interface CaptchaConfig {
|
|||||||
const TURNSTILE_URL =
|
const TURNSTILE_URL =
|
||||||
"https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
"https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
||||||
const RECAPTCHA_URL = "https://www.google.com/recaptcha/api/siteverify";
|
const RECAPTCHA_URL = "https://www.google.com/recaptcha/api/siteverify";
|
||||||
|
const HCAPTCHA_URL = "https://hcaptcha.com/siteverify";
|
||||||
|
|
||||||
/** Public config the register/login pages need to render the widget (no secrets). */
|
/** Public config the register/login pages need to render the widget (no secrets). */
|
||||||
export async function captchaConfig(): Promise<CaptchaConfig> {
|
export async function captchaConfig(): Promise<CaptchaConfig> {
|
||||||
@@ -45,6 +47,13 @@ export async function captchaConfig(): Promise<CaptchaConfig> {
|
|||||||
field: "g-recaptcha-response",
|
field: "g-recaptcha-response",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (provider === "hcaptcha") {
|
||||||
|
return {
|
||||||
|
provider: "hcaptcha",
|
||||||
|
siteKey: (await siteSettings.get("hcaptcha_site_key", "")) ?? "",
|
||||||
|
field: "hcaptcha-response",
|
||||||
|
};
|
||||||
|
}
|
||||||
return { provider: "none", siteKey: "", field: "" };
|
return { provider: "none", siteKey: "", field: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +67,22 @@ export async function verifyCaptcha(
|
|||||||
|
|
||||||
if (!cfg.siteKey) return false;
|
if (!cfg.siteKey) return false;
|
||||||
|
|
||||||
const secretKey =
|
const secretKey: string =
|
||||||
cfg.provider === "turnstile" ? "turnstile_secret" : "recaptcha_secret";
|
cfg.provider === "turnstile"
|
||||||
|
? "turnstile_secret"
|
||||||
|
: cfg.provider === "recaptcha"
|
||||||
|
? "recaptcha_secret"
|
||||||
|
: "hcaptcha_secret";
|
||||||
const secret = (await siteSettings.get(secretKey, "")) ?? "";
|
const secret = (await siteSettings.get(secretKey, "")) ?? "";
|
||||||
if (!secret) return false;
|
if (!secret) return false;
|
||||||
if (!token) return false;
|
if (!token) return false;
|
||||||
|
|
||||||
const url = cfg.provider === "turnstile" ? TURNSTILE_URL : RECAPTCHA_URL;
|
const url: string =
|
||||||
|
cfg.provider === "turnstile"
|
||||||
|
? TURNSTILE_URL
|
||||||
|
: cfg.provider === "recaptcha"
|
||||||
|
? RECAPTCHA_URL
|
||||||
|
: HCAPTCHA_URL;
|
||||||
const body = new URLSearchParams({ secret, response: token });
|
const body = new URLSearchParams({ secret, response: token });
|
||||||
if (remoteIp) body.set("remoteip", remoteIp);
|
if (remoteIp) body.set("remoteip", remoteIp);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user