17 Commits (95cd0a81833469861aaa92b2f28daaab8b91cd6b)

Author SHA1 Message Date
Elijah Voigt 2ccad33921 feat(quotesdb): Cloudflare Turnstile CAPTCHA on submit
- infra/turnstile.tf: provision Turnstile widget (managed mode, quotes.elijah.run domain) with site_key and secret_key outputs
- infra/variables.tf: add var.domain (default: quotes.elijah.run)
- src/lib.rs: add cf_turnstile_token: Option<String> (#[serde(default)]) to CreateQuoteInput; update doctest
- Cargo.toml: add reqwest (0.12, rustls-tls) under native-only dependencies
- src/bin/api/handlers/mod.rs: add verify_turnstile() and CAPTCHA gate in create_handler, both gated on #[cfg(not(target_arch = "wasm32"))]
- src/bin/api/db/native.rs: add cf_turnstile_token: None to all CreateQuoteInput struct literals in tests
- api/openapi.yaml: document cf_turnstile_token field in QuoteCreateRequest schema
- index.html: add Turnstile JS script tag
- src/bin/ui/pages/submit.rs: add turnstile_token state, use_effect_with callback registration, widget div, token included in CreateQuoteInput
- docs/LOCAL_DEV.md: add Cloudflare Turnstile CAPTCHA section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt fb93483f5c feat(quotesdb): date range filter for quotes list
Add 6 optional query parameters to GET /api/quotes:
  date_after_year/month/day and date_before_year/month/day

Changes:
- QuoteRepository::list_quotes gains date_after and date_before params
- NativeRepository and D1Repository build ISO date prefix WHERE clauses;
  quotes with NULL date are excluded when any bound is set
- list_handler validates component ordering (month requires year, etc.)
  and returns 400 on invalid combinations
- build_date_bound helper converts y/m/d components to ISO prefix strings
- UI api::list_quotes and browse page gain From/To year filter inputs
- author page call updated to pass None for the new date params
- openapi.yaml extended with 6 new query parameter entries
- 6 new integration tests covering after, before, range, and 400 cases
- 1 new native DB unit test covering all filter combinations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt caf2246bff feat(quotesdb): admin super auth code for quote moderation
Add an admin_config table storing a single admin auth code that
bypasses per-quote auth checks for update and delete operations.
The code is auto-generated on first startup and printed to stderr.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt c9e4d10934 feat(quotesdb): add workers-rs WASM entry point to api binary
- Gate native Tokio/Axum main() with #[cfg(not(target_arch = "wasm32"))]
- Add #![cfg_attr(target_arch = "wasm32", no_main)] to suppress missing-main error
- Add #[worker::event(fetch)] entry point using worker::HttpRequest / http::Response<axum::body::Body>
- Enable `http` feature on worker dep so fetch handler uses standard http types
- Add axum (json+query features), tower-service, and http to wasm32 deps
- Move async-trait to shared [dependencies] so both targets have it
- Make db::d1 module pub so main.rs can access D1Repository on wasm32
- Fix worker::d1::Database → D1Database and PreparedStatement → D1PreparedStatement
- Add #[cfg_attr(target_arch = "wasm32", worker::send)] to all 7 handler fns
  so their futures satisfy Axum's Handler<Send> bound on single-threaded wasm32
3 months ago
Elijah Voigt 84088ef45b fix(quotesdb): resolve compiler warnings in api and ui
- Remove redundant #![cfg(target_arch="wasm32")] from d1.rs (module
  declaration in mod.rs already gates it)
- Remove unused D1Repository re-export from db/mod.rs
- Drop unused page/total_count fields from UI ListResponse struct
  (only total_pages is consumed by the browse page)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt c52eb820d2 feat(quotesdb): implement D1Repository for Cloudflare Workers
Replace all 7 stub methods in src/bin/api/db/d1.rs with full working
implementations using the Cloudflare D1 API from workers-rs 0.5.

Implements:
- run_migrations: executes four DDL statements via db.exec()
- list_quotes: dynamic WHERE clause with positional params, COUNT query,
  paginated SELECT, per-quote tag fetch
- get_quote: prepared statement with first::<QuoteRow>()
- get_random_quote: ORDER BY RANDOM() LIMIT 1
- create_quote: INSERT + batch tag insert + read-back for timestamps
- update_quote: auth check, dynamic SET clause, optional tag replacement,
  read-back of updated row
- delete_quote: auth check, DELETE, returns DeleteResult enum

Also adds helper structs (QuoteRow, AuthRow, TagRow, CountRow),
fetch_tags() helper method, and unsafe Send/Sync impls required for
Arc<dyn QuoteRepository + Send + Sync> on single-threaded wasm32.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt cd3afa1920 fix(quotesdb): home page friendly empty state when no quotes
Detect 404 from random quote endpoint and render a friendly
'Nothing here yet — Submit a quote!' message with a link to
/submit instead of the generic error display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 8086ca06a1 fix(quotesdb): submit form UX fixes (6a4c61, dfd185, 5b3475)
- Replace 'Submit another' link with 'Browse all quotes' -> Route::Browse
- Change date input from type=text to type=date, remove placeholder
- Make author optional (defaults to Anonymous), update label
- Clarify auth code label to 'Edit/delete passphrase'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 739e6b8476 test(quotesdb): add integration test suite for all API endpoints
Adds 26 integration tests in handlers::integration_tests using a real
NativeRepository backed by a NamedTempFile SQLite database. Each test
gets an isolated database via test_router(). tempfile added to
[dev-dependencies]. Closes tickets: 5f5ba0, 9b581f, 789d0f, aa0eab,
f9f448, 4a4c26, 93f1b6, fae330, 8c87db, 893eba, e8f5cf, ce1e4f.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt d4be4653d8 fix(quotesdb): gate UI binary on wasm32, update UI ticket statuses
- main.rs: add #[cfg(target_arch = "wasm32")] guards on all wasm32-only
  code; add stub fn main() for native targets so cargo check passes
- Ticket statuses updated for all completed UI page/component tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 92e0f691cf feat(quotesdb): add comprehensive BEM stylesheet for all UI components
Covers all component and page classes:
- Navigation bar (nav, nav__brand, nav__links, nav__link)
- QuoteCard (quote-card, quote-card__text, footer, author, source, tags, tag, link)
- ErrorDisplay (error-display, error-display__message)
- Pagination (pagination, pagination__btn, pagination__info)
- AuthModal (auth-modal__overlay, auth-modal, auth-modal__title/input/actions)
- All page containers (page-home, page-browse, page-quote, page-author, page-submit)
- Edit form, submit form, success state with auth code box
- Base styles: reset, typography, buttons, form inputs
- Responsive breakpoint at 640px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 5aa31b59f0 feat(quotesdb): implement all UI page components (Home, Browse, QuoteDetail, Author, Submit)
- HomePage: fetches random quote on mount, displays with QuoteCard and browse/submit links
- BrowsePage: paginated list with author and tag filter inputs, Pagination component
- QuotePage: view/edit/delete with AuthModal gating, 403/404 handling, sessionStorage auth
- AuthorPage: lists quotes by author with tag filter and pagination
- SubmitPage: full form with all fields, success state showing auth code prominently
- Tag filter (d3d502) integrated into Browse and Author pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt a5b7c8d856 feat(quotesdb): implement API DB layer and all HTTP handlers
DB layer (src/bin/api/db/):
- native.rs: NativeRepository (tokio-rusqlite) implementing all CRUD ops,
  dynamic WHERE for filters, two-phase auth check for update, 13 unit tests
- d1.rs: D1Repository wasm32 stub (all methods return Internal error)
- connection.rs: open() helper — WAL + foreign_keys pragmas
- mod.rs: cfg-gate async_trait (Send on native, ?Send on wasm32)

Handlers (src/bin/api/handlers/mod.rs):
- All 7 routes: GET /api/, random, {id}, list, PUT create, POST update, DELETE
- Router order: random BEFORE {id} (prevents "random" matching as id)
- Auth: X-Auth-Code header validation → 403 on mismatch
- 13 handler unit tests with MockRepo

main.rs: opens DB, runs migrations, wraps in Arc<dyn Repo + Send + Sync>,
  binds on $PORT (default 3000)

Cargo.toml: tower dev-dep for ServiceExt::oneshot in tests

All 32 tests pass (26 api + 6 lib)

Tickets closed: 00aff0 a5049d 6e829e 28e7d9 886bfd 2ce22e 5dbb7d 05f8ae
                d792e2 5d9f5a b20b5a 175382 03bb91

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 682d15b40d feat(quotesdb): implement UI app shell, API client, storage, and base components
- Add BrowserRouter with all 5 routes (Home, Browse, QuoteDetail, Author, Submit)
- Implement typed API client (list, get, random, create, update, delete)
- Implement sessionStorage auth code helpers (set/get/clear)
- Add ErrorDisplay, QuoteCard, AuthModal, Pagination components
- Add stub page components for initial compilation
- Fix Cargo.toml: uuid js feature for wasm32, getrandom 0.3 wasm_js for rand dep,
  js-sys and Storage web-sys feature for API client and storage module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt ea6fa981fc feat(quotesdb): add QuoteRepository trait, migrations, and thiserror dependency
- db/mod.rs: QuoteRepository async trait + ListResult/DeleteResult/DbError types
- db/migrations.rs: SQL DDL strings for quotes, quote_tags, and indexes
- lib.rs: fix rand 0.9 trait import (SliceRandom → IndexedRandom)
- Cargo.toml: add thiserror = "2" for DbError derive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 66e5302d9b feat(quotesdb): add generate_id() using UUID v4 — WASM-compatible ID generation
- Add uuid = { version = "1", features = ["v4", "serde"] } to [dependencies]
- Add getrandom = { version = "0.4", features = ["wasm_js"] } under wasm32 cfg
- Implement generate_id() in src/lib.rs with rustdoc and doctest
- Add unit tests for length, hyphen count, and uniqueness

Closes ticket 7a0d9f

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt bd0c2af085 refactor(quotesdb): collapse to single crate with api and ui binaries
Replaces the three separate sub-crates (api/, ui/, tests/) with a single
Cargo crate at the quotesdb/ root. Shared code lives in src/lib.rs; the
api and ui are multi-binary targets; integration tests use the standard
Cargo tests/ layout. Trunk files moved to project root with data-bin="ui".

Closes ticket b38032.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago