@burdenoff/microfe-auth 2026.528.1 → 2026.528.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4,17 +4,18 @@ import { useAuthOperations as n } from "../hooks/useAuthOperations.js";
|
|
|
4
4
|
import { useMyWorkspaces as r } from "../hooks/useMyWorkspaces.js";
|
|
5
5
|
import { useOAuthOperations as i } from "../hooks/useOAuthOperations.js";
|
|
6
6
|
import { AuthLayout as a } from "../components/AuthLayout.js";
|
|
7
|
-
import { AlertIcon as o, EmailIcon as s, EyeIcon as
|
|
7
|
+
import { AlertIcon as o, EmailIcon as s, EyeIcon as ee, EyeOffIcon as te, GoogleIcon as c, LockIcon as l, MicrosoftIcon as ne, SpinnerIcon as u } from "../components/AuthIcons.js";
|
|
8
8
|
import { useCaptchaV3 as re } from "../hooks/useCaptchaV3.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { MFA_REDIRECT as ie } from "../constants/sessionKeys.js";
|
|
10
|
+
import { useCallback as d, useEffect as f, useRef as ae, useState as p } from "react";
|
|
11
|
+
import { SSORedirectSpinner as oe, getSSORedirectUrl as se } from "@burdenoff/fe-libs/shared/components";
|
|
12
|
+
import { useLocation as ce, useNavigate as le, useSearchParams as ue } from "react-router-dom";
|
|
13
|
+
import { useActiveContext as de, useI18n as fe } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
13
14
|
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
14
|
-
import { resolveAuthBridgeConfig as
|
|
15
|
-
import { getProfileTokens as
|
|
15
|
+
import { resolveAuthBridgeConfig as pe } from "@burdenoff/fe-libs/shared/config";
|
|
16
|
+
import { getProfileTokens as me, getProfileUser as he, getProfileWorkspaceToken as ge, getProfiles as _, resolveActiveProfileId as _e } from "@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager";
|
|
16
17
|
//#region src/auth/pages/LoginPage.tsx
|
|
17
|
-
function
|
|
18
|
+
function v() {
|
|
18
19
|
if (typeof window > "u") return !1;
|
|
19
20
|
let e = window;
|
|
20
21
|
if (typeof e.Capacitor?.isNativePlatform == "function") try {
|
|
@@ -22,7 +23,7 @@ function ge() {
|
|
|
22
23
|
} catch {}
|
|
23
24
|
return !!(typeof navigator < "u" && /Electron/i.test(navigator.userAgent || ""));
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
+
function ve(e) {
|
|
26
27
|
if (typeof window > "u") return e;
|
|
27
28
|
try {
|
|
28
29
|
let t = new URL(e, window.location.origin);
|
|
@@ -31,7 +32,7 @@ function _e(e) {
|
|
|
31
32
|
return e;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
function
|
|
35
|
+
function ye(e) {
|
|
35
36
|
try {
|
|
36
37
|
let t = e.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"), n = t.padEnd(Math.ceil(t.length / 4) * 4, "="), r = JSON.parse(atob(n));
|
|
37
38
|
return typeof r.exp == "number" && r.exp > Date.now() / 1e3;
|
|
@@ -39,7 +40,7 @@ function ve(e) {
|
|
|
39
40
|
return !1;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
+
function be(e) {
|
|
43
44
|
if (!e) return null;
|
|
44
45
|
try {
|
|
45
46
|
let t = new URL(decodeURIComponent(e), window.location.origin);
|
|
@@ -58,7 +59,7 @@ function y(e, t) {
|
|
|
58
59
|
let n = e.searchParams.toString();
|
|
59
60
|
return `${e.pathname}${n ? `?${n}` : ""}${e.hash}`;
|
|
60
61
|
}
|
|
61
|
-
function
|
|
62
|
+
function xe(e) {
|
|
62
63
|
try {
|
|
63
64
|
let t = new URL(decodeURIComponent(e), window.location.origin);
|
|
64
65
|
return y(t, { includeOrigin: t.origin !== window.location.origin });
|
|
@@ -67,73 +68,73 @@ function be(e) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
function b() {
|
|
70
|
-
let y =
|
|
71
|
-
let n =
|
|
71
|
+
let y = le(), b = ce(), [x] = ue(), { t: Se } = fe(), S = d((e, t) => {
|
|
72
|
+
let n = Se(e);
|
|
72
73
|
return n === e ? t : n;
|
|
73
|
-
}, [
|
|
74
|
-
if (typeof window > "u" ||
|
|
74
|
+
}, [Se]), { signIn: Ce, isLoading: C, error: w, errorCode: T, resendVerification: we } = n(), { completeAuthFlow: Te } = r(), { setWorkspace: E, setOrganization: D } = de(), { defaultPostLoginPath: O } = e(), { initiateGoogleOAuth: Ee, initiateMicrosoftOAuth: De, isProviderConfigured: k } = i(), A = t((e) => e.mfaRequired), { executeRecaptcha: Oe } = re(), [j, M] = p(!1), [ke, Ae] = p(!1), N = d((e) => {
|
|
75
|
+
if (typeof window > "u" || v()) return e;
|
|
75
76
|
try {
|
|
76
|
-
let t =
|
|
77
|
+
let t = pe(), n = new URL(t.url), r = window.location.origin;
|
|
77
78
|
if (n.origin === r) return e;
|
|
78
|
-
let i = new URL(e, r).href, a =
|
|
79
|
+
let i = new URL(e, r).href, a = _e();
|
|
79
80
|
if (!a) return e;
|
|
80
|
-
let o =
|
|
81
|
+
let o = me(a), s = he(a);
|
|
81
82
|
if (!o || !s) return e;
|
|
82
|
-
let
|
|
83
|
+
let ee = ge(a), te = {
|
|
83
84
|
profileId: a,
|
|
84
85
|
tokens: JSON.stringify({
|
|
85
86
|
...o,
|
|
86
|
-
workspaceToken:
|
|
87
|
+
workspaceToken: ee
|
|
87
88
|
}),
|
|
88
89
|
user: JSON.stringify(s),
|
|
89
90
|
profiles: JSON.stringify(_())
|
|
90
|
-
},
|
|
91
|
-
return
|
|
91
|
+
}, c = btoa(JSON.stringify(te)), l = new URL("/auth/sso-deposit", n.origin);
|
|
92
|
+
return l.searchParams.set("returnTo", i), `${l.href}#sso=${c}`;
|
|
92
93
|
} catch {
|
|
93
94
|
return e;
|
|
94
95
|
}
|
|
95
|
-
}, []),
|
|
96
|
+
}, []), P = d((e) => {
|
|
96
97
|
if (typeof window < "u") {
|
|
97
98
|
try {
|
|
98
99
|
sessionStorage.setItem("bf-post-login-return-to", e);
|
|
99
100
|
} catch {}
|
|
100
|
-
if (
|
|
101
|
-
y(
|
|
101
|
+
if (v()) {
|
|
102
|
+
y(ve(e));
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
window.location.assign(
|
|
105
|
+
window.location.assign(N(e));
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
y(e);
|
|
108
|
-
}, [y,
|
|
109
|
+
}, [y, N]), [F, je] = p(!1), [I, L] = p(!1), [Me, R] = p(!1), [Ne, z] = p(!1), [B, V] = p(!1), [H, U] = p(!1), [Pe, Fe] = p(!1), [Ie, W] = p(!1), [Le, G] = p(null), [Re, ze] = p(null), [K, q] = p({
|
|
109
110
|
email: "",
|
|
110
111
|
password: "",
|
|
111
112
|
rememberMe: !1
|
|
112
|
-
}), [
|
|
113
|
+
}), [J, Y] = p(null), [Be, X] = p(!1), Z = x.get("mode") === "add-account", Q = x.get("returnTo"), $ = ae(Q);
|
|
113
114
|
f(() => {
|
|
114
|
-
|
|
115
|
-
}, [
|
|
116
|
-
if (
|
|
117
|
-
let e =
|
|
115
|
+
!$.current && Q && ($.current = Q);
|
|
116
|
+
}, [Q]), f(() => {
|
|
117
|
+
if (Z || j) return;
|
|
118
|
+
let e = _e();
|
|
118
119
|
if (e) {
|
|
119
|
-
let n =
|
|
120
|
-
if (n?.accessToken && r &&
|
|
121
|
-
t.getState().loadFromProfile(e),
|
|
120
|
+
let n = me(e), r = he(e);
|
|
121
|
+
if (n?.accessToken && r && ye(n.accessToken)) {
|
|
122
|
+
t.getState().loadFromProfile(e), P(be(Q) ?? "/");
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
|
-
let n =
|
|
126
|
+
let n = se();
|
|
126
127
|
n && (Ae(!0), window.location.replace(n));
|
|
127
128
|
}, [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
Z,
|
|
130
|
+
j,
|
|
131
|
+
P,
|
|
132
|
+
Q
|
|
132
133
|
]), f(() => {
|
|
133
134
|
try {
|
|
134
135
|
localStorage.removeItem("remembered_password");
|
|
135
136
|
let e = localStorage.getItem("remembered_email");
|
|
136
|
-
e &&
|
|
137
|
+
e && q((t) => ({
|
|
137
138
|
...t,
|
|
138
139
|
email: e,
|
|
139
140
|
rememberMe: !0
|
|
@@ -143,7 +144,7 @@ function b() {
|
|
|
143
144
|
}
|
|
144
145
|
}, []), f(() => {
|
|
145
146
|
let e = b.state;
|
|
146
|
-
e?.message && ze(e.message), e?.email &&
|
|
147
|
+
e?.message && ze(e.message), e?.email && q((t) => ({
|
|
147
148
|
...t,
|
|
148
149
|
email: e.email
|
|
149
150
|
})), e && window.history.replaceState({}, document.title);
|
|
@@ -163,7 +164,7 @@ function b() {
|
|
|
163
164
|
};
|
|
164
165
|
f(() => {
|
|
165
166
|
let e = x.get("email");
|
|
166
|
-
if (e &&
|
|
167
|
+
if (e && q((t) => ({
|
|
167
168
|
...t,
|
|
168
169
|
email: e
|
|
169
170
|
})), x.get("expired") === "true") {
|
|
@@ -172,36 +173,42 @@ function b() {
|
|
|
172
173
|
window.history.replaceState({}, document.title, e);
|
|
173
174
|
}
|
|
174
175
|
}, [x, S]), f(() => {
|
|
175
|
-
|
|
176
|
-
}, [
|
|
176
|
+
A && y("/auth/mfa-verify");
|
|
177
|
+
}, [A, y]);
|
|
177
178
|
let He = async (e) => {
|
|
178
|
-
e.preventDefault(),
|
|
179
|
-
let n = (
|
|
179
|
+
e.preventDefault(), L(!1), R(!1), z(!1), V(!1), U(!1), X(!1), W(!1), Y(null);
|
|
180
|
+
let n = (K.email ?? "").trim();
|
|
180
181
|
if (!n) {
|
|
181
|
-
|
|
182
|
+
Y(S("auth.login.enterEmailOrUsername", "Please enter your email or username"));
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
184
185
|
if (n.includes("@") && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(n)) {
|
|
185
|
-
|
|
186
|
+
Y(S("auth.login.invalidEmail", "Please enter a valid email address"));
|
|
186
187
|
return;
|
|
187
188
|
}
|
|
188
|
-
if (!
|
|
189
|
-
|
|
189
|
+
if (!K.password || K.password.length < 1) {
|
|
190
|
+
Y(S("auth.login.enterPassword", "Please enter your password"));
|
|
190
191
|
return;
|
|
191
192
|
}
|
|
192
|
-
if (
|
|
193
|
-
|
|
193
|
+
if (Z && _().some((e) => e.email.toLowerCase() === n.toLowerCase())) {
|
|
194
|
+
X(!0), Y(null);
|
|
194
195
|
return;
|
|
195
196
|
}
|
|
196
|
-
|
|
197
|
+
X(!1);
|
|
197
198
|
try {
|
|
198
|
-
let e = await Oe("login"), t = await
|
|
199
|
+
let e = await Oe("login"), t = await Ce({
|
|
199
200
|
email: n,
|
|
200
|
-
password:
|
|
201
|
-
rememberMe:
|
|
201
|
+
password: K.password,
|
|
202
|
+
rememberMe: K.rememberMe,
|
|
202
203
|
...e ? { recaptchaToken: e } : {}
|
|
203
204
|
});
|
|
204
|
-
if (t.requiresMFA)
|
|
205
|
+
if (t.requiresMFA) {
|
|
206
|
+
let e = xe($.current || Q || (O ?? "/dashboard"));
|
|
207
|
+
try {
|
|
208
|
+
sessionStorage.setItem(ie, e);
|
|
209
|
+
} catch {}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
205
212
|
if (t.success) {
|
|
206
213
|
try {
|
|
207
214
|
let e = t.user?.id;
|
|
@@ -214,14 +221,14 @@ function b() {
|
|
|
214
221
|
}
|
|
215
222
|
} catch {}
|
|
216
223
|
try {
|
|
217
|
-
|
|
224
|
+
K.rememberMe ? localStorage.setItem("remembered_email", n) : localStorage.removeItem("remembered_email");
|
|
218
225
|
} catch (e) {
|
|
219
226
|
console.warn("[LoginPage] Failed to save/clear remembered email:", e);
|
|
220
227
|
}
|
|
221
|
-
|
|
222
|
-
let e =
|
|
228
|
+
M(!0);
|
|
229
|
+
let e = xe($.current || Q || (O ?? "/dashboard"));
|
|
223
230
|
try {
|
|
224
|
-
let n = await Promise.race([
|
|
231
|
+
let n = await Promise.race([Te(t.token), new Promise((e) => {
|
|
225
232
|
window.setTimeout(() => {
|
|
226
233
|
e({
|
|
227
234
|
success: !1,
|
|
@@ -229,63 +236,63 @@ function b() {
|
|
|
229
236
|
});
|
|
230
237
|
}, 5e3);
|
|
231
238
|
})]);
|
|
232
|
-
n.success ? (n.workspace && (E(n.workspace.id), n.workspace.organizationId && D(n.workspace.organizationId)),
|
|
239
|
+
n.success ? (n.workspace && (E(n.workspace.id), n.workspace.organizationId && D(n.workspace.organizationId)), P(e)) : n.error?.includes("No workspaces found") ? y("/onboarding/create-workspace") : (n.workspace && (E(n.workspace.id), n.workspace.organizationId && D(n.workspace.organizationId)), console.warn("Auth flow warning:", n.error), P(e));
|
|
233
240
|
} catch (t) {
|
|
234
|
-
console.error("Auth flow error (login succeeded):", t),
|
|
241
|
+
console.error("Auth flow error (login succeeded):", t), P(e);
|
|
235
242
|
} finally {
|
|
236
|
-
|
|
243
|
+
M(!1);
|
|
237
244
|
}
|
|
238
245
|
}
|
|
239
246
|
} catch (e) {
|
|
240
247
|
let n = e instanceof Error ? e.message : String(e);
|
|
241
|
-
n.includes("reCAPTCHA") &&
|
|
248
|
+
n.includes("reCAPTCHA") && Y(n);
|
|
242
249
|
let r = t.getState().errorCode, i = Ve(n, r);
|
|
243
|
-
i === "DELETED" ?
|
|
250
|
+
i === "DELETED" ? R(!0) : i === "SUSPENDED" ? z(!0) : i === "OAUTH_ONLY" ? V(!0) : i === "INACTIVE" ? L(!0) : i === "EMAIL_NOT_VERIFIED" && U(!0);
|
|
244
251
|
}
|
|
245
252
|
};
|
|
246
253
|
f(() => {
|
|
247
254
|
if (w) {
|
|
248
255
|
let e = Ve(w, T);
|
|
249
|
-
e === "DELETED" ?
|
|
256
|
+
e === "DELETED" ? R(!0) : e === "SUSPENDED" ? z(!0) : e === "OAUTH_ONLY" ? V(!0) : e === "INACTIVE" ? L(!0) : e === "EMAIL_NOT_VERIFIED" && U(!0);
|
|
250
257
|
}
|
|
251
258
|
}, [w, T]);
|
|
252
259
|
let Ue = () => {
|
|
253
|
-
|
|
260
|
+
G(null);
|
|
254
261
|
try {
|
|
255
262
|
Ee();
|
|
256
263
|
} catch (e) {
|
|
257
|
-
|
|
264
|
+
G(e instanceof Error ? e.message : S("auth.login.googleSignInFailed", "Failed to initiate Google sign in"));
|
|
258
265
|
}
|
|
259
266
|
}, We = () => {
|
|
260
|
-
|
|
267
|
+
G(null);
|
|
261
268
|
try {
|
|
262
269
|
De();
|
|
263
270
|
} catch (e) {
|
|
264
|
-
|
|
271
|
+
G(e instanceof Error ? e.message : S("auth.login.microsoftSignInFailed", "Failed to initiate Microsoft sign in"));
|
|
265
272
|
}
|
|
266
273
|
}, Ge = async () => {
|
|
267
|
-
Fe(!0),
|
|
274
|
+
Fe(!0), W(!1);
|
|
268
275
|
try {
|
|
269
|
-
(await
|
|
276
|
+
(await we((K.email ?? "").trim())).success ? W(!0) : Y(S("auth.login.resendFailed", "Failed to resend verification email. Please try again."));
|
|
270
277
|
} catch (e) {
|
|
271
|
-
console.error("Failed to resend verification email:", e),
|
|
278
|
+
console.error("Failed to resend verification email:", e), Y(e instanceof Error ? e.message : S("auth.login.resendFailed", "Failed to resend verification email. Please try again."));
|
|
272
279
|
} finally {
|
|
273
280
|
Fe(!1);
|
|
274
281
|
}
|
|
275
|
-
}, Ke =
|
|
282
|
+
}, Ke = k("google"), qe = k("microsoft"), Je = [{
|
|
276
283
|
name: "Google",
|
|
277
|
-
icon:
|
|
284
|
+
icon: c,
|
|
278
285
|
onClick: Ue,
|
|
279
286
|
disabled: !Ke,
|
|
280
287
|
tooltip: Ke ? void 0 : S("auth.login.googleNotConfigured", "Google sign in is not configured")
|
|
281
288
|
}, {
|
|
282
289
|
name: "Microsoft",
|
|
283
|
-
icon:
|
|
290
|
+
icon: ne,
|
|
284
291
|
onClick: We,
|
|
285
|
-
disabled:
|
|
286
|
-
tooltip:
|
|
292
|
+
disabled: !qe,
|
|
293
|
+
tooltip: qe ? void 0 : S("auth.login.microsoftNotConfigured", "Microsoft sign in is not configured")
|
|
287
294
|
}];
|
|
288
|
-
return ke ? /* @__PURE__ */ h(
|
|
295
|
+
return ke ? /* @__PURE__ */ h(oe, {}) : /* @__PURE__ */ g(a, {
|
|
289
296
|
title: S("auth.login.welcomeBack", "Welcome back"),
|
|
290
297
|
subtitle: S("auth.login.enterCredentials", "Enter your credentials to continue"),
|
|
291
298
|
children: [
|
|
@@ -311,11 +318,11 @@ function b() {
|
|
|
311
318
|
children: Re
|
|
312
319
|
})]
|
|
313
320
|
}),
|
|
314
|
-
|
|
321
|
+
J && /* @__PURE__ */ g("div", {
|
|
315
322
|
className: "flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border",
|
|
316
323
|
children: [/* @__PURE__ */ h(o, { className: "size-5 flex-shrink-0 mt-0.5 text-status-warning-text" }), /* @__PURE__ */ h("p", {
|
|
317
324
|
className: "text-sm text-status-warning-text",
|
|
318
|
-
children:
|
|
325
|
+
children: J
|
|
319
326
|
})]
|
|
320
327
|
}),
|
|
321
328
|
Be && /* @__PURE__ */ g("div", {
|
|
@@ -331,9 +338,9 @@ function b() {
|
|
|
331
338
|
className: "mt-1 text-sm text-status-warning-text/80",
|
|
332
339
|
children: S("auth.login.duplicateAccountHint", "Please use a different account, or go back and switch to this account from the profile menu.")
|
|
333
340
|
}),
|
|
334
|
-
(
|
|
341
|
+
($.current || Q) && /* @__PURE__ */ g("button", {
|
|
335
342
|
type: "button",
|
|
336
|
-
onClick: () => y(decodeURIComponent(
|
|
343
|
+
onClick: () => y(decodeURIComponent($.current || Q || "/dashboard")),
|
|
337
344
|
className: "mt-3 inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-status-warning-border bg-bg-surface text-status-warning-text text-sm font-medium hover:bg-status-warning-bg-subtle hover:border-status-warning-border transition-all shadow-sm",
|
|
338
345
|
children: [/* @__PURE__ */ h("svg", {
|
|
339
346
|
className: "size-4",
|
|
@@ -351,22 +358,22 @@ function b() {
|
|
|
351
358
|
]
|
|
352
359
|
})]
|
|
353
360
|
}),
|
|
354
|
-
w && !
|
|
355
|
-
className: `flex items-start gap-3 p-4 rounded-xl border ${
|
|
356
|
-
children: [/* @__PURE__ */ h(o, { className: `size-5 flex-shrink-0 mt-0.5 ${
|
|
361
|
+
w && !J && !Be && /* @__PURE__ */ g("div", {
|
|
362
|
+
className: `flex items-start gap-3 p-4 rounded-xl border ${I || B || H ? "bg-status-warning-bg-subtle border-status-warning-border" : "bg-status-error-bg-subtle border-status-error-border"}`,
|
|
363
|
+
children: [/* @__PURE__ */ h(o, { className: `size-5 flex-shrink-0 mt-0.5 ${I || B || H ? "text-status-warning-text" : "text-status-error-text"}` }), /* @__PURE__ */ g("div", {
|
|
357
364
|
className: "flex-1",
|
|
358
365
|
children: [
|
|
359
366
|
/* @__PURE__ */ h("p", {
|
|
360
|
-
className: `text-sm ${
|
|
361
|
-
children: Me ? S("auth.login.accountDeleted", "This account has been permanently deleted and cannot be recovered.") : Ne ? S("auth.login.accountSuspended", "Your account has been suspended. Please contact support for assistance.") :
|
|
367
|
+
className: `text-sm ${I || B || H ? "text-status-warning-text" : "text-status-error-text"}`,
|
|
368
|
+
children: Me ? S("auth.login.accountDeleted", "This account has been permanently deleted and cannot be recovered.") : Ne ? S("auth.login.accountSuspended", "Your account has been suspended. Please contact support for assistance.") : I ? S("auth.login.accountDeactivated", "Your account has been deactivated.") : B ? S("auth.login.oauthOnly", "This account uses OAuth sign-in. Please use Google or Microsoft to sign in.") : H ? S("auth.login.emailNotVerified", "Please verify your email before signing in. Check your inbox for the verification link.") : w
|
|
362
369
|
}),
|
|
363
|
-
|
|
370
|
+
I && /* @__PURE__ */ h("button", {
|
|
364
371
|
type: "button",
|
|
365
|
-
onClick: () => y("/auth/reactivate", { state: { email:
|
|
372
|
+
onClick: () => y("/auth/reactivate", { state: { email: K.email } }),
|
|
366
373
|
className: "mt-2 text-sm font-semibold text-status-warning-text hover:text-status-warning-text underline underline-offset-2",
|
|
367
374
|
children: S("auth.login.clickReactivate", "Click here to reactivate your account")
|
|
368
375
|
}),
|
|
369
|
-
|
|
376
|
+
H && /* @__PURE__ */ h("div", {
|
|
370
377
|
className: "mt-3",
|
|
371
378
|
children: Ie ? /* @__PURE__ */ g("div", {
|
|
372
379
|
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
@@ -387,7 +394,7 @@ function b() {
|
|
|
387
394
|
onClick: Ge,
|
|
388
395
|
disabled: Pe,
|
|
389
396
|
className: "inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-status-warning-border bg-bg-surface text-status-warning-text text-sm font-medium hover:bg-status-warning-bg-subtle hover:border-status-warning-border transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed",
|
|
390
|
-
children: Pe ? /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(
|
|
397
|
+
children: Pe ? /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(u, { className: "size-4 animate-spin" }), S("auth.login.sending", "Sending...")] }) : /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h("svg", {
|
|
391
398
|
className: "size-4",
|
|
392
399
|
fill: "none",
|
|
393
400
|
viewBox: "0 0 24 24",
|
|
@@ -401,18 +408,18 @@ function b() {
|
|
|
401
408
|
}), S("auth.login.resendVerification", "Resend verification email")] })
|
|
402
409
|
})
|
|
403
410
|
}),
|
|
404
|
-
|
|
411
|
+
B && /* @__PURE__ */ g("div", {
|
|
405
412
|
className: "mt-3 flex flex-col gap-2",
|
|
406
|
-
children: [
|
|
413
|
+
children: [k("google") && /* @__PURE__ */ g("button", {
|
|
407
414
|
type: "button",
|
|
408
415
|
onClick: Ue,
|
|
409
416
|
className: "inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-border-default bg-bg-surface text-text-primary text-sm font-medium hover:bg-bg-canvas hover:border-border-default transition-all shadow-sm",
|
|
410
|
-
children: [/* @__PURE__ */ h(
|
|
411
|
-
}),
|
|
417
|
+
children: [/* @__PURE__ */ h(c, { className: "size-5" }), S("auth.login.signInWithGoogle", "Sign in with Google")]
|
|
418
|
+
}), k("microsoft") && /* @__PURE__ */ g("button", {
|
|
412
419
|
type: "button",
|
|
413
420
|
onClick: We,
|
|
414
421
|
className: "inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-border-default bg-bg-surface text-text-primary text-sm font-medium hover:bg-bg-canvas hover:border-border-default transition-all shadow-sm",
|
|
415
|
-
children: [/* @__PURE__ */ h(
|
|
422
|
+
children: [/* @__PURE__ */ h(ne, { className: "size-5" }), S("auth.login.signInWithMicrosoft", "Sign in with Microsoft")]
|
|
416
423
|
})]
|
|
417
424
|
})
|
|
418
425
|
]
|
|
@@ -436,9 +443,9 @@ function b() {
|
|
|
436
443
|
required: !0,
|
|
437
444
|
"aria-required": "true",
|
|
438
445
|
"aria-label": S("auth.login.emailLabel", "Email"),
|
|
439
|
-
value:
|
|
440
|
-
onChange: (e) =>
|
|
441
|
-
...
|
|
446
|
+
value: K.email,
|
|
447
|
+
onChange: (e) => q({
|
|
448
|
+
...K,
|
|
442
449
|
email: e.target.value
|
|
443
450
|
}),
|
|
444
451
|
className: "block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all",
|
|
@@ -457,18 +464,18 @@ function b() {
|
|
|
457
464
|
children: [
|
|
458
465
|
/* @__PURE__ */ h("div", {
|
|
459
466
|
className: "absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none",
|
|
460
|
-
children: /* @__PURE__ */ h(
|
|
467
|
+
children: /* @__PURE__ */ h(l, { className: "size-5 text-[var(--color-text-tertiary)]" })
|
|
461
468
|
}),
|
|
462
469
|
/* @__PURE__ */ h("input", {
|
|
463
470
|
id: "password",
|
|
464
|
-
type:
|
|
471
|
+
type: F ? "text" : "password",
|
|
465
472
|
autoComplete: "current-password",
|
|
466
473
|
required: !0,
|
|
467
474
|
"aria-required": "true",
|
|
468
475
|
"aria-label": S("auth.login.passwordLabel", "Password"),
|
|
469
|
-
value:
|
|
470
|
-
onChange: (e) =>
|
|
471
|
-
...
|
|
476
|
+
value: K.password,
|
|
477
|
+
onChange: (e) => q({
|
|
478
|
+
...K,
|
|
472
479
|
password: e.target.value
|
|
473
480
|
}),
|
|
474
481
|
className: "block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all",
|
|
@@ -476,10 +483,10 @@ function b() {
|
|
|
476
483
|
}),
|
|
477
484
|
/* @__PURE__ */ h("button", {
|
|
478
485
|
type: "button",
|
|
479
|
-
onClick: () => je(!
|
|
480
|
-
"aria-label":
|
|
486
|
+
onClick: () => je(!F),
|
|
487
|
+
"aria-label": F ? "Hide password" : "Show password",
|
|
481
488
|
className: "absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors",
|
|
482
|
-
children: h(
|
|
489
|
+
children: h(F ? te : ee, { className: "size-5" })
|
|
483
490
|
})
|
|
484
491
|
]
|
|
485
492
|
})]
|
|
@@ -493,11 +500,11 @@ function b() {
|
|
|
493
500
|
children: [/* @__PURE__ */ h("input", {
|
|
494
501
|
type: "checkbox",
|
|
495
502
|
"aria-label": S("auth.login.rememberMe", "Remember me"),
|
|
496
|
-
checked:
|
|
503
|
+
checked: K.rememberMe,
|
|
497
504
|
onChange: (e) => {
|
|
498
505
|
let t = e.target.checked;
|
|
499
|
-
if (
|
|
500
|
-
...
|
|
506
|
+
if (q({
|
|
507
|
+
...K,
|
|
501
508
|
rememberMe: t
|
|
502
509
|
}), !t) try {
|
|
503
510
|
localStorage.removeItem("remembered_email");
|
|
@@ -534,9 +541,9 @@ function b() {
|
|
|
534
541
|
}),
|
|
535
542
|
/* @__PURE__ */ h("button", {
|
|
536
543
|
type: "submit",
|
|
537
|
-
disabled: C ||
|
|
544
|
+
disabled: C || j,
|
|
538
545
|
className: "auth-primary-button flex w-full items-center justify-center gap-2 px-4 py-3 font-semibold",
|
|
539
|
-
children: C ||
|
|
546
|
+
children: C || j ? /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(u, { className: "size-5 animate-spin" }), /* @__PURE__ */ h("span", { children: j ? S("auth.login.settingUpWorkspace", "Setting up workspace...") : S("auth.login.signingIn", "Signing in...") })] }) : S("auth.login.signIn", "Sign in")
|
|
540
547
|
})
|
|
541
548
|
]
|
|
542
549
|
}),
|
|
@@ -562,7 +569,7 @@ function b() {
|
|
|
562
569
|
}),
|
|
563
570
|
/* @__PURE__ */ h("div", {
|
|
564
571
|
className: "grid grid-cols-3 gap-3",
|
|
565
|
-
children:
|
|
572
|
+
children: Je.map((e) => /* @__PURE__ */ g("button", {
|
|
566
573
|
type: "button",
|
|
567
574
|
onClick: e.onClick,
|
|
568
575
|
disabled: e.disabled,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoginPage.js","names":[],"sources":["../../../src/auth/pages/LoginPage.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useNavigate, useSearchParams, useLocation } from \"react-router-dom\";\nimport {\n useI18n,\n useActiveContext,\n} from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n getSSORedirectUrl,\n SSORedirectSpinner,\n} from \"@burdenoff/fe-libs/shared/components\";\nimport { resolveAuthBridgeConfig } from \"@burdenoff/fe-libs/shared/config\";\n\nimport {\n getProfileTokens,\n getProfileUser,\n getProfiles,\n getProfileWorkspaceToken,\n resolveActiveProfileId,\n} from \"@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useMyWorkspaces } from \"../hooks/useMyWorkspaces\";\nimport { useOAuthOperations } from \"../hooks/useOAuthOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport {\n EmailIcon,\n LockIcon,\n EyeIcon,\n EyeOffIcon,\n AlertIcon,\n SpinnerIcon,\n GoogleIcon,\n // GitHubIcon,\n MicrosoftIcon,\n} from \"../components/AuthIcons\";\nimport { useCaptchaV3 } from \"../hooks/useCaptchaV3\";\nimport type { SignInInput } from \"../types\";\n\n/**\n * Detect whether the bundle is running inside a native shell (Capacitor on\n * iOS/Android, or Electron). Cross-origin SSO deposit/relay detours only make\n * sense in a multi-origin web context — in a native shell there is one product\n * origin, tokens already live in localStorage, and `window.location.assign` to\n * an external https URL escapes the embedded webview.\n *\n * Detection is runtime-only and avoids importing any platform-specific module\n * so this stays valid when microfe-auth is consumed by a pure web shell too.\n */\nfunction isNativeShell(): boolean {\n if (typeof window === \"undefined\") return false;\n const w = window as typeof window & {\n Capacitor?: { isNativePlatform?: () => boolean };\n };\n if (typeof w.Capacitor?.isNativePlatform === \"function\") {\n try {\n if (w.Capacitor.isNativePlatform()) return true;\n } catch {\n /* fall through */\n }\n }\n if (\n typeof navigator !== \"undefined\" &&\n /Electron/i.test(navigator.userAgent || \"\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction toSameOriginPath(target: string): string {\n if (typeof window === \"undefined\") return target;\n try {\n const url = new URL(target, window.location.origin);\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return target;\n }\n}\n\nfunction hasValidAccessToken(token: string): boolean {\n try {\n const raw = token.split(\".\")[1].replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = raw.padEnd(Math.ceil(raw.length / 4) * 4, \"=\");\n const payload = JSON.parse(atob(padded)) as { exp?: unknown };\n return typeof payload.exp === \"number\" && payload.exp > Date.now() / 1000;\n } catch {\n return false;\n }\n}\n\nfunction decodeSameOriginReturnTo(returnTo: string | null): string | null {\n if (!returnTo) return null;\n try {\n const decoded = decodeURIComponent(returnTo);\n const url = new URL(decoded, window.location.origin);\n if (url.origin !== window.location.origin) return null;\n return stripStaleSsoParams(url, { includeOrigin: false });\n } catch {\n return null;\n }\n}\n\nfunction stripStaleSsoParams(\n url: URL,\n options: { includeOrigin: boolean },\n): string {\n for (const param of [\"sso_failed\", \"sso-failed\", \"sso-attempted\"] as const) {\n url.searchParams.delete(param);\n }\n\n if (options.includeOrigin) {\n return url.toString();\n }\n\n const search = url.searchParams.toString();\n return `${url.pathname}${search ? `?${search}` : \"\"}${url.hash}`;\n}\n\nfunction cleanReturnToValue(returnTo: string): string {\n try {\n const decoded = decodeURIComponent(returnTo);\n const url = new URL(decoded, window.location.origin);\n // Preserve cross-origin absolute returnTo values (e.g., redirects back to\n // another product's app shell) — only strip stale SSO params.\n const isCrossOrigin = url.origin !== window.location.origin;\n return stripStaleSsoParams(url, { includeOrigin: isCrossOrigin });\n } catch {\n return returnTo;\n }\n}\n\n/**\n * Login page with professional design\n * Provides email/password login and social OAuth options\n */\nexport function LoginPage() {\n const navigate = useNavigate();\n const location = useLocation();\n const [searchParams] = useSearchParams();\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n },\n [t],\n );\n const { signIn, isLoading, error, errorCode, resendVerification } =\n useAuthOperations();\n const { completeAuthFlow } = useMyWorkspaces();\n const { setWorkspace, setOrganization } = useActiveContext();\n const { defaultPostLoginPath } = useAuth();\n const { initiateGoogleOAuth, initiateMicrosoftOAuth, isProviderConfigured } =\n useOAuthOperations();\n const mfaRequired = useAuthStore((state) => state.mfaRequired);\n const { executeRecaptcha: executeCaptchaV3 } = useCaptchaV3();\n const [isCompletingAuth, setIsCompletingAuth] = useState(false);\n const [isSsoRedirecting, setIsSsoRedirecting] = useState(false);\n\n /**\n * Build the URL that should receive the post-login redirect.\n *\n * For cross-eTLD+1 SSO on alpha/prod we detour through the central bridge\n * host (alphaapp/app.burdenoff.com) so the freshly-issued tokens land in\n * the bridge origin's first-party (unpartitioned) localStorage. Without\n * this step, a later direct visit to the bridge host has no local tokens\n * and no shared cookie (cross-eTLD+1 cookies don't propagate), so the user\n * sees the login page again.\n *\n * Skipped when:\n * - Already on the bridge host origin (would be a no-op self-redirect).\n * - The user's intended destination is itself on the bridge host\n * (likely an in-product navigation, no deposit needed).\n * - The freshly-stored profile snapshot can't be read (best-effort only —\n * login still succeeds via the direct redirect).\n */\n const buildPostLoginUrl = useCallback((intendedPath: string): string => {\n if (typeof window === \"undefined\") return intendedPath;\n // Native shells (Electron, Capacitor iOS/Android) run as a single origin —\n // skip the cross-eTLD+1 deposit detour, which would otherwise navigate to\n // an external https URL and escape the embedded webview.\n if (isNativeShell()) return intendedPath;\n try {\n const bridge = resolveAuthBridgeConfig();\n const bridgeUrl = new URL(bridge.url);\n const currentOrigin = window.location.origin;\n if (bridgeUrl.origin === currentOrigin) return intendedPath;\n\n // intendedPath can be a path or an absolute URL; resolve to absolute\n // against current origin so the bridge host knows where to send the\n // user back.\n const absoluteIntended = new URL(intendedPath, currentOrigin).href;\n\n const profileId = resolveActiveProfileId();\n if (!profileId) return intendedPath;\n const tokens = getProfileTokens(profileId);\n const user = getProfileUser(profileId);\n if (!tokens || !user) return intendedPath;\n const workspaceToken = getProfileWorkspaceToken(profileId);\n\n const payload = {\n profileId,\n tokens: JSON.stringify({ ...tokens, workspaceToken }),\n user: JSON.stringify(user),\n profiles: JSON.stringify(getProfiles()),\n };\n const encoded = btoa(JSON.stringify(payload));\n const deposit = new URL(\"/auth/sso-deposit\", bridgeUrl.origin);\n deposit.searchParams.set(\"returnTo\", absoluteIntended);\n return `${deposit.href}#sso=${encoded}`;\n } catch {\n return intendedPath;\n }\n }, []);\n\n const redirectAfterLogin = useCallback(\n (path: string) => {\n if (typeof window !== \"undefined\") {\n try {\n sessionStorage.setItem(\"bf-post-login-return-to\", path);\n } catch {\n // Ignore storage failures; redirect still proceeds.\n }\n // In native shells use SPA navigation so the embedded webview stays in\n // the app — no full reload, no chance of the OS opening the URL\n // externally. Tokens are already in this origin's localStorage.\n if (isNativeShell()) {\n navigate(toSameOriginPath(path));\n return;\n }\n window.location.assign(buildPostLoginUrl(path));\n return;\n }\n navigate(path);\n },\n [navigate, buildPostLoginUrl],\n );\n const [showPassword, setShowPassword] = useState(false);\n const [isAccountInactive, setIsAccountInactive] = useState(false);\n const [isAccountDeleted, setIsAccountDeleted] = useState(false);\n const [isAccountSuspended, setIsAccountSuspended] = useState(false);\n const [isOAuthOnlyAccount, setIsOAuthOnlyAccount] = useState(false);\n const [isEmailNotVerified, setIsEmailNotVerified] = useState(false);\n const [isResendingVerification, setIsResendingVerification] = useState(false);\n const [resendSuccess, setResendSuccess] = useState(false);\n const [oauthError, setOauthError] = useState<string | null>(null);\n const [infoMessage, setInfoMessage] = useState<string | null>(null);\n const [formData, setFormData] = useState<\n SignInInput & { rememberMe?: boolean }\n >({\n email: \"\",\n password: \"\",\n rememberMe: false,\n });\n const [validationError, setValidationError] = useState<string | null>(null);\n const [isDuplicateAccount, setIsDuplicateAccount] = useState(false);\n\n // Detect add-account mode and returnTo from URL params\n const isAddAccountMode = searchParams.get(\"mode\") === \"add-account\";\n const returnTo = searchParams.get(\"returnTo\");\n const initialReturnToRef = useRef<string | null>(returnTo);\n\n useEffect(() => {\n if (!initialReturnToRef.current && returnTo) {\n initialReturnToRef.current = returnTo;\n }\n }, [returnTo]);\n\n useEffect(() => {\n if (isAddAccountMode || isCompletingAuth) return;\n\n const profileId = resolveActiveProfileId();\n if (profileId) {\n const tokens = getProfileTokens(profileId);\n const user = getProfileUser(profileId);\n if (\n tokens?.accessToken &&\n user &&\n hasValidAccessToken(tokens.accessToken)\n ) {\n useAuthStore.getState().loadFromProfile(profileId);\n redirectAfterLogin(decodeSameOriginReturnTo(returnTo) ?? \"/\");\n return;\n }\n }\n\n const ssoUrl = getSSORedirectUrl();\n if (ssoUrl) {\n setIsSsoRedirecting(true);\n window.location.replace(ssoUrl);\n }\n }, [isAddAccountMode, isCompletingAuth, redirectAfterLogin, returnTo]);\n\n // Load remembered email on mount and clean up any legacy plaintext password storage\n useEffect(() => {\n try {\n // Security cleanup: remove any previously stored plaintext passwords\n localStorage.removeItem(\"remembered_password\");\n\n const rememberedEmail = localStorage.getItem(\"remembered_email\");\n\n if (rememberedEmail) {\n setFormData((prev) => ({\n ...prev,\n email: rememberedEmail,\n rememberMe: true,\n }));\n }\n } catch (error) {\n console.warn(\"[LoginPage] Failed to load remembered email:\", error);\n }\n }, []);\n\n // Check for redirect message and email from location state (e.g., from expired MFA or SSO redirect)\n useEffect(() => {\n const state = location.state as { message?: string; email?: string } | null;\n if (state?.message) {\n setInfoMessage(state.message);\n }\n if (state?.email) {\n setFormData((prev) => ({ ...prev, email: state.email }));\n }\n // Clear state to prevent showing message on refresh\n if (state) {\n window.history.replaceState({}, document.title);\n }\n }, [location.state]);\n\n // Determine error type from GraphQL error code (primary) or message (fallback).\n // The backend sends structured codes in extensions.code which are propagated\n // through useAuthOperations → authStore.errorCode.\n const checkErrorType = (\n err: string | null,\n code?: string | null,\n ):\n | \"DELETED\"\n | \"SUSPENDED\"\n | \"OAUTH_ONLY\"\n | \"INACTIVE\"\n | \"EMAIL_NOT_VERIFIED\"\n | null => {\n // Primary: match on GraphQL error code from extensions\n if (code) {\n switch (code) {\n case \"ACCOUNT_DELETED\":\n return \"DELETED\";\n case \"ACCOUNT_SUSPENDED\":\n return \"SUSPENDED\";\n case \"OAUTH_ONLY_ACCOUNT\":\n return \"OAUTH_ONLY\";\n case \"ACCOUNT_INACTIVE\":\n return \"INACTIVE\";\n case \"EMAIL_NOT_VERIFIED\":\n return \"EMAIL_NOT_VERIFIED\";\n default:\n break;\n }\n }\n\n // Fallback: match on error message for non-GraphQL errors or legacy responses\n if (!err) return null;\n const lowerErr = err.toLowerCase();\n\n if (\n lowerErr.includes(\"deleted\") ||\n lowerErr.includes(\"permanently deleted\")\n ) {\n return \"DELETED\";\n }\n if (lowerErr.includes(\"suspended\")) {\n return \"SUSPENDED\";\n }\n if (\n lowerErr.includes(\"oauth provider\") ||\n lowerErr.includes(\"oauth-only\") ||\n lowerErr.includes(\"oauth only\")\n ) {\n return \"OAUTH_ONLY\";\n }\n if (\n lowerErr.includes(\"inactive\") ||\n lowerErr.includes(\"deactivated\") ||\n lowerErr.includes(\"not active\")\n ) {\n return \"INACTIVE\";\n }\n if (\n lowerErr.includes(\"verify your email\") ||\n lowerErr.includes(\"email not verified\") ||\n lowerErr.includes(\"email_not_verified\")\n ) {\n return \"EMAIL_NOT_VERIFIED\";\n }\n return null;\n };\n\n // Pre-fill email from URL query parameter (e.g., after email verification)\n useEffect(() => {\n const emailFromUrl = searchParams.get(\"email\");\n if (emailFromUrl) {\n setFormData((prev) => ({ ...prev, email: emailFromUrl }));\n }\n\n // Check for expired session redirect\n const isExpired = searchParams.get(\"expired\");\n if (isExpired === \"true\") {\n setInfoMessage(\n tr(\n \"auth.login.sessionExpired\",\n \"Your session has expired. Please sign in again.\",\n ),\n );\n // Clear the query param to prevent showing on refresh\n const newUrl = window.location.pathname;\n window.history.replaceState({}, document.title, newUrl);\n }\n }, [searchParams, tr]);\n\n // Redirect to MFA verification when required\n useEffect(() => {\n if (mfaRequired) {\n navigate(\"/auth/mfa-verify\");\n }\n }, [mfaRequired, navigate]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsAccountInactive(false);\n setIsAccountDeleted(false);\n setIsAccountSuspended(false);\n setIsOAuthOnlyAccount(false);\n setIsEmailNotVerified(false);\n setIsDuplicateAccount(false);\n setResendSuccess(false);\n setValidationError(null);\n\n // Client-side validation\n const emailTrimmed = (formData.email ?? \"\").trim();\n if (!emailTrimmed) {\n setValidationError(\n tr(\n \"auth.login.enterEmailOrUsername\",\n \"Please enter your email or username\",\n ),\n );\n return;\n }\n\n // Basic email format validation (if it contains @)\n if (emailTrimmed.includes(\"@\")) {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(emailTrimmed)) {\n setValidationError(\n tr(\"auth.login.invalidEmail\", \"Please enter a valid email address\"),\n );\n return;\n }\n }\n\n if (!formData.password || formData.password.length < 1) {\n setValidationError(\n tr(\"auth.login.enterPassword\", \"Please enter your password\"),\n );\n return;\n }\n\n // In add-account mode, check if this email is already signed in\n if (isAddAccountMode) {\n const existingProfiles = getProfiles();\n const alreadySignedIn = existingProfiles.some(\n (p) => p.email.toLowerCase() === emailTrimmed.toLowerCase(),\n );\n if (alreadySignedIn) {\n setIsDuplicateAccount(true);\n setValidationError(null);\n return;\n }\n }\n setIsDuplicateAccount(false);\n\n try {\n // Get reCAPTCHA v3 token (no-op when captcha is disabled)\n const recaptchaToken = await executeCaptchaV3(\"login\");\n\n const result = await signIn({\n email: emailTrimmed,\n password: formData.password,\n rememberMe: formData.rememberMe,\n ...(recaptchaToken ? { recaptchaToken } : {}),\n });\n\n // If MFA is required, the useEffect above will handle redirect\n if (result.requiresMFA) {\n return;\n }\n\n // Successful login - complete auth flow (fetch workspaces + issue token)\n if (result.success) {\n try {\n const profileId = result.user?.id;\n if (profileId) {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart =\n window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const ssoValue = JSON.stringify({\n id: profileId,\n origin: window.location.origin,\n });\n document.cookie = `bf-sso=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n document.cookie = `bf-sso-profile=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n }\n } catch {\n // Best effort only. Login can continue without the fast-path cookie.\n }\n\n // Handle Remember Me: Save or clear email only (never store passwords)\n try {\n if (formData.rememberMe) {\n localStorage.setItem(\"remembered_email\", emailTrimmed);\n } else {\n localStorage.removeItem(\"remembered_email\");\n }\n } catch (error) {\n console.warn(\n \"[LoginPage] Failed to save/clear remembered email:\",\n error,\n );\n }\n\n setIsCompletingAuth(true);\n // In add-account mode, redirect back to where the user was\n const effectiveReturnTo = initialReturnToRef.current || returnTo;\n const postLoginPath = effectiveReturnTo\n ? cleanReturnToValue(effectiveReturnTo)\n : cleanReturnToValue(defaultPostLoginPath ?? \"/dashboard\");\n try {\n type AuthFlowResult = Awaited<ReturnType<typeof completeAuthFlow>>;\n // Pass the token directly to avoid race condition with React state updates\n const authFlowResult = await Promise.race<AuthFlowResult>([\n completeAuthFlow(result.token),\n new Promise<AuthFlowResult>((resolve) => {\n window.setTimeout(() => {\n resolve({\n success: false,\n error: \"Auth flow timed out after login\",\n });\n }, 5000);\n }),\n ]);\n\n if (authFlowResult.success) {\n // Sync workspace and organization to ActiveContext\n // This is critical for global search and other workspace-scoped features\n if (authFlowResult.workspace) {\n setWorkspace(authFlowResult.workspace.id);\n if (authFlowResult.workspace.organizationId) {\n setOrganization(authFlowResult.workspace.organizationId);\n }\n }\n\n // Full auth flow completed - redirect to dashboard or returnTo\n redirectAfterLogin(postLoginPath);\n } else if (authFlowResult.error?.includes(\"No workspaces found\")) {\n // No workspaces - redirect to onboarding/workspace creation\n navigate(\"/onboarding/create-workspace\");\n } else {\n // Partial success or error - still sync workspace if available and redirect\n if (authFlowResult.workspace) {\n setWorkspace(authFlowResult.workspace.id);\n if (authFlowResult.workspace.organizationId) {\n setOrganization(authFlowResult.workspace.organizationId);\n }\n }\n console.warn(\"Auth flow warning:\", authFlowResult.error);\n redirectAfterLogin(postLoginPath);\n }\n } catch (authFlowError) {\n // Auth flow failed but login succeeded - still redirect\n console.error(\"Auth flow error (login succeeded):\", authFlowError);\n redirectAfterLogin(postLoginPath);\n } finally {\n setIsCompletingAuth(false);\n }\n }\n } catch (err) {\n if (import.meta.env.DEV) {\n console.error(\"Sign in failed:\", err);\n }\n // Check error type using error code (from store) and message\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (errorMsg.includes(\"reCAPTCHA\")) {\n setValidationError(errorMsg);\n }\n const storeErrorCode = useAuthStore.getState().errorCode;\n const errorType = checkErrorType(errorMsg, storeErrorCode);\n if (errorType === \"DELETED\") {\n setIsAccountDeleted(true);\n } else if (errorType === \"SUSPENDED\") {\n setIsAccountSuspended(true);\n } else if (errorType === \"OAUTH_ONLY\") {\n setIsOAuthOnlyAccount(true);\n } else if (errorType === \"INACTIVE\") {\n setIsAccountInactive(true);\n } else if (errorType === \"EMAIL_NOT_VERIFIED\") {\n setIsEmailNotVerified(true);\n }\n }\n };\n\n // Also check the error from useAuthOperations\n useEffect(() => {\n if (error) {\n const errorType = checkErrorType(error, errorCode);\n if (errorType === \"DELETED\") {\n setIsAccountDeleted(true);\n } else if (errorType === \"SUSPENDED\") {\n setIsAccountSuspended(true);\n } else if (errorType === \"OAUTH_ONLY\") {\n setIsOAuthOnlyAccount(true);\n } else if (errorType === \"INACTIVE\") {\n setIsAccountInactive(true);\n } else if (errorType === \"EMAIL_NOT_VERIFIED\") {\n setIsEmailNotVerified(true);\n }\n }\n }, [error, errorCode]);\n\n /**\n * Handle Google OAuth login\n */\n const handleGoogleLogin = () => {\n setOauthError(null);\n try {\n initiateGoogleOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.login.googleSignInFailed\",\n \"Failed to initiate Google sign in\",\n ),\n );\n }\n };\n\n /**\n * Handle Microsoft OAuth login\n */\n const handleMicrosoftLogin = () => {\n setOauthError(null);\n try {\n initiateMicrosoftOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.login.microsoftSignInFailed\",\n \"Failed to initiate Microsoft sign in\",\n ),\n );\n }\n };\n\n /**\n * Handle resend verification email\n */\n const handleResendVerification = async () => {\n setIsResendingVerification(true);\n setResendSuccess(false);\n try {\n const emailTrimmed = (formData.email ?? \"\").trim();\n const result = await resendVerification(emailTrimmed);\n if (result.success) {\n setResendSuccess(true);\n } else {\n setValidationError(\n tr(\n \"auth.login.resendFailed\",\n \"Failed to resend verification email. Please try again.\",\n ),\n );\n }\n } catch (err) {\n console.error(\"Failed to resend verification email:\", err);\n setValidationError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.login.resendFailed\",\n \"Failed to resend verification email. Please try again.\",\n ),\n );\n } finally {\n setIsResendingVerification(false);\n }\n };\n\n // Check if OAuth providers are configured\n const isGoogleConfigured = isProviderConfigured(\"google\");\n const isMicrosoftConfigured = isProviderConfigured(\"microsoft\");\n\n const oauthProviders = [\n {\n name: \"Google\",\n icon: GoogleIcon,\n onClick: handleGoogleLogin,\n disabled: !isGoogleConfigured,\n tooltip: isGoogleConfigured\n ? undefined\n : tr(\n \"auth.login.googleNotConfigured\",\n \"Google sign in is not configured\",\n ),\n },\n // {\n // name: \"GitHub\",\n // icon: GitHubIcon,\n // onClick: () => {},\n // disabled: true,\n // tooltip: tr(\n // \"auth.login.githubNotConfigured\",\n // \"GitHub sign in is not configured\",\n // ),\n // },\n {\n name: \"Microsoft\",\n icon: MicrosoftIcon,\n onClick: handleMicrosoftLogin,\n disabled: !isMicrosoftConfigured,\n tooltip: isMicrosoftConfigured\n ? undefined\n : tr(\n \"auth.login.microsoftNotConfigured\",\n \"Microsoft sign in is not configured\",\n ),\n },\n ];\n\n if (isSsoRedirecting) {\n return <SSORedirectSpinner />;\n }\n\n return (\n <AuthLayout\n title={tr(\"auth.login.welcomeBack\", \"Welcome back\")}\n subtitle={tr(\n \"auth.login.enterCredentials\",\n \"Enter your credentials to continue\",\n )}\n >\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Info Message (e.g., from expired MFA redirect) */}\n {infoMessage && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-info-bg-subtle border-status-info-border\">\n <svg\n className=\"size-5 flex-shrink-0 mt-0.5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n <p className=\"text-sm text-status-info-text\">{infoMessage}</p>\n </div>\n )}\n\n {/* Validation Error */}\n {validationError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-warning-text\" />\n <p className=\"text-sm text-status-warning-text\">\n {validationError}\n </p>\n </div>\n )}\n\n {/* Duplicate Account Warning (Add Account mode) */}\n {isDuplicateAccount && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-warning-text\" />\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n \"auth.login.duplicateAccount\",\n \"This account is already signed in.\",\n )}\n </p>\n <p className=\"mt-1 text-sm text-status-warning-text/80\">\n {tr(\n \"auth.login.duplicateAccountHint\",\n \"Please use a different account, or go back and switch to this account from the profile menu.\",\n )}\n </p>\n {(initialReturnToRef.current || returnTo) && (\n <button\n type=\"button\"\n onClick={() =>\n navigate(\n decodeURIComponent(\n initialReturnToRef.current || returnTo || \"/dashboard\",\n ),\n )\n }\n className=\"mt-3 inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-status-warning-border bg-bg-surface text-status-warning-text text-sm font-medium hover:bg-status-warning-bg-subtle hover:border-status-warning-border transition-all shadow-sm\"\n >\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\n \"auth.login.goBackSwitchAccount\",\n \"Go back and switch account\",\n )}\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Error Alert */}\n {error && !validationError && !isDuplicateAccount && (\n <div\n className={`flex items-start gap-3 p-4 rounded-xl border ${\n isAccountInactive || isOAuthOnlyAccount || isEmailNotVerified\n ? \"bg-status-warning-bg-subtle border-status-warning-border\"\n : \"bg-status-error-bg-subtle border-status-error-border\"\n }`}\n >\n <AlertIcon\n className={`size-5 flex-shrink-0 mt-0.5 ${\n isAccountInactive || isOAuthOnlyAccount || isEmailNotVerified\n ? \"text-status-warning-text\"\n : \"text-status-error-text\"\n }`}\n />\n <div className=\"flex-1\">\n <p\n className={`text-sm ${\n isAccountInactive || isOAuthOnlyAccount || isEmailNotVerified\n ? \"text-status-warning-text\"\n : \"text-status-error-text\"\n }`}\n >\n {isAccountDeleted\n ? tr(\n \"auth.login.accountDeleted\",\n \"This account has been permanently deleted and cannot be recovered.\",\n )\n : isAccountSuspended\n ? tr(\n \"auth.login.accountSuspended\",\n \"Your account has been suspended. Please contact support for assistance.\",\n )\n : isAccountInactive\n ? tr(\n \"auth.login.accountDeactivated\",\n \"Your account has been deactivated.\",\n )\n : isOAuthOnlyAccount\n ? tr(\n \"auth.login.oauthOnly\",\n \"This account uses OAuth sign-in. Please use Google or Microsoft to sign in.\",\n )\n : isEmailNotVerified\n ? tr(\n \"auth.login.emailNotVerified\",\n \"Please verify your email before signing in. Check your inbox for the verification link.\",\n )\n : error}\n </p>\n {isAccountInactive && (\n <button\n type=\"button\"\n onClick={() =>\n navigate(\"/auth/reactivate\", {\n state: { email: formData.email },\n })\n }\n className=\"mt-2 text-sm font-semibold text-status-warning-text hover:text-status-warning-text underline underline-offset-2\"\n >\n {tr(\n \"auth.login.clickReactivate\",\n \"Click here to reactivate your account\",\n )}\n </button>\n )}\n {isEmailNotVerified && (\n <div className=\"mt-3\">\n {resendSuccess ? (\n <div className=\"flex items-center gap-2 text-sm text-status-success-text\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n {tr(\n \"auth.login.verificationSent\",\n \"Verification email sent! Please check your inbox.\",\n )}\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={handleResendVerification}\n disabled={isResendingVerification}\n className=\"inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-status-warning-border bg-bg-surface text-status-warning-text text-sm font-medium hover:bg-status-warning-bg-subtle hover:border-status-warning-border transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isResendingVerification ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" />\n {tr(\"auth.login.sending\", \"Sending...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n {tr(\n \"auth.login.resendVerification\",\n \"Resend verification email\",\n )}\n </>\n )}\n </button>\n )}\n </div>\n )}\n {isOAuthOnlyAccount && (\n <div className=\"mt-3 flex flex-col gap-2\">\n {isProviderConfigured(\"google\") && (\n <button\n type=\"button\"\n onClick={handleGoogleLogin}\n className=\"inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-border-default bg-bg-surface text-text-primary text-sm font-medium hover:bg-bg-canvas hover:border-border-default transition-all shadow-sm\"\n >\n <GoogleIcon className=\"size-5\" />\n {tr(\"auth.login.signInWithGoogle\", \"Sign in with Google\")}\n </button>\n )}\n {isProviderConfigured(\"microsoft\") && (\n <button\n type=\"button\"\n onClick={handleMicrosoftLogin}\n className=\"inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-border-default bg-bg-surface text-text-primary text-sm font-medium hover:bg-bg-canvas hover:border-border-default transition-all shadow-sm\"\n >\n <MicrosoftIcon className=\"size-5\" />\n {tr(\n \"auth.login.signInWithMicrosoft\",\n \"Sign in with Microsoft\",\n )}\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Email Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.login.emailLabel\", \"Email\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <EmailIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.login.emailLabel\", \"Email\")}\n value={formData.email}\n onChange={(e) =>\n setFormData({ ...formData, email: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\"auth.login.emailPlaceholder\", \"you@example.com\")}\n />\n </div>\n </div>\n\n {/* Password Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.login.passwordLabel\", \"Password\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <LockIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n autoComplete=\"current-password\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.login.passwordLabel\", \"Password\")}\n value={formData.password}\n onChange={(e) =>\n setFormData({ ...formData, password: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.login.passwordPlaceholder\",\n \"Enter your password\",\n )}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n aria-label={showPassword ? \"Hide password\" : \"Show password\"}\n className=\"absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors\"\n >\n {showPassword ? (\n <EyeOffIcon className=\"size-5\" />\n ) : (\n <EyeIcon className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n\n {/* Remember & Forgot */}\n <div className=\"flex items-center justify-between\">\n <label className=\"flex items-center gap-2 cursor-pointer group\">\n <div className=\"relative\">\n <input\n type=\"checkbox\"\n aria-label={tr(\"auth.login.rememberMe\", \"Remember me\")}\n checked={formData.rememberMe}\n onChange={(e) => {\n const isChecked = e.target.checked;\n setFormData({ ...formData, rememberMe: isChecked });\n\n // Clear saved email immediately when unchecked\n if (!isChecked) {\n try {\n localStorage.removeItem(\"remembered_email\");\n } catch (error) {\n console.warn(\n \"[LoginPage] Failed to clear remembered email:\",\n error,\n );\n }\n }\n }}\n className=\"sr-only peer\"\n />\n <div className=\"size-5 rounded-md border-2 border-[var(--color-border-default)] bg-[var(--color-bg-surface)] peer-checked:bg-status-info-bg-subtle0 peer-checked:border-action-primary-bg transition-all peer-focus:ring-2 peer-focus:ring-action-primary-bg/20\">\n <svg\n className=\"size-full text-white opacity-0 peer-checked:opacity-100 transition-opacity p-0.5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n </div>\n <span className=\"text-sm text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)] transition-colors\">\n {tr(\"auth.login.rememberMe\", \"Remember me\")}\n </span>\n </label>\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/reset-password\")}\n className=\"text-sm font-medium text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.login.forgotPassword\", \"Forgot password?\")}\n </button>\n </div>\n\n {/* Submit Button */}\n <button\n type=\"submit\"\n disabled={isLoading || isCompletingAuth}\n className=\"auth-primary-button flex w-full items-center justify-center gap-2 px-4 py-3 font-semibold\"\n >\n {isLoading || isCompletingAuth ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>\n {isCompletingAuth\n ? tr(\n \"auth.login.settingUpWorkspace\",\n \"Setting up workspace...\",\n )\n : tr(\"auth.login.signingIn\", \"Signing in...\")}\n </span>\n </>\n ) : (\n tr(\"auth.login.signIn\", \"Sign in\")\n )}\n </button>\n </form>\n\n {/* Divider */}\n <div className=\"relative my-8\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-[var(--color-border-default)]\" />\n </div>\n <div className=\"relative flex justify-center\">\n <span className=\"px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]\">\n {tr(\"auth.login.orContinueWith\", \"or continue with\")}\n </span>\n </div>\n </div>\n\n {/* OAuth Error */}\n {oauthError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{oauthError}</p>\n </div>\n )}\n\n {/* OAuth Buttons */}\n <div className=\"grid grid-cols-3 gap-3\">\n {oauthProviders.map((provider) => (\n <button\n key={provider.name}\n type=\"button\"\n onClick={provider.onClick}\n disabled={provider.disabled}\n title={provider.tooltip}\n aria-label={provider.name}\n className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${\n provider.disabled\n ? \"border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50\"\n : \"border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]\"\n }`}\n >\n <provider.icon\n className={`size-5 ${provider.disabled ? \"opacity-50\" : \"\"}`}\n />\n <span\n className={`sr-only sm:not-sr-only text-sm font-medium transition-colors hidden sm:inline ${\n provider.disabled\n ? \"text-[var(--color-text-disabled)]\"\n : \"text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]\"\n }`}\n >\n {provider.name}\n </span>\n </button>\n ))}\n </div>\n\n {/* Enterprise SSO Option — temporarily hidden */}\n {/* <div className=\"mt-6\">\n <button type=\"button\"\n onClick={() => navigate(\"/auth/sso/login\")}\n className=\"w-full flex items-center justify-center gap-3 px-4 py-3 rounded-xl border-2 border-dashed border-[var(--color-border-default)] hover:border-action-primary-bg bg-[var(--color-bg-surface)] hover:bg-status-info-bg-subtle transition-all duration-200 group\"\n >\n <svg\n className=\"size-5 text-[var(--color-text-tertiary)] group-hover:text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n <span className=\"text-sm font-medium text-[var(--color-text-secondary)] group-hover:text-text-link\">\n {tr(\"auth.login.enterpriseSso\", \"Sign in with Enterprise SSO\")}\n </span>\n </button>\n </div> */}\n\n {/* Magic Link Option */}\n <div className=\"mt-4 text-center\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/magic-link\")}\n className=\"text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors\"\n >\n {tr(\"auth.login.preferPasswordless\", \"Prefer passwordless login?\")}{\" \"}\n <span className=\"font-semibold text-text-link hover:text-text-linkHover\">\n {tr(\"auth.login.signInMagicLink\", \"Sign in with Magic Link\")}\n </span>\n </button>\n </div>\n\n {/* Sign Up Link */}\n <p className=\"mt-6 text-center text-sm text-[var(--color-text-secondary)]\">\n {tr(\"auth.login.dontHaveAccount\", \"Don't have an account?\")}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/register\")}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.login.signUp\", \"Sign up\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LoginPage.js","names":[],"sources":["../../../src/auth/pages/LoginPage.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useNavigate, useSearchParams, useLocation } from \"react-router-dom\";\nimport {\n useI18n,\n useActiveContext,\n} from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n getSSORedirectUrl,\n SSORedirectSpinner,\n} from \"@burdenoff/fe-libs/shared/components\";\nimport { resolveAuthBridgeConfig } from \"@burdenoff/fe-libs/shared/config\";\n\nimport {\n getProfileTokens,\n getProfileUser,\n getProfiles,\n getProfileWorkspaceToken,\n resolveActiveProfileId,\n} from \"@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useMyWorkspaces } from \"../hooks/useMyWorkspaces\";\nimport { useOAuthOperations } from \"../hooks/useOAuthOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport {\n EmailIcon,\n LockIcon,\n EyeIcon,\n EyeOffIcon,\n AlertIcon,\n SpinnerIcon,\n GoogleIcon,\n // GitHubIcon,\n MicrosoftIcon,\n} from \"../components/AuthIcons\";\nimport { useCaptchaV3 } from \"../hooks/useCaptchaV3\";\nimport { MFA_REDIRECT } from \"../constants/sessionKeys\";\nimport type { SignInInput } from \"../types\";\n\n/**\n * Detect whether the bundle is running inside a native shell (Capacitor on\n * iOS/Android, or Electron). Cross-origin SSO deposit/relay detours only make\n * sense in a multi-origin web context — in a native shell there is one product\n * origin, tokens already live in localStorage, and `window.location.assign` to\n * an external https URL escapes the embedded webview.\n *\n * Detection is runtime-only and avoids importing any platform-specific module\n * so this stays valid when microfe-auth is consumed by a pure web shell too.\n */\nfunction isNativeShell(): boolean {\n if (typeof window === \"undefined\") return false;\n const w = window as typeof window & {\n Capacitor?: { isNativePlatform?: () => boolean };\n };\n if (typeof w.Capacitor?.isNativePlatform === \"function\") {\n try {\n if (w.Capacitor.isNativePlatform()) return true;\n } catch {\n /* fall through */\n }\n }\n if (\n typeof navigator !== \"undefined\" &&\n /Electron/i.test(navigator.userAgent || \"\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction toSameOriginPath(target: string): string {\n if (typeof window === \"undefined\") return target;\n try {\n const url = new URL(target, window.location.origin);\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return target;\n }\n}\n\nfunction hasValidAccessToken(token: string): boolean {\n try {\n const raw = token.split(\".\")[1].replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = raw.padEnd(Math.ceil(raw.length / 4) * 4, \"=\");\n const payload = JSON.parse(atob(padded)) as { exp?: unknown };\n return typeof payload.exp === \"number\" && payload.exp > Date.now() / 1000;\n } catch {\n return false;\n }\n}\n\nfunction decodeSameOriginReturnTo(returnTo: string | null): string | null {\n if (!returnTo) return null;\n try {\n const decoded = decodeURIComponent(returnTo);\n const url = new URL(decoded, window.location.origin);\n if (url.origin !== window.location.origin) return null;\n return stripStaleSsoParams(url, { includeOrigin: false });\n } catch {\n return null;\n }\n}\n\nfunction stripStaleSsoParams(\n url: URL,\n options: { includeOrigin: boolean },\n): string {\n for (const param of [\"sso_failed\", \"sso-failed\", \"sso-attempted\"] as const) {\n url.searchParams.delete(param);\n }\n\n if (options.includeOrigin) {\n return url.toString();\n }\n\n const search = url.searchParams.toString();\n return `${url.pathname}${search ? `?${search}` : \"\"}${url.hash}`;\n}\n\nfunction cleanReturnToValue(returnTo: string): string {\n try {\n const decoded = decodeURIComponent(returnTo);\n const url = new URL(decoded, window.location.origin);\n // Preserve cross-origin absolute returnTo values (e.g., redirects back to\n // another product's app shell) — only strip stale SSO params.\n const isCrossOrigin = url.origin !== window.location.origin;\n return stripStaleSsoParams(url, { includeOrigin: isCrossOrigin });\n } catch {\n return returnTo;\n }\n}\n\n/**\n * Login page with professional design\n * Provides email/password login and social OAuth options\n */\nexport function LoginPage() {\n const navigate = useNavigate();\n const location = useLocation();\n const [searchParams] = useSearchParams();\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n },\n [t],\n );\n const { signIn, isLoading, error, errorCode, resendVerification } =\n useAuthOperations();\n const { completeAuthFlow } = useMyWorkspaces();\n const { setWorkspace, setOrganization } = useActiveContext();\n const { defaultPostLoginPath } = useAuth();\n const { initiateGoogleOAuth, initiateMicrosoftOAuth, isProviderConfigured } =\n useOAuthOperations();\n const mfaRequired = useAuthStore((state) => state.mfaRequired);\n const { executeRecaptcha: executeCaptchaV3 } = useCaptchaV3();\n const [isCompletingAuth, setIsCompletingAuth] = useState(false);\n const [isSsoRedirecting, setIsSsoRedirecting] = useState(false);\n\n /**\n * Build the URL that should receive the post-login redirect.\n *\n * For cross-eTLD+1 SSO on alpha/prod we detour through the central bridge\n * host (alphaapp/app.burdenoff.com) so the freshly-issued tokens land in\n * the bridge origin's first-party (unpartitioned) localStorage. Without\n * this step, a later direct visit to the bridge host has no local tokens\n * and no shared cookie (cross-eTLD+1 cookies don't propagate), so the user\n * sees the login page again.\n *\n * Skipped when:\n * - Already on the bridge host origin (would be a no-op self-redirect).\n * - The user's intended destination is itself on the bridge host\n * (likely an in-product navigation, no deposit needed).\n * - The freshly-stored profile snapshot can't be read (best-effort only —\n * login still succeeds via the direct redirect).\n */\n const buildPostLoginUrl = useCallback((intendedPath: string): string => {\n if (typeof window === \"undefined\") return intendedPath;\n // Native shells (Electron, Capacitor iOS/Android) run as a single origin —\n // skip the cross-eTLD+1 deposit detour, which would otherwise navigate to\n // an external https URL and escape the embedded webview.\n if (isNativeShell()) return intendedPath;\n try {\n const bridge = resolveAuthBridgeConfig();\n const bridgeUrl = new URL(bridge.url);\n const currentOrigin = window.location.origin;\n if (bridgeUrl.origin === currentOrigin) return intendedPath;\n\n // intendedPath can be a path or an absolute URL; resolve to absolute\n // against current origin so the bridge host knows where to send the\n // user back.\n const absoluteIntended = new URL(intendedPath, currentOrigin).href;\n\n const profileId = resolveActiveProfileId();\n if (!profileId) return intendedPath;\n const tokens = getProfileTokens(profileId);\n const user = getProfileUser(profileId);\n if (!tokens || !user) return intendedPath;\n const workspaceToken = getProfileWorkspaceToken(profileId);\n\n const payload = {\n profileId,\n tokens: JSON.stringify({ ...tokens, workspaceToken }),\n user: JSON.stringify(user),\n profiles: JSON.stringify(getProfiles()),\n };\n const encoded = btoa(JSON.stringify(payload));\n const deposit = new URL(\"/auth/sso-deposit\", bridgeUrl.origin);\n deposit.searchParams.set(\"returnTo\", absoluteIntended);\n return `${deposit.href}#sso=${encoded}`;\n } catch {\n return intendedPath;\n }\n }, []);\n\n const redirectAfterLogin = useCallback(\n (path: string) => {\n if (typeof window !== \"undefined\") {\n try {\n sessionStorage.setItem(\"bf-post-login-return-to\", path);\n } catch {\n // Ignore storage failures; redirect still proceeds.\n }\n // In native shells use SPA navigation so the embedded webview stays in\n // the app — no full reload, no chance of the OS opening the URL\n // externally. Tokens are already in this origin's localStorage.\n if (isNativeShell()) {\n navigate(toSameOriginPath(path));\n return;\n }\n window.location.assign(buildPostLoginUrl(path));\n return;\n }\n navigate(path);\n },\n [navigate, buildPostLoginUrl],\n );\n const [showPassword, setShowPassword] = useState(false);\n const [isAccountInactive, setIsAccountInactive] = useState(false);\n const [isAccountDeleted, setIsAccountDeleted] = useState(false);\n const [isAccountSuspended, setIsAccountSuspended] = useState(false);\n const [isOAuthOnlyAccount, setIsOAuthOnlyAccount] = useState(false);\n const [isEmailNotVerified, setIsEmailNotVerified] = useState(false);\n const [isResendingVerification, setIsResendingVerification] = useState(false);\n const [resendSuccess, setResendSuccess] = useState(false);\n const [oauthError, setOauthError] = useState<string | null>(null);\n const [infoMessage, setInfoMessage] = useState<string | null>(null);\n const [formData, setFormData] = useState<\n SignInInput & { rememberMe?: boolean }\n >({\n email: \"\",\n password: \"\",\n rememberMe: false,\n });\n const [validationError, setValidationError] = useState<string | null>(null);\n const [isDuplicateAccount, setIsDuplicateAccount] = useState(false);\n\n // Detect add-account mode and returnTo from URL params\n const isAddAccountMode = searchParams.get(\"mode\") === \"add-account\";\n const returnTo = searchParams.get(\"returnTo\");\n const initialReturnToRef = useRef<string | null>(returnTo);\n\n useEffect(() => {\n if (!initialReturnToRef.current && returnTo) {\n initialReturnToRef.current = returnTo;\n }\n }, [returnTo]);\n\n useEffect(() => {\n if (isAddAccountMode || isCompletingAuth) return;\n\n const profileId = resolveActiveProfileId();\n if (profileId) {\n const tokens = getProfileTokens(profileId);\n const user = getProfileUser(profileId);\n if (\n tokens?.accessToken &&\n user &&\n hasValidAccessToken(tokens.accessToken)\n ) {\n useAuthStore.getState().loadFromProfile(profileId);\n redirectAfterLogin(decodeSameOriginReturnTo(returnTo) ?? \"/\");\n return;\n }\n }\n\n const ssoUrl = getSSORedirectUrl();\n if (ssoUrl) {\n setIsSsoRedirecting(true);\n window.location.replace(ssoUrl);\n }\n }, [isAddAccountMode, isCompletingAuth, redirectAfterLogin, returnTo]);\n\n // Load remembered email on mount and clean up any legacy plaintext password storage\n useEffect(() => {\n try {\n // Security cleanup: remove any previously stored plaintext passwords\n localStorage.removeItem(\"remembered_password\");\n\n const rememberedEmail = localStorage.getItem(\"remembered_email\");\n\n if (rememberedEmail) {\n setFormData((prev) => ({\n ...prev,\n email: rememberedEmail,\n rememberMe: true,\n }));\n }\n } catch (error) {\n console.warn(\"[LoginPage] Failed to load remembered email:\", error);\n }\n }, []);\n\n // Check for redirect message and email from location state (e.g., from expired MFA or SSO redirect)\n useEffect(() => {\n const state = location.state as { message?: string; email?: string } | null;\n if (state?.message) {\n setInfoMessage(state.message);\n }\n if (state?.email) {\n setFormData((prev) => ({ ...prev, email: state.email }));\n }\n // Clear state to prevent showing message on refresh\n if (state) {\n window.history.replaceState({}, document.title);\n }\n }, [location.state]);\n\n // Determine error type from GraphQL error code (primary) or message (fallback).\n // The backend sends structured codes in extensions.code which are propagated\n // through useAuthOperations → authStore.errorCode.\n const checkErrorType = (\n err: string | null,\n code?: string | null,\n ):\n | \"DELETED\"\n | \"SUSPENDED\"\n | \"OAUTH_ONLY\"\n | \"INACTIVE\"\n | \"EMAIL_NOT_VERIFIED\"\n | null => {\n // Primary: match on GraphQL error code from extensions\n if (code) {\n switch (code) {\n case \"ACCOUNT_DELETED\":\n return \"DELETED\";\n case \"ACCOUNT_SUSPENDED\":\n return \"SUSPENDED\";\n case \"OAUTH_ONLY_ACCOUNT\":\n return \"OAUTH_ONLY\";\n case \"ACCOUNT_INACTIVE\":\n return \"INACTIVE\";\n case \"EMAIL_NOT_VERIFIED\":\n return \"EMAIL_NOT_VERIFIED\";\n default:\n break;\n }\n }\n\n // Fallback: match on error message for non-GraphQL errors or legacy responses\n if (!err) return null;\n const lowerErr = err.toLowerCase();\n\n if (\n lowerErr.includes(\"deleted\") ||\n lowerErr.includes(\"permanently deleted\")\n ) {\n return \"DELETED\";\n }\n if (lowerErr.includes(\"suspended\")) {\n return \"SUSPENDED\";\n }\n if (\n lowerErr.includes(\"oauth provider\") ||\n lowerErr.includes(\"oauth-only\") ||\n lowerErr.includes(\"oauth only\")\n ) {\n return \"OAUTH_ONLY\";\n }\n if (\n lowerErr.includes(\"inactive\") ||\n lowerErr.includes(\"deactivated\") ||\n lowerErr.includes(\"not active\")\n ) {\n return \"INACTIVE\";\n }\n if (\n lowerErr.includes(\"verify your email\") ||\n lowerErr.includes(\"email not verified\") ||\n lowerErr.includes(\"email_not_verified\")\n ) {\n return \"EMAIL_NOT_VERIFIED\";\n }\n return null;\n };\n\n // Pre-fill email from URL query parameter (e.g., after email verification)\n useEffect(() => {\n const emailFromUrl = searchParams.get(\"email\");\n if (emailFromUrl) {\n setFormData((prev) => ({ ...prev, email: emailFromUrl }));\n }\n\n // Check for expired session redirect\n const isExpired = searchParams.get(\"expired\");\n if (isExpired === \"true\") {\n setInfoMessage(\n tr(\n \"auth.login.sessionExpired\",\n \"Your session has expired. Please sign in again.\",\n ),\n );\n // Clear the query param to prevent showing on refresh\n const newUrl = window.location.pathname;\n window.history.replaceState({}, document.title, newUrl);\n }\n }, [searchParams, tr]);\n\n // Redirect to MFA verification when required\n useEffect(() => {\n if (mfaRequired) {\n navigate(\"/auth/mfa-verify\");\n }\n }, [mfaRequired, navigate]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsAccountInactive(false);\n setIsAccountDeleted(false);\n setIsAccountSuspended(false);\n setIsOAuthOnlyAccount(false);\n setIsEmailNotVerified(false);\n setIsDuplicateAccount(false);\n setResendSuccess(false);\n setValidationError(null);\n\n // Client-side validation\n const emailTrimmed = (formData.email ?? \"\").trim();\n if (!emailTrimmed) {\n setValidationError(\n tr(\n \"auth.login.enterEmailOrUsername\",\n \"Please enter your email or username\",\n ),\n );\n return;\n }\n\n // Basic email format validation (if it contains @)\n if (emailTrimmed.includes(\"@\")) {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(emailTrimmed)) {\n setValidationError(\n tr(\"auth.login.invalidEmail\", \"Please enter a valid email address\"),\n );\n return;\n }\n }\n\n if (!formData.password || formData.password.length < 1) {\n setValidationError(\n tr(\"auth.login.enterPassword\", \"Please enter your password\"),\n );\n return;\n }\n\n // In add-account mode, check if this email is already signed in\n if (isAddAccountMode) {\n const existingProfiles = getProfiles();\n const alreadySignedIn = existingProfiles.some(\n (p) => p.email.toLowerCase() === emailTrimmed.toLowerCase(),\n );\n if (alreadySignedIn) {\n setIsDuplicateAccount(true);\n setValidationError(null);\n return;\n }\n }\n setIsDuplicateAccount(false);\n\n try {\n // Get reCAPTCHA v3 token (no-op when captcha is disabled)\n const recaptchaToken = await executeCaptchaV3(\"login\");\n\n const result = await signIn({\n email: emailTrimmed,\n password: formData.password,\n rememberMe: formData.rememberMe,\n ...(recaptchaToken ? { recaptchaToken } : {}),\n });\n\n // If MFA is required, the useEffect above will handle redirect\n if (result.requiresMFA) {\n // Persist the post-login destination so MfaVerifyPage honors\n // `returnTo` and `defaultPostLoginPath` after the user completes\n // verification — otherwise the MFA path falls back to \"/\" and the\n // host-configured home (e.g. \"/vibecontrols\") is lost.\n const effectiveReturnTo = initialReturnToRef.current || returnTo;\n const postMfaPath = effectiveReturnTo\n ? cleanReturnToValue(effectiveReturnTo)\n : cleanReturnToValue(defaultPostLoginPath ?? \"/dashboard\");\n try {\n sessionStorage.setItem(MFA_REDIRECT, postMfaPath);\n } catch {\n // sessionStorage may be unavailable (private mode, quota) — the\n // MFA page still works, it just defaults to \"/\" on completion.\n }\n return;\n }\n\n // Successful login - complete auth flow (fetch workspaces + issue token)\n if (result.success) {\n try {\n const profileId = result.user?.id;\n if (profileId) {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart =\n window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const ssoValue = JSON.stringify({\n id: profileId,\n origin: window.location.origin,\n });\n document.cookie = `bf-sso=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n document.cookie = `bf-sso-profile=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n }\n } catch {\n // Best effort only. Login can continue without the fast-path cookie.\n }\n\n // Handle Remember Me: Save or clear email only (never store passwords)\n try {\n if (formData.rememberMe) {\n localStorage.setItem(\"remembered_email\", emailTrimmed);\n } else {\n localStorage.removeItem(\"remembered_email\");\n }\n } catch (error) {\n console.warn(\n \"[LoginPage] Failed to save/clear remembered email:\",\n error,\n );\n }\n\n setIsCompletingAuth(true);\n // In add-account mode, redirect back to where the user was\n const effectiveReturnTo = initialReturnToRef.current || returnTo;\n const postLoginPath = effectiveReturnTo\n ? cleanReturnToValue(effectiveReturnTo)\n : cleanReturnToValue(defaultPostLoginPath ?? \"/dashboard\");\n try {\n type AuthFlowResult = Awaited<ReturnType<typeof completeAuthFlow>>;\n // Pass the token directly to avoid race condition with React state updates\n const authFlowResult = await Promise.race<AuthFlowResult>([\n completeAuthFlow(result.token),\n new Promise<AuthFlowResult>((resolve) => {\n window.setTimeout(() => {\n resolve({\n success: false,\n error: \"Auth flow timed out after login\",\n });\n }, 5000);\n }),\n ]);\n\n if (authFlowResult.success) {\n // Sync workspace and organization to ActiveContext\n // This is critical for global search and other workspace-scoped features\n if (authFlowResult.workspace) {\n setWorkspace(authFlowResult.workspace.id);\n if (authFlowResult.workspace.organizationId) {\n setOrganization(authFlowResult.workspace.organizationId);\n }\n }\n\n // Full auth flow completed - redirect to dashboard or returnTo\n redirectAfterLogin(postLoginPath);\n } else if (authFlowResult.error?.includes(\"No workspaces found\")) {\n // No workspaces - redirect to onboarding/workspace creation\n navigate(\"/onboarding/create-workspace\");\n } else {\n // Partial success or error - still sync workspace if available and redirect\n if (authFlowResult.workspace) {\n setWorkspace(authFlowResult.workspace.id);\n if (authFlowResult.workspace.organizationId) {\n setOrganization(authFlowResult.workspace.organizationId);\n }\n }\n console.warn(\"Auth flow warning:\", authFlowResult.error);\n redirectAfterLogin(postLoginPath);\n }\n } catch (authFlowError) {\n // Auth flow failed but login succeeded - still redirect\n console.error(\"Auth flow error (login succeeded):\", authFlowError);\n redirectAfterLogin(postLoginPath);\n } finally {\n setIsCompletingAuth(false);\n }\n }\n } catch (err) {\n if (import.meta.env.DEV) {\n console.error(\"Sign in failed:\", err);\n }\n // Check error type using error code (from store) and message\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (errorMsg.includes(\"reCAPTCHA\")) {\n setValidationError(errorMsg);\n }\n const storeErrorCode = useAuthStore.getState().errorCode;\n const errorType = checkErrorType(errorMsg, storeErrorCode);\n if (errorType === \"DELETED\") {\n setIsAccountDeleted(true);\n } else if (errorType === \"SUSPENDED\") {\n setIsAccountSuspended(true);\n } else if (errorType === \"OAUTH_ONLY\") {\n setIsOAuthOnlyAccount(true);\n } else if (errorType === \"INACTIVE\") {\n setIsAccountInactive(true);\n } else if (errorType === \"EMAIL_NOT_VERIFIED\") {\n setIsEmailNotVerified(true);\n }\n }\n };\n\n // Also check the error from useAuthOperations\n useEffect(() => {\n if (error) {\n const errorType = checkErrorType(error, errorCode);\n if (errorType === \"DELETED\") {\n setIsAccountDeleted(true);\n } else if (errorType === \"SUSPENDED\") {\n setIsAccountSuspended(true);\n } else if (errorType === \"OAUTH_ONLY\") {\n setIsOAuthOnlyAccount(true);\n } else if (errorType === \"INACTIVE\") {\n setIsAccountInactive(true);\n } else if (errorType === \"EMAIL_NOT_VERIFIED\") {\n setIsEmailNotVerified(true);\n }\n }\n }, [error, errorCode]);\n\n /**\n * Handle Google OAuth login\n */\n const handleGoogleLogin = () => {\n setOauthError(null);\n try {\n initiateGoogleOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.login.googleSignInFailed\",\n \"Failed to initiate Google sign in\",\n ),\n );\n }\n };\n\n /**\n * Handle Microsoft OAuth login\n */\n const handleMicrosoftLogin = () => {\n setOauthError(null);\n try {\n initiateMicrosoftOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.login.microsoftSignInFailed\",\n \"Failed to initiate Microsoft sign in\",\n ),\n );\n }\n };\n\n /**\n * Handle resend verification email\n */\n const handleResendVerification = async () => {\n setIsResendingVerification(true);\n setResendSuccess(false);\n try {\n const emailTrimmed = (formData.email ?? \"\").trim();\n const result = await resendVerification(emailTrimmed);\n if (result.success) {\n setResendSuccess(true);\n } else {\n setValidationError(\n tr(\n \"auth.login.resendFailed\",\n \"Failed to resend verification email. Please try again.\",\n ),\n );\n }\n } catch (err) {\n console.error(\"Failed to resend verification email:\", err);\n setValidationError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.login.resendFailed\",\n \"Failed to resend verification email. Please try again.\",\n ),\n );\n } finally {\n setIsResendingVerification(false);\n }\n };\n\n // Check if OAuth providers are configured\n const isGoogleConfigured = isProviderConfigured(\"google\");\n const isMicrosoftConfigured = isProviderConfigured(\"microsoft\");\n\n const oauthProviders = [\n {\n name: \"Google\",\n icon: GoogleIcon,\n onClick: handleGoogleLogin,\n disabled: !isGoogleConfigured,\n tooltip: isGoogleConfigured\n ? undefined\n : tr(\n \"auth.login.googleNotConfigured\",\n \"Google sign in is not configured\",\n ),\n },\n // {\n // name: \"GitHub\",\n // icon: GitHubIcon,\n // onClick: () => {},\n // disabled: true,\n // tooltip: tr(\n // \"auth.login.githubNotConfigured\",\n // \"GitHub sign in is not configured\",\n // ),\n // },\n {\n name: \"Microsoft\",\n icon: MicrosoftIcon,\n onClick: handleMicrosoftLogin,\n disabled: !isMicrosoftConfigured,\n tooltip: isMicrosoftConfigured\n ? undefined\n : tr(\n \"auth.login.microsoftNotConfigured\",\n \"Microsoft sign in is not configured\",\n ),\n },\n ];\n\n if (isSsoRedirecting) {\n return <SSORedirectSpinner />;\n }\n\n return (\n <AuthLayout\n title={tr(\"auth.login.welcomeBack\", \"Welcome back\")}\n subtitle={tr(\n \"auth.login.enterCredentials\",\n \"Enter your credentials to continue\",\n )}\n >\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Info Message (e.g., from expired MFA redirect) */}\n {infoMessage && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-info-bg-subtle border-status-info-border\">\n <svg\n className=\"size-5 flex-shrink-0 mt-0.5 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n <p className=\"text-sm text-status-info-text\">{infoMessage}</p>\n </div>\n )}\n\n {/* Validation Error */}\n {validationError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-warning-text\" />\n <p className=\"text-sm text-status-warning-text\">\n {validationError}\n </p>\n </div>\n )}\n\n {/* Duplicate Account Warning (Add Account mode) */}\n {isDuplicateAccount && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-warning-text\" />\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n \"auth.login.duplicateAccount\",\n \"This account is already signed in.\",\n )}\n </p>\n <p className=\"mt-1 text-sm text-status-warning-text/80\">\n {tr(\n \"auth.login.duplicateAccountHint\",\n \"Please use a different account, or go back and switch to this account from the profile menu.\",\n )}\n </p>\n {(initialReturnToRef.current || returnTo) && (\n <button\n type=\"button\"\n onClick={() =>\n navigate(\n decodeURIComponent(\n initialReturnToRef.current || returnTo || \"/dashboard\",\n ),\n )\n }\n className=\"mt-3 inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-status-warning-border bg-bg-surface text-status-warning-text text-sm font-medium hover:bg-status-warning-bg-subtle hover:border-status-warning-border transition-all shadow-sm\"\n >\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\n \"auth.login.goBackSwitchAccount\",\n \"Go back and switch account\",\n )}\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Error Alert */}\n {error && !validationError && !isDuplicateAccount && (\n <div\n className={`flex items-start gap-3 p-4 rounded-xl border ${\n isAccountInactive || isOAuthOnlyAccount || isEmailNotVerified\n ? \"bg-status-warning-bg-subtle border-status-warning-border\"\n : \"bg-status-error-bg-subtle border-status-error-border\"\n }`}\n >\n <AlertIcon\n className={`size-5 flex-shrink-0 mt-0.5 ${\n isAccountInactive || isOAuthOnlyAccount || isEmailNotVerified\n ? \"text-status-warning-text\"\n : \"text-status-error-text\"\n }`}\n />\n <div className=\"flex-1\">\n <p\n className={`text-sm ${\n isAccountInactive || isOAuthOnlyAccount || isEmailNotVerified\n ? \"text-status-warning-text\"\n : \"text-status-error-text\"\n }`}\n >\n {isAccountDeleted\n ? tr(\n \"auth.login.accountDeleted\",\n \"This account has been permanently deleted and cannot be recovered.\",\n )\n : isAccountSuspended\n ? tr(\n \"auth.login.accountSuspended\",\n \"Your account has been suspended. Please contact support for assistance.\",\n )\n : isAccountInactive\n ? tr(\n \"auth.login.accountDeactivated\",\n \"Your account has been deactivated.\",\n )\n : isOAuthOnlyAccount\n ? tr(\n \"auth.login.oauthOnly\",\n \"This account uses OAuth sign-in. Please use Google or Microsoft to sign in.\",\n )\n : isEmailNotVerified\n ? tr(\n \"auth.login.emailNotVerified\",\n \"Please verify your email before signing in. Check your inbox for the verification link.\",\n )\n : error}\n </p>\n {isAccountInactive && (\n <button\n type=\"button\"\n onClick={() =>\n navigate(\"/auth/reactivate\", {\n state: { email: formData.email },\n })\n }\n className=\"mt-2 text-sm font-semibold text-status-warning-text hover:text-status-warning-text underline underline-offset-2\"\n >\n {tr(\n \"auth.login.clickReactivate\",\n \"Click here to reactivate your account\",\n )}\n </button>\n )}\n {isEmailNotVerified && (\n <div className=\"mt-3\">\n {resendSuccess ? (\n <div className=\"flex items-center gap-2 text-sm text-status-success-text\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n {tr(\n \"auth.login.verificationSent\",\n \"Verification email sent! Please check your inbox.\",\n )}\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={handleResendVerification}\n disabled={isResendingVerification}\n className=\"inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-status-warning-border bg-bg-surface text-status-warning-text text-sm font-medium hover:bg-status-warning-bg-subtle hover:border-status-warning-border transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isResendingVerification ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" />\n {tr(\"auth.login.sending\", \"Sending...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n {tr(\n \"auth.login.resendVerification\",\n \"Resend verification email\",\n )}\n </>\n )}\n </button>\n )}\n </div>\n )}\n {isOAuthOnlyAccount && (\n <div className=\"mt-3 flex flex-col gap-2\">\n {isProviderConfigured(\"google\") && (\n <button\n type=\"button\"\n onClick={handleGoogleLogin}\n className=\"inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-border-default bg-bg-surface text-text-primary text-sm font-medium hover:bg-bg-canvas hover:border-border-default transition-all shadow-sm\"\n >\n <GoogleIcon className=\"size-5\" />\n {tr(\"auth.login.signInWithGoogle\", \"Sign in with Google\")}\n </button>\n )}\n {isProviderConfigured(\"microsoft\") && (\n <button\n type=\"button\"\n onClick={handleMicrosoftLogin}\n className=\"inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-border-default bg-bg-surface text-text-primary text-sm font-medium hover:bg-bg-canvas hover:border-border-default transition-all shadow-sm\"\n >\n <MicrosoftIcon className=\"size-5\" />\n {tr(\n \"auth.login.signInWithMicrosoft\",\n \"Sign in with Microsoft\",\n )}\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Email Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.login.emailLabel\", \"Email\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <EmailIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.login.emailLabel\", \"Email\")}\n value={formData.email}\n onChange={(e) =>\n setFormData({ ...formData, email: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\"auth.login.emailPlaceholder\", \"you@example.com\")}\n />\n </div>\n </div>\n\n {/* Password Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.login.passwordLabel\", \"Password\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <LockIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n autoComplete=\"current-password\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.login.passwordLabel\", \"Password\")}\n value={formData.password}\n onChange={(e) =>\n setFormData({ ...formData, password: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.login.passwordPlaceholder\",\n \"Enter your password\",\n )}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n aria-label={showPassword ? \"Hide password\" : \"Show password\"}\n className=\"absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors\"\n >\n {showPassword ? (\n <EyeOffIcon className=\"size-5\" />\n ) : (\n <EyeIcon className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n\n {/* Remember & Forgot */}\n <div className=\"flex items-center justify-between\">\n <label className=\"flex items-center gap-2 cursor-pointer group\">\n <div className=\"relative\">\n <input\n type=\"checkbox\"\n aria-label={tr(\"auth.login.rememberMe\", \"Remember me\")}\n checked={formData.rememberMe}\n onChange={(e) => {\n const isChecked = e.target.checked;\n setFormData({ ...formData, rememberMe: isChecked });\n\n // Clear saved email immediately when unchecked\n if (!isChecked) {\n try {\n localStorage.removeItem(\"remembered_email\");\n } catch (error) {\n console.warn(\n \"[LoginPage] Failed to clear remembered email:\",\n error,\n );\n }\n }\n }}\n className=\"sr-only peer\"\n />\n <div className=\"size-5 rounded-md border-2 border-[var(--color-border-default)] bg-[var(--color-bg-surface)] peer-checked:bg-status-info-bg-subtle0 peer-checked:border-action-primary-bg transition-all peer-focus:ring-2 peer-focus:ring-action-primary-bg/20\">\n <svg\n className=\"size-full text-white opacity-0 peer-checked:opacity-100 transition-opacity p-0.5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n </div>\n <span className=\"text-sm text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)] transition-colors\">\n {tr(\"auth.login.rememberMe\", \"Remember me\")}\n </span>\n </label>\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/reset-password\")}\n className=\"text-sm font-medium text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.login.forgotPassword\", \"Forgot password?\")}\n </button>\n </div>\n\n {/* Submit Button */}\n <button\n type=\"submit\"\n disabled={isLoading || isCompletingAuth}\n className=\"auth-primary-button flex w-full items-center justify-center gap-2 px-4 py-3 font-semibold\"\n >\n {isLoading || isCompletingAuth ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>\n {isCompletingAuth\n ? tr(\n \"auth.login.settingUpWorkspace\",\n \"Setting up workspace...\",\n )\n : tr(\"auth.login.signingIn\", \"Signing in...\")}\n </span>\n </>\n ) : (\n tr(\"auth.login.signIn\", \"Sign in\")\n )}\n </button>\n </form>\n\n {/* Divider */}\n <div className=\"relative my-8\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-[var(--color-border-default)]\" />\n </div>\n <div className=\"relative flex justify-center\">\n <span className=\"px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]\">\n {tr(\"auth.login.orContinueWith\", \"or continue with\")}\n </span>\n </div>\n </div>\n\n {/* OAuth Error */}\n {oauthError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{oauthError}</p>\n </div>\n )}\n\n {/* OAuth Buttons */}\n <div className=\"grid grid-cols-3 gap-3\">\n {oauthProviders.map((provider) => (\n <button\n key={provider.name}\n type=\"button\"\n onClick={provider.onClick}\n disabled={provider.disabled}\n title={provider.tooltip}\n aria-label={provider.name}\n className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${\n provider.disabled\n ? \"border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50\"\n : \"border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]\"\n }`}\n >\n <provider.icon\n className={`size-5 ${provider.disabled ? \"opacity-50\" : \"\"}`}\n />\n <span\n className={`sr-only sm:not-sr-only text-sm font-medium transition-colors hidden sm:inline ${\n provider.disabled\n ? \"text-[var(--color-text-disabled)]\"\n : \"text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]\"\n }`}\n >\n {provider.name}\n </span>\n </button>\n ))}\n </div>\n\n {/* Enterprise SSO Option — temporarily hidden */}\n {/* <div className=\"mt-6\">\n <button type=\"button\"\n onClick={() => navigate(\"/auth/sso/login\")}\n className=\"w-full flex items-center justify-center gap-3 px-4 py-3 rounded-xl border-2 border-dashed border-[var(--color-border-default)] hover:border-action-primary-bg bg-[var(--color-bg-surface)] hover:bg-status-info-bg-subtle transition-all duration-200 group\"\n >\n <svg\n className=\"size-5 text-[var(--color-text-tertiary)] group-hover:text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n <span className=\"text-sm font-medium text-[var(--color-text-secondary)] group-hover:text-text-link\">\n {tr(\"auth.login.enterpriseSso\", \"Sign in with Enterprise SSO\")}\n </span>\n </button>\n </div> */}\n\n {/* Magic Link Option */}\n <div className=\"mt-4 text-center\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/magic-link\")}\n className=\"text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors\"\n >\n {tr(\"auth.login.preferPasswordless\", \"Prefer passwordless login?\")}{\" \"}\n <span className=\"font-semibold text-text-link hover:text-text-linkHover\">\n {tr(\"auth.login.signInMagicLink\", \"Sign in with Magic Link\")}\n </span>\n </button>\n </div>\n\n {/* Sign Up Link */}\n <p className=\"mt-6 text-center text-sm text-[var(--color-text-secondary)]\">\n {tr(\"auth.login.dontHaveAccount\", \"Don't have an account?\")}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/register\")}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.login.signUp\", \"Sign up\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
2
|
import { AuthLayout as t } from "../components/AuthLayout.js";
|
|
3
3
|
import { AlertIcon as n, SpinnerIcon as r } from "../components/AuthIcons.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { MFA_CHALLENGE_ID as i, MFA_PENDING_REFRESH as a, MFA_PENDING_TOKEN as o, MFA_REDIRECT as s } from "../constants/sessionKeys.js";
|
|
5
|
+
import { useMfaOperations as c } from "../hooks/useMfaOperations.js";
|
|
6
6
|
import { useEffect as l, useRef as u, useState as d } from "react";
|
|
7
7
|
import { useNavigate as f } from "react-router-dom";
|
|
8
8
|
import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
@@ -12,16 +12,16 @@ function _() {
|
|
|
12
12
|
let { t: _ } = p(), v = (e, t) => {
|
|
13
13
|
let n = _(e);
|
|
14
14
|
return n === e ? t : n;
|
|
15
|
-
}, y = f(), { verifyMFA: b, mfaChallengeId: x, isLoading: S } =
|
|
15
|
+
}, y = f(), { verifyMFA: b, mfaChallengeId: x, isLoading: S } = c(), [C, w] = d([
|
|
16
16
|
"",
|
|
17
17
|
"",
|
|
18
18
|
"",
|
|
19
19
|
"",
|
|
20
20
|
"",
|
|
21
21
|
""
|
|
22
|
-
]), [T, E] = d(null), D = u([]), O = typeof window < "u" ? sessionStorage.getItem(
|
|
22
|
+
]), [T, E] = d(null), D = u([]), O = typeof window < "u" ? sessionStorage.getItem(i) : null, k = x || O;
|
|
23
23
|
l(() => {
|
|
24
|
-
D.current[0]?.focus(), sessionStorage.getItem(
|
|
24
|
+
D.current[0]?.focus(), sessionStorage.getItem(o), sessionStorage.getItem(s);
|
|
25
25
|
}, [O]);
|
|
26
26
|
let A = (e, t) => {
|
|
27
27
|
let n = t.replace(/\D/g, "").slice(-1), r = [...C];
|
|
@@ -53,11 +53,11 @@ function _() {
|
|
|
53
53
|
method: "TOTP"
|
|
54
54
|
});
|
|
55
55
|
if (t.success) {
|
|
56
|
-
let e = sessionStorage.getItem(
|
|
57
|
-
sessionStorage.removeItem(
|
|
56
|
+
let e = sessionStorage.getItem(s);
|
|
57
|
+
sessionStorage.removeItem(i), sessionStorage.removeItem(o), sessionStorage.removeItem(a), sessionStorage.removeItem(s), y(e || "/", { replace: !0 });
|
|
58
58
|
} else {
|
|
59
59
|
if (t.error === "mfa_challenge_expired") {
|
|
60
|
-
sessionStorage.removeItem(
|
|
60
|
+
sessionStorage.removeItem(i), sessionStorage.removeItem(o), sessionStorage.removeItem(a), sessionStorage.removeItem(s), e.getState().setMfaRequired(!1), y("/auth/login", {
|
|
61
61
|
replace: !0,
|
|
62
62
|
state: { message: "Your verification session expired. Please sign in again." }
|
|
63
63
|
});
|
|
@@ -75,7 +75,7 @@ function _() {
|
|
|
75
75
|
}
|
|
76
76
|
} catch (t) {
|
|
77
77
|
if (console.error("MFA verification failed:", t), (t instanceof Error ? t.message : String(t)).includes("mfa_challenge_expired")) {
|
|
78
|
-
sessionStorage.removeItem(
|
|
78
|
+
sessionStorage.removeItem(i), sessionStorage.removeItem(o), sessionStorage.removeItem(a), sessionStorage.removeItem(s), e.getState().setMfaRequired(!1), y("/auth/login", {
|
|
79
79
|
replace: !0,
|
|
80
80
|
state: { message: "Your verification session expired. Please sign in again." }
|
|
81
81
|
});
|
|
@@ -176,7 +176,7 @@ function _() {
|
|
|
176
176
|
children: /* @__PURE__ */ h("button", {
|
|
177
177
|
type: "button",
|
|
178
178
|
onClick: () => {
|
|
179
|
-
sessionStorage.removeItem(
|
|
179
|
+
sessionStorage.removeItem(i), sessionStorage.removeItem(o), sessionStorage.removeItem(a), sessionStorage.removeItem(s), e.getState().setMfaRequired(!1), y("/auth/login");
|
|
180
180
|
},
|
|
181
181
|
className: "font-semibold text-text-link hover:text-text-linkHover transition-colors",
|
|
182
182
|
children: v("auth.mfaVerify.backToSignIn", "Back to sign in")
|