Commit graph

16 commits

Author SHA1 Message Date
mwiegand
e28d4fad8c
l4d2web/csp: allow Steam avatar CDN in img-src
The live-state grid renders player avatars as <img src="https://avatars.steamstatic.com/...">,
but the CSP img-src directive was `'self' data:` — so the browser
silently blocked every avatar load, leaving placeholder circles in
place. The DB cache and Steam API path were both healthy; only the
browser-side load was blocked.

Use the wildcard *.steamstatic.com host-source rather than pinning a
single hostname: Steam rotates avatars across steamcdn-a.akamaihd.net,
avatars.akamai/cloudflare/fastly.steamstatic.com over time, and a
single-hostname allowlist would re-break on the next shuffle.

Test now pins img-src explicitly — the previous assertions only
checked default-src/frame-ancestors/form-action, so a regression of
this exact line would have silently passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:23:29 +02:00
mwiegand
74b7f61437
harden(l4d2web): default security response headers and generic error handlers
- after_request hook sets X-Content-Type-Options=nosniff, X-Frame-Options=DENY, Referrer-Policy=strict-origin-when-cross-origin, and a strict CSP (default-src 'self', script-src self+nonce, frame-ancestors 'none', form-action 'self'); HSTS added on secure non-test responses
- per-request CSP nonce minted in g.csp_nonce; servers.html's inline showModal script picks it up
- 404 and 500 handlers return short plain-text responses so a misbehaving deployment can't leak tracebacks via Werkzeug's debug page
2026-05-14 22:21:36 +02:00
mwiegand
2902c9cc82
harden(l4d2web): auth/session — clear on login+logout, constant-time CSRF, role-change invalidation
- login_user clears any pre-login session state before stamping user_id/pw_changed_at/admin so a fixated cookie value cannot smuggle data past the login boundary
- logout_user now session.clear()s instead of only popping user_id, removing leftover pw_changed_at/admin markers
- CSRF token comparison uses hmac.compare_digest
- load_current_user rejects sessions where the stamped admin flag no longer matches the user row, preventing a demoted admin from retaining elevated access until next password change (backward-compatible: sessions issued pre-upgrade lack the marker and pass through until next login)
2026-05-14 22:18:46 +02:00
mwiegand
553b280e40
feat(l4d2-web): backend for RCON console with persisted transcript
- POST /servers/<id>/console runs a command via rcon.execute_command and
  persists every outcome (success / empty / error) to command_history.
- GET /servers/<id>/console/history returns paginated newest-first JSON
  for client-side up-arrow recall.
- server_detail() now passes the last 50 history rows as console_history
  for server-side replay on page load.
- 404 on ownership mismatch — no admin override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:32:13 +02:00
mwiegand
0dc61d5de4
feat(live-state): start daemon poller, prune history, close stuck sessions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:10:55 +02:00
mwiegand
d25fb57f30
profile: POST /profile/password validation branches
Implements the change-password endpoint:
- Per-IP rate limit reusing services/rate_limit
- Required fields, mismatched-confirm, policy, wrong-current
  branches each redirect with a specific ?error= key
- Rotates digest + password_changed_at, then re-stamps the
  current session marker so this browser stays logged in
  while other sessions get rejected by load_current_user

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:57:11 +02:00
mwiegand
eef85f36a9
profile: GET /profile page with change-password form
Adds the page reachable from the username link in the header.
Renders the form skeleton; the POST handler lands in the next
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:55:34 +02:00
mwiegand
67b5521eb6
feat(l4d2-web): periodic state poller refreshes Server.actual_state
A background thread spawned alongside the job workers polls every
server's status every STATE_POLLER_INTERVAL_SECONDS (default 30) and
writes the result via the existing refresh_server_actual_state path.
Servers with in-flight jobs (queued/running/cancelling) are skipped to
avoid racing the post-job refresh. Catches reboot drift, OOM kills,
manual systemctl operations, and any other out-of-band state change.
Spec: docs/superpowers/specs/2026-05-09-l4d2-server-lifecycle-reboot-and-drift-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:31:28 +02:00
mwiegand
a11d030edd
feat(l4d2-web): overlay detail Files section with HTMX file tree + downloads
Adds a server-rendered collapsible file tree section to the overlay
detail page so users can verify what their script/workshop overlays
produced and pull individual artifacts (VPKs, configs) without SSH.
HTMX-driven lazy folder expansion with click-to-download via send_file;
symlinks land anywhere under LEFT4ME_ROOT (so workshop addons stream
from the shared cache) but escapes are refused. Same access rule as the
rest of the page (admin or owner). 39 new tests; full web suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:16:25 +02:00
mwiegand
df1ccb4cca
feat(l4d2-web): workshop overlay UI (routes + templates)
Adds workshop_routes blueprint with add-items / remove-item / manual-
build endpoints plus admin /admin/workshop/refresh. Add-items handles
single ID, single URL, multi-line batch, or a collection ID; auto-
enqueues a coalesced build_overlay job per call. Reject non-L4D2 items
with 400, duplicate associations with friendly toast, intruders with
403.

Generalizes overlay_routes: type+name only on create (no path field);
external is admin-only and system-wide, workshop is per-user and
auto-pathed. Update is name-only. Delete recursively removes the
on-disk dir only for managed paths (path == str(id)); legacy externals
are left in place. The pre-existing in-use guard is preserved.

Page routes filter the overlay listing by user permissions and load
workshop items + the latest related job for the detail view.

Templates: unified Create modal with type radio (no path field).
Type-aware overlay detail: workshop overlays show a multi-line input
+ items/collection radio + item table partial with thumbnails, manual
Rebuild button, and a small status indicator pulled from the latest
related job. Admin page gets a "Refresh all workshop items" button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:50:54 +02:00
mwiegand
0210ecd301
config: allow SESSION_COOKIE_SECURE override and disable on test deploy
The HTTP-only test deployment binds gunicorn to 0.0.0.0:8000 with no TLS
terminator, so a hardcoded SESSION_COOKIE_SECURE=True breaks browser
login. Make it opt-out via env (default True outside TESTING) and set
SESSION_COOKIE_SECURE=false in the generated web.env so the test box
keeps working over HTTP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:56:48 +02:00
mwiegand
f81e839ba2
security: harden boundary inputs and production defaults
- validate instance names at the host lib and web boundary against
  [a-z0-9][a-z0-9_-]{0,63} to prevent path traversal via Server.name
- fail-closed on SECRET_KEY: load_config returns None when env unset,
  create_app raises if missing or "dev" outside TESTING
- close login timing oracle by hashing a dummy digest when the user
  is not found, equalizing response time
- set SESSION_COOKIE_SECURE outside TESTING
- delete_instance tolerates stop_service and fusermount3 failures so
  partially-initialized instances clean up without contract breaks;
  drops the is_mount() preflight that violated AGENTS.md
- document claim_next_job's single-process assumption
- clarify emit_step contract via docstring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:53:33 +02:00
mwiegand
bbfc528354
feat(deploy): add production-like test deployment 2026-05-06 19:30:10 +02:00
mwiegand
91d042cf33
feat(l4d2-web): execute queued lifecycle jobs 2026-05-06 14:08:18 +02:00
mwiegand
0aca36506f
feat(l4d2-web): add login page and safe redirects 2026-05-06 12:52:22 +02:00
mwiegand
288eda7c37
chore(l4d2): flatten component layout 2026-05-05 23:47:06 +02:00
Renamed from components/l4d2-web-app/src/l4d2web/app.py (Browse further)