diff --git a/docs/development.md b/docs/development.md index 7e67a29..74329cd 100644 --- a/docs/development.md +++ b/docs/development.md @@ -34,6 +34,16 @@ docker compose -f deploy/docker/docker-compose.dev.yml down docker volume rm docker_dev-postgres-data ``` +## Troubleshooting + +### I changed `INITIAL_ADMIN_EMAIL` or SSO and can’t sign in + +If you need to re-bootstrap the initial admin account (for example after changing `INITIAL_ADMIN_EMAIL`), re-run the initializer: + +```sh +npm run init +``` + ### Environment variables Local development uses the same env variable names as Docker (for example, `AUTH_SECRET`, `AUTH_URL`, `DATABASE_URL`). The local `.env.example` also includes `TEST_DATABASE_URL` for the test runner. diff --git a/docs/installation.md b/docs/installation.md index f356344..7045c17 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -52,3 +52,13 @@ docker exec rtap-web npm run seed:demo - Server logs emit to stdout/stderr (structured JSON in production, pretty in development). Rely on Docker and the host OS for collection and rotation. - Log level defaults: `debug` in development, `info` in production. Override with `LOG_LEVEL`. + +## Troubleshooting + +### I changed `INITIAL_ADMIN_EMAIL` or SSO and can’t sign in + +The bootstrap admin account is created during initialization. If you need to re-bootstrap it (for example after changing `INITIAL_ADMIN_EMAIL`), re-run the init script inside the web container: + +```sh +docker exec rtap-web npm run init +``` diff --git a/src/server/init/ensure-initialized.ts b/src/server/init/ensure-initialized.ts index 8c8b3d2..d7d17f6 100644 --- a/src/server/init/ensure-initialized.ts +++ b/src/server/init/ensure-initialized.ts @@ -14,11 +14,9 @@ export function ensureInitialized(db: PrismaClient): Promise { if (initPromise) return initPromise; initPromise = (async () => { - // 1) Ensure admin exists (if no users at all) - const userCount = await db.user.count(); - if (userCount === 0) { - const defaultEmail = process.env.INITIAL_ADMIN_EMAIL?.trim().toLowerCase() ?? "admin@example.com"; - + // 1) Ensure the bootstrap admin exists (every init, no auto-provisioning outside env) + const defaultEmail = process.env.INITIAL_ADMIN_EMAIL?.trim().toLowerCase(); + if (defaultEmail) { await db.user.upsert({ where: { email: defaultEmail }, update: { role: UserRole.ADMIN }, @@ -69,3 +67,9 @@ export function ensureInitialized(db: PrismaClient): Promise { return initPromise; } + +export function resetInitializationForTests(): void { + if (process.env.NODE_ENV === "test") { + initPromise = null; + } +} diff --git a/src/test/init.test.ts b/src/test/init.test.ts index 6bc933f..b76f7d0 100644 --- a/src/test/init.test.ts +++ b/src/test/init.test.ts @@ -10,13 +10,15 @@ vi.mock("@/lib/mitreStix", () => ({ })); describe("ensureInitialized", () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); - delete process.env.INITIAL_ADMIN_EMAIL; + process.env.INITIAL_ADMIN_EMAIL = "admin@example.com"; + const { resetInitializationForTests } = await import("@/server/init/ensure-initialized"); + resetInitializationForTests(); }); - it("creates admin when no users and seeds MITRE when empty", async () => { - const user = { count: vi.fn().mockResolvedValue(0), upsert: vi.fn() }; + it("creates admin and seeds MITRE when empty", async () => { + const user = { upsert: vi.fn() }; const mitreTactic = { count: vi.fn().mockResolvedValue(0), upsert: vi.fn() }; const mitreTechnique = { upsert: vi.fn() }; const mitreSubTechnique = { upsert: vi.fn() }; @@ -31,7 +33,7 @@ describe("ensureInitialized", () => { create: Record; } | undefined; expect(upsertArg?.create).toMatchObject({ - email: "admin@example.com", + email: process.env.INITIAL_ADMIN_EMAIL, name: "Admin User", role: "ADMIN", }); @@ -41,4 +43,21 @@ describe("ensureInitialized", () => { expect(mitreTechnique.upsert).toHaveBeenCalled(); expect(mitreSubTechnique.upsert).toHaveBeenCalled(); }); + + it("skips admin creation when INITIAL_ADMIN_EMAIL is unset", async () => { + delete process.env.INITIAL_ADMIN_EMAIL; + const user = { upsert: vi.fn() }; + const mitreTactic = { count: vi.fn().mockResolvedValue(0), upsert: vi.fn() }; + const mitreTechnique = { upsert: vi.fn() }; + const mitreSubTechnique = { upsert: vi.fn() }; + const db = { user, mitreTactic, mitreTechnique, mitreSubTechnique } as unknown as PrismaClient; + + const { ensureInitialized } = await import("@/server/init/ensure-initialized"); + await ensureInitialized(db); + + expect(user.upsert).not.toHaveBeenCalled(); + expect(mitreTactic.upsert).toHaveBeenCalled(); + expect(mitreTechnique.upsert).toHaveBeenCalled(); + expect(mitreSubTechnique.upsert).toHaveBeenCalled(); + }); });