mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-08-28 18:48:17 +00:00
Render the CAPTCHA widgets with React on server-rendered pages
Instead of provider-specific markup and script tags in the head, the captcha.form() template macro now emits a placeholder element carrying the CAPTCHA configuration in data attributes, on which a new entrypoint mounts the React Captcha component. The widget still injects its hidden response field inside the form, so the server-side verification flow is unchanged. This is the first use of inline React components to progressively enhance server-rendered pages.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
// Entrypoint which mounts the React `Captcha` component as an inline
|
||||
// "island" on server-rendered pages. The server templates emit a
|
||||
// placeholder element carrying the CAPTCHA configuration:
|
||||
//
|
||||
// <div data-captcha-service="…" data-captcha-site-key="…"></div>
|
||||
//
|
||||
// and include this entrypoint, which mounts the widget onto every such
|
||||
// element. This is the pattern we intend to use for progressively
|
||||
// enhancing server-rendered pages with React components.
|
||||
|
||||
import { StrictMode, Suspense } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import * as v from "valibot";
|
||||
import { Captcha, CaptchaPlaceholder } from "../components/Captcha";
|
||||
|
||||
const schema = v.object({
|
||||
captchaService: v.picklist([
|
||||
"recaptcha_v2",
|
||||
"cloudflare_turnstile",
|
||||
"hcaptcha",
|
||||
]),
|
||||
captchaSiteKey: v.string(),
|
||||
});
|
||||
|
||||
for (const node of document.querySelectorAll<HTMLElement>(
|
||||
"[data-captcha-service]",
|
||||
)) {
|
||||
const config = v.parse(schema, node.dataset);
|
||||
createRoot(node).render(
|
||||
<StrictMode>
|
||||
<Suspense fallback={<CaptchaPlaceholder />}>
|
||||
<Captcha
|
||||
config={{
|
||||
service: config.captchaService,
|
||||
site_key: config.captchaSiteKey,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
+1
-2
@@ -1,4 +1,5 @@
|
||||
{#
|
||||
Copyright 2025, 2026 Element Creations Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -15,7 +16,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
{% import "components/errors.html" as errors %}
|
||||
{% import "components/icon.html" as icon %}
|
||||
{% import "components/scope.html" as scope %}
|
||||
{% import "components/captcha.html" as captcha %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}">
|
||||
@@ -27,7 +27,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
{{ include_asset('src/entrypoints/shared.css') | indent(4) | safe }}
|
||||
{{ include_asset('src/entrypoints/templates.css') | indent(4) | safe }}
|
||||
{{ include_asset('src/entrypoints/templates.ts') | indent(4) | safe }}
|
||||
{{ captcha.head() }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout-container{% if consent_page is defined %} consent{% endif %}">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{#
|
||||
Copyright 2025, 2026 Element Creations Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -14,28 +15,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
{%- if captcha_config.service == "recaptcha_v2" -%}
|
||||
<div class="g-recaptcha {{ class }}" data-sitekey="{{ captcha_config.site_key }}"></div>
|
||||
{%- elif captcha_config.service == "cloudflare_turnstile" -%}
|
||||
<div class="cf-turnstile {{ class }}" data-sitekey="{{ captcha_config.site_key }}"></div>
|
||||
{%- elif captcha_config.service == "hcaptcha" -%}
|
||||
<div class="h-captcha {{ class }}" data-sitekey="{{ captcha_config.site_key }}"></div>
|
||||
{%- else -%}
|
||||
{{ throw(message="Invalid captcha service setup") }}
|
||||
{%- endif %}
|
||||
{{ include_asset('src/entrypoints/captcha.tsx') }}
|
||||
<div class="{{ class }}" data-captcha-service="{{ captcha_config.service }}" data-captcha-site-key="{{ captcha_config.site_key }}"></div>
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro head() -%}
|
||||
{%- if captcha_config|default(False) -%}
|
||||
{%- if captcha_config.service == "recaptcha_v2" -%}
|
||||
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
|
||||
{%- elif captcha_config.service == "cloudflare_turnstile" -%}
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
{%- elif captcha_config.service == "hcaptcha" -%}
|
||||
<script src="https://js.hcaptcha.com/1/api.js?recaptchacompat=off" async defer></script>
|
||||
{%- else -%}
|
||||
{{ throw(message="Invalid captcha service setup") }}
|
||||
{%- endif %}
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{#
|
||||
Copyright 2025, 2026 Element Creations Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -8,6 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% import "components/captcha.html" as captcha %}
|
||||
|
||||
{% block content %}
|
||||
<header class="page-heading">
|
||||
<div class="icon">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:71:28-48, pages/device_link.html:42:28-48, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:52:28-48"
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:71:28-48, pages/device_link.html:42:28-48, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:80:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:52:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"email_address": "Email address",
|
||||
"@email_address": {
|
||||
"context": "pages/recovery/start.html:34:33-58, pages/register/password.html:40:35-60, pages/upstream_oauth2/do_register.html:115:37-62"
|
||||
"context": "pages/recovery/start.html:34:33-58, pages/register/password.html:43:35-60, pages/upstream_oauth2/do_register.html:115:37-62"
|
||||
},
|
||||
"loading": "Loading…",
|
||||
"@loading": {
|
||||
@@ -91,15 +91,15 @@
|
||||
},
|
||||
"password": "Password",
|
||||
"@password": {
|
||||
"context": "pages/login.html:56:37-57, pages/reauth.html:28:35-55, pages/register/password.html:45:33-53"
|
||||
"context": "pages/login.html:56:37-57, pages/reauth.html:28:35-55, pages/register/password.html:48:33-53"
|
||||
},
|
||||
"password_confirm": "Confirm password",
|
||||
"@password_confirm": {
|
||||
"context": "pages/register/password.html:49:33-61"
|
||||
"context": "pages/register/password.html:52:33-61"
|
||||
},
|
||||
"username": "Username",
|
||||
"@username": {
|
||||
"context": "pages/login.html:51:39-59, pages/register/index.html:30:35-55, pages/register/password.html:34:33-53, pages/upstream_oauth2/do_register.html:101:35-55, pages/upstream_oauth2/do_register.html:107:39-59"
|
||||
"context": "pages/login.html:51:39-59, pages/register/index.html:30:35-55, pages/register/password.html:37:33-53, pages/upstream_oauth2/do_register.html:101:35-55, pages/upstream_oauth2/do_register.html:107:39-59"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -149,7 +149,7 @@
|
||||
"captcha": {
|
||||
"noscript": "This form is protected by a CAPTCHA and requires JavaScript to be enabled to submit it. Please enable JavaScript in your browser and reload this page.",
|
||||
"@noscript": {
|
||||
"context": "components/captcha.html:13:11-36"
|
||||
"context": "components/captcha.html:14:11-36"
|
||||
}
|
||||
},
|
||||
"change_password": {
|
||||
@@ -660,7 +660,7 @@
|
||||
"register": {
|
||||
"call_to_login": "Already have an account?",
|
||||
"@call_to_login": {
|
||||
"context": "pages/register/index.html:63:35-66, pages/register/password.html:80:33-64",
|
||||
"context": "pages/register/index.html:63:35-66, pages/register/password.html:83:33-64",
|
||||
"description": "Displayed on the registration page to suggest to log in instead"
|
||||
},
|
||||
"continue_with_email": "Continue with email address",
|
||||
@@ -678,12 +678,12 @@
|
||||
},
|
||||
"heading": "Create an account",
|
||||
"@heading": {
|
||||
"context": "pages/register/index.html:21:29-69, pages/register/password.html:18:27-67"
|
||||
"context": "pages/register/index.html:21:29-69, pages/register/password.html:21:27-67"
|
||||
}
|
||||
},
|
||||
"terms_of_service": "I agree to the <a href=\"%s\" data-kind=\"primary\" class=\"cpd-link\">Terms and Conditions</a>",
|
||||
"@terms_of_service": {
|
||||
"context": "pages/register/password.html:54:35-95, pages/upstream_oauth2/do_register.html:180:35-95"
|
||||
"context": "pages/register/password.html:57:35-95, pages/upstream_oauth2/do_register.html:180:35-95"
|
||||
}
|
||||
},
|
||||
"registration_token": {
|
||||
|
||||
Reference in New Issue
Block a user