Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
14 changes: 9 additions & 5 deletions src/server/init/ensure-initialized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ export function ensureInitialized(db: PrismaClient): Promise<void> {
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 },
Expand Down Expand Up @@ -69,3 +67,9 @@ export function ensureInitialized(db: PrismaClient): Promise<void> {

return initPromise;
}

export function resetInitializationForTests(): void {
if (process.env.NODE_ENV === "test") {
initPromise = null;
}
}
29 changes: 24 additions & 5 deletions src/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() };
Expand All @@ -31,7 +33,7 @@ describe("ensureInitialized", () => {
create: Record<string, unknown>;
} | undefined;
expect(upsertArg?.create).toMatchObject({
email: "admin@example.com",
email: process.env.INITIAL_ADMIN_EMAIL,
name: "Admin User",
role: "ADMIN",
});
Expand All @@ -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();
});
});
Loading