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:
parent
6c7d481494
commit
cf2ed37c1e
111
FUTURE.md
111
FUTURE.md
|
|
@ -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**
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
20
server.js
20
server.js
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -63,33 +63,62 @@ 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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue