10 Commits

Author SHA1 Message Date
kitadmin fe5cbaec2b feat(auth): guest login with random telegraph operator name
No email required — players pick male/female/mystery, get assigned
a humorous period-appropriate operator name (e.g. "Dusty Clicksworth",
"Ada Dottsworth", "The Phantom Sender"), can reroll, then hit
"Begin Transmitting!" for instant play. Email/magic link moved to
a collapsible section for cross-device access only.

- server: POST /api/auth/guest creates guest profile + session
- server/db: profiles.email now nullable, is_guest column added,
  migration recreates table for existing installs
- client: names.ts with 15 names per gender category
- client: LoginPage redesigned — gender picker → name display → play
- client: useAuth gains setAuth() for direct session injection
- 55 server + 44 client tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 04:22:05 +00:00
kitadmin 45f1e0e7f6 fix(deploy): requireTLS for SMTP relay + bridge network for docker-compose
- mailer.js: add requireTLS to force STARTTLS before AUTH on internal relay
- docker-compose.yml: use network_mode:bridge to reach 172.17.0.1:2587 SMTP relay
  and switch to ${SMTP_HOST:-172.17.0.1}/${SMTP_PORT:-2587} defaults

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 03:28:54 +00:00
kitadmin 12522f2478 fix(server): catch SMTP errors in auth/request gracefully
Return 200 ok even if email send fails — token is saved in DB and
magic link is logged to console. Prevents 500 on transient SMTP issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 03:27:01 +00:00
kitadmin bbebc31624 fix(mailer): add TLS settings for internal SMTP relay
- rejectUnauthorized: false (relay cert is for mail.keylinkit.net, not 172.17.0.1)
- connection/greeting/socket timeouts for reliability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 03:23:44 +00:00
kitadmin 85ba992432 feat: server HTTP API with auth, progress, and admin routes (Tasks 6+7)
- server.js: Node HTTP server with all API routes, static file serving, SPA fallback
- Path traversal protection on static file serving
- Async handler wrapped with .catch() to prevent unhandled rejections
- readBody: size limit (1MB) + error handler
- letter validation: single [A-Z] char check on progress/mnemonics routes
- phrase length limit (500 chars) on POST /api/mnemonics
- requireAdmin reads ADMIN_PASSWORD at request time for testability
- logout uses session.token from requireAuth (not re-read from header)
- 51 server tests passing (auth, progress, admin routes + edge cases)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 02:04:57 +00:00
kitadmin 339fa60455 feat: mailer with magic link email (allegiance pattern, MorseQuest branding) 2026-04-30 01:02:43 +00:00
kitadmin b229e6871f fix(db): transactional useMagicToken, O(2) getSession, ?? for displayName
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 01:00:20 +00:00
kitadmin d8de79ce7d fix(db): atomic saveMnemonic, const normalizedLetter, updateProfileSeen guard, ?? 0 for avgScore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 00:46:05 +00:00
kitadmin 2d96d37866 fix(db): instance-scoped closures, FK constraints, letter normalization, test coverage
- Remove module-level `let db` singleton; all query functions now close
  over the local `instance` from their `initDb` call, preventing
  cross-contamination when multiple callers each invoke `initDb`
- Add REFERENCES … ON DELETE CASCADE to profile_id in magic_tokens,
  sessions, user_progress, user_mnemonics, and letter_stats so that
  the already-enabled foreign_keys pragma is actually enforced by DDL
- recordAnswer: normalise letter to uppercase before storing stats so
  lowercase 'a' and uppercase 'A' are never tracked separately
- updateLevel: call getOrCreateProgress first to avoid a silent no-op
  UPDATE when no progress row exists yet
- saveMnemonic: replace SELECT-then-INSERT with an atomic UPSERT
  (INSERT … ON CONFLICT DO UPDATE) and detect first-save by checking
  whether created_at equals the current timestamp
- getSession: re-fetch the row after updating last_seen so the returned
  object reflects the freshly written timestamp
- db.test.js: add tests for findProfileById, findProfileById (null),
  updateProfileSeen, updateLevel (normal + no-row-yet), getAdminUsers,
  and getAdminStats (21 tests total, all pass)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 00:41:40 +00:00
kitadmin 9b31119c7a feat: database layer with better-sqlite3, full schema and query functions
Implements Task 4 (TDD): wrote failing tests first, then the full SQLite
database layer covering profiles, magic tokens, sessions, user progress,
mnemonics, and letter stats. All 14 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 00:34:59 +00:00