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
This commit is contained in:
null 2026-05-09 16:38:28 -05:00
parent 6c7d481494
commit cf2ed37c1e
4 changed files with 82 additions and 121 deletions

111
FUTURE.md
View File

@ -17,29 +17,6 @@ This file is a living document. Agents should:
## Pending Recommendations ## 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 ### Billing Cycle Sub-categories for Weekly/Monthly
**Priority:** MEDIUM **Priority:** MEDIUM
**Added:** 2026-05-08 by _null **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 ### Implement proper error boundaries
**Priority:** CRITICAL **Priority:** CRITICAL
**Added:** 2026-05-08 by Scarlett **Added:** 2026-05-08 by Scarlett
@ -581,25 +534,19 @@ Some error messages leak database or implementation details.
## Database Migration System Issues ## Database Migration System Issues
### 🔴 CRITICAL: No Explicit Version Tracking for Migrations ### Skip First-Login User Creation When ENV Seeds Users
**Priority:** CRITICAL **Priority:** MEDIUM
**Status:** PENDING **Added:** 2026-05-09 by _null
**Added:** 2026-05-09 by Neo
**Description:** **Description:**
Database migrations lack explicit version tracking. System doesn't know which migrations have been applied, risking duplicate application or incomplete upgrades. 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.
**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
**Implementation Notes:** **Implementation Notes:**
- Create `schema_migrations` tracking table - When `INIT_ADMIN_USER` is set, the app should skip the first-login user creation screen
- Log each applied migration with timestamp - Admin should go directly to the main app after login
- Query table before running each migration - The `first_login` flag on seeded users should be `0` (not forced to change password or create account)
- Mark as CRITICAL for production deployment - 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` - 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**

View File

@ -453,6 +453,26 @@ function runMigrations() {
console.log('[migration] categories.is_seeded column added'); 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) // All migrations are now versioned
// 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)');
} }
function seedDefaults() { function seedDefaults() {

View File

@ -146,6 +146,26 @@ async function main() {
const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count; const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count;
if (userCount === 0) await require('./setup/firstRun').run(db); 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, () => { app.listen(PORT, () => {
console.log(`Bill Tracker running on port ${PORT}`); console.log(`Bill Tracker running on port ${PORT}`);
if (userCount > 0) console.log(`Users found: ${userCount}`); if (userCount > 0) console.log(`Users found: ${userCount}`);

View File

@ -62,34 +62,63 @@ async function createUser(db, username, password, role) {
async function runFromEnv(db) { async function runFromEnv(db) {
const adminUser = process.env.INIT_ADMIN_USER; const adminUser = process.env.INIT_ADMIN_USER;
const adminPass = process.env.INIT_ADMIN_PASS; const adminPass = process.env.INIT_ADMIN_PASS;
const regularUser = process.env.INIT_REGULAR_USER;
const regularPass = process.env.INIT_REGULAR_PASS;
const errors = []; const errors = [];
if (!adminUser || adminUser.length < 3) errors.push('INIT_ADMIN_USER must be at least 3 characters'); 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 (!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) { if (errors.length) {
console.error('\n[first-run] Environment variable setup failed:'); console.error('\n[first-run] Environment variable setup failed:');
errors.forEach(e => console.error(' ✗ ' + e)); errors.forEach(e => console.error(' ✗ ' + e));
console.error('\nSet both vars: INIT_ADMIN_USER and INIT_ADMIN_PASS'); 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'); console.error('Then open the web UI to create your first user account.\n');
process.exit(1); process.exit(1);
} }
const existingAdmin = db.prepare('SELECT id FROM users WHERE role = ? AND username = ?').get('admin', adminUser); 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) { if (existingAdmin) {
// Update existing admin's password // 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}".`); console.log(`[first-run] Admin password updated for "${adminUser}".`);
} else { } else {
// Create new admin user // Create new admin user
db.prepare(` db.prepare(`
INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin) INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin)
VALUES (?, ?, ?, 0, 0, 1) VALUES (?, ?, ?, 0, 0, 1)
`).run(adminUser, hash, 'admin'); `).run(adminUser, adminHash, 'admin');
console.log(`[first-run] Admin "${adminUser}" created.`); 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.'); console.log('[first-run] You can now log in with these credentials.');
} }