diff --git a/artifacts/api-server/src/middlewares/setupGate.ts b/artifacts/api-server/src/middlewares/setupGate.ts index 29e43f9e..8c503e08 100644 --- a/artifacts/api-server/src/middlewares/setupGate.ts +++ b/artifacts/api-server/src/middlewares/setupGate.ts @@ -4,6 +4,14 @@ import { isSetupOpen } from "../lib/setupService"; // Blocks /api/setup/{validate,complete} once the system is installed. // /api/setup/status is intentionally NOT guarded — the SPA must always // be able to read install state to decide its routing. +// +// Gate semantics: "open" means BOTH system_settings.installed=false AND +// no admin user exists. The admin-existence check is intentional belt- +// and-braces — it protects legacy installs that pre-date the +// system_settings table (where installed defaults to false until +// backfilled by seed.ts). Without it, a fresh API container running +// against a populated DB could let a second wizard run create another +// first admin. Stricter than "installed=true only" by design. export async function requireSetupOpen( _req: Request, res: Response, diff --git a/scripts/local-setup.sh b/scripts/local-setup.sh index dd1cb811..94b21dca 100755 --- a/scripts/local-setup.sh +++ b/scripts/local-setup.sh @@ -186,15 +186,19 @@ else log "Generating local certificate via mkcert ..." mkcert -cert-file "$CERT_FILE" -key-file "$KEY_FILE" \ "$LOCAL_DOMAIN" localhost 127.0.0.1 "$LOCAL_IP" - if command -v mkcert >/dev/null 2>&1; then - CAROOT="$(mkcert -CAROOT 2>/dev/null || true)" - if [ -n "$CAROOT" ]; then - echo - echo " Root CA stored in: $CAROOT" - echo " To trust HTTPS on phones / other devices, install rootCA.pem" - echo " from that directory." - echo - fi +fi + +# Always print the mkcert root CA location so an operator running this +# script (first time or repeat) can find rootCA.pem to install on +# phones / other devices. +if command -v mkcert >/dev/null 2>&1; then + CAROOT="$(mkcert -CAROOT 2>/dev/null || true)" + if [ -n "$CAROOT" ]; then + echo + echo " Root CA stored in: $CAROOT" + echo " To trust HTTPS on phones / other devices, install rootCA.pem" + echo " from that directory." + echo fi fi diff --git a/scripts/src/seed.ts b/scripts/src/seed.ts index 70f81808..371fe0ef 100644 --- a/scripts/src/seed.ts +++ b/scripts/src/seed.ts @@ -112,8 +112,19 @@ async function main() { const adminPassword = process.env.SEED_ADMIN_PASSWORD; const userPassword = process.env.SEED_USER_PASSWORD; - // Detect existing install state. The system_settings row may not exist - // yet on a brand-new DB; treat "no row" as installed=false. + // Ensure a single source-of-truth row in system_settings always + // exists at id=1. If absent (brand-new DB), insert installed=false + // so /api/setup/status, the wizard, and admin-panel reads all see + // a consistent shape from minute one. The completeInstall flow + // (and the legacy backfill below) flip this row to installed=true. + await db + .insert(systemSettingsTable) + .values({ id: 1, installed: false }) + .onConflictDoNothing(); + + // Detect existing install state. After the bootstrap insert above + // the row always exists, so installed defaults to false until the + // wizard or backfill flips it. const sysRows = await db.select().from(systemSettingsTable).limit(1); const installedFlag = sysRows[0]?.installed ?? false;