Commit Graph

17 Commits

Author SHA1 Message Date
funman300 0dcb783e94 feat(analytics): opt-in usage analytics with server ingest and settings toggle
- Server: POST /api/analytics endpoint with per-IP rate limit (5/min),
  batch validation (≤50 events, event_type regex, UUID dedup, clock check),
  INSERT OR IGNORE for idempotency, and migration 004_analytics.sql
- Client (solitaire_data): AnalyticsClient with in-memory Mutex buffer,
  UUID session_id per launch, async flush via background task
- Engine: AnalyticsPlugin records game_won, game_forfeit, game_start,
  achievement_unlocked; flushes immediately on game-end, every 60 s otherwise
- Settings UI: Privacy section with ON/OFF toggle, hidden in local-only mode
- Default: analytics_enabled = false (explicit opt-in required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 20:06:34 -07:00
funman300 d60dc18add fix(server): add CSP/security headers middleware, gitignore jks.bak*
Content-Security-Policy, X-Content-Type-Options, and X-Frame-Options are
now injected by a single Axum middleware on the web router subtree, so
all HTML pages get consistent headers without touching each file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:41:50 -07:00
funman300 5b3925a619 feat(web): account page with sign in / sign up tabs
- Add account.html: tabbed form for login and registration, signed-in
  state with sign-out, links to leaderboard and replays
- Wire /account route in build_router_inner
- Add Account card to landing page
- Link leaderboard login prompt to /account for new users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:09:57 -07:00
funman300 8325bf6cf7 chore: rename app from Solitaire Quest to Ferrous Solitaire
Replace all display-name occurrences across web pages, Rust source,
docs, and Cargo metadata. Update localStorage token key from sq_token
to fs_token. Tagline "Klondike Solitaire" retained as genre descriptor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:04:45 -07:00
funman300 c518255a2d feat(web): leaderboard and replays pages with nav from landing
- Add leaderboard.html: JWT login form + localStorage token + table
- Add replays.html: public listing of recent replays, row click to viewer
- Wire /leaderboard and /replays routes in build_router_inner
- Fix home.html Recent Replays link from /api/replays/recent to /replays

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:50:54 -07:00
funman300 00f2d890f1 feat(web): add landing page at / with links to play, leaderboard, replays
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:21:38 -07:00
funman300 1e6d153cd0 feat(wasm): playable browser game at /play
Add `SolitaireGame` WASM binding to `solitaire_wasm` exposing draw(),
move_cards(), undo(), auto_complete_step(), and state() — all backed by
the real solitaire_core rules engine.

Add /play route to solitaire_server serving a full vanilla-JS
interactive Klondike game (game.html / game.css / game.js). Features:
drag-and-drop card moves (mouse + touch via PointerEvents), click stock
to draw, double-click card to auto-move to foundation, undo, draw-1/3
toggle, new game, auto-complete animation, win overlay, seed display.
Rebuild solitaire_wasm.js + solitaire_wasm_bg.wasm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 09:42:56 -07:00
funman300 75146847f6 feat(server): add --reset-password admin subcommand
Self-hosters can now run:
  ./solitaire_server --reset-password <username>
to update a player's password and invalidate all their refresh tokens
(forcing re-login on every device). Password is read from stdin so it
can be piped from scripts or a password manager without appearing in
shell history.

Implementation:
- reset_password() in auth.rs: validates length, bcrypt-hashes new
  password, updates users.password_hash, deletes all refresh_tokens
  rows for the user.
- main.rs: --reset-password dispatch before HTTP server startup;
  JWT_SECRET not required for this path.
- 4 integration tests covering: login works after reset, old password
  rejected, refresh tokens invalidated, unknown user → NotFound,
  short password → BadRequest.
- README_SERVER.md: admin password-reset section with examples.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:10:13 -07:00
funman300 198df75f94 test(data): add push retry-on-401 integration test + server test pool helper
Adds push_retries_after_401_on_expired_access_token to sync_round_trip.rs,
closing the push-side coverage gap alongside the existing pull test
(jwt_refresh_on_401_succeeds). Both tests use an expired-but-validly-signed
access token to trigger the 401 → refresh → retry path in
SolitaireServerClient.

Also exposes build_test_pool() from solitaire_server so downstream crates
can boot a test server without duplicating the migration boilerplate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:04:26 -07:00
funman300 6e6f3ef1ff feat(server): per-user rate limiting on protected sync endpoints
Adds a UserIdKeyExtractor that decodes the Authorization JWT to rate-limit
each user individually (falls back to client IP for unauthenticated
requests). Protected routes now throttle at 10-request burst / 1 token
per 10 s steady-state (6/min), matching the surface attack area of the
1 MB sync/push endpoint.

Also adds an integration test: sync_push_rate_limit_returns_429_on_11th_request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:55:07 -07:00
funman300 07b8ecd9b2 feat(server): web replay viewer (HTML/CSS + WASM bindings)
Wires the WASM module from the previous commit into a minimal web
viewer served at <server>/replays/<id>. Two new server routes:

- `GET /replays/:id`  — returns the same embedded HTML page for any
  id; the page itself reads the path from window.location in JS and
  fetches the replay JSON via /api/replays/:id.
- `/web/*` — ServeDir for the static assets (replay.css, replay.js,
  and the wasm-bindgen-generated pkg/).

Web layer:
- index.html — header, board, controls, status. Module script.
- replay.css — midnight-purple palette matching the desktop client,
  dark felt board, CSS-grid pile layout, tableau fan via per-card
  inline `top` offset.
- replay.js — fetches the replay, instantiates the wasm
  ReplayPlayer, drives state(), step(). Controls: Restart, Play/Pause
  toggle, Step. Auto-tick at 600 ms.
- pkg/ — generated by wasm-bindgen (committed so deployers don't
  have to install wasm-bindgen-cli + the wasm32 target).

`tower-http = "0.6"` added to solitaire_server with the `fs` feature
for ServeDir.

To regenerate pkg/ after a solitaire_wasm change:
    RUSTFLAGS='--cfg getrandom_backend="wasm_js"' \
      cargo build -p solitaire_wasm \
      --target wasm32-unknown-unknown --release
    wasm-bindgen --target web \
      --out-dir solitaire_server/web/pkg --no-typescript \
      target/wasm32-unknown-unknown/release/solitaire_wasm.wasm

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:54:01 +00:00
funman300 93182fa251 feat(server): replay upload + fetch endpoints
API surface for the web replay viewer to come:

- `POST /api/replays`        — auth required; persists the JSON body
  verbatim, mints a server-side UUID, returns `{id}`. Three columns
  (final_score, time_seconds, recorded_at) are projected out of the
  payload at insert time so list endpoints don't have to scan blobs.
- `GET  /api/replays/recent` — public; returns the N most-recent
  replays across users (limit defaults to 20, capped at 50). Joins
  the username so the feed reads as "AliceWon · 2:14 win".
- `GET  /api/replays/:id`    — public; returns the full replay JSON
  the desktop client uploaded.

Migration `002_replays.sql` adds the `replays` table with indexes
on `received_at DESC` (recent feed) and `user_id` (per-user views).

Schema-version compatibility is the playback side's responsibility,
matching the desktop's existing `schema_version` gate — the server
just stores and serves whatever JSON came in.

`AppError::NotFound` added so `GET /api/replays/:id` can return a
proper 404 instead of an internal-server-error.

`.sqlx` cache regenerated for the new `query!` invocations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:50:25 +00:00
funman300 f27a002c91 fix(server,core): use SmartIpKeyExtractor for rate limiter, collapse nested if
- tower_governor: switch from PeerIpKeyExtractor (socket address) to
  SmartIpKeyExtractor so x-forwarded-for headers are honoured in tests
  and behind reverse proxies. Fixes auth_rate_limit_returns_429 test
  returning 500 instead of 429.
- solitaire_core: collapse nested if/if-let per clippy::collapsible_if.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:54:53 +00:00
funman300 ccfeb055e5 fix(server): load JWT_SECRET at startup, add auth logging, fix challenge race
- Introduce AppState { pool, jwt_secret } so JWT_SECRET is loaded once in
  main() and any missing value is a fatal startup error rather than a 500
  on the first request.  All four env::var("JWT_SECRET") call sites in
  auth.rs and middleware.rs are replaced with state.jwt_secret.
- build_test_router embeds the fixed test secret so integration tests do
  not need to set JWT_SECRET in the environment.
- Add tracing::warn! in login (invalid password) and register (username
  taken) to surface brute-force attempts in production logs.
- Fix daily-challenge race condition: after INSERT OR IGNORE, re-SELECT
  the persisted row so concurrent requests both return the winner's data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:35:46 +00:00
funman300 c8553dc8c5 chore(deps): migrate to Bevy 0.16, axum 0.8, and other package updates
- Bump bevy 0.15 → 0.16; fixes all breaking API changes:
  ChildBuilder → ChildSpawnerCommands, Parent → ChildOf,
  despawn_descendants → despawn_related::<Children>(),
  despawn_recursive → despawn (now recursive by default),
  EventWriter::send → write, Query::{get_single,get_single_mut}
  → {single,single_mut}, ChildOf::get → parent()
- Bump axum 0.7 → 0.8; remove axum::async_trait from FromRequestParts
- Bump tower_governor 0.4 → 0.8; fix GovernorLayer::new() API
- Bump jsonwebtoken 9 → 10 with rust_crypto feature only
- Bump thiserror 1 → 2, dirs 5 → 6, bcrypt 0.15 → 0.19,
  reqwest 0.12 → 0.13 (rustls feature rename)
- Regenerate .sqlx offline cache for sqlx compile-time query checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 12:31:12 -07:00
root 648c5c18d9 feat(leaderboard): opt-out support — server endpoint, client method, UI button
- Server: DELETE /api/leaderboard/opt-in sets leaderboard_opt_in=0,
  hiding the player without deleting their row (scores preserved for re-opt-in)
- SyncProvider trait: opt_out_leaderboard() default no-op method + blanket impl
- SolitaireServerClient: implements opt_out_leaderboard via DELETE request with JWT refresh
- Leaderboard UI: "Opt Out" button (dark red) alongside existing "Opt In" button
- Server integration test: opt-out hides, opt-in restores (round-trip verified)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 02:01:20 +00:00
root 34ba4dc6ed feat(workspace): full server + sync implementation, all tests green
- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
  challenge, account deletion, JWT middleware, rate limiting via
  tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
  integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
  SolitaireServerClient, auth_tokens keyring integration, blanket
  Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
  connections share the same schema

All 191 tests pass; cargo clippy -D warnings clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 23:32:56 +00:00