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
25 changes: 4 additions & 21 deletions src/app/api/setup-status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { NextResponse } from "next/server";
import { env, isDemoMode } from "@/lib/env";
import { localProjectRequestIdentityPayload, unsafeLocalProjectResponse } from "@/lib/local-project-guard";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { formatSupabaseUnavailableError, isSupabaseUnavailableError, probeSupabaseHealth } from "@/lib/supabase/health";
import { checkSupabaseProjectConfig, formatSupabaseProjectCheck } from "@/lib/supabase/project";

Expand Down Expand Up @@ -409,27 +408,11 @@ async function readSetupStatusPayload() {
}
}

async function requireProductionSetupStatusAuth(request: Request) {
export async function GET(request: Request) {
const identity = localProjectRequestIdentityPayload(request);
if (process.env.NODE_ENV !== "production" || identity.localServer.currentUrl) {
return identity;
if (!identity.localServer.safeLocalOrigin) {
return unsafeLocalProjectResponse(identity);
}
await requireAuthenticatedUser(request, createAdminClient());
return identity;
}

export async function GET(request: Request) {
try {
const identity = await requireProductionSetupStatusAuth(request);
if (!identity.localServer.safeLocalOrigin) {
return unsafeLocalProjectResponse(identity);
}

return setupStatusResponse(await readSetupStatusPayload());
} catch (error) {
if (error instanceof AuthenticationError) {
return unauthorizedResponse(error);
}
throw error;
}
return setupStatusResponse(await readSetupStatusPayload());
}
34 changes: 25 additions & 9 deletions tests/setup-status-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ afterEach(() => {
});

describe("/api/setup-status", () => {
it("requires auth for non-local production requests before returning setup posture", async () => {
it("returns setup posture for anonymous production requests without exposing secret values", async () => {
vi.stubEnv("NODE_ENV", "production");
const getUser = vi.fn();
const createAdminClient = vi.fn(() => ({ auth: { getUser } }));
const from = vi.fn(async () => ({ error: null, data: [], count: 0 }));
const createAdminClient = vi.fn(() => ({
from,
rpc: vi.fn(),
}));
vi.doMock("@/lib/env", () => ({
env: {
NEXT_PUBLIC_SUPABASE_URL: "https://sjrfecxgysukkwxsowpy.supabase.co",
Expand All @@ -24,16 +27,29 @@ describe("/api/setup-status", () => {
isLocalNoAuthMode: () => false,
}));
vi.doMock("@/lib/supabase/admin", () => ({ createAdminClient }));
vi.doMock("@/lib/supabase/health", () => ({
probeSupabaseHealth: vi.fn(async () => ({ ok: true })),
isSupabaseUnavailableError: () => false,
formatSupabaseUnavailableError: (error: unknown) => String(error),
}));
vi.doMock("@/lib/supabase/project", () => ({
checkSupabaseProjectConfig: () => ({ status: "ready", detail: "Clinical KB Database target is configured." }),
formatSupabaseProjectCheck: () => "Clinical KB Database target is configured.",
}));
const { GET } = await import("../src/app/api/setup-status/route");

const response = await GET(new Request("https://clinical.example/api/setup-status"));
const body = await response.json();

expect(response.status).toBe(401);
expect(body).toEqual({ error: "Authentication required." });
expect(JSON.stringify(body)).not.toContain("OPENAI");
expect(JSON.stringify(body)).not.toContain("Supabase");
expect(createAdminClient).toHaveBeenCalledTimes(1);
expect(getUser).not.toHaveBeenCalled();
expect(response.status).toBe(200);
expect(body).toMatchObject({
demoMode: false,
checks: expect.arrayContaining([
expect.objectContaining({ id: "env" }),
expect.objectContaining({ id: "openai" }),
]),
});
expect(JSON.stringify(body)).not.toContain("service-role-key");
expect(JSON.stringify(body)).not.toContain("openai-key");
});
});
Loading