diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json index 9cd33ec1..71ccd91d 100644 --- a/artifacts/api-server/package.json +++ b/artifacts/api-server/package.json @@ -9,7 +9,7 @@ "start": "node --enable-source-maps ./dist/index.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", "test:wait": "node ./scripts/wait-for-server.mjs", - "test": "node --test 'tests/**/*.test.mjs'", + "test": "node --test 'tests/*.test.mjs' && node --test --test-concurrency=1 'tests/serial/**/*.test.mjs'", "migrate": "pnpm --filter @workspace/db run push && pnpm --filter scripts run seed" }, "dependencies": { diff --git a/artifacts/api-server/src/middlewares/setupGate.ts b/artifacts/api-server/src/middlewares/setupGate.ts index 8c503e08..abdd5378 100644 --- a/artifacts/api-server/src/middlewares/setupGate.ts +++ b/artifacts/api-server/src/middlewares/setupGate.ts @@ -1,25 +1,32 @@ import type { Request, Response, NextFunction } from "express"; -import { isSetupOpen } from "../lib/setupService"; +import { db } from "@workspace/db"; +import { systemSettingsTable } from "@workspace/db"; // 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. +// Gate semantics: this middleware uses `system_settings.installed` as +// the SINGLE source of truth, per the Stage 1 contract. The +// admin-existence cross-check lives inside the POST /setup/complete +// transaction (see lib/setupService.ts) where it can run under the +// advisory lock — that's the right place to defend against legacy +// installs that have an admin but never wrote system_settings. +// Backfill (in scripts/src/seed.ts) ensures any such legacy install +// flips system_settings.installed=true on next boot, so this gate +// stays consistent with the source-of-truth model. export async function requireSetupOpen( _req: Request, res: Response, next: NextFunction, ): Promise { try { - const open = await isSetupOpen(); - if (!open) { + const rows = await db + .select({ installed: systemSettingsTable.installed }) + .from(systemSettingsTable) + .limit(1); + const installed = rows[0]?.installed ?? false; + if (installed) { res.status(409).json({ error: "already_installed", message: "Setup has already been completed.", diff --git a/artifacts/api-server/tests/setup-wizard.test.mjs b/artifacts/api-server/tests/serial/setup-wizard.test.mjs similarity index 100% rename from artifacts/api-server/tests/setup-wizard.test.mjs rename to artifacts/api-server/tests/serial/setup-wizard.test.mjs