diff --git a/.env.example b/.env.example index 293f815..f91cd54 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,12 @@ AUTH_DISABLED=false OIDC_ISSUER=https://authentik.example.com OIDC_AUDIENCE=groombook +# ── Setup Wizard ───────────────────────────────────────────────────────────── +# When SKIP_OOBE=true, the setup wizard is bypassed regardless of whether a +# super user exists in the database. Useful in dev/test environments where the +# database has data but the setup wizard would otherwise block access. +SKIP_OOBE=false + # ── API ─────────────────────────────────────────────────────────────────────── PORT=3000 CORS_ORIGIN=http://localhost:8080 diff --git a/apps/api/src/__tests__/setup.test.ts b/apps/api/src/__tests__/setup.test.ts index 8fc4ffd..9884e96 100644 --- a/apps/api/src/__tests__/setup.test.ts +++ b/apps/api/src/__tests__/setup.test.ts @@ -418,6 +418,48 @@ describe("GET /setup/status — OOBE bootstrap logic", () => { expect(body.showAuthProviderStep).toBe(false); // DB config already exists expect(body.authConfigExists).toBe(true); }); + + it("SKIP_OOBE=true bypasses setup check regardless of DB state", async () => { + dbStaffRows = []; // no super user + dbAuthConfigRows = []; + process.env.SKIP_OOBE = "true"; + + const app = makeApp(); + const { status, body } = await getStatus(app); + + expect(status).toBe(200); + expect(body.needsSetup).toBe(false); + expect(body.showAuthProviderStep).toBe(false); + expect(body.authConfigExists).toBe(false); + expect(body.authEnvVarsSet).toBe(false); + expect(body.skipped).toBe(true); + }); + + it("SKIP_OOBE=1 also bypasses setup check", async () => { + dbStaffRows = []; + dbAuthConfigRows = []; + process.env.SKIP_OOBE = "1"; + + const app = makeApp(); + const { status, body } = await getStatus(app); + + expect(status).toBe(200); + expect(body.needsSetup).toBe(false); + expect(body.skipped).toBe(true); + }); + + it("SKIP_OOBE=yes also bypasses setup check", async () => { + dbStaffRows = []; + dbAuthConfigRows = []; + process.env.SKIP_OOBE = "yes"; + + const app = makeApp(); + const { status, body } = await getStatus(app); + + expect(status).toBe(200); + expect(body.needsSetup).toBe(false); + expect(body.skipped).toBe(true); + }); }); describe("POST /setup/auth-provider — OOBE bootstrap", () => { diff --git a/apps/api/src/routes/setup.ts b/apps/api/src/routes/setup.ts index 079636a..a614b5c 100644 --- a/apps/api/src/routes/setup.ts +++ b/apps/api/src/routes/setup.ts @@ -9,6 +9,17 @@ export const setupRouter = new Hono(); // GET /api/setup/status — public (no auth), returns whether setup is needed // and whether the auth provider bootstrap step should be shown setupRouter.get("/status", async (c) => { + const skipOobe = ["true", "1", "yes"].includes((process.env.SKIP_OOBE || "").toLowerCase()); + if (skipOobe) { + return c.json({ + needsSetup: false, + showAuthProviderStep: false, + authConfigExists: false, + authEnvVarsSet: false, + skipped: true, + }); + } + const db = getDb(); // Check if any super user exists diff --git a/packages/db/migrations/0025_rate_limit.sql b/packages/db/migrations/0025_rate_limit.sql new file mode 100644 index 0000000..0a83e14 --- /dev/null +++ b/packages/db/migrations/0025_rate_limit.sql @@ -0,0 +1,6 @@ +-- Better-Auth rate limiting table (GRO-574) +CREATE TABLE "rate_limit" ( + key TEXT NOT NULL PRIMARY KEY, + count INTEGER NOT NULL, + last_request BIGINT NOT NULL +); diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 3a1ec9c..7ca1dc5 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1775396067192, "tag": "0024_invoice_indexes", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1775482467192, + "tag": "0025_rate_limit", + "breakpoints": true } ] } \ No newline at end of file