From f28d401f345ff563230accea84747555cab65d99 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:25:20 +1100 Subject: [PATCH 1/6] Ensure bootstrap admin on every init --- src/server/init/ensure-initialized.ts | 26 +++++++++++--------------- src/test/init.test.ts | 4 ++-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/server/init/ensure-initialized.ts b/src/server/init/ensure-initialized.ts index 8c8b3d2..373ad4c 100644 --- a/src/server/init/ensure-initialized.ts +++ b/src/server/init/ensure-initialized.ts @@ -14,21 +14,17 @@ 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"; - - await db.user.upsert({ - where: { email: defaultEmail }, - update: { role: UserRole.ADMIN }, - create: { - email: defaultEmail, - name: "Admin User", - role: UserRole.ADMIN, - }, - }); - } + // 1) Ensure the bootstrap admin exists (every init, no auto-provisioning outside env) + const defaultEmail = process.env.INITIAL_ADMIN_EMAIL?.trim().toLowerCase() ?? "admin@example.com"; + await db.user.upsert({ + where: { email: defaultEmail }, + update: { role: UserRole.ADMIN }, + create: { + email: defaultEmail, + name: "Admin User", + role: UserRole.ADMIN, + }, + }); // 2) Ensure MITRE data exists const tacticCount = await db.mitreTactic.count(); diff --git a/src/test/init.test.ts b/src/test/init.test.ts index 6bc933f..2068c08 100644 --- a/src/test/init.test.ts +++ b/src/test/init.test.ts @@ -15,8 +15,8 @@ describe("ensureInitialized", () => { delete process.env.INITIAL_ADMIN_EMAIL; }); - 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() }; From 8d61923c7f198d618b16e6c1054c49e14394938c Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:36:44 +1100 Subject: [PATCH 2/6] Document admin rebootstrap in troubleshooting --- docs/development.md | 10 ++++++++++ docs/installation.md | 10 ++++++++++ 2 files changed, 20 insertions(+) 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 +``` From c5404386ef4220ed5a9ca9c8261e270c632cff5f Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:41:10 +1100 Subject: [PATCH 3/6] Fix init test to honor admin env --- src/test/init.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/init.test.ts b/src/test/init.test.ts index 2068c08..c5d868f 100644 --- a/src/test/init.test.ts +++ b/src/test/init.test.ts @@ -12,7 +12,7 @@ vi.mock("@/lib/mitreStix", () => ({ describe("ensureInitialized", () => { beforeEach(() => { vi.clearAllMocks(); - delete process.env.INITIAL_ADMIN_EMAIL; + process.env.INITIAL_ADMIN_EMAIL = "admin@example.com"; }); it("creates admin and seeds MITRE when empty", async () => { @@ -31,7 +31,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", }); From 17ce90c7b62405b9729522a47c4f8f9a36c23766 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:46:19 +1100 Subject: [PATCH 4/6] Skip bootstrap admin when env unset --- src/server/init/ensure-initialized.ts | 22 ++++++++++++---------- src/test/init.test.ts | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/server/init/ensure-initialized.ts b/src/server/init/ensure-initialized.ts index 373ad4c..f6b4dc8 100644 --- a/src/server/init/ensure-initialized.ts +++ b/src/server/init/ensure-initialized.ts @@ -15,16 +15,18 @@ export function ensureInitialized(db: PrismaClient): Promise { initPromise = (async () => { // 1) Ensure the bootstrap admin exists (every init, no auto-provisioning outside env) - const defaultEmail = process.env.INITIAL_ADMIN_EMAIL?.trim().toLowerCase() ?? "admin@example.com"; - await db.user.upsert({ - where: { email: defaultEmail }, - update: { role: UserRole.ADMIN }, - create: { - email: defaultEmail, - name: "Admin User", - role: UserRole.ADMIN, - }, - }); + const defaultEmail = process.env.INITIAL_ADMIN_EMAIL?.trim().toLowerCase(); + if (defaultEmail) { + await db.user.upsert({ + where: { email: defaultEmail }, + update: { role: UserRole.ADMIN }, + create: { + email: defaultEmail, + name: "Admin User", + role: UserRole.ADMIN, + }, + }); + } // 2) Ensure MITRE data exists const tacticCount = await db.mitreTactic.count(); diff --git a/src/test/init.test.ts b/src/test/init.test.ts index c5d868f..ffb9c63 100644 --- a/src/test/init.test.ts +++ b/src/test/init.test.ts @@ -41,4 +41,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(); + }); }); From 3d3885ab985ede9102405acb8c522034959e0029 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:49:06 +1100 Subject: [PATCH 5/6] Reset init module between tests --- src/test/init.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/init.test.ts b/src/test/init.test.ts index ffb9c63..95d598b 100644 --- a/src/test/init.test.ts +++ b/src/test/init.test.ts @@ -12,6 +12,7 @@ vi.mock("@/lib/mitreStix", () => ({ describe("ensureInitialized", () => { beforeEach(() => { vi.clearAllMocks(); + vi.resetModules(); process.env.INITIAL_ADMIN_EMAIL = "admin@example.com"; }); From 84c82e255968248a45fb7fc61a571e3a9916e85e Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:51:26 +1100 Subject: [PATCH 6/6] Reset init promise in tests --- src/server/init/ensure-initialized.ts | 6 ++++++ src/test/init.test.ts | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/server/init/ensure-initialized.ts b/src/server/init/ensure-initialized.ts index f6b4dc8..d7d17f6 100644 --- a/src/server/init/ensure-initialized.ts +++ b/src/server/init/ensure-initialized.ts @@ -67,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 95d598b..b76f7d0 100644 --- a/src/test/init.test.ts +++ b/src/test/init.test.ts @@ -10,10 +10,11 @@ vi.mock("@/lib/mitreStix", () => ({ })); describe("ensureInitialized", () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); - vi.resetModules(); process.env.INITIAL_ADMIN_EMAIL = "admin@example.com"; + const { resetInitializationForTests } = await import("@/server/init/ensure-initialized"); + resetInitializationForTests(); }); it("creates admin and seeds MITRE when empty", async () => {