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 && (
-