feat: add CAPTCHA protection to subscribe form (#729) - #803
Conversation
…nalytics Providers)
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdded CAPTCHA support for hCaptcha, reCAPTCHA, and Turnstile. The subscription flow now verifies CAPTCHA tokens before login. Added provider management, shared settings UI, documentation, tests, and permission wiring. ChangesCAPTCHA protection
Provider management
Documentation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SubscribeMenu
participant CaptchaConfigEndpoint
participant CaptchaSDK
participant SubscriptionAPI
participant CaptchaController
participant CaptchaProvider
SubscribeMenu->>CaptchaConfigEndpoint: Fetch active provider and site key
CaptchaConfigEndpoint-->>SubscribeMenu: Return public CAPTCHA configuration
SubscribeMenu->>CaptchaSDK: Load SDK and render widget
CaptchaSDK-->>SubscribeMenu: Return token and readiness
SubscribeMenu->>SubscriptionAPI: Submit email and captchaToken
SubscriptionAPI->>CaptchaController: VerifyCaptchaToken(captchaToken)
CaptchaController->>CaptchaProvider: Send verification request
CaptchaProvider-->>CaptchaController: Return success status
CaptchaController-->>SubscriptionAPI: Return verification result
SubscriptionAPI-->>SubscribeMenu: Return login response
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| Filename | Overview |
|---|---|
| src/lib/server/controllers/captchaController.ts | Resolves provider configuration and verifies tokens, but maps incomplete enabled configuration differently between public discovery and server enforcement. |
| src/lib/components/SubscribeMenu.svelte | Integrates CAPTCHA gating and now replaces consumed tokens by returning resend users to a newly mounted challenge. |
| src/lib/components/Captcha.svelte | Loads provider SDKs, renders widgets, exposes reset behavior, and handles asynchronous SDK readiness. |
| src/routes/(kener)/captcha-config.json/+server.ts | Publishes only provider and site-key data, while treating incomplete enabled configuration as though CAPTCHA were disabled. |
| src/lib/server/api-server/subscription/post.ts | Verifies CAPTCHA before invoking the existing OTP login operation. |
| src/lib/components/manage/ProviderSettingsPanel.svelte | Adds reusable provider configuration UI with single-active-provider enforcement and guarded persistence. |
Sequence Diagram
sequenceDiagram
participant User
participant UI as Subscribe Menu
participant Config as captcha-config.json
participant Provider as CAPTCHA Provider
participant API as Subscription API
participant Mail as OTP Sender
UI->>Config: Request active provider and site key
Config-->>UI: Provider configuration
UI->>Provider: Render and solve challenge
Provider-->>UI: Single-use token
UI->>API: Login request with email and token
API->>Provider: Verify token with secret key
Provider-->>API: Verification result
alt Verification succeeds
API->>Mail: Send OTP
API-->>UI: Success
else Verification fails
API-->>UI: Captcha verification failed
end
Reviews (6): Last reviewed commit: "fix: send user back to solve a fresh cap..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/components/Captcha.svelte`:
- Around line 34-45: Update loadScript so an existing provider script is awaited
until it finishes loading rather than resolving immediately. Cache and reuse the
in-flight Promise at module scope, or attach one-time load and error listeners
to the existing script, ensuring callers reject on load failure and only proceed
after the provider script is ready.
In `@src/lib/components/manage/ProviderSettingsPanel.svelte`:
- Around line 59-81: Update fetchData in ProviderSettingsPanel.svelte (lines
59-81) to surface result.error via toast.error and assign selected =
providers[0] ?? null in finally so failed loads still initialize the form.
Update save (lines 100-112) to avoid sending client-side requirements for other
providers; send only their disable intent, preserve stored requirements
server-side, or block saving until loading succeeds.
- Around line 204-215: Update the requirements rendering in the selected
provider settings panel so the Input id and Label for use a whitespace-free slug
derived from req.label, while keeping req.label as the visible label text.
Ensure the same derived identifier is used for both attributes to preserve their
association.
- Around line 14-29: Move the exported ProviderDefinition and
ProviderRequirement interfaces out of ProviderSettingsPanel.svelte into
src/lib/client/types/provider-settings.ts, then import and re-export them from
the component as needed so existing type-only consumers can use
ProviderDefinition without conflicting with the component’s default export.
Update +page.svelte or other consumers to import the type from the dedicated
module or component type export while preserving the existing shapes.
In `@src/lib/components/SubscribeMenu.svelte`:
- Around line 116-119: Update the failed login request handling in SubscribeMenu
so a rejected CAPTCHA token resets or remounts the Captcha instance, not just
captchaToken, restoring the Continue flow with a replacement token. Use the
Captcha rendering/configuration near the existing instance and add a browser
test covering a completed token being rejected and the widget issuing a new
token.
In `@src/lib/server/controllers/captchaController.ts`:
- Around line 51-55: Update the CAPTCHA verification fetch in the controller to
use an AbortController with a short configured timeout, passing its signal to
fetch and clearing the timeout after completion. Treat an abort like other
verification failures and return the existing failure response instead of
allowing the request to wait indefinitely.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9f8ac08e-adc4-4d20-9ea6-7ce77eafa6ed
⛔ Files ignored due to path filters (3)
static/hcaptcha.pngis excluded by!**/*.pngstatic/recaptcha.pngis excluded by!**/*.pngstatic/turnstile.pngis excluded by!**/*.png
📒 Files selected for processing (23)
src/lib/allPerms.tssrc/lib/components/Captcha.sveltesrc/lib/components/Captcha.svelte.test.tssrc/lib/components/SubscribeMenu.sveltesrc/lib/components/SubscribeMenu.svelte.test.tssrc/lib/components/manage/ProviderSettingsPanel.sveltesrc/lib/server/api-server/index.tssrc/lib/server/api-server/subscription/post.test.tssrc/lib/server/api-server/subscription/post.tssrc/lib/server/controllers/captchaController.test.tssrc/lib/server/controllers/captchaController.tssrc/lib/server/controllers/siteDataController.tssrc/lib/server/controllers/siteDataKeys.tssrc/lib/server/db/dbimpl.tssrc/lib/server/db/repositories/site-data.tssrc/routes/(docs)/docs.jsonsrc/routes/(docs)/docs/content/v4/captcha.mdsrc/routes/(docs)/docs/content/v4/subscriptions.mdsrc/routes/(kener)/captcha-config.json/+server.tssrc/routes/(kener)/captcha-config.json/captcha-config.test.tssrc/routes/(manage)/+layout.sveltesrc/routes/(manage)/manage/app/analytics-providers/+page.sveltesrc/routes/(manage)/manage/app/captcha-providers/+page.svelte
There was a problem hiding this comment.
Pull request overview
Adds optional CAPTCHA protection to the public subscription login flow to mitigate email/OTP flooding (#729), with admin-configurable provider settings and a public site-key-only config endpoint consumed by the client widget.
Changes:
- Introduces CAPTCHA provider management in the admin UI (single-active provider) and a reusable
ProviderSettingsPanel. - Adds public
/captcha-config.jsonendpoint + client-sideCaptchawidget integration intoSubscribeMenu, and server-side token verification on subscription login. - Updates docs/navigation/permissions and adds tests for controller, API route, and UI gating.
Reviewed changes
Copilot reviewed 23 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/(manage)/manage/app/captcha-providers/+page.svelte | New admin page to configure CAPTCHA providers via shared settings panel. |
| src/routes/(manage)/manage/app/analytics-providers/+page.svelte | Refactors analytics provider settings to use ProviderSettingsPanel. |
| src/routes/(manage)/+layout.svelte | Adds “Captcha Providers” navigation entry. |
| src/routes/(kener)/captcha-config.json/+server.ts | Adds public endpoint returning active provider + site key only. |
| src/routes/(kener)/captcha-config.json/captcha-config.test.ts | Tests config endpoint output and secret non-disclosure. |
| src/routes/(docs)/docs/content/v4/subscriptions.md | Documents CAPTCHA requirement in subscription flow when enabled. |
| src/routes/(docs)/docs/content/v4/captcha.md | New documentation page for CAPTCHA setup and providers. |
| src/routes/(docs)/docs.json | Adds Captcha docs entry to docs navigation. |
| src/lib/server/db/repositories/site-data.ts | Adds getAllSiteDataByPrefix helper and reuses it for analytics. |
| src/lib/server/db/dbimpl.ts | Exposes/binds getAllSiteDataByPrefix on db impl. |
| src/lib/server/controllers/siteDataKeys.ts | Registers new captcha.* site-data keys as valid JSON objects. |
| src/lib/server/controllers/siteDataController.ts | Adds GetAllCaptchaData() to fetch/parse captcha site-data entries. |
| src/lib/server/controllers/captchaController.ts | Adds active-provider resolution and server-side token verification. |
| src/lib/server/controllers/captchaController.test.ts | Unit tests for provider resolution + verification behavior. |
| src/lib/server/api-server/subscription/post.ts | Requires CAPTCHA verification for subscription “login” action. |
| src/lib/server/api-server/subscription/post.test.ts | Tests CAPTCHA gating behavior for subscription login action. |
| src/lib/server/api-server/index.ts | Narrows eager glob import to method files to avoid bundling co-located tests. |
| src/lib/components/SubscribeMenu.svelte | Adds CAPTCHA widget + token submission and dialog iframe interaction handling. |
| src/lib/components/SubscribeMenu.svelte.test.ts | Tests client-side gating of Continue button based on captcha config. |
| src/lib/components/manage/ProviderSettingsPanel.svelte | New reusable provider settings component (shared by analytics + captcha). |
| src/lib/components/Captcha.svelte | New client CAPTCHA loader/renderer with provider-agnostic render path. |
| src/lib/components/Captcha.svelte.test.ts | Tests captcha widget presence + required-state reporting. |
| src/lib/allPerms.ts | Adds route permission mapping for captcha providers admin page. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/server/controllers/captchaController.test.ts`:
- Around line 103-123: Update the “aborts the verify request after a timeout
instead of hanging indefinitely” test to advance fake timers by exactly the
five-second timeout boundary rather than 10,000ms, so it fails if
VERIFY_TIMEOUT_MS exceeds the intended limit. Keep the existing assertions and
cleanup unchanged.
In `@src/lib/server/controllers/captchaController.ts`:
- Around line 30-44: Update resolveCaptchaConfig and the related
GetActiveCaptchaProvider/VerifyCaptchaToken flow to enforce a single enabled
supported CAPTCHA provider before selecting or validating one. Detect multiple
enabled keys from GetAllCaptchaData and return a consistent misconfigured result
or otherwise reject the configuration, ensuring both callers cannot choose
different providers from unordered data.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ef5876dc-79d1-4f9e-b4ac-950d44114511
📒 Files selected for processing (2)
src/lib/server/controllers/captchaController.test.tssrc/lib/server/controllers/captchaController.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/components/SubscribeMenu.svelte.test.ts`:
- Around line 68-86: Update the test around the Turnstile setup and
Captcha.loadScript flow to stub script loading or dispatch a load event on the
created script after configuring window.turnstile. Ensure the script-loading
promise resolves without external I/O while preserving the existing render and
reset assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a6379d56-96b9-4769-8392-243a5e44e807
📒 Files selected for processing (6)
src/lib/components/Captcha.sveltesrc/lib/components/Captcha.svelte.test.tssrc/lib/components/SubscribeMenu.sveltesrc/lib/components/SubscribeMenu.svelte.test.tssrc/lib/server/api-server/subscription/post.test.tssrc/lib/server/api-server/subscription/post.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/components/Captcha.svelte`:
- Around line 16-24: Update loadScript to handle an existing script that
completed before listeners were attached: track load/error completion for
scripts created by the loader, and check CAPTCHA provider readiness for
externally existing scripts before awaiting events. Ensure the promise resolves
or rejects immediately when completion is already known, and add a regression
test that dispatches load before invoking loadScript.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: dc6dc7d3-3279-41bc-962d-77cd5f6e53f9
📒 Files selected for processing (7)
src/lib/client/types/provider-settings.tssrc/lib/components/Captcha.sveltesrc/lib/components/Captcha.svelte.test.tssrc/lib/components/manage/ProviderSettingsPanel.sveltesrc/lib/components/manage/ProviderSettingsPanel.svelte.test.tssrc/routes/(manage)/manage/app/analytics-providers/+page.sveltesrc/routes/(manage)/manage/app/captcha-providers/+page.svelte
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/components/Captcha.svelte`:
- Around line 31-38: Update loadScript to check whether an existing provider
script is already loaded before attaching listeners or waiting on it, resolving
immediately when ready. Ensure existing failed scripts are removed or replaced
so retries cannot reuse a dead element, and add regression tests covering both
previously-loaded and previously-failed script states.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6d7b8efe-5422-412d-8db2-7f4e44bd68b2
📒 Files selected for processing (2)
src/lib/components/Captcha.sveltesrc/lib/components/Captcha.svelte.test.ts
…laying consumed token
Fixes the email flooding issue from #729 - the subscribe form's login action had no rate limiting or human verification, so anyone could spam unlimited emails and trigger real OTP sends.
Added CAPTCHA support with hCaptcha, Google reCAPTCHA, and Cloudflare Turnstile, following the same pattern the app already uses for Analytics Providers: a Captcha Providers admin page, site-data backed config, and a public site-key-only endpoint the client widget reads from. Only one provider can be active at a time.
The subscribe form now requires a solved token before the OTP email gets sent, verified server-side against the provider's siteverify endpoint. Falls back to normal (no captcha) behavior if nothing's configured, so this is fully backwards compatible.
A couple of problems came up wiring the actual widgets that are worth calling out for review (since i used Claude to help me with these Issues):
.rendertowindow.grecaptchaasynchronously after script load, so calling it immediately can throwgrecaptcha.render is not a function. Gated on.ready()when the SDK exposes it.pointer-events: noneon<body>while open (its scroll-lock). reCAPTCHA's expanded image challenge gets injected as a direct child of<body>too, so it was silently inheriting that and becoming click-through, closing the dialog when you tried to interact with it. Re-enabled pointer-events on the challenge overlay specifically.Tested against all three providers on a live instance, including the actual reCAPTCHA image challenge end to end, not just the checkbox.
Fixes #729
Preview:
Summary by CodeRabbit