Commit graph

12 commits

Author SHA1 Message Date
cf63f27839
fix(auth): split login and signup into separate 1Password-compatible forms
1Password showed "No items to display" on the signup password field
because login and registration shared a single form whose password input
switched autocomplete between current-password and new-password, and the
display-name field was dynamically added via v-if. Per 1Password compatible
website design, unrelated flows must be separate forms with stable fields.

Changes:
- Split into two distinct <form> elements: login (autocomplete
  current-password) and signup (autocomplete new-password), each with
  unique field ids so 1Password does not cache the field as a login field
- Add passwordrules, minlength=8, and maxlength=128 to the signup password
  input so 1Password can generate a password matching the server rules
  (min 8 chars)
- Remove the dynamic autocomplete computed and the v-if display-name field;
  each form now has a static, complete field set
- Add .fields form styling (flex column, gap) now that the forms sit inside
  the card container
2026-08-16 16:58:51 +02:00
f97b4f92f0
fix(auth): make login/signup fields password-manager friendly
Password managers (1Password, Bitwarden, …) could not reliably detect or
fill the auth fields because the inputs were loose labels inside a div with
no real form, no name attributes, and a static autocomplete hint.

Changes:
- Wrap the auth card in a real <form @submit.prevent> so password managers
  recognize the credential form and its submit flow
- Add id/name attributes to all fields (username, displayName, password)
- Use a dynamic autocomplete hint on the password field:
  current-password for login (autofill) and new-password for registration
  (offer a generated strong password)
- Set autocomplete="nickname" on the display-name field so it is not
  mistaken for a username or password field
- Mark the close, tab-switch, and OIDC buttons as type="button" so they no
  longer default to type="submit" and trigger form submission
- Make the primary action a type="submit" button so Enter submits natively
- Add margin: 0 to .card since it is now a form element
2026-08-16 16:51:16 +02:00
d400a4a2a0
fix(docker): harden data-dir permissions and surface actionable DB errors
The bind-mount permission fix only takes effect after a rebuild, so a
stale image keeps failing with the opaque "unable to open database file".
This hardens both sides so any remaining failure is self-explanatory:

- docker-entrypoint.sh: fall back to chmod 777 when chown is unsupported
  (network/9p mounts), and fall back to running as root when su-exec is
  unavailable, so the data dir is always writable on any filesystem.
- server/db.mjs: wrap the DatabaseSync open in a try/catch and, on
  failure, report the exact path, whether the directory is writable, and
  the process UID instead of the bare SQLite error.

Verified with a real container: a normal bind mount serves /health and
creates trxtd.db; a read-only mount now prints the directory-permission
diagnostic instead of the raw SQLite error.
2026-08-16 16:40:45 +02:00
1abc08b60f
fix(docker): make /app/data writable by node user on bind-mounted volumes
The container ran as the unprivileged "node" user, but bind-mounted host
directories (e.g. ./data:/app/data in docker-compose or an Unraid appdata
folder) are mounted with root ownership. The node user therefore could not
create trxtd.db in /app/data, failing at startup with:

  Error: unable to open database file
      at file:///app/server/db.mjs:12

Fix:
- Add docker-entrypoint.sh that mkdir/chown /app/data to node:node on
  container start, then drops privileges back to node via su-exec
- Install su-exec in the runtime stage (apk add --no-cache su-exec)
- Remove USER node so the entrypoint runs as root and can fix ownership,
  with privilege dropping handled inside the entrypoint instead
- Wire the script up as ENTRYPOINT, keeping CMD as the app itself

The mount path itself is unchanged (/app/data, host ./data). Verified with
a real bind mount: /health responds ok and trxtd.db/-shm/-wal are created
in the mounted directory.
2026-08-16 16:35:08 +02:00
1a9ac5bf45
feat: account system, OIDC, meta-progression research tree, and automated tests
Account & Persistence Layer (server/db.mjs)
- SQLite via node:sqlite DatabaseSync with WAL mode, foreign keys, and
  synchronous=NORMAL for microsecond response times
- Users table: UUID primary key, unique lowercase username, scrypt-hashed
  password, optional OIDC sub/issuer, crystal balance, timestamps
- Sessions table: 64-byte random hex token, FK to users, configurable TTL
  with automatic expiry cleanup every 10 minutes
- User upgrades table: composite PK (user_id, upgrade_id), level tracking,
  ON CONFLICT DO UPDATE for idempotent merges
- User stats table: games played/won, total kills/score, highest wave
- Password hashing: crypto.scrypt with 16-byte salt and 64-byte derived
  key, constant-time comparison via crypto.timingSafeEqual
- Guest merge: caps crystals at 100,000, caps levels to defined maxLevels,
  uses MAX(level, new) to preserve higher account levels, ignores unknown
  upgrade IDs, transactional with BEGIN IMMEDIATE/COMMIT/ROLLBACK
- Atomic crystal purchasing: subtracts cost only if balance sufficient,
  upgrades level within transaction, returns full user object on success

Meta-Progression Definitions (shared/meta-upgrades.mjs)
- Single source of truth shared between server (authoritative validation)
  and client (talent tree UI)
- 3 branches: Economy (start_gold, wave_bonus, obstacle_discount),
  Defense (bonus_lives, shockwave, fortress_shield),
  Towers (tower_range, tower_speed, dot_potency)
- 9 upgrades with 1–5 levels each, progressive cost curves
- calcCrystalsEarned(wave, score, win): base 2.5 per wave, +40 for victory,
  +floor(score/250), minimum 1 crystal per game

REST API (server/server.mjs)
- /api/auth/register: username 3–16 chars a-z0-9_-; password min 8 chars;
  case-insensitive uniqueness; auto-creates user_stats row; returns
  session cookie (HttpOnly, SameSite=Lax, Secure when HTTPS)
- /api/auth/login: constant-time username lookup via scrypt verify;
  rate-limited 20 auth attempts/IP/minute
- /api/auth/logout: deletes session server-side, clears cookie
- /api/auth/me: returns publicUser (id, username, crystals, upgrades,
  stats, oidc flag) or null
- /api/auth/oidc/login: PKCE Authorization Code flow with SHA-256 S256
  challenge/verifier; discovers .well-known/openid-configuration;
  verifies RS256 id_token signature via JWKS public key; validates
  issuer, audience, and expiry; finds or creates user by OIDC sub/issuer
- /api/auth/oidc/callback: exchanges code for tokens, verifies id_token,
  issues session cookie, redirects to /?auth=ok or /?auth=error
- /api/upgrades/buy: validates upgrade ID against META_UPGRADES,
  checks current level < maxLevel, deducts cost from crystals
- /api/game/finish: server-authoritative crystal calculation;
  bounds-checks inputs (wave ≤ 9999, score ≤ 10M, kills ≤ 1M);
  updates user_stats (games_played, games_won, total_kills, total_score,
  highest_wave via MAX)
- /api/auth/merge-guest: one-time guest-to-account crystal and upgrade
  migration with level caps
- /api/config: public endpoint exposing OIDC enabled state and button label
- Security: CSP header on all responses, X-Content-Type-Options: nosniff,
  X-Frame-Options: DENY, Referrer-Policy: no-referrer, cache-control
  no-store on API responses, path-traversal protection on static serving

Multiplayer Fairness
- Meta-upgrades (start_gold, bonus_lives, tower_range, tower_speed,
  dot_potency, wave_bonus, obstacle_discount, shockwave, fortress_shield)
  applied only in solo campaign mode
- Co-op and Duel multiplayer sessions reset all meta buffs to zero,
  preserving lockstep determinism and competitive balance
- Multiplayer results set crystalsEarned: 0 to prevent duplicate rewards

Frontend (Vue 3 + TypeScript)
- AuthModal.vue: username/password login and registration form with
  validation, OIDC single sign-on button (shown when configured),
  guest-to-account upgrade on first login
- UserProfileBar.vue: top-bar indicator showing crystal count (💎),
  user display name, research and login/logout buttons, reactive
  auth state via auth controller
- ResearchTree.vue: interactive talent tree modal with 3 branches,
  per-upgrade cost/level display, purchase confirmation, disabled
  state for unaffordable/maxed upgrades, branch icons and descriptions
- auth.ts: reactive controller managing login, registration, OIDC
  redirect detection (?auth=ok/?auth=error), guest profile migration
  on first login, upgrade purchasing, and game result reporting
- meta.ts: frontend helpers for branch definitions, upgrade costs,
  and guest profile persistence in localStorage
- engine.ts: solo meta bonus application (start_gold, bonus_lives,
  tower_range, tower_speed, dot_potency, wave_bonus, obstacle_discount,
  shockwave, fortress_shield); crystal rewards in finish() path
- mpgame.ts: meta buff reset in multiplayer sessions; crystalsEarned: 0
- sound.ts: shield sound synthesis for fortress_shield absorption
- store.ts: auth state, research tree toggle, upgrade snapshot
- types.ts: SfxName extended with "shield" sound

Docker Configuration
- Multi-stage build: node:22-alpine build → node:22-alpine runtime
  (production deps only: ws)
- VOLUME /app/data for persistent SQLite database
- HEALTHCHECK on /health endpoint
- docker-compose.yml: port 3001, persistent ./data volume,
  commented OIDC environment variables (OIDC_ENABLED, OIDC_ISSUER,
  OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_BUTTON_LABEL)
- .gitignore: data/, *.db, *.db-journal, *.db-wal, *.db-shm

Automated Tests (npm test)
- scripts/test-db.mjs (17 unit tests): isolated SQLite persistence –
  scrypt hash/verify roundtrip, timing-safe constant-time comparison,
  user creation with lowercase enforcement, UNIQUE constraint, session
  create/get/delete lifecycle, expired session invalidation, crystal
  addition, 5-level upgrade cost progression with max-level guard,
  guest merge (crystal cap 100k, level cap, MAX() semantics, unknown
  upgrade rejection), game result stats accumulation, calcCrystalsEarned
  formula verification
- scripts/test-auth.mjs (31 integration tests): spawns real server with
  isolated DATA_DIR, exercises full REST flow – register validation
  (username too short, password too short, duplicate, case-insensitive),
  login (wrong password, correct), session cookie attributes (HttpOnly,
  SameSite=Lax, Path=/), /me endpoint, upgrade purchase (insufficient
  crystals, unknown ID, successful purchase), game finish rewards
  (victory, defeat, negative values clamped to 1 crystal minimum),
  guest merge (crystals, level caps, unknown upgrades), logout,
  OIDC-disabled endpoints (400), unauthenticated guards (401), 404
  routing, CSP header on static files, rate limiting (429 after 20+
  auth attempts per minute), and persistence across server restart
  (kill + respawn with same DATA_DIR preserves all state)
2026-08-16 16:22:38 +02:00
ce2484bb8d
feat: add multi-map system with 4 distinct biomes, layouts, and multiplayer sync
- Introduce 4 playable map environments:
  * Meadow (Grüne Lichtung): Classic balanced S-curve path with lush grasslands, flowers, and standard obstacles
  * Desert (Sonnendünen): Winding dune trail with sandstone rocks, oasis palms, and scrub
  * Frostland (Frostgipfel): Icy serpentine path through snowfields with frost-covered pines and crystal rocks
  * Volcano (Lavabruch): Tight aggressive layout through dark obsidian/basalt with glowing magma streams and floating ember sparks
- Add comprehensive MapDef & MapTheme configuration for distinct waypoint routes, flying trajectories, biome color palettes, portal/keep styles, and obstacle densities
- Extend Canvas 2D renderer to dynamically adapt backgrounds, path texturing, portal effects, and procedurally drawn biome foliage/decorations per map
- Upgrade GameEngine with dynamic map loading, obstacle clearing logic, and per-map highscore persistence
- Implement multiplayer map synchronization in WebSocket server (set-map event), room state, and lobby UI allowing host map selection
- Update StartScreen and LobbyScreen components with interactive map selection cards and per-map highscore tracking
- Add automated test suites for map path integrity, determinism, playability, and room map protocol synchronization
2026-08-16 15:03:34 +02:00
a7fb3efa61
ui: remove technical terminology and CLI references from user interface 2026-08-16 14:10:51 +02:00
4f8400c480
feat: add 9-level tower evolution system with distinct tier abilities
Extend all 5 towers to 9 upgrade levels, divided into 3 visual and
functional evolution tiers:
- Tier 1 (L1-3, Yellow): Base stats (damage, range, rate).
- Tier 2 (L4-6, Orange): First major ability evolution with pulsing orange aura.
- Tier 3 (L7-9, Blue): Final devastating evolution with blue aura.

Evolutions per tower:
- Arrow Tower:
  * L4-6 (Orange): Multishot firing 2-3 arrows simultaneously at distinct targets.
  * L7-9 (Blue): 4-arrow multishot + Poison DoT (up to 28 dmg/s).
- Cannon:
  * L4-6 (Orange): Incendiary shells applying Burn DoT in splash radius.
  * L7-9 (Blue): Flak shells hitting flying units with full splash + burn damage.
- Frost Tower:
  * L4-6 (Orange): Permafrost - every 3rd pulse completely freezes enemies (speed = 0).
  * L7-9 (Blue): Shatter - frozen enemies receive +50% damage from all sources.
- Tesla Tower:
  * L4-6 (Orange): Chain lightning applies stun on each hit.
  * L7-9 (Blue): Lightning Storm - every 4th shot strikes and stuns ALL enemies in range.
- Laser Tower:
  * L4-6 (Orange): Prism refraction splitting the beam onto 2-3 secondary targets.
  * L7-9 (Blue): Piercing beam penetrating all enemies in a line up to 280px range.

Engine & Renderer updates:
- Added DoT tick handling (poison/burn) and freeze/stun status effects.
- Added visual indicators for frozen enemies (ice spikes) and DoT particles.
- Added 3-slot tier-colored pips on tower base plates + pulsating evolution auras.
- Updated TowerPanel with 9-star tier display (★★★ yellow, ★★★ orange, ★★★ blue)
  and dynamic special ability descriptions.

Testing & Validation:
- Added `scripts/test-evolution.mts` covering all 10 evolved mechanics (all green).
- Verified lockstep multiplayer determinism via `scripts/test-mp.mts`.
- Verified solo campaign balance preservation via `scripts/sim.mts`.
- Full TypeScript typecheck and production build passing.
2026-08-16 13:54:09 +02:00
ef75203db7
feat(duel): allow swapping main view to opponent's board
- Add a toggle on the picture-in-picture window to swap the big board
  with the mini view of the opponent's field
- Color-coded frame and badge identify whose board is currently shown
  (blue = player 1, orange = player 2)
- Block building and rushing while spectating the opponent's board;
  the PiP then shows your own field instead
- Clarify duel-mode descriptions in lobby and start screen, add hints
  in the multiplayer bar
- Replace previous announcement banners so only one shows at a time
2026-08-16 13:35:46 +02:00
4ec0e483aa
feat(server): add security hardening for public hosting
- Add CSP, frame and referrer headers for served static files
- Enforce WebSocket origin check to prevent cross-site hijacking
- Trust X-Forwarded-For only when the peer is from a private proxy network
- Limit concurrent connections (500 total / 20 per IP) and rooms (300)
- Add ALLOWED_ORIGINS env var for additional WebSocket origins
- Document reverse proxy setup (NPM/NPMplus) in README
- Add scripts/security-test.mjs to verify origin and limit behavior
2026-08-16 13:16:20 +02:00
b506ffad55
feat(docker): add self-hosted all-in-one container setup
- Add Dockerfile (multi-stage build, unprivileged user, healthcheck),
  docker-compose.yml and .dockerignore
- Server now serves static dist/ frontend and WebSocket relay on a
  single port (PORT, default 3001) with path-traversal protection,
  immutable asset caching and SPA fallback
- Client connects via same origin in production; dedicated ws port is
  only used for vite dev/preview
- Document Docker usage, env vars and manual production mode in README
2026-08-16 12:15:52 +02:00
c4347f8420
feat: initialize TRXTD browser tower defense 2026-08-15 16:07:09 +02:00