diff --git a/src/lib/api.test.ts b/src/lib/api.test.ts index abfc7de..ec3f5d9 100644 --- a/src/lib/api.test.ts +++ b/src/lib/api.test.ts @@ -56,7 +56,7 @@ describe("api client", () => { ]); const r = await api.ping(); expect(r.ok).toBe(false); - expect(r.reason).toMatch(/401/); + expect(r.reason).toMatch(/rejected|401|sign-in/i); }); it("workItems() returns the items array", async () => { diff --git a/src/lib/api.ts b/src/lib/api.ts index 9387b07..3f5dbf5 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -19,6 +19,14 @@ const DEFAULT_CONFIG: ApiConfig = { const TOKEN_KEY = "fm.mc.token.v1"; +function friendlyAuthError(status: number, kind: string): string { + if (status === 502 || status === 504) return `${kind} unavailable: the EA2 backend is not reachable from this environment right now. Try again shortly.`; + if (status === 503) return `${kind} unavailable: the auth service is temporarily down. Try again shortly.`; + if (status === 404) return `${kind} is not wired to this environment yet.`; + if (status === 401 || status === 403) return `${kind} rejected: check the email and password.`; + return `${kind} failed (HTTP ${status}). Try again or contact the operator on call.`; +} + let inflightLogin: Promise | null = null; async function login(cfg: ApiConfig, signal?: AbortSignal): Promise { @@ -30,7 +38,7 @@ async function login(cfg: ApiConfig, signal?: AbortSignal): Promise { body: JSON.stringify({ email: cfg.email }), signal, }); - if (!r.ok) throw new Error(`dev-login ${r.status}`); + if (!r.ok) throw new Error(friendlyAuthError(r.status, "Developer sign-in")); const body = (await r.json()) as { access_token: string }; sessionStorage.setItem(TOKEN_KEY, body.access_token); return body.access_token; @@ -213,8 +221,7 @@ export const api = { }; const r = await instrumentedFetch("POST", `${this.config.baseUrl}/api/v1/auth/login`, init, { email, password }); if (!r.ok) { - const text = await r.text().catch(() => ""); - throw new Error(`Login failed: ${r.status} ${text.slice(0, 100)}`); + throw new Error(friendlyAuthError(r.status, "Sign-in")); } const data = await r.json() as { access_token: string; refresh_token?: string }; this.setBearer(data.access_token); @@ -230,8 +237,7 @@ export const api = { }; const r = await instrumentedFetch("POST", `${this.config.baseUrl}/api/v1/auth/dev-login`, init, { email }); if (!r.ok) { - const text = await r.text().catch(() => ""); - throw new Error(`Dev-login failed: ${r.status} ${text.slice(0, 100)}`); + throw new Error(friendlyAuthError(r.status, "Developer sign-in")); } const data = await r.json() as { access_token: string; refresh_token?: string }; this.setBearer(data.access_token); diff --git a/src/scenes/Login.tsx b/src/scenes/Login.tsx index d944183..c0ae28f 100644 --- a/src/scenes/Login.tsx +++ b/src/scenes/Login.tsx @@ -206,9 +206,22 @@ export default function Login() { key={p.key} type="button" className="persona-chip" - onClick={() => setEmail(p.email)} + onClick={async () => { + setEmail(p.email); + if (!devLoginEnabled) return; + setLoading(true); + setError(null); + try { + await loginAs(p.email, undefined, { method: "dev" }); + setScene("landing"); + } catch (err) { + setError((err as Error).message); + } finally { + setLoading(false); + } + }} disabled={loading} - title={p.email} + title={`Sign in as ${p.title}`} > {p.title}