fix(login): split passwordLogin / devLogin so SIGN IN cannot bypass

Oracle round-3 finding: passing password=undefined to api.signIn silently
hit /api/v1/auth/dev-login. With dev-login disabled in a production
build, a buyer typing only their email and pressing SIGN IN would still
have been authenticated as that user.

- api.passwordLogin(email, password) explicitly requires a password and
  always hits /api/v1/auth/login. Throws on missing password.
- api.devLogin(email) explicitly hits /api/v1/auth/dev-login and is the
  only entry point to that endpoint.
- store.loginAs(email, password, { method: 'password' | 'dev' }) routes
  to the right call. Default is password (requires password).
- Login.handleSubmit rejects empty password with 'Password required'
  before any network call.
- Login.handleDevLogin bails when devLoginEnabled is false.
- SSO button is disabled while probe state is 'unknown'.
- Login.test.tsx: regression test 'blocks empty-password SIGN IN —
  must NOT silently call dev-login'. Plus SSO test asserts the button
  starts disabled.

29/29 unit tests green.
This commit is contained in:
2026-06-14 13:34:54 +04:00
parent 456243c31c
commit b507ffe7e3
4 changed files with 61 additions and 44 deletions
+8 -3
View File
@@ -96,7 +96,7 @@ interface AppState {
userDisplayName: string | null;
isAuthed: boolean;
setUserEmail: (e: string) => void;
loginAs: (email: string, password?: string) => Promise<void>;
loginAs: (email: string, password?: string, options?: { method?: "password" | "dev" }) => Promise<void>;
/** Live polling — lightweight tick that only refreshes the active
* scenario's work-items + headline runtime. Full refresh is manual. */
@@ -258,13 +258,18 @@ export const useApp = create<AppState>((set, get) => {
userDisplayName: null,
isAuthed: typeof sessionStorage !== "undefined" && !!sessionStorage.getItem("fm.mc.token.v1"),
setUserEmail: (e) => { set({ userEmail: e }); persist(); },
loginAs: async (email, password) => {
loginAs: async (email, password, options) => {
api.clearToken();
api.config = { ...api.config, email };
set({ userEmail: email });
persist();
try {
await api.signIn(email, password);
if (options?.method === "dev") {
await api.devLogin(email);
} else {
if (!password) throw new Error("Password required");
await api.passwordLogin(email, password);
}
const me = await api.me();
set({
actor: { mode: "direct_user", user_id: me.user_id },