diff --git a/nginx.conf b/nginx.conf index 9d4c54f..f3e47af 100644 --- a/nginx.conf +++ b/nginx.conf @@ -85,6 +85,15 @@ server { proxy_buffering off; } + # Dev/demo auth discovery. The SPA uses this to decide whether to show and + # enable the dev-login affordance. Keep this exact route ahead of the SPA + # fallback or nginx will return index.html and the client will disable it. + location = /internal/dev-login-config { + default_type application/json; + add_header Cache-Control "no-store" always; + return 200 '{"enabled":true,"email":"dev@flow-master.ai"}'; + } + # SPA fallback to index.html for client-side routing. location / { try_files $uri $uri/ /index.html; diff --git a/src/lib/api.ts b/src/lib/api.ts index 3f5dbf5..16553ef 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -244,13 +244,13 @@ export const api = { return data; }, - async devLoginConfig(signal?: AbortSignal): Promise<{ enabled: boolean | null }> { + async devLoginConfig(signal?: AbortSignal): Promise<{ enabled: boolean | null; email?: string }> { try { const init: RequestInit = { method: "GET", headers: { "Accept": "application/json" }, signal }; const r = await instrumentedFetch("GET", `${this.config.baseUrl}/internal/dev-login-config`, init); if (r.ok) { - const body = await r.json() as { enabled: boolean }; - return { enabled: !!body.enabled }; + const body = await r.json() as { enabled: boolean; email?: string }; + return { enabled: !!body.enabled, email: body.email }; } } catch { // network/parse error — caller treats null as "no opinion" diff --git a/src/scenes/Login.test.tsx b/src/scenes/Login.test.tsx index 79d9efe..51a96b8 100644 --- a/src/scenes/Login.test.tsx +++ b/src/scenes/Login.test.tsx @@ -71,6 +71,22 @@ describe("Login Scene", () => { }); }); + it("uses configured dev-login email without requiring typed credentials", async () => { + (api.devLoginConfig as any).mockResolvedValueOnce({ enabled: true, email: "dev@flow-master.ai" }); + mockLoginAs.mockResolvedValueOnce(undefined); + + render(); + + const devButton = await screen.findByRole("button", { name: /DEV-LOGIN/i }); + expect((devButton as HTMLButtonElement).disabled).toBe(false); + fireEvent.click(devButton); + + await waitFor(() => { + expect(mockLoginAs).toHaveBeenCalledWith("dev@flow-master.ai", undefined, { method: "dev" }); + expect(mockSetScene).toHaveBeenCalledWith("mission"); + }); + }); + it("SSO button is disabled until probe resolves to ready", async () => { render(); const ssoBtn = screen.getAllByRole("button", { name: /MICROSOFT SIGN-IN|CONTINUE WITH MICROSOFT/i })[0] as HTMLButtonElement; @@ -78,4 +94,4 @@ describe("Login Scene", () => { fireEvent.click(ssoBtn); expect(mockLoginAs).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/src/scenes/Login.tsx b/src/scenes/Login.tsx index feb037e..2a2c4c3 100644 --- a/src/scenes/Login.tsx +++ b/src/scenes/Login.tsx @@ -11,6 +11,7 @@ export default function Login() { const [rememberMe, setRememberMe] = useState(false); const buildAllowsDev = (import.meta.env.VITE_ENABLE_DEV_LOGIN ?? "true") !== "false"; const [devLoginEnabled, setDevLoginEnabled] = useState(buildAllowsDev); + const [devLoginEmail, setDevLoginEmail] = useState("dev@flow-master.ai"); const [ssoState, setSsoState] = useState<"unknown" | "ready" | "not-configured" | "not-wired">("unknown"); const [backendState, setBackendState] = useState<"unknown" | "up" | "auth-down" | "proxy-down">("unknown"); const [loading, setLoading] = useState(false); @@ -21,7 +22,13 @@ export default function Login() { setDevLoginEnabled(false); } else { api.devLoginConfig() - .then((cfg) => { if (cfg.enabled === false) setDevLoginEnabled(false); }) + .then((cfg) => { + if (cfg.enabled === false) setDevLoginEnabled(false); + if (cfg.email) { + setDevLoginEmail(cfg.email); + setEmail((current) => current || cfg.email || ""); + } + }) .catch(() => { /* endpoint absent → keep state */ }); } fetch("/api/v1/auth/microsoft/login", { method: "GET", redirect: "manual" }) @@ -69,14 +76,15 @@ export default function Login() { setError("Developer sign-in is disabled in this build."); return; } - if (!email) { - setError("Email required for dev-login"); + const selectedEmail = email || devLoginEmail; + if (!selectedEmail) { + setError("Developer sign-in has no configured email."); return; } setLoading(true); setError(null); try { - await loginAs(email, undefined, { method: "dev" }); + await loginAs(selectedEmail, undefined, { method: "dev" }); setScene("mission"); } catch (err) { setError((err as Error).message); @@ -211,7 +219,7 @@ export default function Login() { {devLoginEnabled && ( - )}