You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vibed/quotesdb/.beans/quotesdb-3mk8--cloudflare-t...

148 lines
5.0 KiB
Markdown

---
# quotesdb-3mk8
title: Cloudflare Turnstile CAPTCHA on quote submission
status: completed
type: feature
priority: high
created_at: 2026-03-10T23:32:09Z
updated_at: 2026-03-10T23:32:09Z
---
## Feature
Add Cloudflare Turnstile CAPTCHA to protect the `PUT /api/quotes` endpoint (and the submit form in the UI) from bots and spam. This is a three-part change: infra, API, and UI.
---
## Part 1 — Infra: Turnstile widget resource
Create `infra/turnstile.tf` with a `cloudflare_turnstile_widget` resource.
```hcl
# Turnstile CAPTCHA widget protecting the quote submission form.
# Provides a site_key (public, embedded in the UI) and secret_key
# (private, used by the API to verify tokens server-side).
resource "cloudflare_turnstile_widget" "submit" {
account_id = var.cloudflare_account_id
name = "quotesdb-submit"
# "managed" mode: Turnstile decides whether to show a visible challenge.
mode = "managed"
# Restrict the widget to the production domain.
domains = [var.domain]
}
output "turnstile_site_key" {
description = "Turnstile site key — safe to embed in the UI."
value = cloudflare_turnstile_widget.submit.id
}
output "turnstile_secret_key" {
description = "Turnstile secret key — inject into Workers via wrangler secret."
value = cloudflare_turnstile_widget.submit.secret
sensitive = true
}
```
The `var.domain` variable should already exist or be added alongside `var.cloudflare_account_id` in `providers.tf` / `variables.tf`.
After `tofu apply`, inject the secret into the Worker:
```sh
wrangler secret put TURNSTILE_SECRET_KEY
# paste the value from `tofu output -raw turnstile_secret_key`
```
**Validate:**
```sh
# From infra/ directory
tofu validate
tofu plan
```
---
## Part 2 — API: Verify Turnstile token in create handler
The API must verify the Turnstile token before creating a quote.
### Changes
**`src/lib.rs` (or a new `turnstile` module in `src/bin/api/`):**
Add a `verify_turnstile(token: &str, secret: &str, remote_ip: Option<&str>) -> Result<bool, Error>` function that POSTs to `https://challenges.cloudflare.com/turnstile/v0/siteverify`.
**`quotesdb::CreateQuoteInput` in `src/lib.rs`:**
Add a `cf_turnstile_token: Option<String>` field. It is optional so that local/test environments can skip verification when no secret is configured.
**`src/bin/api/handlers/mod.rs``create_handler`:**
Before calling `repo.create_quote(input)`, check:
1. Read `TURNSTILE_SECRET_KEY` from the environment.
2. If the env var is set:
- Extract `cf_turnstile_token` from the request body.
- If the token is absent, return `400 Bad Request`.
- Call `verify_turnstile(token, secret, remote_ip)`.
- If verification fails, return `403 Forbidden` with `{"error": "CAPTCHA verification failed"}`.
3. If the env var is absent (local dev), skip verification.
**HTTP client:** Add `reqwest` (with `default-features = false, features = ["json"]`) as a non-wasm32 dependency for the Turnstile API call. On wasm32 the create handler does not exist, so no conflict.
**Important:** Strip `cf_turnstile_token` from the `CreateQuoteInput` before passing it to the repository — the DB doesn't store it.
**Validation:**
```sh
# From quotesdb/ root
cargo fmt && cargo check && cargo clippy && cargo test
```
---
## Part 3 — UI: Embed Turnstile widget in submit form
### `index.html`
Add the Turnstile JS script tag to the `<head>`:
```html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
```
### `src/bin/ui/pages/submit.rs`
1. Add a `turnstile_token: UseStateHandle<Option<String>>` state handle.
2. Add the Turnstile widget div in the form, before the submit button:
```html
<div class="cf-turnstile"
data-sitekey="TURNSTILE_SITE_KEY_HERE"
data-callback="turnstile_callback">
</div>
```
The `data-callback` JS function name must be registered in `window`. Use `web_sys::window()` and `js_sys::Function` to expose a Rust closure that sets `turnstile_token` state.
3. Include the token in the `CreateQuoteInput` sent to the API.
**Site key:** The Turnstile site key is public and safe to hardcode in the UI source. Retrieve it from `tofu output -raw turnstile_site_key` after applying infra. Add a note in `docs/LOCAL_DEV.md` that local dev skips CAPTCHA (no env var set).
**Validation:**
```sh
cargo fmt && cargo check && cargo clippy && cargo test
trunk build
```
Manually verify: the submit form shows the Turnstile widget and submission is blocked if the challenge is not completed.
---
## Files touched
- `infra/turnstile.tf` (new)
- `src/lib.rs``CreateQuoteInput` + `verify_turnstile`
- `src/bin/api/handlers/mod.rs``create_handler`
- `src/bin/ui/pages/submit.rs` — widget embed + token state
- `index.html` — Turnstile JS script
- `Cargo.toml``reqwest` dependency (non-wasm32)
- `api/openapi.yaml` — document `cf_turnstile_token` field
- `docs/LOCAL_DEV.md` — note on local dev CAPTCHA bypass
## Commit scope
`feat(quotesdb): Cloudflare Turnstile CAPTCHA on submit`