From 8c7710d981ed94d35f868bb703178c946c71b845 Mon Sep 17 00:00:00 2001 From: ort Date: Sat, 15 Aug 2026 18:07:52 -0400 Subject: [PATCH] Add household admin roles: invite gating, promote/demote, removal, passwords Every household now always has exactly one or more admins. Admins can invite new members (previously open to any parent -- now gated), promote/ demote other admins, remove parents, and directly set another parent's password (no email infra exists for a reset-link flow, so this is a direct admin-sets-the-value action). Every parent can change their own password with current-password confirmation. A sole admin can't remove themselves or demote until they promote someone else -- this falls out of a single "household must have >=1 admin" invariant rather than needing special-case code. Schema: parents.is_admin, added via a new idempotent ensureColumn() helper (SQLite has no ADD COLUMN IF NOT EXISTS, and this needed to run safely against the already-populated production parents table on next boot, not just fresh installs). A boot-time backfill promotes the earliest-created parent in any household with zero admins -- verified against a simulated copy of the real production schema/data (including the exact "spouse joined via invite" scenario), confirming correct promotion and clean idempotency across repeated boots. Fixed a real foreign-key landmine along the way: household_invites. used_by_parent_id had no ON DELETE clause, so deleting any parent who'd ever accepted an invite -- i.e. any spouse, in this app's normal usage -- would have thrown a constraint violation. src/lib/removeParent.js nulls that reference before deleting, wrapped in an explicit transaction (first use of manual BEGIN/COMMIT/ROLLBACK in this codebase, verified working with node:sqlite before relying on it). Verified extensively: every route's permission/edge cases via curl (cross-household isolation, sole-admin guards, password round-trips via real login), and the full UI flow (promote/demote/remove/both password modals/leave-with-error-toast) across two independent real browser sessions acting as admin and non-admin simultaneously. Full-app regression and a Docker build/boot check both pass with the new code in place. --- public/dashboard.html | 39 ++++++++++++ public/js/dashboard.js | 96 ++++++++++++++++++++++++++-- src/db/backfillAdmins.js | 31 +++++++++ src/db/columnMigrations.js | 13 ++++ src/db/migrate.js | 4 ++ src/db/schema.sql | 1 + src/lib/removeParent.js | 23 +++++++ src/middleware/requireAdmin.js | 7 +++ src/middleware/requireAuth.js | 2 +- src/routes/auth.js | 14 ++++- src/routes/household.js | 112 ++++++++++++++++++++++++++++++++- src/routes/invites.js | 10 ++- 12 files changed, 340 insertions(+), 12 deletions(-) create mode 100644 src/db/backfillAdmins.js create mode 100644 src/db/columnMigrations.js create mode 100644 src/lib/removeParent.js create mode 100644 src/middleware/requireAdmin.js diff --git a/public/dashboard.html b/public/dashboard.html index 4aa25a0..eb8b15e 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -107,6 +107,45 @@ + + + + + +