chore(quotesdb): add tickets for footer, hidden quotes, reporting, moderation, and rate limiting
New tickets: - b2af7f: ui — footer with contact email - 8a7fba: api — hidden flag for quotes (schema + endpoints) - 77237f: api — reports table + POST /api/quotes/:id/report - 6c5904: api — admin moderation endpoints - f4930e: ui — hidden toggle on quote pages - 354276: ui — report button with modal and Turnstile captcha - 3f22f2: ui — admin moderation tab - cb8de0: ui — admin auth-first flow, remove from default nav - 06d304: infra — Cloudflare rate limiting Reopened sub-project and root tickets; wired dependencies.quotesdb
parent
00d195c86f
commit
549accded0
@ -0,0 +1,52 @@
|
||||
+++
|
||||
title = "quotesdb/infra: Cloudflare rate limiting (WAF rules or Workers rate limiting per IP)"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = []
|
||||
+++
|
||||
## Summary
|
||||
Investigate and implement rate limiting for the quotesdb API and frontend using Cloudflare's native tooling.
|
||||
|
||||
## Options to Evaluate
|
||||
|
||||
### 1. Cloudflare WAF Rate Limiting Rules (Recommended starting point)
|
||||
- Available on free tier with limits; full control on Pro+
|
||||
- Rules can match on IP, path, method
|
||||
- Configure via OpenTofu (cloudflare_ruleset resource, phase: http_ratelimit)
|
||||
- Example: max 10 POST/PUT/DELETE requests per IP per minute to /api/*
|
||||
|
||||
### 2. Cloudflare Workers Rate Limiting API
|
||||
- workers-rs has bindings for the Rate Limiting API (available on paid plans)
|
||||
- More fine-grained: can key on IP + user-defined keys (e.g., quote ID)
|
||||
- Useful for per-resource rate limits (e.g., max N reports per IP per quote)
|
||||
|
||||
### 3. KV-based rate limiting in the Worker
|
||||
- Manual implementation using Cloudflare KV as a counter store
|
||||
- Works on free tier but adds latency and KV cost
|
||||
- Last resort if WAF rules are insufficient
|
||||
|
||||
## Suggested Limits (to start)
|
||||
- POST /api/quotes (create): 5 per IP per 10 minutes
|
||||
- POST /api/quotes/:id/report: 3 per IP per hour
|
||||
- POST /api/quotes/:id (update): 10 per IP per minute
|
||||
- DELETE /api/quotes/:id: 10 per IP per minute
|
||||
- GET endpoints: more generous or no limit (Cloudflare CDN caches anyway)
|
||||
|
||||
## Tasks
|
||||
- [ ] Research which Cloudflare plan features are available for this project
|
||||
- [ ] Implement WAF rate limiting rules in OpenTofu (infra/main.tf or new infra/rate-limits.tf)
|
||||
- [ ] If Workers Rate Limiting API is needed, add workers-rs bindings and implement in api/main.rs
|
||||
- [ ] Document the approach and any plan requirements in docs/ARCHITECTURE.md
|
||||
- [ ] Verify rules are applied with a test script (curl loop)
|
||||
|
||||
## Notes
|
||||
- CAPTCHA on the report endpoint (ticket 354276) provides an additional layer of bot protection
|
||||
- Rate limiting should complement, not replace, CAPTCHA
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
# from infra/
|
||||
tofu validate
|
||||
tofu plan
|
||||
```
|
||||
@ -0,0 +1,43 @@
|
||||
+++
|
||||
title = "quotesdb/ui: report button with modal (reason field + captcha)"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = ["77237f"]
|
||||
+++
|
||||
## Summary
|
||||
Add a Report button to quote detail pages that opens a modal with an optional reason field and a CAPTCHA to prevent abuse.
|
||||
|
||||
## Details
|
||||
- Report button appears on /quotes/:id page
|
||||
- Clicking opens a modal with:
|
||||
- Optional reason textarea (max 256 characters, show character counter)
|
||||
- CAPTCHA widget (Cloudflare Turnstile — see infra, or hCaptcha as fallback)
|
||||
- Submit button (disabled until CAPTCHA is completed)
|
||||
- Cancel button
|
||||
- On submit:
|
||||
- POST /api/quotes/:id/report with { reason?, captcha_token }
|
||||
- API verifies CAPTCHA server-side before creating report
|
||||
- Show success message on 201
|
||||
- Show error message on failure
|
||||
|
||||
## CAPTCHA
|
||||
Use Cloudflare Turnstile (free, privacy-friendly). Site key stored as an environment variable in Trunk.toml or index.html. The API worker verifies the token via the Turnstile verify endpoint.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Report button visible on /quotes/:id
|
||||
- [ ] Modal opens with reason textarea and CAPTCHA
|
||||
- [ ] Character counter on reason field
|
||||
- [ ] Submit disabled until CAPTCHA solved
|
||||
- [ ] Correct POST request on submit
|
||||
- [ ] Success and error feedback shown
|
||||
- [ ] Modal closes on cancel
|
||||
|
||||
## Depends on
|
||||
- 77237f (reports API endpoint)
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy
|
||||
trunk build
|
||||
```
|
||||
@ -0,0 +1,41 @@
|
||||
+++
|
||||
title = "quotesdb/ui: admin moderation tab (paginated reports, per-quote modal with delete/hide)"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = ["6c5904", "cb8de0"]
|
||||
+++
|
||||
## Summary
|
||||
Add a Moderation tab to the admin page showing a paginated list of reported quotes. Clicking a report opens a detail modal with the quote and all reports, plus action buttons.
|
||||
|
||||
## Tab: Moderation
|
||||
- Only visible on the admin page after auth is unlocked
|
||||
- Paginated list (10/page) of quotes with at least one report
|
||||
- Each row shows: quote text (truncated), author, report count, most recent report date
|
||||
- Clicking a row opens a Report Detail Modal
|
||||
|
||||
## Report Detail Modal
|
||||
- Shows the full quote (text, author, source, date, tags)
|
||||
- Lists all reports below: reason (or "No reason given"), reported date
|
||||
- Action buttons:
|
||||
- "Delete Quote" — calls DELETE /api/admin/reports/:quote_id/quote, closes modal, refreshes list
|
||||
- "Hide Quote" — calls POST /api/admin/reports/:quote_id/hide, shows success, refreshes list
|
||||
- "Dismiss Reports" — calls DELETE /api/admin/reports/:quote_id/reports, closes modal, refreshes list
|
||||
- Close/Cancel button
|
||||
|
||||
## Depends on
|
||||
- 6c5904 (admin moderation API endpoints)
|
||||
- cb8de0 (admin auth-first flow)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Moderation tab visible on /admin after unlock
|
||||
- [ ] Paginated list of reported quotes
|
||||
- [ ] Report detail modal with quote + reports
|
||||
- [ ] Delete, Hide, Dismiss actions work and refresh list
|
||||
- [ ] Loading and error states handled
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy
|
||||
trunk build
|
||||
```
|
||||
@ -0,0 +1,48 @@
|
||||
+++
|
||||
title = "quotesdb/api: admin moderation endpoints (list reports, delete/hide from report)"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = ["8a7fba", "77237f"]
|
||||
+++
|
||||
## Summary
|
||||
Add admin endpoints for viewing and acting on reported quotes.
|
||||
|
||||
## Endpoints
|
||||
GET /api/admin/reports?page=N
|
||||
- Returns paginated list of reported quotes (10/page)
|
||||
- Each entry: quote summary + report count + most recent report date
|
||||
- Requires X-Admin-Auth-Code header (admin auth)
|
||||
- Returns 403 on auth mismatch
|
||||
|
||||
GET /api/admin/reports/:quote_id
|
||||
- Returns the full quote + all reports for that quote (id, reason, created_at)
|
||||
- Requires X-Admin-Auth-Code header
|
||||
|
||||
DELETE /api/admin/reports/:quote_id/quote
|
||||
- Deletes the quote (cascades to reports via FK)
|
||||
- Requires X-Admin-Auth-Code header
|
||||
|
||||
POST /api/admin/reports/:quote_id/hide
|
||||
- Sets hidden=1 on the quote
|
||||
- Requires X-Admin-Auth-Code header
|
||||
|
||||
DELETE /api/admin/reports/:quote_id/reports
|
||||
- Clears all reports for a quote (dismiss reports without acting on the quote)
|
||||
- Requires X-Admin-Auth-Code header
|
||||
|
||||
## Notes
|
||||
- Admin auth is validated against the admin_auth_code in the DB (same as existing admin endpoints)
|
||||
- Depends on: 8a7fba (hidden flag), 77237f (reports table)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] All endpoints return correct data
|
||||
- [ ] All endpoints require and validate admin auth
|
||||
- [ ] Pagination works for GET /api/admin/reports
|
||||
- [ ] Delete cascades correctly
|
||||
- [ ] Unit tests for each endpoint
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy && cargo test
|
||||
```
|
||||
@ -0,0 +1,42 @@
|
||||
+++
|
||||
title = "quotesdb/api: reports table and POST /api/quotes/:id/report endpoint"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = []
|
||||
+++
|
||||
## Summary
|
||||
Create a reports table and a public endpoint for reporting quotes for moderation review.
|
||||
|
||||
## Schema
|
||||
```sql
|
||||
CREATE TABLE reports (
|
||||
id TEXT PRIMARY KEY, -- NanoID
|
||||
quote_id TEXT NOT NULL REFERENCES quotes(id) ON DELETE CASCADE,
|
||||
reason TEXT, -- optional, max 256 chars
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## API Endpoint
|
||||
POST /api/quotes/:id/report
|
||||
- Body: { reason?: string } — reason is optional, max 256 chars
|
||||
- Validates reason length (400 if > 256 chars)
|
||||
- Creates a report record
|
||||
- Returns 201 on success
|
||||
- Returns 404 if quote not found
|
||||
|
||||
## Rate Limiting Note
|
||||
Rate limiting will be handled separately at the Cloudflare layer (see infra ticket 06d304). No application-level rate limiting needed here.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] reports table created in migration
|
||||
- [ ] POST /api/quotes/:id/report works
|
||||
- [ ] reason is optional and validated (max 256 chars)
|
||||
- [ ] 404 on unknown quote_id
|
||||
- [ ] Unit tests cover success, missing quote, reason too long
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy && cargo test
|
||||
```
|
||||
@ -0,0 +1,37 @@
|
||||
+++
|
||||
title = "quotesdb/api: hidden flag for quotes (schema migration + endpoints)"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = []
|
||||
+++
|
||||
## Summary
|
||||
Add a boolean hidden flag to quotes. Hidden quotes are excluded from listing endpoints and require direct URL access. Changing a quote from hidden to public requires the auth code.
|
||||
|
||||
## Schema Migration
|
||||
Add column to quotes table:
|
||||
```sql
|
||||
ALTER TABLE quotes ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;
|
||||
```
|
||||
|
||||
## API Changes
|
||||
- GET /api/quotes — filter out hidden=1 quotes by default
|
||||
- GET /api/quotes/random — exclude hidden quotes
|
||||
- GET /api/quotes/:id — return hidden quotes (direct access allowed)
|
||||
- PUT /api/quotes — new quotes default to hidden=0 (not hidden)
|
||||
- POST /api/quotes/:id — allow toggling hidden field; requires X-Auth-Code header
|
||||
- Changing hidden=1 → hidden=0 (unhide) requires valid auth code
|
||||
- Changing hidden=0 → hidden=1 (hide) also requires valid auth code
|
||||
- The quote response body should include the hidden field
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Schema migration applied
|
||||
- [ ] Listing endpoints exclude hidden quotes
|
||||
- [ ] Direct quote access (/api/quotes/:id) works for hidden quotes
|
||||
- [ ] Toggle hidden requires valid X-Auth-Code (403 on mismatch)
|
||||
- [ ] All existing tests pass
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy && cargo test
|
||||
```
|
||||
@ -0,0 +1,27 @@
|
||||
+++
|
||||
title = "quotesdb/ui: add footer with contact email"
|
||||
priority = 3
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = []
|
||||
+++
|
||||
## Summary
|
||||
Add a footer to every page in the quotesdb UI displaying a contact email address.
|
||||
|
||||
## Details
|
||||
- Footer text: "Contact: quotes@elijah.run"
|
||||
- Should appear at the bottom of every page/route
|
||||
- Style consistently with the rest of the site (minimal, unobtrusive)
|
||||
- Make the email a clickable mailto: link
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Footer is visible on all routes (/, /browse, /quotes/:id, /author/:name, /submit, /admin)
|
||||
- [ ] Email is a mailto: link
|
||||
- [ ] Styling is consistent with site theme
|
||||
|
||||
## Validation
|
||||
Run from quotesdb/ root:
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy
|
||||
trunk build
|
||||
```
|
||||
@ -0,0 +1,40 @@
|
||||
+++
|
||||
title = "quotesdb/ui: admin page auth-first flow and remove from default nav"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = []
|
||||
+++
|
||||
## Summary
|
||||
Two related admin UX improvements:
|
||||
1. Remove the admin link from all default navigation/page footers — admins access /admin directly via URL.
|
||||
2. Rework the /admin page so it prompts for the auth code first; the rest of the admin controls are locked until auth succeeds.
|
||||
|
||||
## Details
|
||||
|
||||
### Remove Admin from Nav
|
||||
- Audit all pages and the nav component for any link to /admin
|
||||
- Remove them — /admin should not be discoverable from normal browsing
|
||||
- The route itself (/admin) remains accessible by direct URL
|
||||
|
||||
### Auth-First Admin Page
|
||||
Currently the /admin page may show controls before authenticating. Change the flow:
|
||||
- On load, /admin shows only an auth code input field and a submit button
|
||||
- On submit, call the existing admin status/verify endpoint (or any lightweight admin endpoint) with the provided auth code
|
||||
- On success: unlock and display all admin tabs (existing controls + new Moderation tab)
|
||||
- On failure (403): show an error message, keep page locked
|
||||
- The auth code is kept in component state (not localStorage) — refreshing the page requires re-entering it
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] No /admin link anywhere in default navigation or footer
|
||||
- [ ] /admin loads in locked state showing only auth input
|
||||
- [ ] Correct admin endpoints called with entered auth code
|
||||
- [ ] On success: all tabs visible and functional
|
||||
- [ ] On failure: error shown, page remains locked
|
||||
- [ ] Re-visiting /admin requires re-authenticating
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy
|
||||
trunk build
|
||||
```
|
||||
@ -0,0 +1,36 @@
|
||||
+++
|
||||
title = "quotesdb/ui: hidden toggle on quote pages (auth required to unhide)"
|
||||
priority = 5
|
||||
status = "todo"
|
||||
ticket_type = "feature"
|
||||
dependencies = ["8a7fba"]
|
||||
+++
|
||||
## Summary
|
||||
Add UI for toggling the hidden state of a quote on the quote detail page. Requires the quote's auth code to change hidden state.
|
||||
|
||||
## Details
|
||||
On the /quotes/:id page:
|
||||
- If the quote is hidden, show a visible "Hidden" badge/banner near the top of the quote
|
||||
- Show a toggle button: "Make Public" (if hidden) or "Hide" (if visible)
|
||||
- Clicking the toggle opens an auth prompt modal asking for the auth code
|
||||
- On submit, call POST /api/quotes/:id with the new hidden value and the X-Auth-Code header
|
||||
- On success, update the UI to reflect new state
|
||||
- On 403, show an error message (invalid auth code)
|
||||
|
||||
## Notes
|
||||
- Hidden quotes are still accessible at /quotes/:id (direct URL)
|
||||
- Depends on: 8a7fba (API hidden flag support)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Hidden badge visible when quote.hidden is true
|
||||
- [ ] Toggle button shown on quote detail page
|
||||
- [ ] Auth modal appears on click
|
||||
- [ ] Correct API call on submit
|
||||
- [ ] Success and error states handled
|
||||
- [ ] Works for both hiding and unhiding
|
||||
|
||||
## Validation
|
||||
```sh
|
||||
cargo fmt && cargo check && cargo clippy
|
||||
trunk build
|
||||
```
|
||||
Loading…
Reference in New Issue