@burdenoff/microfe-auth 2026.825.1 → 2026.828.1
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.
|
@@ -3,23 +3,24 @@ import { useAuthStore as t } from "../store/authStore.js";
|
|
|
3
3
|
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
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
6
|
+
import { sanitizePostLoginPath as a } from "../utils/redirect.js";
|
|
7
|
+
import { AuthLayout as o } from "../components/AuthLayout.js";
|
|
8
|
+
import { AlertIcon as s, EmailIcon as c, EyeIcon as l, EyeOffIcon as ee, GoogleIcon as te, LockIcon as ne, MicrosoftIcon as u, SpinnerIcon as d } from "../components/AuthIcons.js";
|
|
9
|
+
import { useCaptchaV3 as re } from "../hooks/useCaptchaV3.js";
|
|
10
|
+
import { MFA_REDIRECT as ie } from "../constants/sessionKeys.js";
|
|
11
|
+
import { useCallback as f, useEffect as p, useRef as ae, useState as m } from "react";
|
|
12
|
+
import { SSORedirectSpinner as oe, getSSORedirectUrl as se } from "@burdenoff/fe-libs/shared/components";
|
|
13
|
+
import { useLocation as ce, useNavigate as le, useSearchParams as ue } from "react-router";
|
|
14
|
+
import { useActiveContext as de, useI18n as fe } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
15
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
16
|
+
import { beginSsoNavigation as pe, buildSsoDepositUrl as me, isSsoNavigationPending as he, markSsoDepositPending as ge, mintSsoSessionTokens as _e } from "@burdenoff/fe-libs/shared/utils";
|
|
17
|
+
import { resolveAuthBridgeConfig as ve } from "@burdenoff/fe-libs/shared/config";
|
|
18
|
+
import { resolveGatewayUrls as ye } from "@burdenoff/fe-libs/shared/config/gatewayUrls";
|
|
19
|
+
import { Button as be } from "@burdenoff/fe-libs/button";
|
|
20
|
+
import { PagePurpose as xe } from "@burdenoff/fe-libs/ui";
|
|
21
|
+
import { getProfileTokens as Se, getProfileUser as Ce, getProfileWorkspaceToken as we, getProfiles as Te, resolveActiveProfileId as Ee } from "@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager";
|
|
21
22
|
//#region src/auth/pages/LoginPage.tsx
|
|
22
|
-
function
|
|
23
|
+
function De() {
|
|
23
24
|
if (typeof window > "u") return !1;
|
|
24
25
|
let e = window;
|
|
25
26
|
if (typeof e.Capacitor?.isNativePlatform == "function") try {
|
|
@@ -27,7 +28,7 @@ function Te() {
|
|
|
27
28
|
} catch {}
|
|
28
29
|
return !!(typeof navigator < "u" && /Electron/i.test(navigator.userAgent || ""));
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
+
function Oe(e) {
|
|
31
32
|
if (typeof window > "u") return e;
|
|
32
33
|
try {
|
|
33
34
|
let t = new URL(e, window.location.origin);
|
|
@@ -36,7 +37,7 @@ function Ee(e) {
|
|
|
36
37
|
return e;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
function
|
|
40
|
+
function ke(e) {
|
|
40
41
|
try {
|
|
41
42
|
let t = e.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"), n = t.padEnd(Math.ceil(t.length / 4) * 4, "="), r = JSON.parse(atob(n));
|
|
42
43
|
return typeof r.exp == "number" && r.exp > Date.now() / 1e3;
|
|
@@ -44,16 +45,16 @@ function De(e) {
|
|
|
44
45
|
return !1;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
+
function Ae(e) {
|
|
48
49
|
if (!e) return null;
|
|
49
50
|
try {
|
|
50
51
|
let t = new URL(decodeURIComponent(e), window.location.origin);
|
|
51
|
-
return t.origin === window.location.origin ?
|
|
52
|
+
return t.origin === window.location.origin ? v(t, { includeOrigin: !1 }) : null;
|
|
52
53
|
} catch {
|
|
53
54
|
return null;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
|
-
function
|
|
57
|
+
function v(e, t) {
|
|
57
58
|
for (let t of [
|
|
58
59
|
"sso_failed",
|
|
59
60
|
"sso-failed",
|
|
@@ -63,92 +64,97 @@ function y(e, t) {
|
|
|
63
64
|
let n = e.searchParams.toString();
|
|
64
65
|
return `${e.pathname}${n ? `?${n}` : ""}${e.hash}`;
|
|
65
66
|
}
|
|
66
|
-
function
|
|
67
|
+
function y(e) {
|
|
67
68
|
try {
|
|
68
69
|
let t = new URL(decodeURIComponent(e), window.location.origin);
|
|
69
|
-
return
|
|
70
|
+
return v(t, { includeOrigin: t.origin !== window.location.origin });
|
|
70
71
|
} catch {
|
|
71
72
|
return e;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
function
|
|
75
|
-
let
|
|
76
|
-
let n =
|
|
75
|
+
function b() {
|
|
76
|
+
let v = le(), b = ce(), [x] = ue(), { t: S } = fe(), C = f((e, t) => {
|
|
77
|
+
let n = S(e);
|
|
77
78
|
return n === e ? t : n;
|
|
78
|
-
}, [
|
|
79
|
-
if (typeof window > "u" ||
|
|
79
|
+
}, [S]), { signIn: je, isLoading: w, error: T, errorCode: E, resendVerification: Me } = n(), { completeAuthFlow: Ne } = r(), { setWorkspace: D, setOrganization: Pe } = de(), { defaultPostLoginPath: Fe } = e(), { initiateGoogleOAuth: Ie, initiateMicrosoftOAuth: Le, isProviderConfigured: O } = i(), Re = t((e) => e.mfaRequired), { executeRecaptcha: ze } = re(), [k, Be] = m(!1), [Ve, He] = m(!1), Ue = f(async (e) => {
|
|
80
|
+
if (typeof window > "u" || De()) return e;
|
|
80
81
|
try {
|
|
81
|
-
let t =
|
|
82
|
+
let t = ve(), n = new URL(t.url), r = window.location.origin;
|
|
82
83
|
if (n.origin === r) return e;
|
|
83
|
-
let i = new URL(e, r).href, a =
|
|
84
|
+
let i = new URL(e, r).href, a = Ee();
|
|
84
85
|
if (!a) return e;
|
|
85
|
-
let o =
|
|
86
|
+
let o = Se(a), s = Ce(a);
|
|
86
87
|
if (!o || !s) return e;
|
|
87
|
-
let c =
|
|
88
|
+
let c = we(a), l = {
|
|
88
89
|
...o,
|
|
89
90
|
workspaceToken: c
|
|
90
91
|
};
|
|
91
92
|
if (o.refreshToken) {
|
|
92
|
-
let { globalBaseUrl: e } =
|
|
93
|
+
let { globalBaseUrl: e } = ye(), { tokens: t } = await _e(`${e}/global/graphql`, o.refreshToken);
|
|
93
94
|
t && (l = {
|
|
94
95
|
...t,
|
|
95
96
|
workspaceToken: c
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
|
-
return
|
|
99
|
+
return ge(a), me(n.origin, i, {
|
|
99
100
|
profileId: a,
|
|
100
101
|
tokens: JSON.stringify(l),
|
|
101
102
|
user: JSON.stringify(s),
|
|
102
|
-
profiles: JSON.stringify(
|
|
103
|
+
profiles: JSON.stringify(Te())
|
|
103
104
|
});
|
|
104
105
|
} catch {
|
|
105
106
|
return e;
|
|
106
107
|
}
|
|
107
|
-
}, []), A =
|
|
108
|
+
}, []), A = ae(!1), j = f((e) => {
|
|
109
|
+
let t = a(e);
|
|
108
110
|
if (typeof window > "u") {
|
|
109
|
-
|
|
111
|
+
v(t);
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
114
|
try {
|
|
113
|
-
sessionStorage.setItem("bf-post-login-return-to",
|
|
115
|
+
sessionStorage.setItem("bf-post-login-return-to", t);
|
|
114
116
|
} catch {}
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
+
if (De()) {
|
|
118
|
+
v(Oe(t));
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
119
121
|
A.current = !0, (async () => {
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
let e = await Promise.race([Ue(t), new Promise((e) => {
|
|
123
|
+
window.setTimeout(() => e(t), 3e3);
|
|
124
|
+
})]);
|
|
125
|
+
e.includes("/auth/sso-deposit") ? (pe(), window.location.assign(e), window.setTimeout(() => {
|
|
126
|
+
pe(), window.location.replace(e);
|
|
127
|
+
}, 2500)) : window.location.assign(e), window.setTimeout(() => {
|
|
128
|
+
typeof window < "u" && window.location.pathname === "/auth/login" && v("/dashboard");
|
|
129
|
+
}, 3500);
|
|
124
130
|
})();
|
|
125
|
-
}, [
|
|
131
|
+
}, [v, Ue]), [M, We] = m(!1), [N, P] = m(!1), [Ge, F] = m(!1), [Ke, I] = m(!1), [L, R] = m(!1), [z, B] = m(!1), [qe, Je] = m(!1), [Ye, V] = m(!1), [Xe, H] = m(null), [Ze, U] = m(null), [W, G] = m({
|
|
126
132
|
email: "",
|
|
127
133
|
password: "",
|
|
128
134
|
rememberMe: !1
|
|
129
|
-
}), [K, q] =
|
|
130
|
-
|
|
135
|
+
}), [K, q] = m(null), [Qe, J] = m(!1), Y = x.get("mode") === "add-account", X = x.get("returnTo"), Z = ae(X), Q = x.get("reason");
|
|
136
|
+
p(() => {
|
|
131
137
|
Q === "session-expired" ? U(C("auth.login.sessionExpired", "Your session has expired. Please sign in again.")) : Q === "logout" && U(C("auth.login.signedOut", "You have been signed out."));
|
|
132
|
-
}, [Q, C]),
|
|
138
|
+
}, [Q, C]), p(() => {
|
|
133
139
|
!Z.current && X && (Z.current = X);
|
|
134
|
-
}, [X]),
|
|
135
|
-
if (Y ||
|
|
136
|
-
let e =
|
|
140
|
+
}, [X]), p(() => {
|
|
141
|
+
if (Y || k || A.current || he()) return;
|
|
142
|
+
let e = Ee();
|
|
137
143
|
if (e) {
|
|
138
|
-
let n =
|
|
139
|
-
if (n?.accessToken && r &&
|
|
140
|
-
t.getState().loadFromProfile(e), j(
|
|
144
|
+
let n = Se(e), r = Ce(e);
|
|
145
|
+
if (n?.accessToken && r && ke(n.accessToken)) {
|
|
146
|
+
t.getState().loadFromProfile(e), j(Ae(X) ?? "/");
|
|
141
147
|
return;
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
|
-
let n =
|
|
150
|
+
let n = se();
|
|
145
151
|
n && (He(!0), window.location.replace(n));
|
|
146
152
|
}, [
|
|
147
153
|
Y,
|
|
148
|
-
|
|
154
|
+
k,
|
|
149
155
|
j,
|
|
150
156
|
X
|
|
151
|
-
]),
|
|
157
|
+
]), p(() => {
|
|
152
158
|
try {
|
|
153
159
|
localStorage.removeItem("remembered_password");
|
|
154
160
|
let e = localStorage.getItem("remembered_email");
|
|
@@ -160,14 +166,14 @@ function x() {
|
|
|
160
166
|
} catch (e) {
|
|
161
167
|
console.warn("[LoginPage] Failed to load remembered email:", e);
|
|
162
168
|
}
|
|
163
|
-
}, []),
|
|
164
|
-
let e =
|
|
169
|
+
}, []), p(() => {
|
|
170
|
+
let e = b.state;
|
|
165
171
|
e?.message && U(e.message), e?.email && G((t) => ({
|
|
166
172
|
...t,
|
|
167
173
|
email: e.email
|
|
168
174
|
})), e && window.history.replaceState({}, document.title);
|
|
169
|
-
}, [
|
|
170
|
-
let
|
|
175
|
+
}, [b.state]);
|
|
176
|
+
let $e = (e, t) => {
|
|
171
177
|
if (t) switch (t) {
|
|
172
178
|
case "ACCOUNT_DELETED": return "DELETED";
|
|
173
179
|
case "ACCOUNT_SUSPENDED": return "SUSPENDED";
|
|
@@ -180,20 +186,20 @@ function x() {
|
|
|
180
186
|
let n = e.toLowerCase();
|
|
181
187
|
return n.includes("deleted") || n.includes("permanently deleted") ? "DELETED" : n.includes("suspended") ? "SUSPENDED" : n.includes("oauth provider") || n.includes("oauth-only") || n.includes("oauth only") ? "OAUTH_ONLY" : n.includes("inactive") || n.includes("deactivated") || n.includes("not active") ? "INACTIVE" : n.includes("verify your email") || n.includes("email not verified") || n.includes("email_not_verified") ? "EMAIL_NOT_VERIFIED" : null;
|
|
182
188
|
};
|
|
183
|
-
|
|
184
|
-
let e =
|
|
189
|
+
p(() => {
|
|
190
|
+
let e = x.get("email");
|
|
185
191
|
if (e && G((t) => ({
|
|
186
192
|
...t,
|
|
187
193
|
email: e
|
|
188
|
-
})),
|
|
194
|
+
})), x.get("expired") === "true") {
|
|
189
195
|
U(C("auth.login.sessionExpired", "Your session has expired. Please sign in again."));
|
|
190
196
|
let e = window.location.pathname;
|
|
191
197
|
window.history.replaceState({}, document.title, e);
|
|
192
198
|
}
|
|
193
|
-
}, [
|
|
194
|
-
|
|
195
|
-
}, [
|
|
196
|
-
let
|
|
199
|
+
}, [x, C]), p(() => {
|
|
200
|
+
Re && v("/auth/mfa-verify");
|
|
201
|
+
}, [Re, v]);
|
|
202
|
+
let et = async (e) => {
|
|
197
203
|
e.preventDefault(), P(!1), F(!1), I(!1), R(!1), B(!1), J(!1), V(!1), q(null);
|
|
198
204
|
let n = (W.email ?? "").trim();
|
|
199
205
|
if (!n) {
|
|
@@ -208,22 +214,22 @@ function x() {
|
|
|
208
214
|
q(C("auth.login.enterPassword", "Please enter your password"));
|
|
209
215
|
return;
|
|
210
216
|
}
|
|
211
|
-
if (Y &&
|
|
217
|
+
if (Y && Te().some((e) => e.email.toLowerCase() === n.toLowerCase())) {
|
|
212
218
|
J(!0), q(null);
|
|
213
219
|
return;
|
|
214
220
|
}
|
|
215
221
|
J(!1);
|
|
216
222
|
try {
|
|
217
|
-
let e = await
|
|
223
|
+
let e = await ze("login"), t = await je({
|
|
218
224
|
email: n,
|
|
219
225
|
password: W.password,
|
|
220
226
|
rememberMe: W.rememberMe,
|
|
221
227
|
...e ? { recaptchaToken: e } : {}
|
|
222
228
|
});
|
|
223
229
|
if (t.requiresMFA) {
|
|
224
|
-
let e =
|
|
230
|
+
let e = y(Z.current || X || (Fe ?? "/dashboard"));
|
|
225
231
|
try {
|
|
226
|
-
sessionStorage.setItem(
|
|
232
|
+
sessionStorage.setItem(ie, e);
|
|
227
233
|
} catch {}
|
|
228
234
|
return;
|
|
229
235
|
}
|
|
@@ -243,10 +249,10 @@ function x() {
|
|
|
243
249
|
} catch (e) {
|
|
244
250
|
console.warn("[LoginPage] Failed to save/clear remembered email:", e);
|
|
245
251
|
}
|
|
246
|
-
|
|
247
|
-
let e =
|
|
252
|
+
Be(!0);
|
|
253
|
+
let e = y(Z.current || X || (Fe ?? "/dashboard"));
|
|
248
254
|
try {
|
|
249
|
-
let n = await Promise.race([
|
|
255
|
+
let n = await Promise.race([Ne(t.token), new Promise((e) => {
|
|
250
256
|
window.setTimeout(() => {
|
|
251
257
|
e({
|
|
252
258
|
success: !1,
|
|
@@ -254,122 +260,122 @@ function x() {
|
|
|
254
260
|
});
|
|
255
261
|
}, 5e3);
|
|
256
262
|
})]);
|
|
257
|
-
n.success ? (n.workspace && (
|
|
263
|
+
n.success ? (n.workspace && (D(n.workspace.id), n.workspace.organizationId && Pe(n.workspace.organizationId)), j(e)) : n.error?.includes("No workspaces found") ? v("/onboarding/create-workspace") : (n.workspace && (D(n.workspace.id), n.workspace.organizationId && Pe(n.workspace.organizationId)), console.warn("Auth flow warning:", n.error), j(e));
|
|
258
264
|
} catch (t) {
|
|
259
265
|
console.error("Auth flow error (login succeeded):", t), j(e);
|
|
260
266
|
} finally {
|
|
261
|
-
A.current ||
|
|
267
|
+
A.current || Be(!1);
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
270
|
} catch (e) {
|
|
265
271
|
let n = e instanceof Error ? e.message : String(e);
|
|
266
272
|
n.includes("reCAPTCHA") && q(n);
|
|
267
|
-
let r = t.getState().errorCode, i =
|
|
273
|
+
let r = t.getState().errorCode, i = $e(n, r);
|
|
268
274
|
i === "DELETED" ? F(!0) : i === "SUSPENDED" ? I(!0) : i === "OAUTH_ONLY" ? R(!0) : i === "INACTIVE" ? P(!0) : i === "EMAIL_NOT_VERIFIED" && B(!0);
|
|
269
275
|
}
|
|
270
276
|
};
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
let e =
|
|
277
|
+
p(() => {
|
|
278
|
+
if (T) {
|
|
279
|
+
let e = $e(T, E);
|
|
274
280
|
e === "DELETED" ? F(!0) : e === "SUSPENDED" ? I(!0) : e === "OAUTH_ONLY" ? R(!0) : e === "INACTIVE" ? P(!0) : e === "EMAIL_NOT_VERIFIED" && B(!0);
|
|
275
281
|
}
|
|
276
|
-
}, [
|
|
277
|
-
let
|
|
282
|
+
}, [T, E]);
|
|
283
|
+
let tt = () => {
|
|
278
284
|
H(null);
|
|
279
285
|
try {
|
|
280
|
-
|
|
286
|
+
Ie();
|
|
281
287
|
} catch (e) {
|
|
282
288
|
H(e instanceof Error ? e.message : C("auth.login.googleSignInFailed", "Failed to initiate Google sign in"));
|
|
283
289
|
}
|
|
284
|
-
},
|
|
290
|
+
}, nt = () => {
|
|
285
291
|
H(null);
|
|
286
292
|
try {
|
|
287
|
-
|
|
293
|
+
Le();
|
|
288
294
|
} catch (e) {
|
|
289
295
|
H(e instanceof Error ? e.message : C("auth.login.microsoftSignInFailed", "Failed to initiate Microsoft sign in"));
|
|
290
296
|
}
|
|
291
|
-
},
|
|
292
|
-
|
|
297
|
+
}, rt = async () => {
|
|
298
|
+
Je(!0), V(!1);
|
|
293
299
|
try {
|
|
294
|
-
(await
|
|
300
|
+
(await Me((W.email ?? "").trim())).success ? V(!0) : q(C("auth.login.resendFailed", "Failed to resend verification email. Please try again."));
|
|
295
301
|
} catch (e) {
|
|
296
302
|
console.error("Failed to resend verification email:", e), q(e instanceof Error ? e.message : C("auth.login.resendFailed", "Failed to resend verification email. Please try again."));
|
|
297
303
|
} finally {
|
|
298
|
-
|
|
304
|
+
Je(!1);
|
|
299
305
|
}
|
|
300
|
-
},
|
|
306
|
+
}, it = O("google"), $ = O("microsoft"), at = [{
|
|
301
307
|
name: "Google",
|
|
302
|
-
icon:
|
|
303
|
-
onClick:
|
|
304
|
-
disabled: !
|
|
305
|
-
tooltip:
|
|
308
|
+
icon: te,
|
|
309
|
+
onClick: tt,
|
|
310
|
+
disabled: !it,
|
|
311
|
+
tooltip: it ? void 0 : C("auth.login.googleNotConfigured", "Google sign in is not configured")
|
|
306
312
|
}, {
|
|
307
313
|
name: "Microsoft",
|
|
308
|
-
icon:
|
|
309
|
-
onClick:
|
|
314
|
+
icon: u,
|
|
315
|
+
onClick: nt,
|
|
310
316
|
disabled: !$,
|
|
311
317
|
tooltip: $ ? void 0 : C("auth.login.microsoftNotConfigured", "Microsoft sign in is not configured")
|
|
312
318
|
}];
|
|
313
|
-
return Ve ? /* @__PURE__ */
|
|
319
|
+
return Ve ? /* @__PURE__ */ g(oe, {}) : /* @__PURE__ */ _(o, {
|
|
314
320
|
title: C("auth.login.welcomeBack", "Welcome back"),
|
|
315
321
|
subtitle: C("auth.login.enterCredentials", "Enter your credentials to continue"),
|
|
316
322
|
children: [
|
|
317
|
-
/* @__PURE__ */
|
|
323
|
+
/* @__PURE__ */ g(xe, {
|
|
318
324
|
className: "mb-5",
|
|
319
325
|
children: C("auth.login.purpose", "Sign in to reach your workspaces, agents, and settings. Use your email and password, a social provider, or a passwordless magic link.")
|
|
320
326
|
}),
|
|
321
|
-
/* @__PURE__ */
|
|
322
|
-
onSubmit:
|
|
327
|
+
/* @__PURE__ */ _("form", {
|
|
328
|
+
onSubmit: et,
|
|
323
329
|
className: "space-y-5",
|
|
324
330
|
children: [
|
|
325
|
-
|
|
331
|
+
Ze && /* @__PURE__ */ _("div", {
|
|
326
332
|
className: "flex items-start gap-3 p-4 rounded-xl border bg-status-info-bg-subtle border-status-info-border",
|
|
327
|
-
children: [/* @__PURE__ */
|
|
333
|
+
children: [/* @__PURE__ */ g("svg", {
|
|
328
334
|
className: "size-5 flex-shrink-0 mt-0.5 text-text-link",
|
|
329
335
|
fill: "none",
|
|
330
336
|
viewBox: "0 0 24 24",
|
|
331
337
|
stroke: "currentColor",
|
|
332
|
-
children: /* @__PURE__ */
|
|
338
|
+
children: /* @__PURE__ */ g("path", {
|
|
333
339
|
strokeLinecap: "round",
|
|
334
340
|
strokeLinejoin: "round",
|
|
335
341
|
strokeWidth: 2,
|
|
336
342
|
d: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
337
343
|
})
|
|
338
|
-
}), /* @__PURE__ */
|
|
344
|
+
}), /* @__PURE__ */ g("p", {
|
|
339
345
|
className: "text-sm text-status-info-text",
|
|
340
|
-
children:
|
|
346
|
+
children: Ze
|
|
341
347
|
})]
|
|
342
348
|
}),
|
|
343
|
-
K && /* @__PURE__ */
|
|
349
|
+
K && /* @__PURE__ */ _("div", {
|
|
344
350
|
className: "flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border",
|
|
345
|
-
children: [/* @__PURE__ */
|
|
351
|
+
children: [/* @__PURE__ */ g(s, { className: "size-5 flex-shrink-0 mt-0.5 text-status-warning-text" }), /* @__PURE__ */ g("p", {
|
|
346
352
|
className: "text-sm text-status-warning-text",
|
|
347
353
|
children: K
|
|
348
354
|
})]
|
|
349
355
|
}),
|
|
350
|
-
|
|
356
|
+
Qe && /* @__PURE__ */ _("div", {
|
|
351
357
|
className: "flex items-start gap-3 p-4 rounded-xl border bg-status-warning-bg-subtle border-status-warning-border",
|
|
352
|
-
children: [/* @__PURE__ */
|
|
358
|
+
children: [/* @__PURE__ */ g(s, { className: "size-5 flex-shrink-0 mt-0.5 text-status-warning-text" }), /* @__PURE__ */ _("div", {
|
|
353
359
|
className: "flex-1",
|
|
354
360
|
children: [
|
|
355
|
-
/* @__PURE__ */
|
|
361
|
+
/* @__PURE__ */ g("p", {
|
|
356
362
|
className: "text-sm font-medium text-status-warning-text",
|
|
357
363
|
children: C("auth.login.duplicateAccount", "This account is already signed in.")
|
|
358
364
|
}),
|
|
359
|
-
/* @__PURE__ */
|
|
365
|
+
/* @__PURE__ */ g("p", {
|
|
360
366
|
className: "mt-1 text-sm text-status-warning-text/80",
|
|
361
367
|
children: C("auth.login.duplicateAccountHint", "Please use a different account, or go back and switch to this account from the profile menu.")
|
|
362
368
|
}),
|
|
363
|
-
(Z.current || X) && /* @__PURE__ */
|
|
369
|
+
(Z.current || X) && /* @__PURE__ */ _("button", {
|
|
364
370
|
type: "button",
|
|
365
|
-
onClick: () =>
|
|
371
|
+
onClick: () => v(decodeURIComponent(Z.current || X || "/dashboard")),
|
|
366
372
|
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",
|
|
367
|
-
children: [/* @__PURE__ */
|
|
373
|
+
children: [/* @__PURE__ */ g("svg", {
|
|
368
374
|
className: "size-4",
|
|
369
375
|
fill: "none",
|
|
370
376
|
viewBox: "0 0 24 24",
|
|
371
377
|
stroke: "currentColor",
|
|
372
|
-
children: /* @__PURE__ */
|
|
378
|
+
children: /* @__PURE__ */ g("path", {
|
|
373
379
|
strokeLinecap: "round",
|
|
374
380
|
strokeLinejoin: "round",
|
|
375
381
|
strokeWidth: 2,
|
|
@@ -380,48 +386,48 @@ function x() {
|
|
|
380
386
|
]
|
|
381
387
|
})]
|
|
382
388
|
}),
|
|
383
|
-
|
|
389
|
+
T && !K && !Qe && /* @__PURE__ */ _("div", {
|
|
384
390
|
className: `flex items-start gap-3 p-4 rounded-xl border ${N || L || z ? "bg-status-warning-bg-subtle border-status-warning-border" : "bg-status-error-bg-subtle border-status-error-border"}`,
|
|
385
|
-
children: [/* @__PURE__ */
|
|
391
|
+
children: [/* @__PURE__ */ g(s, { className: `size-5 flex-shrink-0 mt-0.5 ${N || L || z ? "text-status-warning-text" : "text-status-error-text"}` }), /* @__PURE__ */ _("div", {
|
|
386
392
|
className: "flex-1",
|
|
387
393
|
children: [
|
|
388
|
-
/* @__PURE__ */
|
|
394
|
+
/* @__PURE__ */ g("p", {
|
|
389
395
|
className: `text-sm ${N || L || z ? "text-status-warning-text" : "text-status-error-text"}`,
|
|
390
|
-
children:
|
|
396
|
+
children: Ge ? C("auth.login.accountDeleted", "This account has been permanently deleted and cannot be recovered.") : Ke ? C("auth.login.accountSuspended", "Your account has been suspended. Please contact support for assistance.") : N ? C("auth.login.accountDeactivated", "Your account has been deactivated.") : L ? C("auth.login.oauthOnly", "This account uses OAuth sign-in. Please use Google or Microsoft to sign in.") : z ? C("auth.login.emailNotVerified", "Please verify your email before signing in. Check your inbox for the verification link.") : T
|
|
391
397
|
}),
|
|
392
|
-
N && /* @__PURE__ */
|
|
398
|
+
N && /* @__PURE__ */ g("button", {
|
|
393
399
|
type: "button",
|
|
394
|
-
onClick: () =>
|
|
400
|
+
onClick: () => v("/auth/reactivate", { state: { email: W.email } }),
|
|
395
401
|
className: "mt-2 text-sm font-semibold text-status-warning-text hover:text-status-warning-text underline underline-offset-2",
|
|
396
402
|
children: C("auth.login.clickReactivate", "Click here to reactivate your account")
|
|
397
403
|
}),
|
|
398
|
-
z && /* @__PURE__ */
|
|
404
|
+
z && /* @__PURE__ */ g("div", {
|
|
399
405
|
className: "mt-3",
|
|
400
|
-
children:
|
|
406
|
+
children: Ye ? /* @__PURE__ */ _("div", {
|
|
401
407
|
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
402
|
-
children: [/* @__PURE__ */
|
|
408
|
+
children: [/* @__PURE__ */ g("svg", {
|
|
403
409
|
className: "size-4",
|
|
404
410
|
fill: "none",
|
|
405
411
|
viewBox: "0 0 24 24",
|
|
406
412
|
stroke: "currentColor",
|
|
407
|
-
children: /* @__PURE__ */
|
|
413
|
+
children: /* @__PURE__ */ g("path", {
|
|
408
414
|
strokeLinecap: "round",
|
|
409
415
|
strokeLinejoin: "round",
|
|
410
416
|
strokeWidth: 2,
|
|
411
417
|
d: "M5 13l4 4L19 7"
|
|
412
418
|
})
|
|
413
419
|
}), C("auth.login.verificationSent", "Verification email sent! Please check your inbox.")]
|
|
414
|
-
}) : /* @__PURE__ */
|
|
420
|
+
}) : /* @__PURE__ */ g("button", {
|
|
415
421
|
type: "button",
|
|
416
|
-
onClick:
|
|
417
|
-
disabled:
|
|
422
|
+
onClick: rt,
|
|
423
|
+
disabled: qe,
|
|
418
424
|
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",
|
|
419
|
-
children:
|
|
425
|
+
children: qe ? /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(d, { className: "size-4 animate-spin" }), C("auth.login.sending", "Sending...")] }) : /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g("svg", {
|
|
420
426
|
className: "size-4",
|
|
421
427
|
fill: "none",
|
|
422
428
|
viewBox: "0 0 24 24",
|
|
423
429
|
stroke: "currentColor",
|
|
424
|
-
children: /* @__PURE__ */
|
|
430
|
+
children: /* @__PURE__ */ g("path", {
|
|
425
431
|
strokeLinecap: "round",
|
|
426
432
|
strokeLinejoin: "round",
|
|
427
433
|
strokeWidth: 2,
|
|
@@ -430,35 +436,35 @@ function x() {
|
|
|
430
436
|
}), C("auth.login.resendVerification", "Resend verification email")] })
|
|
431
437
|
})
|
|
432
438
|
}),
|
|
433
|
-
L && /* @__PURE__ */
|
|
439
|
+
L && /* @__PURE__ */ _("div", {
|
|
434
440
|
className: "mt-3 flex flex-col gap-2",
|
|
435
|
-
children: [
|
|
441
|
+
children: [O("google") && /* @__PURE__ */ _("button", {
|
|
436
442
|
type: "button",
|
|
437
|
-
onClick:
|
|
443
|
+
onClick: tt,
|
|
438
444
|
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",
|
|
439
|
-
children: [/* @__PURE__ */
|
|
440
|
-
}),
|
|
445
|
+
children: [/* @__PURE__ */ g(te, { className: "size-5" }), C("auth.login.signInWithGoogle", "Sign in with Google")]
|
|
446
|
+
}), O("microsoft") && /* @__PURE__ */ _("button", {
|
|
441
447
|
type: "button",
|
|
442
|
-
onClick:
|
|
448
|
+
onClick: nt,
|
|
443
449
|
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",
|
|
444
|
-
children: [/* @__PURE__ */
|
|
450
|
+
children: [/* @__PURE__ */ g(u, { className: "size-5" }), C("auth.login.signInWithMicrosoft", "Sign in with Microsoft")]
|
|
445
451
|
})]
|
|
446
452
|
})
|
|
447
453
|
]
|
|
448
454
|
})]
|
|
449
455
|
}),
|
|
450
|
-
/* @__PURE__ */
|
|
456
|
+
/* @__PURE__ */ _("div", {
|
|
451
457
|
className: "space-y-2",
|
|
452
|
-
children: [/* @__PURE__ */
|
|
458
|
+
children: [/* @__PURE__ */ g("label", {
|
|
453
459
|
htmlFor: "email",
|
|
454
460
|
className: "block text-sm font-medium text-[var(--color-text-primary)]",
|
|
455
461
|
children: C("auth.login.emailLabel", "Email")
|
|
456
|
-
}), /* @__PURE__ */
|
|
462
|
+
}), /* @__PURE__ */ _("div", {
|
|
457
463
|
className: "relative",
|
|
458
|
-
children: [/* @__PURE__ */
|
|
464
|
+
children: [/* @__PURE__ */ g("div", {
|
|
459
465
|
className: "absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none",
|
|
460
|
-
children: /* @__PURE__ */
|
|
461
|
-
}), /* @__PURE__ */
|
|
466
|
+
children: /* @__PURE__ */ g(c, { className: "size-5 text-[var(--color-text-tertiary)]" })
|
|
467
|
+
}), /* @__PURE__ */ g("input", {
|
|
462
468
|
id: "email",
|
|
463
469
|
type: "email",
|
|
464
470
|
autoComplete: "email",
|
|
@@ -475,20 +481,20 @@ function x() {
|
|
|
475
481
|
})]
|
|
476
482
|
})]
|
|
477
483
|
}),
|
|
478
|
-
/* @__PURE__ */
|
|
484
|
+
/* @__PURE__ */ _("div", {
|
|
479
485
|
className: "space-y-2",
|
|
480
|
-
children: [/* @__PURE__ */
|
|
486
|
+
children: [/* @__PURE__ */ g("label", {
|
|
481
487
|
htmlFor: "password",
|
|
482
488
|
className: "block text-sm font-medium text-[var(--color-text-primary)]",
|
|
483
489
|
children: C("auth.login.passwordLabel", "Password")
|
|
484
|
-
}), /* @__PURE__ */
|
|
490
|
+
}), /* @__PURE__ */ _("div", {
|
|
485
491
|
className: "relative",
|
|
486
492
|
children: [
|
|
487
|
-
/* @__PURE__ */
|
|
493
|
+
/* @__PURE__ */ g("div", {
|
|
488
494
|
className: "absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none",
|
|
489
|
-
children: /* @__PURE__ */
|
|
495
|
+
children: /* @__PURE__ */ g(ne, { className: "size-5 text-[var(--color-text-tertiary)]" })
|
|
490
496
|
}),
|
|
491
|
-
/* @__PURE__ */
|
|
497
|
+
/* @__PURE__ */ g("input", {
|
|
492
498
|
id: "password",
|
|
493
499
|
type: M ? "text" : "password",
|
|
494
500
|
autoComplete: "current-password",
|
|
@@ -503,23 +509,23 @@ function x() {
|
|
|
503
509
|
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",
|
|
504
510
|
placeholder: C("auth.login.passwordPlaceholder", "Enter your password")
|
|
505
511
|
}),
|
|
506
|
-
/* @__PURE__ */
|
|
512
|
+
/* @__PURE__ */ g("button", {
|
|
507
513
|
type: "button",
|
|
508
|
-
onClick: () =>
|
|
514
|
+
onClick: () => We(!M),
|
|
509
515
|
"aria-label": M ? "Hide password" : "Show password",
|
|
510
516
|
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",
|
|
511
|
-
children:
|
|
517
|
+
children: g(M ? ee : l, { className: "size-5" })
|
|
512
518
|
})
|
|
513
519
|
]
|
|
514
520
|
})]
|
|
515
521
|
}),
|
|
516
|
-
/* @__PURE__ */
|
|
522
|
+
/* @__PURE__ */ _("div", {
|
|
517
523
|
className: "flex items-center justify-between",
|
|
518
|
-
children: [/* @__PURE__ */
|
|
524
|
+
children: [/* @__PURE__ */ _("label", {
|
|
519
525
|
className: "flex items-center gap-2 cursor-pointer group",
|
|
520
|
-
children: [/* @__PURE__ */
|
|
526
|
+
children: [/* @__PURE__ */ _("div", {
|
|
521
527
|
className: "relative",
|
|
522
|
-
children: [/* @__PURE__ */
|
|
528
|
+
children: [/* @__PURE__ */ g("input", {
|
|
523
529
|
type: "checkbox",
|
|
524
530
|
"aria-label": C("auth.login.rememberMe", "Remember me"),
|
|
525
531
|
checked: W.rememberMe,
|
|
@@ -535,101 +541,101 @@ function x() {
|
|
|
535
541
|
}
|
|
536
542
|
},
|
|
537
543
|
className: "sr-only peer"
|
|
538
|
-
}), /* @__PURE__ */
|
|
544
|
+
}), /* @__PURE__ */ g("div", {
|
|
539
545
|
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",
|
|
540
|
-
children: /* @__PURE__ */
|
|
546
|
+
children: /* @__PURE__ */ g("svg", {
|
|
541
547
|
className: "size-full text-white opacity-0 peer-checked:opacity-100 transition-opacity p-0.5",
|
|
542
548
|
fill: "none",
|
|
543
549
|
viewBox: "0 0 24 24",
|
|
544
550
|
stroke: "currentColor",
|
|
545
551
|
strokeWidth: 3,
|
|
546
|
-
children: /* @__PURE__ */
|
|
552
|
+
children: /* @__PURE__ */ g("path", {
|
|
547
553
|
strokeLinecap: "round",
|
|
548
554
|
strokeLinejoin: "round",
|
|
549
555
|
d: "M5 13l4 4L19 7"
|
|
550
556
|
})
|
|
551
557
|
})
|
|
552
558
|
})]
|
|
553
|
-
}), /* @__PURE__ */
|
|
559
|
+
}), /* @__PURE__ */ g("span", {
|
|
554
560
|
className: "text-sm text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)] transition-colors",
|
|
555
561
|
children: C("auth.login.rememberMe", "Remember me")
|
|
556
562
|
})]
|
|
557
|
-
}), /* @__PURE__ */
|
|
563
|
+
}), /* @__PURE__ */ g("button", {
|
|
558
564
|
type: "button",
|
|
559
|
-
onClick: () =>
|
|
565
|
+
onClick: () => v("/auth/reset-password"),
|
|
560
566
|
className: "text-sm font-medium text-text-link hover:text-text-linkHover transition-colors",
|
|
561
567
|
children: C("auth.login.forgotPassword", "Forgot password?")
|
|
562
568
|
})]
|
|
563
569
|
}),
|
|
564
|
-
/* @__PURE__ */
|
|
570
|
+
/* @__PURE__ */ g(be, {
|
|
565
571
|
type: "submit",
|
|
566
|
-
disabled:
|
|
572
|
+
disabled: w || k,
|
|
567
573
|
variant: "default",
|
|
568
574
|
size: "lg",
|
|
569
575
|
className: "w-full font-semibold",
|
|
570
|
-
children:
|
|
576
|
+
children: w || k ? /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(d, { className: "size-5 animate-spin" }), /* @__PURE__ */ g("span", { children: k ? C("auth.login.settingUpWorkspace", "Setting up workspace...") : C("auth.login.signingIn", "Signing in...") })] }) : C("auth.login.signIn", "Sign in")
|
|
571
577
|
})
|
|
572
578
|
]
|
|
573
579
|
}),
|
|
574
|
-
/* @__PURE__ */
|
|
580
|
+
/* @__PURE__ */ _("div", {
|
|
575
581
|
className: "relative my-8",
|
|
576
|
-
children: [/* @__PURE__ */
|
|
582
|
+
children: [/* @__PURE__ */ g("div", {
|
|
577
583
|
className: "absolute inset-0 flex items-center",
|
|
578
|
-
children: /* @__PURE__ */
|
|
579
|
-
}), /* @__PURE__ */
|
|
584
|
+
children: /* @__PURE__ */ g("div", { className: "w-full border-t border-[var(--color-border-default)]" })
|
|
585
|
+
}), /* @__PURE__ */ g("div", {
|
|
580
586
|
className: "relative flex justify-center",
|
|
581
|
-
children: /* @__PURE__ */
|
|
587
|
+
children: /* @__PURE__ */ g("span", {
|
|
582
588
|
className: "px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]",
|
|
583
589
|
children: C("auth.login.orContinueWith", "or continue with")
|
|
584
590
|
})
|
|
585
591
|
})]
|
|
586
592
|
}),
|
|
587
|
-
|
|
593
|
+
Xe && /* @__PURE__ */ _("div", {
|
|
588
594
|
className: "flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border",
|
|
589
|
-
children: [/* @__PURE__ */
|
|
595
|
+
children: [/* @__PURE__ */ g(s, { className: "size-5 flex-shrink-0 mt-0.5 text-status-error-text" }), /* @__PURE__ */ g("p", {
|
|
590
596
|
className: "text-sm text-status-error-text",
|
|
591
|
-
children:
|
|
597
|
+
children: Xe
|
|
592
598
|
})]
|
|
593
599
|
}),
|
|
594
|
-
/* @__PURE__ */
|
|
600
|
+
/* @__PURE__ */ g("div", {
|
|
595
601
|
className: "grid grid-cols-3 gap-3",
|
|
596
|
-
children:
|
|
602
|
+
children: at.map((e) => /* @__PURE__ */ _("button", {
|
|
597
603
|
type: "button",
|
|
598
604
|
onClick: e.onClick,
|
|
599
605
|
disabled: e.disabled,
|
|
600
606
|
title: e.tooltip,
|
|
601
607
|
"aria-label": e.name,
|
|
602
608
|
className: `flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${e.disabled ? "border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50" : "border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]"}`,
|
|
603
|
-
children: [/* @__PURE__ */
|
|
609
|
+
children: [/* @__PURE__ */ g(e.icon, { className: `size-5 ${e.disabled ? "opacity-50" : ""}` }), /* @__PURE__ */ g("span", {
|
|
604
610
|
className: `sr-only sm:not-sr-only text-sm font-medium transition-colors ${e.disabled ? "text-[var(--color-text-disabled)]" : "text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]"}`,
|
|
605
611
|
children: e.name
|
|
606
612
|
})]
|
|
607
613
|
}, e.name))
|
|
608
614
|
}),
|
|
609
|
-
/* @__PURE__ */
|
|
615
|
+
/* @__PURE__ */ g("div", {
|
|
610
616
|
className: "mt-4 text-center",
|
|
611
|
-
children: /* @__PURE__ */
|
|
617
|
+
children: /* @__PURE__ */ _("button", {
|
|
612
618
|
type: "button",
|
|
613
|
-
onClick: () =>
|
|
619
|
+
onClick: () => v("/auth/magic-link"),
|
|
614
620
|
className: "text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors",
|
|
615
621
|
children: [
|
|
616
622
|
C("auth.login.preferPasswordless", "Prefer passwordless login?"),
|
|
617
623
|
" ",
|
|
618
|
-
/* @__PURE__ */
|
|
624
|
+
/* @__PURE__ */ g("span", {
|
|
619
625
|
className: "font-semibold text-text-link hover:text-text-linkHover",
|
|
620
626
|
children: C("auth.login.signInMagicLink", "Sign in with Magic Link")
|
|
621
627
|
})
|
|
622
628
|
]
|
|
623
629
|
})
|
|
624
630
|
}),
|
|
625
|
-
/* @__PURE__ */
|
|
631
|
+
/* @__PURE__ */ _("p", {
|
|
626
632
|
className: "mt-6 text-center text-sm text-[var(--color-text-secondary)]",
|
|
627
633
|
children: [
|
|
628
634
|
C("auth.login.dontHaveAccount", "Don't have an account?"),
|
|
629
635
|
" ",
|
|
630
|
-
/* @__PURE__ */
|
|
636
|
+
/* @__PURE__ */ g("button", {
|
|
631
637
|
type: "button",
|
|
632
|
-
onClick: () =>
|
|
638
|
+
onClick: () => v("/auth/register"),
|
|
633
639
|
className: "font-semibold text-text-link hover:text-text-linkHover transition-colors",
|
|
634
640
|
children: C("auth.login.signUp", "Sign up")
|
|
635
641
|
})
|
|
@@ -639,6 +645,6 @@ function x() {
|
|
|
639
645
|
});
|
|
640
646
|
}
|
|
641
647
|
//#endregion
|
|
642
|
-
export {
|
|
648
|
+
export { b as LoginPage };
|
|
643
649
|
|
|
644
650
|
//# sourceMappingURL=LoginPage.js.map
|
|
@@ -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\";\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\";\nimport { resolveGatewayUrls } from \"@burdenoff/fe-libs/shared/config/gatewayUrls\";\nimport {\n beginSsoNavigation,\n buildSsoDepositUrl,\n isSsoNavigationPending,\n markSsoDepositPending,\n mintSsoSessionTokens,\n} from \"@burdenoff/fe-libs/shared/utils\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\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(\n async (intendedPath: string): Promise<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 // BOFF-4463: mint a DEDICATED refresh-token lineage for the bridge\n // host. Copying this origin's pair verbatim meant the bridge (and\n // every product it later relayed to) shared ONE rotating refresh\n // token — the first origin to refresh revoked it for all others,\n // surfacing as \"products log me out after minutes/hours\". Falls back\n // to the legacy copy if the mutation is unreachable (rollout gap /\n // transient failure) so SSO keeps working.\n let depositTokens: Record<string, unknown> = {\n ...tokens,\n workspaceToken,\n };\n if (tokens.refreshToken) {\n const { globalBaseUrl } = resolveGatewayUrls();\n const { tokens: minted } = await mintSsoSessionTokens(\n `${globalBaseUrl}/global/graphql`,\n tokens.refreshToken,\n );\n if (minted) {\n depositTokens = { ...minted, workspaceToken };\n }\n }\n\n markSsoDepositPending(profileId);\n return buildSsoDepositUrl(bridgeUrl.origin, absoluteIntended, {\n profileId,\n tokens: JSON.stringify(depositTokens),\n user: JSON.stringify(user),\n profiles: JSON.stringify(getProfiles()),\n });\n } catch {\n return intendedPath;\n }\n },\n [],\n );\n\n // Once a cross-document post-login redirect is initiated, the login page\n // must FREEZE: resetting isCompletingAuth re-runs the auto-redirect effect\n // and any state churn schedules same-document navigations that cancel the\n // provisional deposit hop (BOFF-4463).\n const redirectInitiatedRef = useRef(false);\n\n const redirectAfterLogin = useCallback(\n (path: string) => {\n if (typeof window === \"undefined\") {\n navigate(path);\n return;\n }\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 redirectInitiatedRef.current = true;\n void (async () => {\n const target = await buildPostLoginUrl(path);\n if (target.includes(\"/auth/sso-deposit\")) {\n // Cross-document deposit hop: suppress same-document navigators\n // while it is provisional, and retry once if it still gets\n // cancelled — on a committed navigation this document unloads and\n // the timer never fires.\n beginSsoNavigation();\n window.location.assign(target);\n window.setTimeout(() => {\n beginSsoNavigation();\n window.location.replace(target);\n }, 2500);\n } else {\n window.location.assign(target);\n }\n })();\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 // Tell the user WHY they are back on the login page. `session-expired` is\n // set by the shell's forced logout (expired session whose refresh failed);\n // `logout` covers explicit sign-outs and the SSO-clear fallback (which can\n // rewrite a session-expired redirect's reason on the cross-product bounce).\n // Without this the auto-logout looked like a silent, unexplained kick-out.\n const reasonParam = searchParams.get(\"reason\");\n useEffect(() => {\n if (reasonParam === \"session-expired\") {\n setInfoMessage(\n tr(\n \"auth.login.sessionExpired\",\n \"Your session has expired. Please sign in again.\",\n ),\n );\n } else if (reasonParam === \"logout\") {\n setInfoMessage(tr(\"auth.login.signedOut\", \"You have been signed out.\"));\n }\n }, [reasonParam, tr]);\n\n useEffect(() => {\n if (!initialReturnToRef.current && returnTo) {\n initialReturnToRef.current = returnTo;\n }\n }, [returnTo]);\n\n useEffect(() => {\n if (isAddAccountMode || isCompletingAuth) return;\n // A cross-document SSO navigation (deposit/relay hop) is already in\n // flight — issuing another redirect here would cancel it (BOFF-4463).\n if (redirectInitiatedRef.current || isSsoNavigationPending()) 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 // Keep the completing state (spinner) once a redirect is in\n // flight: resetting it re-arms the auto-redirect effect whose\n // competing navigation cancels the deposit hop (BOFF-4463). The\n // page is about to unload anyway.\n if (!redirectInitiatedRef.current) {\n setIsCompletingAuth(false);\n }\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 <PagePurpose className=\"mb-5\">\n {tr(\n \"auth.login.purpose\",\n \"Sign in to reach your workspaces, agents, and settings. Use your email and password, a social provider, or a passwordless magic link.\",\n )}\n </PagePurpose>\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 variant=\"default\"\n size=\"lg\"\n className=\"w-full 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 ${\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":";;;;;;;;;;;;;;;;;;;;;AA4DA,SAAS,KAAyB;AAChC,KAAI,OAAO,SAAW,IAAa,QAAO;CAC1C,IAAM,IAAI;AAGV,KAAI,OAAO,EAAE,WAAW,oBAAqB,WAC3C,KAAI;AACF,MAAI,EAAE,UAAU,kBAAkB,CAAE,QAAO;SACrC;AAUV,QANA,GACE,OAAO,YAAc,OACrB,YAAY,KAAK,UAAU,aAAa,GAAG;;AAO/C,SAAS,GAAiB,GAAwB;AAChD,KAAI,OAAO,SAAW,IAAa,QAAO;AAC1C,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAQ,OAAO,SAAS,OAAO;AACnD,SAAO,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI;SACpC;AACN,SAAO;;;AAIX,SAAS,GAAoB,GAAwB;AACnD,KAAI;EACF,IAAM,IAAM,EAAM,MAAM,IAAI,CAAC,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,EAC/D,IAAS,EAAI,OAAO,KAAK,KAAK,EAAI,SAAS,EAAE,GAAG,GAAG,IAAI,EACvD,IAAU,KAAK,MAAM,KAAK,EAAO,CAAC;AACxC,SAAO,OAAO,EAAQ,OAAQ,YAAY,EAAQ,MAAM,KAAK,KAAK,GAAG;SAC/D;AACN,SAAO;;;AAIX,SAAS,GAAyB,GAAwC;AACxE,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EAEF,IAAM,IAAM,IAAI,IADA,mBAAmB,EAAS,EACf,OAAO,SAAS,OAAO;AAEpD,SADI,EAAI,WAAW,OAAO,SAAS,SAC5B,EAAoB,GAAK,EAAE,eAAe,IAAO,CAAC,GADP;SAE5C;AACN,SAAO;;;AAIX,SAAS,EACP,GACA,GACQ;AACR,MAAK,IAAM,KAAS;EAAC;EAAc;EAAc;EAAgB,CAC/D,GAAI,aAAa,OAAO,EAAM;AAGhC,KAAI,EAAQ,cACV,QAAO,EAAI,UAAU;CAGvB,IAAM,IAAS,EAAI,aAAa,UAAU;AAC1C,QAAO,GAAG,EAAI,WAAW,IAAS,IAAI,MAAW,KAAK,EAAI;;AAG5D,SAAS,EAAmB,GAA0B;AACpD,KAAI;EAEF,IAAM,IAAM,IAAI,IADA,mBAAmB,EAAS,EACf,OAAO,SAAS,OAAO;AAIpD,SAAO,EAAoB,GAAK,EAAE,eADZ,EAAI,WAAW,OAAO,SAAS,QACW,CAAC;SAC3D;AACN,SAAO;;;AAQX,SAAgB,IAAY;CAC1B,IAAM,IAAW,IAAa,EACxB,IAAW,IAAa,EACxB,CAAC,KAAgB,IAAiB,EAClC,EAAE,UAAM,IAAS,EACjB,IAAK,GACR,GAAa,MAA6B;EACzC,IAAM,IAAa,GAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEzC,CAAC,GAAE,CACJ,EACK,EAAE,YAAQ,eAAW,UAAO,eAAW,2BAC3C,GAAmB,EACf,EAAE,yBAAqB,GAAiB,EACxC,EAAE,kBAAc,wBAAoB,IAAkB,EACtD,EAAE,6BAAyB,GAAS,EACpC,EAAE,yBAAqB,4BAAwB,4BACnD,GAAoB,EAChB,IAAc,GAAc,MAAU,EAAM,YAAY,EACxD,EAAE,kBAAkB,OAAqB,IAAc,EACvD,CAAC,GAAkB,KAAuB,EAAS,GAAM,EACzD,CAAC,IAAkB,MAAuB,EAAS,GAAM,EAmBzD,IAAoB,EACxB,OAAO,MAA0C;AAK/C,MAJI,OAAO,SAAW,OAIlB,IAAe,CAAE,QAAO;AAC5B,MAAI;GACF,IAAM,IAAS,IAAyB,EAClC,IAAY,IAAI,IAAI,EAAO,IAAI,EAC/B,IAAgB,OAAO,SAAS;AACtC,OAAI,EAAU,WAAW,EAAe,QAAO;GAK/C,IAAM,IAAmB,IAAI,IAAI,GAAc,EAAc,CAAC,MAExD,IAAY,IAAwB;AAC1C,OAAI,CAAC,EAAW,QAAO;GACvB,IAAM,IAAS,EAAiB,EAAU,EACpC,IAAO,EAAe,EAAU;AACtC,OAAI,CAAC,KAAU,CAAC,EAAM,QAAO;GAC7B,IAAM,IAAiB,GAAyB,EAAU,EAStD,IAAyC;IAC3C,GAAG;IACH;IACD;AACD,OAAI,EAAO,cAAc;IACvB,IAAM,EAAE,qBAAkB,IAAoB,EACxC,EAAE,QAAQ,MAAW,MAAM,GAC/B,GAAG,EAAc,kBACjB,EAAO,aACR;AACD,IAAI,MACF,IAAgB;KAAE,GAAG;KAAQ;KAAgB;;AAKjD,UADA,GAAsB,EAAU,EACzB,GAAmB,EAAU,QAAQ,GAAkB;IAC5D;IACA,QAAQ,KAAK,UAAU,EAAc;IACrC,MAAM,KAAK,UAAU,EAAK;IAC1B,UAAU,KAAK,UAAU,GAAa,CAAC;IACxC,CAAC;UACI;AACN,UAAO;;IAGX,EAAE,CACH,EAMK,IAAuB,GAAO,GAAM,EAEpC,IAAqB,GACxB,MAAiB;AAChB,MAAI,OAAO,SAAW,KAAa;AACjC,KAAS,EAAK;AACd;;AAEF,MAAI;AACF,kBAAe,QAAQ,2BAA2B,EAAK;UACjD;AAMR,MAAI,IAAe,EAAE;AACnB,KAAS,GAAiB,EAAK,CAAC;AAChC;;AAGF,EADA,EAAqB,UAAU,KACzB,YAAY;GAChB,IAAM,IAAS,MAAM,EAAkB,EAAK;AAC5C,GAAI,EAAO,SAAS,oBAAoB,IAKtC,IAAoB,EACpB,OAAO,SAAS,OAAO,EAAO,EAC9B,OAAO,iBAAiB;AAEtB,IADA,IAAoB,EACpB,OAAO,SAAS,QAAQ,EAAO;MAC9B,KAAK,IAER,OAAO,SAAS,OAAO,EAAO;MAE9B;IAEN,CAAC,GAAU,EAAkB,CAC9B,EACK,CAAC,GAAc,MAAmB,EAAS,GAAM,EACjD,CAAC,GAAmB,KAAwB,EAAS,GAAM,EAC3D,CAAC,IAAkB,KAAuB,EAAS,GAAM,EACzD,CAAC,IAAoB,KAAyB,EAAS,GAAM,EAC7D,CAAC,GAAoB,KAAyB,EAAS,GAAM,EAC7D,CAAC,GAAoB,KAAyB,EAAS,GAAM,EAC7D,CAAC,IAAyB,MAA8B,EAAS,GAAM,EACvE,CAAC,IAAe,KAAoB,EAAS,GAAM,EACnD,CAAC,IAAY,KAAiB,EAAwB,KAAK,EAC3D,CAAC,IAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAU,KAAe,EAE9B;EACA,OAAO;EACP,UAAU;EACV,YAAY;EACb,CAAC,EACI,CAAC,GAAiB,KAAsB,EAAwB,KAAK,EACrE,CAAC,IAAoB,KAAyB,EAAS,GAAM,EAG7D,IAAmB,EAAa,IAAI,OAAO,KAAK,eAChD,IAAW,EAAa,IAAI,WAAW,EACvC,IAAqB,GAAsB,EAAS,EAOpD,IAAc,EAAa,IAAI,SAAS;AAqE9C,CApEA,QAAgB;AACd,EAAI,MAAgB,oBAClB,EACE,EACE,6BACA,kDACD,CACF,GACQ,MAAgB,YACzB,EAAe,EAAG,wBAAwB,4BAA4B,CAAC;IAExE,CAAC,GAAa,EAAG,CAAC,EAErB,QAAgB;AACd,EAAI,CAAC,EAAmB,WAAW,MACjC,EAAmB,UAAU;IAE9B,CAAC,EAAS,CAAC,EAEd,QAAgB;AAId,MAHI,KAAoB,KAGpB,EAAqB,WAAW,IAAwB,CAAE;EAE9D,IAAM,IAAY,IAAwB;AAC1C,MAAI,GAAW;GACb,IAAM,IAAS,EAAiB,EAAU,EACpC,IAAO,EAAe,EAAU;AACtC,OACE,GAAQ,eACR,KACA,GAAoB,EAAO,YAAY,EACvC;AAEA,IADA,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAClD,EAAmB,GAAyB,EAAS,IAAI,IAAI;AAC7D;;;EAIJ,IAAM,IAAS,IAAmB;AAClC,EAAI,MACF,GAAoB,GAAK,EACzB,OAAO,SAAS,QAAQ,EAAO;IAEhC;EAAC;EAAkB;EAAkB;EAAoB;EAAS,CAAC,EAGtE,QAAgB;AACd,MAAI;AAEF,gBAAa,WAAW,sBAAsB;GAE9C,IAAM,IAAkB,aAAa,QAAQ,mBAAmB;AAEhE,GAAI,KACF,GAAa,OAAU;IACrB,GAAG;IACH,OAAO;IACP,YAAY;IACb,EAAE;WAEE,GAAO;AACd,WAAQ,KAAK,gDAAgD,EAAM;;IAEpE,EAAE,CAAC,EAGN,QAAgB;EACd,IAAM,IAAQ,EAAS;AAQvB,EAPI,GAAO,WACT,EAAe,EAAM,QAAQ,EAE3B,GAAO,SACT,GAAa,OAAU;GAAE,GAAG;GAAM,OAAO,EAAM;GAAO,EAAE,EAGtD,KACF,OAAO,QAAQ,aAAa,EAAE,EAAE,SAAS,MAAM;IAEhD,CAAC,EAAS,MAAM,CAAC;CAKpB,IAAM,MACJ,GACA,MAOU;AAEV,MAAI,EACF,SAAQ,GAAR;GACE,KAAK,kBACH,QAAO;GACT,KAAK,oBACH,QAAO;GACT,KAAK,qBACH,QAAO;GACT,KAAK,mBACH,QAAO;GACT,KAAK,qBACH,QAAO;GACT,QACE;;AAKN,MAAI,CAAC,EAAK,QAAO;EACjB,IAAM,IAAW,EAAI,aAAa;AAgClC,SA7BE,EAAS,SAAS,UAAU,IAC5B,EAAS,SAAS,sBAAsB,GAEjC,YAEL,EAAS,SAAS,YAAY,GACzB,cAGP,EAAS,SAAS,iBAAiB,IACnC,EAAS,SAAS,aAAa,IAC/B,EAAS,SAAS,aAAa,GAExB,eAGP,EAAS,SAAS,WAAW,IAC7B,EAAS,SAAS,cAAc,IAChC,EAAS,SAAS,aAAa,GAExB,aAGP,EAAS,SAAS,oBAAoB,IACtC,EAAS,SAAS,qBAAqB,IACvC,EAAS,SAAS,qBAAqB,GAEhC,uBAEF;;AA0BT,CAtBA,QAAgB;EACd,IAAM,IAAe,EAAa,IAAI,QAAQ;AAO9C,MANI,KACF,GAAa,OAAU;GAAE,GAAG;GAAM,OAAO;GAAc,EAAE,EAIzC,EAAa,IAAI,UAAU,KAC3B,QAAQ;AACxB,KACE,EACE,6BACA,kDACD,CACF;GAED,IAAM,IAAS,OAAO,SAAS;AAC/B,UAAO,QAAQ,aAAa,EAAE,EAAE,SAAS,OAAO,EAAO;;IAExD,CAAC,GAAc,EAAG,CAAC,EAGtB,QAAgB;AACd,EAAI,KACF,EAAS,mBAAmB;IAE7B,CAAC,GAAa,EAAS,CAAC;CAE3B,IAAM,KAAe,OAAO,MAAuB;AASjD,EARA,EAAE,gBAAgB,EAClB,EAAqB,GAAM,EAC3B,EAAoB,GAAM,EAC1B,EAAsB,GAAM,EAC5B,EAAsB,GAAM,EAC5B,EAAsB,GAAM,EAC5B,EAAsB,GAAM,EAC5B,EAAiB,GAAM,EACvB,EAAmB,KAAK;EAGxB,IAAM,KAAgB,EAAS,SAAS,IAAI,MAAM;AAClD,MAAI,CAAC,GAAc;AACjB,KACE,EACE,mCACA,sCACD,CACF;AACD;;AAIF,MAAI,EAAa,SAAS,IAAI,IAExB,CADe,6BACH,KAAK,EAAa,EAAE;AAClC,KACE,EAAG,2BAA2B,qCAAqC,CACpE;AACD;;AAIJ,MAAI,CAAC,EAAS,YAAY,EAAS,SAAS,SAAS,GAAG;AACtD,KACE,EAAG,4BAA4B,6BAA6B,CAC7D;AACD;;AAIF,MAAI,KACuB,GAAa,CACG,MACtC,MAAM,EAAE,MAAM,aAAa,KAAK,EAAa,aAAa,CAC5D,EACoB;AAEnB,GADA,EAAsB,GAAK,EAC3B,EAAmB,KAAK;AACxB;;AAGJ,IAAsB,GAAM;AAE5B,MAAI;GAEF,IAAM,IAAiB,MAAM,GAAiB,QAAQ,EAEhD,IAAS,MAAM,GAAO;IAC1B,OAAO;IACP,UAAU,EAAS;IACnB,YAAY,EAAS;IACrB,GAAI,IAAiB,EAAE,mBAAgB,GAAG,EAAE;IAC7C,CAAC;AAGF,OAAI,EAAO,aAAa;IAKtB,IACM,IACF,EAFsB,EAAmB,WAAW,MAGjC,MAAwB,cAAa;AAC5D,QAAI;AACF,oBAAe,QAAQ,IAAc,EAAY;YAC3C;AAIR;;AAIF,OAAI,EAAO,SAAS;AAClB,QAAI;KACF,IAAM,IAAY,EAAO,MAAM;AAC/B,SAAI,GAAW;MAEb,IAAM,IADW,OAAO,SAAS,SACT,SAAS,uBAAuB,GACpD,yBACA,KAAA,GACE,IAAa,IAAS,YAAY,MAAW,IAC7C,IACJ,OAAO,SAAS,aAAa,WAAW,aAAa,IACjD,IAAW,KAAK,UAAU;OAC9B,IAAI;OACJ,QAAQ,OAAO,SAAS;OACzB,CAAC;AAEF,MADA,SAAS,SAAS,UAAU,mBAAmB,EAAS,GAAG,EAAW,wBAAwB,EAAW,kBACzG,SAAS,SAAS,kBAAkB,mBAAmB,EAAS,GAAG,EAAW,wBAAwB,EAAW;;YAE7G;AAKR,QAAI;AACF,KAAI,EAAS,aACX,aAAa,QAAQ,oBAAoB,EAAa,GAEtD,aAAa,WAAW,mBAAmB;aAEtC,GAAO;AACd,aAAQ,KACN,sDACA,EACD;;AAGH,MAAoB,GAAK;IAEzB,IACM,IACF,EAFsB,EAAmB,WAAW,MAGjC,MAAwB,cAAa;AAC5D,QAAI;KAGF,IAAM,IAAiB,MAAM,QAAQ,KAAqB,CACxD,GAAiB,EAAO,MAAM,EAC9B,IAAI,SAAyB,MAAY;AACvC,aAAO,iBAAiB;AACtB,SAAQ;QACN,SAAS;QACT,OAAO;QACR,CAAC;SACD,IAAK;OACR,CACH,CAAC;AAEF,KAAI,EAAe,WAGb,EAAe,cACjB,GAAa,EAAe,UAAU,GAAG,EACrC,EAAe,UAAU,kBAC3B,GAAgB,EAAe,UAAU,eAAe,GAK5D,EAAmB,EAAc,IACxB,EAAe,OAAO,SAAS,sBAAsB,GAE9D,EAAS,+BAA+B,IAGpC,EAAe,cACjB,GAAa,EAAe,UAAU,GAAG,EACrC,EAAe,UAAU,kBAC3B,GAAgB,EAAe,UAAU,eAAe,GAG5D,QAAQ,KAAK,sBAAsB,EAAe,MAAM,EACxD,EAAmB,EAAc;aAE5B,GAAe;AAGtB,KADA,QAAQ,MAAM,sCAAsC,EAAc,EAClE,EAAmB,EAAc;cACzB;AAKR,KAAK,EAAqB,WACxB,EAAoB,GAAM;;;WAIzB,GAAK;GAKZ,IAAM,IAAW,aAAe,QAAQ,EAAI,UAAU,OAAO,EAAI;AACjE,GAAI,EAAS,SAAS,YAAY,IAChC,EAAmB,EAAS;GAE9B,IAAM,IAAiB,EAAa,UAAU,CAAC,WACzC,IAAY,GAAe,GAAU,EAAe;AAC1D,GAAI,MAAc,YAChB,EAAoB,GAAK,GAChB,MAAc,cACvB,EAAsB,GAAK,GAClB,MAAc,eACvB,EAAsB,GAAK,GAClB,MAAc,aACvB,EAAqB,GAAK,GACjB,MAAc,wBACvB,EAAsB,GAAK;;;AAMjC,SAAgB;AACd,MAAI,GAAO;GACT,IAAM,IAAY,GAAe,GAAO,GAAU;AAClD,GAAI,MAAc,YAChB,EAAoB,GAAK,GAChB,MAAc,cACvB,EAAsB,GAAK,GAClB,MAAc,eACvB,EAAsB,GAAK,GAClB,MAAc,aACvB,EAAqB,GAAK,GACjB,MAAc,wBACvB,EAAsB,GAAK;;IAG9B,CAAC,GAAO,GAAU,CAAC;CAKtB,IAAM,WAA0B;AAC9B,IAAc,KAAK;AACnB,MAAI;AACF,OAAqB;WACd,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EACE,iCACA,oCACD,CACN;;IAOC,WAA6B;AACjC,IAAc,KAAK;AACnB,MAAI;AACF,OAAwB;WACjB,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EACE,oCACA,uCACD,CACN;;IAOC,KAA2B,YAAY;AAE3C,EADA,GAA2B,GAAK,EAChC,EAAiB,GAAM;AACvB,MAAI;AAGF,IADe,MAAM,IADC,EAAS,SAAS,IAAI,MAAM,CACG,EAC1C,UACT,EAAiB,GAAK,GAEtB,EACE,EACE,2BACA,yDACD,CACF;WAEI,GAAK;AAEZ,GADA,QAAQ,MAAM,wCAAwC,EAAI,EAC1D,EACE,aAAe,QACX,EAAI,UACJ,EACE,2BACA,yDACD,CACN;YACO;AACR,MAA2B,GAAM;;IAK/B,KAAqB,EAAqB,SAAS,EACnD,IAAwB,EAAqB,YAAY,EAEzD,KAAiB,CACrB;EACE,MAAM;EACN,MAAM;EACN,SAAS;EACT,UAAU,CAAC;EACX,SAAS,KACL,KAAA,IACA,EACE,kCACA,mCACD;EACN,EAWD;EACE,MAAM;EACN,MAAM;EACN,SAAS;EACT,UAAU,CAAC;EACX,SAAS,IACL,KAAA,IACA,EACE,qCACA,sCACD;EACN,CACF;AAMD,QAJI,KACK,kBAAC,IAAD,EAAsB,CAAA,GAI7B,kBAAC,GAAD;EACE,OAAO,EAAG,0BAA0B,eAAe;EACnD,UAAU,EACR,+BACA,qCACD;YALH;GAOE,kBAAC,IAAD;IAAa,WAAU;cACpB,EACC,sBACA,wIACD;IACW,CAAA;GACd,kBAAC,QAAD;IAAM,UAAU;IAAc,WAAU;cAAxC;KAEG,MACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OACE,WAAU;OACV,MAAK;OACL,SAAQ;OACR,QAAO;iBAEP,kBAAC,QAAD;QACE,eAAc;QACd,gBAAe;QACf,aAAa;QACb,GAAE;QACF,CAAA;OACE,CAAA,EACN,kBAAC,KAAD;OAAG,WAAU;iBAAiC;OAAgB,CAAA,CAC1D;;KAIP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAW,WAAU,wDAAyD,CAAA,EAC9E,kBAAC,KAAD;OAAG,WAAU;iBACV;OACC,CAAA,CACA;;KAIP,MACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAW,WAAU,wDAAyD,CAAA,EAC9E,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,+BACA,qCACD;SACC,CAAA;QACJ,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,mCACA,+FACD;SACC,CAAA;SACF,EAAmB,WAAW,MAC9B,kBAAC,UAAD;SACE,MAAK;SACL,eACE,EACE,mBACE,EAAmB,WAAW,KAAY,aAC3C,CACF;SAEH,WAAU;mBATZ,CAWE,kBAAC,OAAD;UACE,WAAU;UACV,MAAK;UACL,SAAQ;UACR,QAAO;oBAEP,kBAAC,QAAD;WACE,eAAc;WACd,gBAAe;WACf,aAAa;WACb,GAAE;WACF,CAAA;UACE,CAAA,EACL,EACC,kCACA,6BACD,CACM;;QAEP;SACF;;KAIP,KAAS,CAAC,KAAmB,CAAC,MAC7B,kBAAC,OAAD;MACE,WAAW,gDACT,KAAqB,KAAsB,IACvC,6DACA;gBAJR,CAOE,kBAAC,GAAD,EACE,WAAW,+BACT,KAAqB,KAAsB,IACvC,6BACA,4BAEN,CAAA,EACF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,KAAD;SACE,WAAW,WACT,KAAqB,KAAsB,IACvC,6BACA;mBAGL,KACG,EACE,6BACA,qEACD,GACD,KACE,EACE,+BACA,0EACD,GACD,IACE,EACE,iCACA,qCACD,GACD,IACE,EACE,wBACA,8EACD,GACD,IACE,EACE,+BACA,0FACD,GACD;SACV,CAAA;QACH,KACC,kBAAC,UAAD;SACE,MAAK;SACL,eACE,EAAS,oBAAoB,EAC3B,OAAO,EAAE,OAAO,EAAS,OAAO,EACjC,CAAC;SAEJ,WAAU;mBAET,EACC,8BACA,wCACD;SACM,CAAA;QAEV,KACC,kBAAC,OAAD;SAAK,WAAU;mBACZ,KACC,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WACE,WAAU;WACV,MAAK;WACL,SAAQ;WACR,QAAO;qBAEP,kBAAC,QAAD;YACE,eAAc;YACd,gBAAe;YACf,aAAa;YACb,GAAE;YACF,CAAA;WACE,CAAA,EACL,EACC,+BACA,oDACD,CACG;cAEN,kBAAC,UAAD;UACE,MAAK;UACL,SAAS;UACT,UAAU;UACV,WAAU;oBAET,KACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,IAAD,EAAa,WAAU,uBAAwB,CAAA,EAC9C,EAAG,sBAAsB,aAAa,CACtC,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;WACE,WAAU;WACV,MAAK;WACL,SAAQ;WACR,QAAO;qBAEP,kBAAC,QAAD;YACE,eAAc;YACd,gBAAe;YACf,aAAa;YACb,GAAE;YACF,CAAA;WACE,CAAA,EACL,EACC,iCACA,4BACD,CACA,EAAA,CAAA;UAEE,CAAA;SAEP,CAAA;QAEP,KACC,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACG,EAAqB,SAAS,IAC7B,kBAAC,UAAD;UACE,MAAK;UACL,SAAS;UACT,WAAU;oBAHZ,CAKE,kBAAC,IAAD,EAAY,WAAU,UAAW,CAAA,EAChC,EAAG,+BAA+B,sBAAsB,CAClD;aAEV,EAAqB,YAAY,IAChC,kBAAC,UAAD;UACE,MAAK;UACL,SAAS;UACT,WAAU;oBAHZ,CAKE,kBAAC,IAAD,EAAe,WAAU,UAAW,CAAA,EACnC,EACC,kCACA,yBACD,CACM;YAEP;;QAEJ;SACF;;KAIR,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OACE,SAAQ;OACR,WAAU;iBAET,EAAG,yBAAyB,QAAQ;OAC/B,CAAA,EACR,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD,EAAW,WAAU,4CAA6C,CAAA;QAC9D,CAAA,EACN,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,cAAa;QACb,UAAA;QACA,iBAAc;QACd,cAAY,EAAG,yBAAyB,QAAQ;QAChD,OAAO,EAAS;QAChB,WAAW,MACT,EAAY;SAAE,GAAG;SAAU,OAAO,EAAE,OAAO;SAAO,CAAC;QAErD,WAAU;QACV,aAAa,EAAG,+BAA+B,kBAAkB;QACjE,CAAA,CACE;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OACE,SAAQ;OACR,WAAU;iBAET,EAAG,4BAA4B,WAAW;OACrC,CAAA,EACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,IAAD,EAAU,WAAU,4CAA6C,CAAA;SAC7D,CAAA;QACN,kBAAC,SAAD;SACE,IAAG;SACH,MAAM,IAAe,SAAS;SAC9B,cAAa;SACb,UAAA;SACA,iBAAc;SACd,cAAY,EAAG,4BAA4B,WAAW;SACtD,OAAO,EAAS;SAChB,WAAW,MACT,EAAY;UAAE,GAAG;UAAU,UAAU,EAAE,OAAO;UAAO,CAAC;SAExD,WAAU;SACV,aAAa,EACX,kCACA,sBACD;SACD,CAAA;QACF,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,GAAgB,CAAC,EAAa;SAC7C,cAAY,IAAe,kBAAkB;SAC7C,WAAU;mBAGR,EADD,IACE,IAEA,GAFD,EAAY,WAAU,UAAW,CAEH;SAEzB,CAAA;QACL;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OAAO,WAAU;iBAAjB,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,SAAD;SACE,MAAK;SACL,cAAY,EAAG,yBAAyB,cAAc;SACtD,SAAS,EAAS;SAClB,WAAW,MAAM;UACf,IAAM,IAAY,EAAE,OAAO;AAI3B,cAHA,EAAY;WAAE,GAAG;WAAU,YAAY;WAAW,CAAC,EAG/C,CAAC,EACH,KAAI;AACF,wBAAa,WAAW,mBAAmB;mBACpC,GAAO;AACd,mBAAQ,KACN,iDACA,EACD;;;SAIP,WAAU;SACV,CAAA,EACF,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,OAAD;UACE,WAAU;UACV,MAAK;UACL,SAAQ;UACR,QAAO;UACP,aAAa;oBAEb,kBAAC,QAAD;WACE,eAAc;WACd,gBAAe;WACf,GAAE;WACF,CAAA;UACE,CAAA;SACF,CAAA,CACF;WACN,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,yBAAyB,cAAc;QACtC,CAAA,CACD;UACR,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAS,uBAAuB;OAC/C,WAAU;iBAET,EAAG,6BAA6B,mBAAmB;OAC7C,CAAA,CACL;;KAGN,kBAAC,IAAD;MACE,MAAK;MACL,UAAU,MAAa;MACvB,SAAQ;MACR,MAAK;MACL,WAAU;gBAET,MAAa,IACZ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,IAAD,EAAa,WAAU,uBAAwB,CAAA,EAC/C,kBAAC,QAAD,EAAA,UACG,IACG,EACE,iCACA,0BACD,GACD,EAAG,wBAAwB,gBAAgB,EAC1C,CAAA,CACN,EAAA,CAAA,GAEH,EAAG,qBAAqB,UAAU;MAE7B,CAAA;KACJ;;GAGP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD,EAAK,WAAU,wDAAyD,CAAA;KACpE,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAG,6BAA6B,mBAAmB;MAC/C,CAAA;KACH,CAAA,CACF;;GAGL,MACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAW,WAAU,sDAAuD,CAAA,EAC5E,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAe,CAAA,CAC1D;;GAIR,kBAAC,OAAD;IAAK,WAAU;cACZ,GAAe,KAAK,MACnB,kBAAC,UAAD;KAEE,MAAK;KACL,SAAS,EAAS;KAClB,UAAU,EAAS;KACnB,OAAO,EAAS;KAChB,cAAY,EAAS;KACrB,WAAW,wGACT,EAAS,WACL,oGACA;eAVR,CAaE,kBAAC,EAAS,MAAV,EACE,WAAW,UAAU,EAAS,WAAW,eAAe,MACxD,CAAA,EACF,kBAAC,QAAD;MACE,WAAW,gEACT,EAAS,WACL,sCACA;gBAGL,EAAS;MACL,CAAA,CACA;OAxBF,EAAS,KAwBP,CACT;IACE,CAAA;GA4BN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,mBAAmB;KAC3C,WAAU;eAHZ;MAKG,EAAG,iCAAiC,6BAA6B;MAAE;MACpE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,8BAA8B,0BAA0B;OACvD,CAAA;MACA;;IACL,CAAA;GAGN,kBAAC,KAAD;IAAG,WAAU;cAAb;KACG,EAAG,8BAA8B,yBAAyB;KAAE;KAC7D,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,iBAAiB;MACzC,WAAU;gBAET,EAAG,qBAAqB,UAAU;MAC5B,CAAA;KACP;;GACO"}
|
|
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\";\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\";\nimport { resolveGatewayUrls } from \"@burdenoff/fe-libs/shared/config/gatewayUrls\";\nimport {\n beginSsoNavigation,\n buildSsoDepositUrl,\n isSsoNavigationPending,\n markSsoDepositPending,\n mintSsoSessionTokens,\n} from \"@burdenoff/fe-libs/shared/utils\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\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 { sanitizePostLoginPath } from \"../utils/redirect\";\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(\n async (intendedPath: string): Promise<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 // BOFF-4463: mint a DEDICATED refresh-token lineage for the bridge\n // host. Copying this origin's pair verbatim meant the bridge (and\n // every product it later relayed to) shared ONE rotating refresh\n // token — the first origin to refresh revoked it for all others,\n // surfacing as \"products log me out after minutes/hours\". Falls back\n // to the legacy copy if the mutation is unreachable (rollout gap /\n // transient failure) so SSO keeps working.\n let depositTokens: Record<string, unknown> = {\n ...tokens,\n workspaceToken,\n };\n if (tokens.refreshToken) {\n const { globalBaseUrl } = resolveGatewayUrls();\n const { tokens: minted } = await mintSsoSessionTokens(\n `${globalBaseUrl}/global/graphql`,\n tokens.refreshToken,\n );\n if (minted) {\n depositTokens = { ...minted, workspaceToken };\n }\n }\n\n markSsoDepositPending(profileId);\n return buildSsoDepositUrl(bridgeUrl.origin, absoluteIntended, {\n profileId,\n tokens: JSON.stringify(depositTokens),\n user: JSON.stringify(user),\n profiles: JSON.stringify(getProfiles()),\n });\n } catch {\n return intendedPath;\n }\n },\n [],\n );\n\n // Once a cross-document post-login redirect is initiated, the login page\n // must FREEZE: resetting isCompletingAuth re-runs the auto-redirect effect\n // and any state churn schedules same-document navigations that cancel the\n // provisional deposit hop (BOFF-4463).\n const redirectInitiatedRef = useRef(false);\n\n const redirectAfterLogin = useCallback(\n (path: string) => {\n const safePath = sanitizePostLoginPath(path);\n if (typeof window === \"undefined\") {\n navigate(safePath);\n return;\n }\n try {\n sessionStorage.setItem(\"bf-post-login-return-to\", safePath);\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(safePath));\n return;\n }\n redirectInitiatedRef.current = true;\n void (async () => {\n // Cap the bridge URL build so a hanging SSO token mint cannot keep the\n // login page stuck on the spinner (BOFF-6807). The sanitized path is\n // always safe to redirect to directly.\n const target = await Promise.race([\n buildPostLoginUrl(safePath),\n new Promise<string>((resolve) => {\n window.setTimeout(() => resolve(safePath), 3000);\n }),\n ]);\n if (target.includes(\"/auth/sso-deposit\")) {\n // Cross-document deposit hop: suppress same-document navigators\n // while it is provisional, and retry once if it still gets\n // cancelled — on a committed navigation this document unloads and\n // the timer never fires.\n beginSsoNavigation();\n window.location.assign(target);\n window.setTimeout(() => {\n beginSsoNavigation();\n window.location.replace(target);\n }, 2500);\n } else {\n window.location.assign(target);\n }\n // Fallback: if the document is still stuck on the login page after a\n // full redirect attempt, force an in-app navigation so the spinner\n // cannot stall forever (BOFF-6807). This only fires when the browser\n // failed to leave /auth/login, e.g. because the computed target was\n // still an auth URL or the location.assign was a no-op.\n window.setTimeout(() => {\n if (\n typeof window !== \"undefined\" &&\n window.location.pathname === \"/auth/login\"\n ) {\n navigate(\"/dashboard\");\n }\n }, 3500);\n })();\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 // Tell the user WHY they are back on the login page. `session-expired` is\n // set by the shell's forced logout (expired session whose refresh failed);\n // `logout` covers explicit sign-outs and the SSO-clear fallback (which can\n // rewrite a session-expired redirect's reason on the cross-product bounce).\n // Without this the auto-logout looked like a silent, unexplained kick-out.\n const reasonParam = searchParams.get(\"reason\");\n useEffect(() => {\n if (reasonParam === \"session-expired\") {\n setInfoMessage(\n tr(\n \"auth.login.sessionExpired\",\n \"Your session has expired. Please sign in again.\",\n ),\n );\n } else if (reasonParam === \"logout\") {\n setInfoMessage(tr(\"auth.login.signedOut\", \"You have been signed out.\"));\n }\n }, [reasonParam, tr]);\n\n useEffect(() => {\n if (!initialReturnToRef.current && returnTo) {\n initialReturnToRef.current = returnTo;\n }\n }, [returnTo]);\n\n useEffect(() => {\n if (isAddAccountMode || isCompletingAuth) return;\n // A cross-document SSO navigation (deposit/relay hop) is already in\n // flight — issuing another redirect here would cancel it (BOFF-4463).\n if (redirectInitiatedRef.current || isSsoNavigationPending()) 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 // Keep the completing state (spinner) once a redirect is in\n // flight: resetting it re-arms the auto-redirect effect whose\n // competing navigation cancels the deposit hop (BOFF-4463). The\n // page is about to unload anyway.\n if (!redirectInitiatedRef.current) {\n setIsCompletingAuth(false);\n }\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 <PagePurpose className=\"mb-5\">\n {tr(\n \"auth.login.purpose\",\n \"Sign in to reach your workspaces, agents, and settings. Use your email and password, a social provider, or a passwordless magic link.\",\n )}\n </PagePurpose>\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 variant=\"default\"\n size=\"lg\"\n className=\"w-full 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 ${\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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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect helpers for the post-login flow.
|
|
3
|
+
*
|
|
4
|
+
* Keeps redirect-sanitization logic pure and unit-testable so the login page
|
|
5
|
+
* cannot accidentally redirect back into an auth route (BOFF-6807).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Ensure the post-login destination never lands back on an auth route.
|
|
9
|
+
* Falls back to `/dashboard` when the supplied path is empty, "/", or an
|
|
10
|
+
* `/auth/*` path.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sanitizePostLoginPath(path: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/auth/utils/redirect.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
if (typeof window > "u") return !1;
|
|
4
|
+
try {
|
|
5
|
+
return new URL(e, window.location.origin).pathname.startsWith("/auth");
|
|
6
|
+
} catch {
|
|
7
|
+
return !1;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function t(t) {
|
|
11
|
+
return typeof window > "u" ? t : !t || t === "/" || e(t) ? "/dashboard" : t;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { t as sanitizePostLoginPath };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=redirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect.js","names":[],"sources":["../../../src/auth/utils/redirect.ts"],"sourcesContent":["/**\n * Redirect helpers for the post-login flow.\n *\n * Keeps redirect-sanitization logic pure and unit-testable so the login page\n * cannot accidentally redirect back into an auth route (BOFF-6807).\n */\n\nfunction isAuthRoutePath(path: string): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n const url = new URL(path, window.location.origin);\n return url.pathname.startsWith(\"/auth\");\n } catch {\n return false;\n }\n}\n\n/**\n * Ensure the post-login destination never lands back on an auth route.\n * Falls back to `/dashboard` when the supplied path is empty, \"/\", or an\n * `/auth/*` path.\n */\nexport function sanitizePostLoginPath(path: string): string {\n if (typeof window === \"undefined\") return path;\n if (!path || path === \"/\" || isAuthRoutePath(path)) {\n return \"/dashboard\";\n }\n return path;\n}\n"],"mappings":";AAOA,SAAS,EAAgB,GAAuB;AAC9C,KAAI,OAAO,SAAW,IAAa,QAAO;AAC1C,KAAI;AAEF,SADY,IAAI,IAAI,GAAM,OAAO,SAAS,OAAO,CACtC,SAAS,WAAW,QAAQ;SACjC;AACN,SAAO;;;AASX,SAAgB,EAAsB,GAAsB;AAK1D,QAJI,OAAO,SAAW,MAAoB,IACtC,CAAC,KAAQ,MAAS,OAAO,EAAgB,EAAK,GACzC,eAEF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-auth",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.828.1",
|
|
4
4
|
"description": "Auth microfrontend for Burdenoff products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
|
|
37
37
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
38
38
|
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
39
|
-
"
|
|
39
|
+
"test": "bash scripts/build-limits.sh vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"test:coverage": "bash scripts/build-limits.sh vitest run --coverage",
|
|
42
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build'",
|
|
40
43
|
"switch:status": "bun scripts/deps-mode.ts status",
|
|
41
44
|
"switch:remote": "bun scripts/deps-mode.ts remote",
|
|
42
45
|
"switch:local": "bun scripts/deps-mode.ts local",
|
|
@@ -63,6 +66,9 @@
|
|
|
63
66
|
"devDependencies": {
|
|
64
67
|
"@eslint/js": "^9.39.1",
|
|
65
68
|
"@graphql-codegen/add": "^6.0.0",
|
|
69
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
70
|
+
"jsdom": "^25.0.1",
|
|
71
|
+
"vitest": "^3.2.4",
|
|
66
72
|
"@graphql-codegen/cli": "^5.0.0",
|
|
67
73
|
"@graphql-codegen/import-types-preset": "^3.0.1",
|
|
68
74
|
"@graphql-codegen/schema-ast": "^5.0.0",
|