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>
- 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 `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>
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>
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
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>