-
/api/admin/* accepted only the T6 ADMIN_PASSWORD cookie until now — MVCP M.1/M.10's stated rule is users.role='admin' + a real session, and M.2's credit_ledger needs a real admin identity to attribute manual balance corrections to. isAdminAuthed() now accepts either path (the ADMIN_PASSWORD channel stays as a fallback so nobody's locked out before any role='admin' account exists by hand — dev-plan-4.md T30's explicit requirement). New POST /api/admin/credit/adjust wraps the new creditLedger.adjust() (db.js), which — unlike spend/refund — takes either sign of delta and records actorUserId (null over the legacy ADMIN_PASSWORD channel, which has no per-admin identity to attribute to). credit_ledger's actorUserId column needed an idempotent ALTER TABLE guard (no migration framework exists anywhere in this project) since the table's CREATE TABLE IF NOT EXISTS already shipped in T23 without it. Also switched db.js to WAL journal mode — surfaced by two processes legitimately touching the same data.sqlite at once in this session (the long-running server + a test run) hitting "database is locked" on the default rollback-journal mode; worth having before this bites in production too, not just to unblock local test runs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
credit_ledger (dev-plan-4.md T23; schema M.2) joins users/sessions in the same SQLite db.js — architecture.md 1.4 named this as the other half of why SQLite got picked in T18 ("扣积分 + 创建生成任务 + 扣减片段 配额" needs real atomicity, T18 only used the credential-storage half). spend()/refund()/topup() each read-modify-write users.creditBalance inside a transaction; spend() refuses to take a balance negative (INSUFFICIENT_BALANCE) instead of allowing debt, and topup() dedupes by paymentRef so a Stripe webhook's guaranteed at-least-once delivery can't double-credit (T24 will rely on this). pricing_config (M.12) stays a JSON file via a new pricing.js — it's operator-edited config, not on the atomic spend path, so SQLite would be over-engineering here; keyed by `key` rather than an id, hence its own tiny module instead of store.js's createStore. Seeded with placeholder credit amounts (dev-plan-4.md §1.2 item 3 — real numbers are still TBD). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo -
New db.js opens data.sqlite (node:sqlite's DatabaseSync — a Node 24 built-in, experimental but functional) and creates the users/sessions tables per dressup-dance-room-mvcp.md M.1/M.10. Zero new npm dependency: no bcrypt/argon2 (node:crypto's scryptSync covers password hashing in T19), no better-sqlite3, no package.json needed. Mirrors store.js's ergonomics (create/get/update, plain JS objects in and out — booleans and the oauthProviders array marshalled to/from SQLite's TEXT/INTEGER columns internally) rather than reaching for an ORM. Exports the openDb/createUsersStore/createSessionsStore factories alongside a default singleton so tests can point at an isolated tmp file instead of the real data.sqlite (same pattern dressup-generate.test.js uses for library.json). data.sqlite is gitignored under the secrets section, not the "runtime state" one — unlike library.json it holds real credential material (password hashes, session tokens), not just regenerable content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y