-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Friendly captcha #6546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Friendly captcha #6546
Changes from all commits
7c72cca
3f353e5
d988793
fa1d811
5b162fd
2cbec79
d4bc5e7
f62e808
df118f5
f71599c
5084688
dee11b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| defmodule PlausibleWeb.Components.Captcha do | ||
| @moduledoc """ | ||
| Friendly Captcha widget shared between the registration and password-reset forms. | ||
|
|
||
| Renders the (invisible) widget placeholder, the SDK script tags, and the reveal | ||
| script that: | ||
|
|
||
| * matches the widget to the app's resolved light/dark theme, | ||
| * reveals the widget only when the user must interact (or on error/slow solve), | ||
| * dispatches `frc-captcha-ready` / `frc-captcha-reset` window events so the | ||
| submit button can gate on a valid solution. | ||
|
|
||
| Pass `live?={true}` from a LiveView so the widget and scripts carry | ||
| `phx-update="ignore"` and survive DOM patching. | ||
| """ | ||
| use Phoenix.Component, global_prefixes: ~w(x-) | ||
|
|
||
| attr :live?, :boolean, default: false | ||
| attr :error, :string, default: nil | ||
|
|
||
| def widget(assigns) do | ||
| ~H""" | ||
| <div> | ||
| <div | ||
| phx-update={if @live?, do: "ignore"} | ||
| id="frc-captcha-placeholder" | ||
| class="frc-captcha hidden mb-2" | ||
| data-sitekey={PlausibleWeb.Captcha.sitekey()} | ||
| data-start="auto" | ||
| style="width: 100%" | ||
| > | ||
| </div> | ||
| <p :if={@error} class="text-xs text-red-500 mt-2"> | ||
| {@error} | ||
| </p> | ||
| <p class="text-xs text-gray-500 dark:text-gray-400"> | ||
| This site is protected by | ||
| <PlausibleWeb.Components.Generic.styled_link href="https://friendlycaptcha.com" new_tab={true}> | ||
| Friendly Captcha | ||
| </PlausibleWeb.Components.Generic.styled_link> | ||
| </p> | ||
| <script | ||
| phx-update={if @live?, do: "ignore"} | ||
| id="frc-captcha-script" | ||
| type="module" | ||
| src={ | ||
| PlausibleWeb.Router.Helpers.static_path( | ||
| PlausibleWeb.Endpoint, | ||
| "/js/friendly-captcha/site.min.js" | ||
| ) | ||
| } | ||
| async | ||
| defer | ||
| > | ||
| </script> | ||
| <script | ||
| phx-update={if @live?, do: "ignore"} | ||
| id="frc-captcha-script-compat" | ||
| nomodule | ||
| src={ | ||
| PlausibleWeb.Router.Helpers.static_path( | ||
| PlausibleWeb.Endpoint, | ||
| "/js/friendly-captcha/site.compat.min.js" | ||
| ) | ||
| } | ||
| async | ||
| defer | ||
| > | ||
| </script> | ||
| <script phx-update={if @live?, do: "ignore"} id="frc-captcha-reveal"> | ||
| (function () { | ||
| var SHOW_AFTER_LONG_WAIT_MS = 5000; | ||
| var el = document.getElementById("frc-captcha-placeholder"); | ||
| if (!el) return; | ||
|
|
||
| // Match the widget to the app's resolved light/dark theme. This runs | ||
| // before the (deferred) SDK initializes, so the widget picks up the | ||
| // right theme from the start, and the observer keeps it in sync. | ||
| function applyTheme() { | ||
| el.dataset.theme = | ||
| document.documentElement.classList.contains("dark") ? "dark" : "light"; | ||
| } | ||
| applyTheme(); | ||
| new MutationObserver(applyTheme).observe(document.documentElement, { | ||
| attributes: true, | ||
| attributeFilter: ["class"] | ||
| }); | ||
|
|
||
| function show() { el.classList.remove("hidden"); } | ||
| var timeout; | ||
| // Friendly Captcha carries the event payload on `e.detail` (not `e`). | ||
| el.addEventListener("frc:widget.statechange", function (e) { | ||
| var d = e.detail || {}; | ||
| // Interactive mode means the user must click to solve: reveal the widget. | ||
| if (d.mode === "interactive") { show(); } | ||
| // Reveal if solving takes unusually long, then stop waiting once done. | ||
| if (d.state === "requesting") { | ||
| clearTimeout(timeout); | ||
| timeout = setTimeout(show, SHOW_AFTER_LONG_WAIT_MS); | ||
| } else if (d.state === "completed") { | ||
| clearTimeout(timeout); | ||
| } | ||
| // Reveal on error or expiry so the user can recover. | ||
| if (d.state === "error" || d.state === "expired") { show(); } | ||
| // Enable the submit button only once we hold a valid solution. | ||
| window.dispatchEvent(new Event( | ||
| d.state === "completed" ? "frc-captcha-ready" : "frc-captcha-reset" | ||
| )); | ||
| }); | ||
| })(); | ||
| </script> | ||
| </div> | ||
| """ | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,31 +131,7 @@ defmodule PlausibleWeb.Live.RegisterForm do | |
| </div> | ||
|
|
||
| <%= if PlausibleWeb.Captcha.enabled?() do %> | ||
| <div> | ||
| <div | ||
| phx-update="ignore" | ||
| id="hcaptcha-placeholder" | ||
| class="h-captcha" | ||
| data-sitekey={PlausibleWeb.Captcha.sitekey()} | ||
| > | ||
| </div> | ||
| <p | ||
| :if={@captcha_error} | ||
| class="text-xs text-red-500 mt-2" | ||
| x-data | ||
| x-init="hcaptcha.reset()" | ||
| > | ||
| {@captcha_error} | ||
| </p> | ||
| <script | ||
| phx-update="ignore" | ||
| id="hcaptcha-script" | ||
| src="https://hcaptcha.com/1/api.js" | ||
| async | ||
| defer | ||
| > | ||
| </script> | ||
| </div> | ||
| <PlausibleWeb.Components.Captcha.widget live?={true} error={@captcha_error} /> | ||
| <% end %> | ||
|
|
||
| <div class="flex flex-col gap-y-4"> | ||
|
|
@@ -165,7 +141,16 @@ defmodule PlausibleWeb.Live.RegisterForm do | |
| else | ||
| "Start my free trial" | ||
| end %> | ||
| <.button id="register" disabled={@disable_submit} type="submit" class="w-full" mt?={false}> | ||
| <.button | ||
| id="register" | ||
| type="submit" | ||
| class="w-full" | ||
| mt?={false} | ||
| x-data={"{ captchaReady: #{not PlausibleWeb.Captcha.enabled?()} }"} | ||
| x-on:frc-captcha-ready.window="captchaReady = true" | ||
| x-on:frc-captcha-reset.window="captchaReady = false" | ||
| x-bind:disabled={"!captchaReady || #{@disable_submit}"} | ||
| > | ||
|
Comment on lines
+144
to
+153
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, a failed submit (for whatever reason, e.g. email already exists) will use up the one-time token and the next submit is guaranteed to fail with a "Please complete the captcha" error. Only a page refresh will fix that, meaning that the user needs to fill out all the fields again. In LiveViews, when a form submit failure comes back from the server, we should make sure to reset the captcha automatically. |
||
| {submit_text} | ||
| </.button> | ||
|
|
||
|
|
@@ -259,7 +244,7 @@ defmodule PlausibleWeb.Live.RegisterForm do | |
| %{assigns: %{invitation: %{} = invitation}} = socket | ||
| ) do | ||
| if not PlausibleWeb.Captcha.enabled?() or | ||
| PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do | ||
| PlausibleWeb.Captcha.verify(params["frc-captcha-response"]) do | ||
|
Comment on lines
246
to
+247
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: We could omit the Same goes for line 262 in this file (non-invitation register event handler). |
||
| user = | ||
| params["user"] | ||
| |> Map.put("email", invitation.email) | ||
|
|
@@ -275,7 +260,7 @@ defmodule PlausibleWeb.Live.RegisterForm do | |
|
|
||
| def handle_event("register", %{"user" => _} = params, socket) do | ||
| if not PlausibleWeb.Captcha.enabled?() or | ||
| PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do | ||
| PlausibleWeb.Captcha.verify(params["frc-captcha-response"]) do | ||
| user = Auth.User.new(params["user"]) | ||
|
|
||
| add_user(socket, user) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.