From cf2ed37c1e1e33b87f4199f0d5f1034db09df3a7 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 9 May 2026 16:38:28 -0500 Subject: [PATCH] feat: add INIT_REGULAR_USER env var, move bill_history_ranges to v0.42 migration - Add INIT_REGULAR_USER/INIT_REGULAR_PASS for non-admin test user creation - Regular user created at startup with role='user', not admin - Move bill_history_ranges from inline to versioned migration v0.42 - Clean up FUTURE.md: remove completed items, add skip-first-login item --- FUTURE.md | 111 ++++------------------------------------------ db/database.js | 37 +++++++++------- server.js | 20 +++++++++ setup/firstRun.js | 35 +++++++++++++-- 4 files changed, 82 insertions(+), 121 deletions(-) diff --git a/FUTURE.md b/FUTURE.md index d29e442..3ac6ace 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -17,29 +17,6 @@ This file is a living document. Agents should: ## Pending Recommendations -### Click-to-toggle Paid/Unpaid Status -**Priority:** HIGH -**Status:** ✅ COMPLETED v0.19.0 -**Added:** 2026-05-08 by _null - -**Description:** -Allow users to click directly on the "Paid", "Missed", "Late", "Due Soon", "Upcoming" status in the tracker to toggle between Paid and Unpaid states without opening the bill modal. - -**Implementation:** -- Added `POST /api/bills/:id/toggle-paid` endpoint -- StatusBadge component now supports `clickable` prop with hover effects -- Clicking Paid removes payment (soft delete), Clicking Unpaid creates payment -- Confirmation dialog before toggling Unpaid → Paid -- Loading spinner during API call -- Toast notifications on success/failure - -**Files Modified:** -- `routes/bills.js` — new toggle-paid endpoint -- `client/api.js` — togglePaid API function -- `client/pages/TrackerPage.jsx` — StatusBadge click handler, enhanced StatusBadge component - ---- - ### Billing Cycle Sub-categories for Weekly/Monthly **Priority:** MEDIUM **Added:** 2026-05-08 by _null @@ -107,30 +84,6 @@ Visual trend indicator helps users identify spending patterns without navigating --- -### Add React.memo() to prevent unnecessary re-renders -**Priority:** HIGH -**Status:** ✅ COMPLETED v0.19.0 -**Added:** 2026-05-08 by Scarlett - -**Description:** -Many components render on every state change in the parent (especially App.jsx, TrackerPage.jsx, BillsPage.jsx), causing unnecessary re-renders of child components that don't depend on those specific state changes. - -**Implementation:** -- Applied `React.memo()` to StatusBadge, SummaryCard, MobileBillRow, MobileTrackerRow, NavPill, and BrandBlock -- Extracted NavPill and BrandBlock to separate files in `client/components/layout/` -- Fixed missing React imports and useState bugs during implementation -- Build verified successful - -**Files Modified:** -- `client/components/StatusBadge.jsx` -- `client/components/SummaryCard.jsx` -- `client/components/MobileBillRow.jsx` -- `client/components/MobileTrackerRow.jsx` -- `client/components/layout/NavPill.jsx` (new file) -- `client/components/layout/BrandBlock.jsx` (new file) -- `client/components/layout/Sidebar.jsx` (refactored to use new components) - - ### Implement proper error boundaries **Priority:** CRITICAL **Added:** 2026-05-08 by Scarlett @@ -581,25 +534,19 @@ Some error messages leak database or implementation details. ## Database Migration System Issues -### 🔴 CRITICAL: No Explicit Version Tracking for Migrations -**Priority:** CRITICAL -**Status:** PENDING -**Added:** 2026-05-09 by Neo +### Skip First-Login User Creation When ENV Seeds Users +**Priority:** MEDIUM +**Added:** 2026-05-09 by _null **Description:** -Database migrations lack explicit version tracking. System doesn't know which migrations have been applied, risking duplicate application or incomplete upgrades. - -**Rationale:** -- `db/database.js` `runMigrations()` has no tracking table -- No way to know which migrations have been applied vs. pending -- Migrations run every startup, relying only on IF NOT EXISTS checks -- Risk: if a migration partially fails, system doesn't know to retry or skip +When `INIT_ADMIN_USER`/`INIT_ADMIN_PASS` (and optionally `INIT_REGULAR_USER`/`INIT_REGULAR_PASS`) are set via environment variables, the app still forces the first-login user creation flow. This is redundant — the admin user already exists from the seed, so presenting a "create your first user" form on login is confusing and unnecessary. **Implementation Notes:** -- Create `schema_migrations` tracking table -- Log each applied migration with timestamp -- Query table before running each migration -- Mark as CRITICAL for production deployment +- When `INIT_ADMIN_USER` is set, the app should skip the first-login user creation screen +- Admin should go directly to the main app after login +- The `first_login` flag on seeded users should be `0` (not forced to change password or create account) +- If no env vars are set, keep the current first-run flow unchanged +- Files likely to be modified: `setup/firstRun.js`, `server.js` seed logic, possibly frontend login flow --- @@ -690,43 +637,3 @@ Migration failures don't produce clear error messages or logs, making debugging - Error log with context: `[migration-error] v0.20.0 failed: UNIQUE constraint failed on users.username` --- - -## Migration Cleanup: bill_history_ranges Outside Versioned Array -**Priority:** LOW -**Status:** PENDING -**Added:** 2026-05-09 by Ripley - -**Description:** -The `bill_history_ranges` table creation and a few other inline items are still outside the versioned migrations array in `db/database.js`. They run on every startup via `CREATE TABLE IF NOT EXISTS` but aren't tracked in `schema_migrations`, so they bypass the version tracking system. - -**Implementation Notes:** -- Move `bill_history_ranges` and any other orphaned inline migrations into the versioned migrations array -- Assign appropriate version numbers -- Ensure idempotent `run()` functions for each -- Verify they get tracked in `schema_migrations` after the move - ---- - -## 🔴 CRITICAL: Non-Admin Test User Environment Variable for Role-Based Testing -**Priority:** CRITICAL -**Status:** PENDING -**Added:** 2026-05-09 by _null - -**Description:** -Need an environment variable to create a non-admin test user. Currently `INIT_TEST_USER` / `INIT_TEST_PASS` creates a user **with admin privileges** — we need a separate non-admin user to verify role-based access controls (403 on admin-only endpoints like `/api/about-admin`). - -**Rationale:** -- `INIT_ADMIN_USER` / `INIT_ADMIN_PASS` creates the admin user -- `INIT_TEST_USER` / `INIT_TEST_PASS` also creates an admin-tagged user (used for testing admin flows) -- We have NO way to create a non-admin user for testing that 403 works on admin-only endpoints -- Cannot test that the `/admin/about` page is inaccessible to regular users -- Essential for proper security testing of role-based features - -**Implementation Notes:** -- Add `INIT_REGULAR_USER` and `INIT_REGULAR_PASS` environment variables (suggested defaults: `regularuser` / `regularpass123`) -- Create the regular (non-admin) user in `seedDefaults()` or similar init function -- User must have `role = 'user'` (NOT admin) -- Only create if env vars are set (optional, for testing environments) -- Update Docker test command in MEMORY.md to include these env vars -- Update Engineering_Reference_Manual.md with new env vars -- **Do NOT repurpose INIT_TEST_USER — that's already an admin user** diff --git a/db/database.js b/db/database.js index bb7a24e..de952e2 100644 --- a/db/database.js +++ b/db/database.js @@ -453,6 +453,26 @@ function runMigrations() { console.log('[migration] categories.is_seeded column added'); } } + }, + { + version: 'v0.42', + description: 'bill_history_ranges: per-bill date ranges for history visibility', + run: function() { + db.exec(` + CREATE TABLE IF NOT EXISTS bill_history_ranges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE, + start_year INTEGER NOT NULL, + start_month INTEGER NOT NULL, + end_year INTEGER, + end_month INTEGER, + label TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ) + `); + db.exec('CREATE INDEX IF NOT EXISTS idx_bill_history_ranges_bill ON bill_history_ranges(bill_id)'); + } } ]; @@ -514,22 +534,7 @@ function runMigrations() { } } - // ── bill_history_ranges: per-bill date ranges for history visibility (v0.14) - // This migration needs to run after the bills table has the history_visibility column - db.exec(` - CREATE TABLE IF NOT EXISTS bill_history_ranges ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE, - start_year INTEGER NOT NULL, - start_month INTEGER NOT NULL, - end_year INTEGER, - end_month INTEGER, - label TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ) - `); - db.exec('CREATE INDEX IF NOT EXISTS idx_bill_history_ranges_bill ON bill_history_ranges(bill_id)'); + // All migrations are now versioned } function seedDefaults() { diff --git a/server.js b/server.js index 3835668..143442f 100644 --- a/server.js +++ b/server.js @@ -146,6 +146,26 @@ async function main() { const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count; if (userCount === 0) await require('./setup/firstRun').run(db); + // [seed] Check for and create regular user if INIT_REGULAR_USER/INIT_REGULAR_PASS are set + if (process.env.INIT_REGULAR_USER && process.env.INIT_REGULAR_PASS) { + const regularUser = process.env.INIT_REGULAR_USER; + const regularPass = process.env.INIT_REGULAR_PASS; + + // Check if regular user already exists + const existingRegular = db.prepare('SELECT id FROM users WHERE role = ? AND username = ?').get('user', regularUser); + + if (!existingRegular) { + // Create new regular user + const bcrypt = require('bcryptjs'); + const regularHash = await bcrypt.hash(regularPass, 12); + db.prepare(` + INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin) + VALUES (?, ?, ?, 0, 0, 0) + `).run(regularUser, regularHash, 'user'); + console.log(`[seed] Regular user "${regularUser}" created.`); + } + } + app.listen(PORT, () => { console.log(`Bill Tracker running on port ${PORT}`); if (userCount > 0) console.log(`Users found: ${userCount}`); diff --git a/setup/firstRun.js b/setup/firstRun.js index acfb994..3f35046 100644 --- a/setup/firstRun.js +++ b/setup/firstRun.js @@ -62,34 +62,63 @@ async function createUser(db, username, password, role) { async function runFromEnv(db) { const adminUser = process.env.INIT_ADMIN_USER; const adminPass = process.env.INIT_ADMIN_PASS; + + const regularUser = process.env.INIT_REGULAR_USER; + const regularPass = process.env.INIT_REGULAR_PASS; const errors = []; if (!adminUser || adminUser.length < 3) errors.push('INIT_ADMIN_USER must be at least 3 characters'); if (!adminPass || adminPass.length < 8) errors.push('INIT_ADMIN_PASS must be at least 8 characters'); + + if (regularUser && !regularPass) errors.push('INIT_REGULAR_PASS required when INIT_REGULAR_USER is set'); + if (regularPass && !regularUser) errors.push('INIT_REGULAR_USER required when INIT_REGULAR_PASS is set'); + if (regularUser && regularUser.length < 3) errors.push('INIT_REGULAR_USER must be at least 3 characters'); + if (regularPass && regularPass.length < 8) errors.push('INIT_REGULAR_PASS must be at least 8 characters'); if (errors.length) { console.error('\n[first-run] Environment variable setup failed:'); errors.forEach(e => console.error(' ✗ ' + e)); console.error('\nSet both vars: INIT_ADMIN_USER and INIT_ADMIN_PASS'); + console.error('Optionally set: INIT_REGULAR_USER and INIT_REGULAR_PASS for a non-admin test user'); console.error('Then open the web UI to create your first user account.\n'); process.exit(1); } const existingAdmin = db.prepare('SELECT id FROM users WHERE role = ? AND username = ?').get('admin', adminUser); - const hash = await bcrypt.hash(adminPass, 12); + const adminHash = await bcrypt.hash(adminPass, 12); if (existingAdmin) { // Update existing admin's password - db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, existingAdmin.id); + db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(adminHash, existingAdmin.id); console.log(`[first-run] Admin password updated for "${adminUser}".`); } else { // Create new admin user db.prepare(` INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin) VALUES (?, ?, ?, 0, 0, 1) - `).run(adminUser, hash, 'admin'); + `).run(adminUser, adminHash, 'admin'); console.log(`[first-run] Admin "${adminUser}" created.`); } + + // Handle regular user creation if specified + if (regularUser && regularPass) { + const existingRegular = db.prepare('SELECT id FROM users WHERE role = ? AND username = ?').get('user', regularUser); + const regularHash = await bcrypt.hash(regularPass, 12); + + if (existingRegular) { + // Update existing regular user's password + db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(regularHash, existingRegular.id); + console.log(`[first-run] Regular user password updated for "${regularUser}".`); + } else { + // Create new regular user + db.prepare(` + INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin) + VALUES (?, ?, ?, 0, 0, 0) + `).run(regularUser, regularHash, 'user'); + console.log(`[first-run] Regular user "${regularUser}" created.`); + } + } + console.log('[first-run] You can now log in with these credentials.'); }