- Add HideAuth action variant to the Action enum
- Show yellow badge when quote.hidden is true
- Add Hide/Make Public toggle button in the actions bar
- Reuse AuthModal for the hide/unhide auth prompt
- Call POST /api/quotes/:id with { hidden: !current } and X-Auth-Code
- Update quote state on success; re-prompt on 403
- Add .page-quote__hidden-badge CSS styles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Report button to /quotes/:id that opens a modal containing:
- Optional reason textarea (max 256 chars) with live character counter
- Cloudflare Turnstile CAPTCHA widget (always-passes test sitekey as const)
- Submit button disabled until CAPTCHA token is non-empty
- Cancel button
On submit POSTs { reason?, captcha_token } to POST /api/quotes/:id/report.
Shows a success banner on 201 or an error via ErrorDisplay on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a toggle button ("Filters ▼ / ▲") above the quote list on the
browse page. Filter controls are hidden by default and expand when
clicked. Each filter (Author, Tag, Date range) is on its own labelled
row with consistent styling. Existing API query logic is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ticket 6c5904 — five admin-authenticated endpoints for
moderation workflows:
GET /api/admin/reports paginated list of reported quotes
GET /api/admin/reports/:id full quote + all report rows
DELETE /api/admin/reports/:id/quote unconditionally delete a quote
POST /api/admin/reports/:id/hide set hidden=1 on a quote
DELETE /api/admin/reports/:id/reports clear all reports for a quote
All endpoints require X-Admin-Code header; 403 on missing/wrong code.
DB layer additions:
- QuoteRepository trait gains list_reports, get_reports_for_quote,
admin_delete_quote, hide_quote, and clear_reports methods
- New ReportRow, ReportSummary, ReportListResult, and QuoteReports
types added to db/mod.rs
- Implementations in native.rs (rusqlite) and d1.rs (Cloudflare D1)
Tests added:
- 14 unit handler tests using MockRepo (3 per endpoint covering
success, 404, and 403 cases)
- 5 integration tests using real SQLite via NativeRepository
- 10 DB-layer unit tests in native.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove the Admin link from the top navigation bar; /admin remains
reachable by direct URL but is no longer discoverable from normal
browsing.
- Rework /admin to an auth-first flow: on load the page shows only a
password input and an Unlock button. On success, the admin controls
(submission lock/unlock, auth code reset) are revealed; on failure a
clear error message is shown and the page stays locked. Refreshing
always resets to locked state (code is in component state only).
- Add api::verify_admin_code() — calls POST /api/admin/reset-auth-code
with new_code equal to the entered code, making the call idempotent
on success (code unchanged) while still returning 403 on mismatch.
- Fix pre-existing wasm build breakage in quote.rs: UpdateQuoteInput
gained a hidden field in an earlier ticket but quote.rs was never
updated. Added hidden: None to the struct literal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CREATE_REPORTS migration constant (was unused — now wired in)
- Wire CREATE_REPORTS into run_migrations for both NativeRepository and D1Repository
- Add create_report to QuoteRepository trait with NotFound semantics
- Implement create_report in NativeRepository (two-step: existence check then insert)
- Implement create_report in D1Repository (two-step: COUNT check then insert)
- Add report_handler: POST /api/quotes/{id}/report, 201/400/404/500
- Register route before /{id} in router so static /report suffix wins
- Add create_report to MockRepo in handler tests
- Add handler tests: test_report_success, test_report_quote_not_found, test_report_reason_too_long
- Add DB tests: test_create_report_success, test_create_report_not_found
- Add ReportInput schema and /api/quotes/{id}/report path to openapi.yaml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add three tests verifying hidden-quote filtering behaviour in
NativeRepository:
- list_quotes_excludes_hidden: hidden quotes do not appear in paginated
listing results.
- get_random_quote_excludes_hidden: get_random_quote returns None when
the only quote is hidden.
- get_quote_returns_hidden_quote: get_quote (direct ID lookup) still
returns the quote when it is hidden.
Also refactor the inline row-mapping closure in list_quotes to use the
existing row_to_quote helper, eliminating duplicated column mapping
logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `hidden: bool` to the `Quote` struct and `hidden: Option<bool>` to
`UpdateQuoteInput` in `src/lib.rs`
- Add `ALTER_QUOTES_ADD_HIDDEN` migration constant in `db/migrations.rs`
- Apply the ALTER TABLE migration in `NativeRepository::run_migrations` and
`D1Repository::run_migrations` with try/ignore for idempotency
- Exclude hidden quotes from `list_quotes` (WHERE hidden = 0) and
`get_random_quote` in both native and D1 implementations
- Update all SELECT queries to include the `hidden` column
- Handle `hidden` field in `update_quote` SET clause for both implementations
- Update `MockRepo` and `sample_quote` in handler tests to include `hidden`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Check GET /api/status on mount; if submissions_locked is true, hide the
form and show a .submissions-closed-banner instead. Fail-open: on error,
treat as unlocked and display the form normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass "" for the unused `current` param in admin_reset_auth_code (the
server only checks X-Admin-Code). Handle get_status() failure in the
on-mount effect with fail-open behaviour: set submissions_locked=Some(false)
and display "Could not fetch status." so the UI never hangs on
"Loading status...".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the two-step read-check-write in update_admin_auth_code with a
single atomic UPDATE … WHERE key = 'admin_auth_code' AND value = ?current
in both NativeRepository and D1Repository. Rows-affected count is checked:
zero means the code was absent or mismatched → DbError::Forbidden; one
means success.
Also remove the now-unnecessary replacement2 clone binding in native.rs.
Fix the reset_auth_code handler doc comment to accurately describe that a
missing X-Admin-Code header is caught by the handler itself (before any DB
call), while a wrong-but-present code reaches the DB layer which returns
DbError::Forbidden.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds handler, route registration, request/response types, and five unit
tests for the admin auth-code rotation endpoint. Updates openapi.yaml
with the new path and a ResetAuthCodeResponse component schema.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a pre-flight check at the top of create_handler that calls
get_submissions_locked() before processing the request. Returns
423 Locked with {"error": "submissions are closed"} when locked.
Update openapi.yaml to document the 423 response on PUT /api/quotes.
Add three unit tests: locked → 423, unlocked → 201, unlock-then-create → 201.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Clarify verify_admin_code docstring to say "standard string equality"
instead of leaving comparison method implicit
- Add missing "500" response entries to /api/admin/lock and
/api/admin/unlock in openapi.yaml
- Remove pub from lock_submissions and unlock_submissions to match all
other handlers in the file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two admin-protected endpoints that toggle the global submissions lock:
- POST /api/admin/lock — sets submissions_locked = true
- POST /api/admin/unlock — sets submissions_locked = false
Both require the X-Admin-Code header and return { "submissions_locked": bool }
on success, or 403 on missing/wrong code. Operation is idempotent.
Shared helper verify_admin_code() fetches and compares the stored admin code.
Routes registered in the router() function. Five unit tests added covering
correct code, wrong code, missing header, and idempotent lock behaviour.
OpenAPI spec updated with AdminCode security scheme, LockResponse schema,
/api/admin/lock and /api/admin/unlock path entries, and an admin tag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the GET /api/status handler that returns {"submissions_locked": bool}.
Registers the route in the router before the quotes routes.
Adds three unit tests covering unlocked state, locked state, and default false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ApiError::Forbidden variant, StatusResponse / ResetAuthCodeResponse /
LockResponse / ResetAuthCodeBody types, and four new async functions to
src/bin/ui/api.rs:
- get_status() → GET /api/status
- admin_reset_auth_code() → POST /api/admin/reset-auth-code (X-Admin-Code + X-Auth-Code)
- admin_lock() → POST /api/admin/lock (X-Admin-Code)
- admin_unlock() → POST /api/admin/unlock (X-Admin-Code)
HTTP 403 responses map to ApiError::Forbidden in all three admin functions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make MockRepo stateful for admin_auth_code and submissions_locked so
the new QuoteRepository methods can be exercised without a real DB.
Add four tests to src/bin/api/handlers/mod.rs:
- get_submissions_locked returns false by default
- set_submissions_locked(true) then get_submissions_locked returns true
- update_admin_auth_code with correct current succeeds and returns new code
- update_admin_auth_code with wrong current returns Forbidden
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add three new QuoteRepository trait methods and a seed helper:
- update_admin_auth_code(current, new_code): replaces the admin code if
`current` matches; generates a fresh passphrase when new_code is None;
returns DbError::Forbidden on mismatch.
- get_submissions_locked(): reads the submissions_locked key from
admin_config; returns false when the key is absent.
- set_submissions_locked(locked): upserts "1"/"0" into admin_config.
- seed_submissions_locked(): INSERT OR IGNORE "0" — safe to call on every
startup without clobbering an active lock.
Implemented in both NativeRepository (rusqlite) and D1Repository (wasm32).
Updated startup seeding in main.rs (native and wasm32 paths) to call
seed_submissions_locked after the existing admin auth code seeding.
Added 7 unit tests in db/native.rs covering all four specified scenarios:
default false, set-then-get, seed does not overwrite, correct code succeeds,
None new_code generates passphrase, wrong code returns Forbidden, stored
code unchanged after Forbidden.
MockRepo in handlers/mod.rs updated with stub implementations of all four
new trait methods to satisfy the trait bound.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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
- 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>
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>
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>
- 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>
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>
- 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>
- 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>
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>