diff --git a/quotesdb/.nbd/tickets/06d304.md b/quotesdb/.nbd/tickets/06d304.md new file mode 100644 index 0000000..1a0656e --- /dev/null +++ b/quotesdb/.nbd/tickets/06d304.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/25c413.md b/quotesdb/.nbd/tickets/25c413.md index 183c1ba..c74f4d0 100644 --- a/quotesdb/.nbd/tickets/25c413.md +++ b/quotesdb/.nbd/tickets/25c413.md @@ -1,9 +1,9 @@ +++ title = "quotesdb/infra" priority = 7 -status = "done" +status = "todo" ticket_type = "project" -dependencies = ["07feaa", "5c0c64", "fc9bfd", "07cafb", "e2bd9b", "efee79", "2d1371", "d0da0b", "a23489", "ae886f", "ae6a82", "657836", "75489a", "71b1d4", "d5839a", "3781c9", "5137d7", "57fe5e"] +dependencies = ["06d304"] +++ diff --git a/quotesdb/.nbd/tickets/354276.md b/quotesdb/.nbd/tickets/354276.md new file mode 100644 index 0000000..591a106 --- /dev/null +++ b/quotesdb/.nbd/tickets/354276.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/3f22f2.md b/quotesdb/.nbd/tickets/3f22f2.md new file mode 100644 index 0000000..407ac22 --- /dev/null +++ b/quotesdb/.nbd/tickets/3f22f2.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/6c5904.md b/quotesdb/.nbd/tickets/6c5904.md new file mode 100644 index 0000000..3f2d000 --- /dev/null +++ b/quotesdb/.nbd/tickets/6c5904.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/77237f.md b/quotesdb/.nbd/tickets/77237f.md new file mode 100644 index 0000000..4d22a78 --- /dev/null +++ b/quotesdb/.nbd/tickets/77237f.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/8a7fba.md b/quotesdb/.nbd/tickets/8a7fba.md new file mode 100644 index 0000000..d3b1cf2 --- /dev/null +++ b/quotesdb/.nbd/tickets/8a7fba.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/b2af7f.md b/quotesdb/.nbd/tickets/b2af7f.md new file mode 100644 index 0000000..a2849b1 --- /dev/null +++ b/quotesdb/.nbd/tickets/b2af7f.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/c3503b.md b/quotesdb/.nbd/tickets/c3503b.md index 8f1e4b7..ce687e7 100644 --- a/quotesdb/.nbd/tickets/c3503b.md +++ b/quotesdb/.nbd/tickets/c3503b.md @@ -1,9 +1,9 @@ +++ title = "quotesdb/ui" priority = 7 -status = "done" +status = "todo" ticket_type = "project" -dependencies = ["166996", "5e3e37", "a9534d", "93515e", "dc3d2b", "04f865", "1e6a09", "0d987f", "2c5a57", "d3d502", "fc2f51", "f850c6", "1a274d", "1ba523", "5f1112", "5cdbd9", "b3ef98", "372790", "0fbdd5", "00d6d7", "9ef703", "5379eb"] +dependencies = ["b2af7f", "f4930e", "354276", "3f22f2", "cb8de0"] +++ diff --git a/quotesdb/.nbd/tickets/cb8de0.md b/quotesdb/.nbd/tickets/cb8de0.md new file mode 100644 index 0000000..390b49b --- /dev/null +++ b/quotesdb/.nbd/tickets/cb8de0.md @@ -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 +``` \ No newline at end of file diff --git a/quotesdb/.nbd/tickets/ec118c.md b/quotesdb/.nbd/tickets/ec118c.md index 442ebfd..5e78c33 100644 --- a/quotesdb/.nbd/tickets/ec118c.md +++ b/quotesdb/.nbd/tickets/ec118c.md @@ -1,7 +1,7 @@ +++ title = "quotesdb" priority = 8 -status = "done" +status = "todo" ticket_type = "project" dependencies = ["ce1e4f", "f3dc74", "c3503b", "25c413"] +++ diff --git a/quotesdb/.nbd/tickets/f3dc74.md b/quotesdb/.nbd/tickets/f3dc74.md index fbbddd8..d86a825 100644 --- a/quotesdb/.nbd/tickets/f3dc74.md +++ b/quotesdb/.nbd/tickets/f3dc74.md @@ -1,9 +1,9 @@ +++ title = "quotesdb/api" priority = 7 -status = "done" +status = "todo" ticket_type = "project" -dependencies = ["00aff0", "af56a7", "9c9546", "8892d5"] +dependencies = ["8a7fba", "77237f", "6c5904"] +++ diff --git a/quotesdb/.nbd/tickets/f4930e.md b/quotesdb/.nbd/tickets/f4930e.md new file mode 100644 index 0000000..dfc610d --- /dev/null +++ b/quotesdb/.nbd/tickets/f4930e.md @@ -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 +``` \ No newline at end of file