feat: public setup-status probe, bootstrap admin no longer throws on empty store

This commit is contained in:
m4
2026-08-12 22:36:11 +08:00
parent dac9c4a693
commit 78c98afb52
3 changed files with 76 additions and 7 deletions
@@ -0,0 +1,58 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, describe, expect, it, vi } from "vitest";
vi.mock("server-only", () => ({}));
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "evosci-setup-status-"));
const ORIGINAL_ENV = {
EVOSCIENTIST_DATA_DIR: process.env.EVOSCIENTIST_DATA_DIR,
WEBUI_AUTH_ENABLED: process.env.WEBUI_AUTH_ENABLED,
WEBUI_AUTH_SECRET: process.env.WEBUI_AUTH_SECRET,
WEBUI_AUTH_USERNAME: process.env.WEBUI_AUTH_USERNAME,
WEBUI_AUTH_PASSWORD: process.env.WEBUI_AUTH_PASSWORD,
};
process.env.EVOSCIENTIST_DATA_DIR = dataDir;
process.env.WEBUI_AUTH_ENABLED = "true";
process.env.WEBUI_AUTH_SECRET = "test-secret-with-at-least-32-characters";
delete process.env.WEBUI_AUTH_USERNAME;
delete process.env.WEBUI_AUTH_PASSWORD;
const { GET } = await import("./route");
const { closeUserStoreForTests, countUsers } = await import(
"@/lib/server/userStore"
);
const { verifyCredentials } = await import("@/lib/server/auth");
afterAll(() => {
closeUserStoreForTests();
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
fs.rmSync(dataDir, { recursive: true, force: true });
});
describe("GET /api/auth/setup-status", () => {
it("reports needsSetup when the store is empty and no env seeding is set", async () => {
const res = await GET();
expect(res.status).toBe(200);
expect(res.headers.get("cache-control")).toBe("no-store");
expect(await res.json()).toEqual({ needsSetup: true });
});
it("no longer throws on the auth path with an empty store and no env", () => {
expect(verifyCredentials("nobody", "wrong-password")).toBeNull();
});
it("seeds from env on the first probe and then reports needsSetup false", async () => {
process.env.WEBUI_AUTH_USERNAME = "env-admin";
process.env.WEBUI_AUTH_PASSWORD = "env-password-123";
const res = await GET();
expect(await res.json()).toEqual({ needsSetup: false });
expect(countUsers()).toBe(1);
expect(verifyCredentials("env-admin", "env-password-123")).not.toBeNull();
});
});
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { countUsers, ensureBootstrapAdmin } from "@/lib/server/userStore";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const NO_STORE = { "Cache-Control": "no-store" };
export async function GET() {
ensureBootstrapAdmin();
return NextResponse.json(
{ needsSetup: countUsers() === 0 },
{ headers: NO_STORE }
);
}
+3 -7
View File
@@ -210,17 +210,13 @@ export function verifyUserPassword(
/**
* Seed the first admin from the deployment environment when the store is
* empty (design doc 7.2: the first admin comes from env or an init command;
* later users are managed by admins through the API).
* empty. Without env seeding the store stays empty and the first-start
* setup flow (/api/auth/setup-status, /api/auth/setup) creates the admin.
*/
export function ensureBootstrapAdmin(): void {
if (countUsers() > 0) return;
const username = process.env.WEBUI_AUTH_USERNAME?.trim();
const password = process.env.WEBUI_AUTH_PASSWORD;
if (!username || !password) {
throw new UserStoreError(
"No WebUI users exist yet. Set WEBUI_AUTH_USERNAME and WEBUI_AUTH_PASSWORD to create the first admin."
);
}
if (!username || !password) return;
createUser(username, password, "admin");
}