55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
|
|
interface Props {
|
|
/** Render the mCaptcha widget. When false (or unconfigured) nothing is rendered. */
|
|
enabled: boolean;
|
|
/** Public base URL of the self-hosted mCaptcha instance (web config / MCAPTCHA_INSTANCE_URL). */
|
|
instanceUrl?: string;
|
|
/** Public sitekey for this surface (e.g. MCAPTCHA_SITEKEY_LOGIN). */
|
|
sitekey?: string;
|
|
}
|
|
|
|
// mCaptcha's vanilla glue mounts the widget into an element with this id and
|
|
// injects a hidden <input name="mcaptcha__token"> into the enclosing <form>
|
|
// on solve. Keep in sync with @mcaptcha/vanilla-glue (exported `ID`).
|
|
const MOUNT_ID = "mcaptcha__widget-container";
|
|
|
|
/**
|
|
* mCaptcha (proof-of-work) widget — replaces the former no-op RecaptchaWrapper.
|
|
*
|
|
* Render this **inside** the <form> it should protect: the glue injects the
|
|
* hidden `mcaptcha__token` field next to this mount, so it is submitted with
|
|
* the form and verified server-side by `@toolctrl/captcha` (see AuthService /
|
|
* api.leads.capture / infra/mcaptcha/README.md + AD-7).
|
|
*
|
|
* Client load failures are non-fatal — the server verifier fails open
|
|
* (CAPTCHA_FAIL_OPEN, default true) when no token is present. The glue is
|
|
* imported dynamically so it never runs during SSR.
|
|
*/
|
|
export default function Captcha({ enabled, instanceUrl, sitekey }: Props) {
|
|
const active = enabled && Boolean(instanceUrl && sitekey);
|
|
const initialized = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!active || initialized.current) return;
|
|
initialized.current = true;
|
|
let cancelled = false;
|
|
void (async () => {
|
|
try {
|
|
const { default: Widget } = await import("@mcaptcha/vanilla-glue");
|
|
if (cancelled) return;
|
|
// Widget self-mounts into #MOUNT_ID and injects the hidden mcaptcha__token field.
|
|
void new Widget({ siteKey: { instanceUrl: new URL(instanceUrl!), key: sitekey! } });
|
|
} catch (err) {
|
|
console.error("mCaptcha widget failed to initialise", err);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [active, instanceUrl, sitekey]);
|
|
|
|
if (!active) return null;
|
|
return <div id={MOUNT_ID} className="m-captcha" data-ui-id="ui.captcha.mount" />;
|
|
}
|