@vaultix.ai/react 0.3.2 → 0.3.3
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.
- package/dist/index.d.mts +22 -7
- package/dist/index.d.ts +22 -7
- package/dist/index.js +556 -128
- package/dist/index.mjs +558 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -57,28 +57,35 @@ function resolveApiOrigin(key, apiUrlProp) {
|
|
|
57
57
|
throw new Error(`Unknown publishable key prefix "${parts[0]}".`);
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
60
|
-
return atob(parts.slice(3).join("_")).replace(/\/$/, "");
|
|
60
|
+
return atob(parts.slice(3).join("_")).replace(/\|.+$/, "").replace(/\/$/, "");
|
|
61
61
|
} catch {
|
|
62
62
|
throw new Error("Publishable key has an unreadable API origin payload.");
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
var
|
|
65
|
+
var TOKEN_COOKIE = "vaultix-token";
|
|
66
|
+
var COOKIE_DAYS = 30;
|
|
66
67
|
function storeJwt(jwt) {
|
|
68
|
+
if (typeof document === "undefined") return;
|
|
67
69
|
try {
|
|
68
|
-
|
|
70
|
+
const expires = new Date(Date.now() + COOKIE_DAYS * 864e5).toUTCString();
|
|
71
|
+
const secure = location.protocol === "https:" ? "; Secure" : "";
|
|
72
|
+
document.cookie = `${TOKEN_COOKIE}=${encodeURIComponent(jwt)}; path=/; expires=${expires}; SameSite=Lax${secure}`;
|
|
69
73
|
} catch {
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
function loadJwt() {
|
|
77
|
+
if (typeof document === "undefined") return null;
|
|
73
78
|
try {
|
|
74
|
-
|
|
79
|
+
const m = document.cookie.match(new RegExp(`(?:^|; )${TOKEN_COOKIE}=([^;]+)`));
|
|
80
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
75
81
|
} catch {
|
|
76
82
|
return null;
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
85
|
function clearJwt() {
|
|
86
|
+
if (typeof document === "undefined") return;
|
|
80
87
|
try {
|
|
81
|
-
|
|
88
|
+
document.cookie = `${TOKEN_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`;
|
|
82
89
|
} catch {
|
|
83
90
|
}
|
|
84
91
|
}
|
|
@@ -149,26 +156,27 @@ function VaultixProvider({
|
|
|
149
156
|
async function hydrate() {
|
|
150
157
|
let jwt = null;
|
|
151
158
|
if (typeof window !== "undefined") {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
url.searchParams.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
jwt = loadJwt();
|
|
160
|
+
if (!jwt) {
|
|
161
|
+
const url = new URL(window.location.href);
|
|
162
|
+
const handshakeToken = url.searchParams.get("__vaultix_handshake");
|
|
163
|
+
if (handshakeToken) {
|
|
164
|
+
url.searchParams.delete("__vaultix_handshake");
|
|
165
|
+
window.history.replaceState({}, "", url.toString());
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ handshake_token: handshakeToken })
|
|
171
|
+
});
|
|
172
|
+
if (res.ok) {
|
|
173
|
+
const data = await res.json();
|
|
174
|
+
jwt = data.session_jwt;
|
|
175
|
+
storeJwt(jwt);
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
167
178
|
}
|
|
168
|
-
} catch {
|
|
169
179
|
}
|
|
170
|
-
} else {
|
|
171
|
-
jwt = loadJwt();
|
|
172
180
|
}
|
|
173
181
|
jwtRef.current = jwt;
|
|
174
182
|
}
|
|
@@ -209,7 +217,7 @@ function VaultixProvider({
|
|
|
209
217
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
210
218
|
};
|
|
211
219
|
}, [apiOrigin, publishableKey, scheduleRefresh]);
|
|
212
|
-
const signOut = (0, import_react.useCallback)(async () => {
|
|
220
|
+
const signOut = (0, import_react.useCallback)(async (redirectUrl) => {
|
|
213
221
|
const jwt = jwtRef.current;
|
|
214
222
|
clearAuth();
|
|
215
223
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
@@ -221,8 +229,29 @@ function VaultixProvider({
|
|
|
221
229
|
headers
|
|
222
230
|
}).catch(() => {
|
|
223
231
|
});
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
const dest = redirectUrl ?? afterSignUpUrl ?? "/";
|
|
233
|
+
if (typeof window !== "undefined") window.location.href = dest;
|
|
234
|
+
}, [apiOrigin, afterSignUpUrl, clearAuth]);
|
|
235
|
+
const updateUser = (0, import_react.useCallback)(async (params) => {
|
|
236
|
+
const userId = user?.id;
|
|
237
|
+
if (!userId) throw new Error("No active session");
|
|
238
|
+
const jwt = jwtRef.current;
|
|
239
|
+
const headers = { "Content-Type": "application/json" };
|
|
240
|
+
if (jwt) headers["Authorization"] = `Bearer ${jwt}`;
|
|
241
|
+
const res = await fetch(`${apiOrigin}/api/v1/users/${userId}`, {
|
|
242
|
+
method: "PATCH",
|
|
243
|
+
credentials: jwt ? "omit" : "include",
|
|
244
|
+
headers,
|
|
245
|
+
body: JSON.stringify(params)
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
const err = await res.json().catch(() => ({ error: "Update failed" }));
|
|
249
|
+
throw new Error(err.error ?? "Update failed");
|
|
250
|
+
}
|
|
251
|
+
const updated = await res.json();
|
|
252
|
+
setUser(updated);
|
|
253
|
+
return updated;
|
|
254
|
+
}, [apiOrigin, user?.id]);
|
|
226
255
|
const connectPlatform = (0, import_react.useCallback)((provider, options = {}) => {
|
|
227
256
|
const params = new URLSearchParams({ mode: "connect" });
|
|
228
257
|
params.set("redirect_url", options.redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "/"));
|
|
@@ -240,7 +269,9 @@ function VaultixProvider({
|
|
|
240
269
|
organization,
|
|
241
270
|
isLoaded,
|
|
242
271
|
isSignedIn: !!session,
|
|
272
|
+
apiOrigin,
|
|
243
273
|
signOut,
|
|
274
|
+
updateUser,
|
|
244
275
|
connectPlatform
|
|
245
276
|
};
|
|
246
277
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VaultixContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -266,8 +297,8 @@ function useSession() {
|
|
|
266
297
|
return { session, isLoaded, isSignedIn };
|
|
267
298
|
}
|
|
268
299
|
function useUser() {
|
|
269
|
-
const { user, isLoaded, isSignedIn } = useVaultixContext();
|
|
270
|
-
return { user, isLoaded, isSignedIn };
|
|
300
|
+
const { user, isLoaded, isSignedIn, updateUser } = useVaultixContext();
|
|
301
|
+
return { user, isLoaded, isSignedIn, update: updateUser };
|
|
271
302
|
}
|
|
272
303
|
function useOrganization() {
|
|
273
304
|
const { organization, isLoaded } = useVaultixContext();
|
|
@@ -278,7 +309,8 @@ function useAuth() {
|
|
|
278
309
|
const getToken = (0, import_react2.useCallback)(async () => {
|
|
279
310
|
if (!isSignedIn) return null;
|
|
280
311
|
try {
|
|
281
|
-
|
|
312
|
+
const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
|
|
313
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
282
314
|
} catch {
|
|
283
315
|
return null;
|
|
284
316
|
}
|
|
@@ -392,6 +424,23 @@ function SignIn({
|
|
|
392
424
|
setLoading(false);
|
|
393
425
|
}
|
|
394
426
|
}
|
|
427
|
+
async function handleMagicLinkRequest() {
|
|
428
|
+
setError(null);
|
|
429
|
+
setLoading(true);
|
|
430
|
+
try {
|
|
431
|
+
const target = resolveAfterSignInUrl(redirectUrl);
|
|
432
|
+
await fetch(`${apiOrigin}/api/v1/auth/magic-link/send`, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: { "Content-Type": "application/json" },
|
|
435
|
+
body: JSON.stringify({ email, redirect_url: target })
|
|
436
|
+
});
|
|
437
|
+
setStep("magic-link-sent");
|
|
438
|
+
} catch {
|
|
439
|
+
setErr("Network error. Please try again.");
|
|
440
|
+
} finally {
|
|
441
|
+
setLoading(false);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
395
444
|
async function handlePasswordSubmit(e) {
|
|
396
445
|
e.preventDefault();
|
|
397
446
|
setError(null);
|
|
@@ -560,6 +609,7 @@ function SignIn({
|
|
|
560
609
|
password: "Enter password",
|
|
561
610
|
passkey: "Passkey sign-in",
|
|
562
611
|
totp: "Two-factor code",
|
|
612
|
+
"magic-link-sent": "Check your inbox",
|
|
563
613
|
forgot: "Forgot password",
|
|
564
614
|
"forgot-verify": "Enter reset code",
|
|
565
615
|
"forgot-reset": "New password"
|
|
@@ -569,6 +619,7 @@ function SignIn({
|
|
|
569
619
|
password: email,
|
|
570
620
|
passkey: email,
|
|
571
621
|
totp: "Enter the 6-digit code from your authenticator app",
|
|
622
|
+
"magic-link-sent": `We sent a sign-in link to ${email}`,
|
|
572
623
|
forgot: "We'll send a reset code to your email",
|
|
573
624
|
"forgot-verify": `Code sent to ${email}`,
|
|
574
625
|
"forgot-reset": "Choose a new password"
|
|
@@ -626,12 +677,31 @@ function SignIn({
|
|
|
626
677
|
}
|
|
627
678
|
),
|
|
628
679
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Sign in" }),
|
|
680
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
681
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-1 h-px bg-white/8" }),
|
|
682
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-[10px] text-[#475569]", children: "or" }),
|
|
683
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-1 h-px bg-white/8" })
|
|
684
|
+
] }),
|
|
685
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: handleMagicLinkRequest, children: "\u2709 Email me a sign-in link" }),
|
|
629
686
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => {
|
|
630
687
|
setStep("forgot");
|
|
631
688
|
setError(null);
|
|
632
689
|
}, children: "Forgot password?" }),
|
|
633
690
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
|
|
634
691
|
] }),
|
|
692
|
+
step === "magic-link-sent" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-4 text-center", children: [
|
|
693
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-14 h-14 rounded-2xl bg-purple-500/15 border border-purple-500/30 flex items-center justify-center mx-auto", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EnvelopeIcon, {}) }),
|
|
694
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-sm text-[#94a3b8] leading-relaxed", children: [
|
|
695
|
+
"The link expires in ",
|
|
696
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-white/80 font-medium", children: "15 minutes" }),
|
|
697
|
+
" and can only be used once."
|
|
698
|
+
] }),
|
|
699
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: handleMagicLinkRequest, children: "Resend link" }),
|
|
700
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => {
|
|
701
|
+
setStep("password");
|
|
702
|
+
setError(null);
|
|
703
|
+
}, children: "Use password instead" })
|
|
704
|
+
] }),
|
|
635
705
|
step === "passkey" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-3", children: [
|
|
636
706
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(PrimaryButton, { loading, onClick: handlePasskeySignIn, children: [
|
|
637
707
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FingerprintIcon, {}),
|
|
@@ -874,6 +944,12 @@ function GitHubIcon() {
|
|
|
874
944
|
function XIcon() {
|
|
875
945
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.737-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) });
|
|
876
946
|
}
|
|
947
|
+
function EnvelopeIcon() {
|
|
948
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#a78bfa", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
949
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
|
|
950
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })
|
|
951
|
+
] });
|
|
952
|
+
}
|
|
877
953
|
|
|
878
954
|
// src/components/SignUp.tsx
|
|
879
955
|
var import_clsx2 = require("clsx");
|
|
@@ -1216,20 +1292,15 @@ function RedirectToSignUp({ redirectUrl = "/sign-up" }) {
|
|
|
1216
1292
|
var import_clsx3 = require("clsx");
|
|
1217
1293
|
var import_react6 = require("react");
|
|
1218
1294
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1219
|
-
function UserButton({
|
|
1220
|
-
showName = false,
|
|
1221
|
-
afterSignOutUrl,
|
|
1222
|
-
className
|
|
1223
|
-
}) {
|
|
1295
|
+
function UserButton({ showName = false, afterSignOutUrl, className }) {
|
|
1224
1296
|
const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
|
|
1225
1297
|
const [open, setOpen] = (0, import_react6.useState)(false);
|
|
1226
1298
|
const [signingOut, setSigningOut] = (0, import_react6.useState)(false);
|
|
1299
|
+
const [showProfile, setShowProfile] = (0, import_react6.useState)(false);
|
|
1227
1300
|
const containerRef = (0, import_react6.useRef)(null);
|
|
1228
1301
|
(0, import_react6.useEffect)(() => {
|
|
1229
1302
|
function onPointerDown(e) {
|
|
1230
|
-
if (containerRef.current && !containerRef.current.contains(e.target))
|
|
1231
|
-
setOpen(false);
|
|
1232
|
-
}
|
|
1303
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
|
|
1233
1304
|
}
|
|
1234
1305
|
document.addEventListener("pointerdown", onPointerDown);
|
|
1235
1306
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
@@ -1242,98 +1313,456 @@ function UserButton({
|
|
|
1242
1313
|
await signOut();
|
|
1243
1314
|
if (afterSignOutUrl) window.location.href = afterSignOutUrl;
|
|
1244
1315
|
}
|
|
1245
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1246
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1316
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
1317
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { ref: containerRef, className: (0, import_clsx3.clsx)("relative", className), children: [
|
|
1318
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1319
|
+
"button",
|
|
1320
|
+
{
|
|
1321
|
+
onClick: () => setOpen((o) => !o),
|
|
1322
|
+
className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
|
|
1323
|
+
children: [
|
|
1324
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl }),
|
|
1325
|
+
showName && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
|
|
1326
|
+
]
|
|
1327
|
+
}
|
|
1328
|
+
),
|
|
1329
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1330
|
+
"div",
|
|
1331
|
+
{
|
|
1332
|
+
className: (0, import_clsx3.clsx)("absolute right-0 mt-2 w-64 rounded-2xl border border-white/8", "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"),
|
|
1333
|
+
style: { background: "rgba(22,27,45,0.96)" },
|
|
1334
|
+
children: [
|
|
1335
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
|
|
1336
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
|
|
1337
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
1338
|
+
(user.firstName || user.lastName) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm font-semibold text-white/90 truncate", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
|
|
1339
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-[#475569] truncate", children: user.email })
|
|
1340
|
+
] })
|
|
1341
|
+
] }),
|
|
1342
|
+
session && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-4 py-2 border-b border-white/8", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
1343
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
|
|
1344
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RiskBadge, { level: session.riskLevel })
|
|
1345
|
+
] }) }),
|
|
1346
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-2", children: [
|
|
1347
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownItem, { label: "Manage account", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(UserIcon, {}), onClick: () => {
|
|
1348
|
+
setOpen(false);
|
|
1349
|
+
setShowProfile(true);
|
|
1350
|
+
} }),
|
|
1351
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-px bg-white/8 my-1" }),
|
|
1352
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownItem, { label: signingOut ? "Signing out\u2026" : "Sign out", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SignOutIcon, {}), onClick: handleSignOut, destructive: true })
|
|
1353
|
+
] })
|
|
1354
|
+
]
|
|
1355
|
+
}
|
|
1356
|
+
)
|
|
1357
|
+
] }),
|
|
1358
|
+
showProfile && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ProfileModal, { onClose: () => setShowProfile(false) })
|
|
1359
|
+
] });
|
|
1360
|
+
}
|
|
1361
|
+
function ProfileModal({ onClose }) {
|
|
1362
|
+
const { user, updateUser } = useVaultixContext();
|
|
1363
|
+
const [tab, setTab] = (0, import_react6.useState)("profile");
|
|
1364
|
+
const [firstName, setFirstName] = (0, import_react6.useState)(user?.firstName ?? "");
|
|
1365
|
+
const [lastName, setLastName] = (0, import_react6.useState)(user?.lastName ?? "");
|
|
1366
|
+
const [imageUrl, setImageUrl] = (0, import_react6.useState)(user?.imageUrl ?? "");
|
|
1367
|
+
const [currentPassword, setCurrentPassword] = (0, import_react6.useState)("");
|
|
1368
|
+
const [newPassword, setNewPassword] = (0, import_react6.useState)("");
|
|
1369
|
+
const [confirmPassword, setConfirmPassword] = (0, import_react6.useState)("");
|
|
1370
|
+
const [saving, setSaving] = (0, import_react6.useState)(false);
|
|
1371
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
1372
|
+
const [success, setSuccess] = (0, import_react6.useState)(null);
|
|
1373
|
+
(0, import_react6.useEffect)(() => {
|
|
1374
|
+
function onKey(e) {
|
|
1375
|
+
if (e.key === "Escape") onClose();
|
|
1376
|
+
}
|
|
1377
|
+
document.addEventListener("keydown", onKey);
|
|
1378
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1379
|
+
}, [onClose]);
|
|
1380
|
+
async function saveProfile(e) {
|
|
1381
|
+
e.preventDefault();
|
|
1382
|
+
setError(null);
|
|
1383
|
+
setSuccess(null);
|
|
1384
|
+
setSaving(true);
|
|
1385
|
+
try {
|
|
1386
|
+
await updateUser({ firstName: firstName.trim() || null, lastName: lastName.trim() || null, imageUrl: imageUrl.trim() || null });
|
|
1387
|
+
setSuccess("Profile updated.");
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
setError(err instanceof Error ? err.message : "Update failed");
|
|
1390
|
+
} finally {
|
|
1391
|
+
setSaving(false);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async function savePassword(e) {
|
|
1395
|
+
e.preventDefault();
|
|
1396
|
+
setError(null);
|
|
1397
|
+
setSuccess(null);
|
|
1398
|
+
if (newPassword !== confirmPassword) {
|
|
1399
|
+
setError("Passwords do not match.");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (newPassword.length < 8) {
|
|
1403
|
+
setError("Password must be at least 8 characters.");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
setSaving(true);
|
|
1407
|
+
try {
|
|
1408
|
+
await updateUser({ currentPassword, newPassword });
|
|
1409
|
+
setSuccess("Password changed.");
|
|
1410
|
+
setCurrentPassword("");
|
|
1411
|
+
setNewPassword("");
|
|
1412
|
+
setConfirmPassword("");
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
setError(err instanceof Error ? err.message : "Update failed");
|
|
1415
|
+
} finally {
|
|
1416
|
+
setSaving(false);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const TABS = [
|
|
1420
|
+
{ id: "profile", label: "Profile" },
|
|
1421
|
+
{ id: "password", label: "Change password" },
|
|
1422
|
+
{ id: "security", label: "Security" }
|
|
1423
|
+
];
|
|
1424
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1425
|
+
"div",
|
|
1426
|
+
{
|
|
1427
|
+
className: "fixed inset-0 z-[9999] flex items-center justify-center p-4",
|
|
1428
|
+
style: { background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" },
|
|
1429
|
+
onPointerDown: (e) => {
|
|
1430
|
+
if (e.target === e.currentTarget) onClose();
|
|
1431
|
+
},
|
|
1432
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "w-full max-w-md rounded-2xl border border-white/8 shadow-2xl", style: { background: "rgba(22,27,45,0.98)" }, children: [
|
|
1433
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/8", children: [
|
|
1434
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { className: "text-sm font-semibold text-white/90", children: "Manage account" }),
|
|
1435
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { onClick: onClose, className: "text-[#475569] hover:text-white transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CloseIcon, {}) })
|
|
1436
|
+
] }),
|
|
1437
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex gap-1 px-6 pt-4 flex-wrap", children: TABS.map(({ id, label }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1438
|
+
"button",
|
|
1439
|
+
{
|
|
1440
|
+
onClick: () => {
|
|
1441
|
+
setTab(id);
|
|
1442
|
+
setError(null);
|
|
1443
|
+
setSuccess(null);
|
|
1444
|
+
},
|
|
1445
|
+
className: (0, import_clsx3.clsx)(
|
|
1446
|
+
"px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",
|
|
1447
|
+
tab === id ? "bg-purple-500/15 text-purple-300 border border-purple-500/30" : "text-[#475569] hover:text-white/70"
|
|
1448
|
+
),
|
|
1449
|
+
children: label
|
|
1450
|
+
},
|
|
1451
|
+
id
|
|
1452
|
+
)) }),
|
|
1453
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 pt-3", children: [
|
|
1454
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
|
|
1455
|
+
success && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-emerald-500/10 border border-emerald-500/30 px-3 py-2 text-xs text-emerald-400", children: success })
|
|
1456
|
+
] }),
|
|
1457
|
+
tab === "profile" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: saveProfile, className: "px-6 py-4 space-y-4", children: [
|
|
1458
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
1459
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "First name", value: firstName, onChange: setFirstName, placeholder: "Jane" }),
|
|
1460
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Last name", value: lastName, onChange: setLastName, placeholder: "Smith" })
|
|
1461
|
+
] }),
|
|
1462
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Avatar URL", value: imageUrl, onChange: setImageUrl, placeholder: "https://\u2026", type: "url" }),
|
|
1463
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 pt-1", children: [
|
|
1464
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaveButton, { saving }),
|
|
1465
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
|
|
1466
|
+
] })
|
|
1467
|
+
] }),
|
|
1468
|
+
tab === "password" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: savePassword, className: "px-6 py-4 space-y-4", children: [
|
|
1469
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Current password", value: currentPassword, onChange: setCurrentPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
|
|
1470
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "New password", value: newPassword, onChange: setNewPassword, type: "password", placeholder: "Min 8 characters" }),
|
|
1471
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Confirm new password", value: confirmPassword, onChange: setConfirmPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
|
|
1472
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 pt-1", children: [
|
|
1473
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaveButton, { saving, label: "Change password" }),
|
|
1474
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
|
|
1475
|
+
] })
|
|
1476
|
+
] }),
|
|
1477
|
+
tab === "security" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SecurityTab, {})
|
|
1478
|
+
] })
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
function SecurityTab() {
|
|
1483
|
+
const { apiOrigin } = useVaultixContext();
|
|
1484
|
+
const [totpEnabled, setTotpEnabled] = (0, import_react6.useState)(null);
|
|
1485
|
+
const [backupCodesLeft, setBackupCodesLeft] = (0, import_react6.useState)(null);
|
|
1486
|
+
const [enrollStep, setEnrollStep] = (0, import_react6.useState)("idle");
|
|
1487
|
+
const [qrDataUrl, setQrDataUrl] = (0, import_react6.useState)("");
|
|
1488
|
+
const [manualKey, setManualKey] = (0, import_react6.useState)("");
|
|
1489
|
+
const [enrollCode, setEnrollCode] = (0, import_react6.useState)("");
|
|
1490
|
+
const [disableCode, setDisableCode] = (0, import_react6.useState)("");
|
|
1491
|
+
const [backupCodes, setBackupCodes] = (0, import_react6.useState)([]);
|
|
1492
|
+
const [loading, setLoading] = (0, import_react6.useState)(false);
|
|
1493
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
1494
|
+
const [showDisable, setShowDisable] = (0, import_react6.useState)(false);
|
|
1495
|
+
const fetchStatus = (0, import_react6.useCallback)(async () => {
|
|
1496
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp/status`, { credentials: "include" });
|
|
1497
|
+
if (res.ok) {
|
|
1498
|
+
const d = await res.json();
|
|
1499
|
+
setTotpEnabled(d.enabled);
|
|
1500
|
+
setBackupCodesLeft(d.backup_codes_remaining);
|
|
1501
|
+
}
|
|
1502
|
+
}, [apiOrigin]);
|
|
1503
|
+
(0, import_react6.useEffect)(() => {
|
|
1504
|
+
void fetchStatus();
|
|
1505
|
+
}, [fetchStatus]);
|
|
1506
|
+
async function startEnroll() {
|
|
1507
|
+
setError(null);
|
|
1508
|
+
setLoading(true);
|
|
1509
|
+
try {
|
|
1510
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp/enroll`, { method: "POST", credentials: "include" });
|
|
1511
|
+
const d = await res.json();
|
|
1512
|
+
if (!res.ok) {
|
|
1513
|
+
setError(d.error ?? "Failed to start enrollment.");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
setQrDataUrl(d.qr_data_url);
|
|
1517
|
+
setManualKey(d.manual_entry_key);
|
|
1518
|
+
setEnrollStep("qr");
|
|
1519
|
+
} finally {
|
|
1520
|
+
setLoading(false);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function verifyEnroll(e) {
|
|
1524
|
+
e.preventDefault();
|
|
1525
|
+
setError(null);
|
|
1526
|
+
setLoading(true);
|
|
1527
|
+
try {
|
|
1528
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp/verify-enroll`, {
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
credentials: "include",
|
|
1531
|
+
headers: { "Content-Type": "application/json" },
|
|
1532
|
+
body: JSON.stringify({ code: enrollCode })
|
|
1533
|
+
});
|
|
1534
|
+
const d = await res.json();
|
|
1535
|
+
if (!res.ok) {
|
|
1536
|
+
setError(d.error ?? "Invalid code. Try again.");
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
setBackupCodes(d.backup_codes);
|
|
1540
|
+
setEnrollStep("backup-codes");
|
|
1541
|
+
setTotpEnabled(true);
|
|
1542
|
+
setBackupCodesLeft(d.backup_codes.length);
|
|
1543
|
+
} finally {
|
|
1544
|
+
setLoading(false);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async function disableTotp(e) {
|
|
1548
|
+
e.preventDefault();
|
|
1549
|
+
setError(null);
|
|
1550
|
+
setLoading(true);
|
|
1551
|
+
try {
|
|
1552
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp`, {
|
|
1553
|
+
method: "DELETE",
|
|
1554
|
+
credentials: "include",
|
|
1555
|
+
headers: { "Content-Type": "application/json" },
|
|
1556
|
+
body: JSON.stringify({ code: disableCode })
|
|
1557
|
+
});
|
|
1558
|
+
const d = await res.json();
|
|
1559
|
+
if (!res.ok) {
|
|
1560
|
+
setError(d.error ?? "Invalid code.");
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
setTotpEnabled(false);
|
|
1564
|
+
setBackupCodesLeft(null);
|
|
1565
|
+
setShowDisable(false);
|
|
1566
|
+
setDisableCode("");
|
|
1567
|
+
} finally {
|
|
1568
|
+
setLoading(false);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (totpEnabled === null) {
|
|
1572
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-6 py-8 text-center text-[#475569] text-sm", children: "Loading\u2026" });
|
|
1573
|
+
}
|
|
1574
|
+
if (enrollStep === "backup-codes") {
|
|
1575
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
|
|
1576
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "rounded-xl bg-emerald-500/10 border border-emerald-500/30 px-4 py-3", children: [
|
|
1577
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs font-semibold text-emerald-400 mb-1", children: "Authenticator app enabled!" }),
|
|
1578
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-emerald-300/70", children: "Save these backup codes somewhere safe. Each can be used once if you lose your phone." })
|
|
1579
|
+
] }),
|
|
1580
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "grid grid-cols-2 gap-1.5", children: backupCodes.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-mono text-xs bg-white/5 border border-white/8 rounded-lg px-3 py-1.5 text-white/80 text-center", children: c }, c)) }),
|
|
1581
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1582
|
+
"button",
|
|
1583
|
+
{
|
|
1584
|
+
onClick: () => {
|
|
1585
|
+
const t = backupCodes.join("\n");
|
|
1586
|
+
navigator.clipboard.writeText(t).catch(() => {
|
|
1587
|
+
});
|
|
1588
|
+
},
|
|
1589
|
+
className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1 border border-white/8 rounded-lg",
|
|
1590
|
+
children: "Copy all codes"
|
|
1591
|
+
}
|
|
1592
|
+
),
|
|
1593
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1594
|
+
"button",
|
|
1595
|
+
{
|
|
1596
|
+
onClick: () => setEnrollStep("idle"),
|
|
1597
|
+
className: "w-full py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 transition-all",
|
|
1598
|
+
children: "Done"
|
|
1599
|
+
}
|
|
1600
|
+
)
|
|
1601
|
+
] });
|
|
1602
|
+
}
|
|
1603
|
+
if (enrollStep === "qr") {
|
|
1604
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
|
|
1605
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-[#475569]", children: "Scan this QR code with Google Authenticator, Authy, or any TOTP app." }),
|
|
1606
|
+
qrDataUrl && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: qrDataUrl, alt: "TOTP QR code", className: "rounded-xl", width: 180, height: 180 }) }),
|
|
1607
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("details", { className: "text-[11px] text-[#475569]", children: [
|
|
1608
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("summary", { className: "cursor-pointer hover:text-white/60", children: "Can't scan? Enter manually" }),
|
|
1609
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mt-1 font-mono break-all bg-white/5 rounded px-2 py-1 text-white/60 select-all", children: manualKey })
|
|
1610
|
+
] }),
|
|
1611
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
|
|
1612
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: verifyEnroll, className: "space-y-3", children: [
|
|
1613
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1614
|
+
"input",
|
|
1615
|
+
{
|
|
1616
|
+
type: "text",
|
|
1617
|
+
inputMode: "numeric",
|
|
1618
|
+
pattern: "[0-9]{6}",
|
|
1619
|
+
maxLength: 6,
|
|
1620
|
+
placeholder: "000000",
|
|
1621
|
+
value: enrollCode,
|
|
1622
|
+
onChange: (e) => setEnrollCode(e.target.value),
|
|
1623
|
+
autoFocus: true,
|
|
1624
|
+
required: true,
|
|
1625
|
+
className: "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5 text-sm text-white/90 placeholder:text-[#475569] focus:outline-none focus:border-purple-500/60 transition-colors text-center tracking-[0.4em]"
|
|
1626
|
+
}
|
|
1627
|
+
),
|
|
1628
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1629
|
+
"button",
|
|
1630
|
+
{
|
|
1631
|
+
type: "submit",
|
|
1632
|
+
disabled: loading || enrollCode.length !== 6,
|
|
1633
|
+
className: "w-full py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 transition-all disabled:opacity-60",
|
|
1634
|
+
children: loading ? "Verifying\u2026" : "Verify and enable"
|
|
1635
|
+
}
|
|
1636
|
+
),
|
|
1637
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1638
|
+
"button",
|
|
1639
|
+
{
|
|
1640
|
+
type: "button",
|
|
1641
|
+
onClick: () => {
|
|
1642
|
+
setEnrollStep("idle");
|
|
1643
|
+
setError(null);
|
|
1644
|
+
},
|
|
1645
|
+
className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
|
|
1646
|
+
children: "\u2190 Cancel"
|
|
1647
|
+
}
|
|
1648
|
+
)
|
|
1649
|
+
] })
|
|
1650
|
+
] });
|
|
1651
|
+
}
|
|
1652
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
|
|
1653
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-xl border border-white/8 p-4", style: { background: "rgba(255,255,255,0.03)" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
1654
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
|
|
1655
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm font-medium text-white/90", children: "Authenticator app" }),
|
|
1656
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-[#475569] mt-0.5", children: totpEnabled ? `Enabled \xB7 ${backupCodesLeft ?? 0} backup code${backupCodesLeft === 1 ? "" : "s"} remaining` : "Add a second layer of security to your account" })
|
|
1657
|
+
] }),
|
|
1658
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_clsx3.clsx)(
|
|
1659
|
+
"text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full border",
|
|
1660
|
+
totpEnabled ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" : "bg-white/5 text-[#475569] border-white/8"
|
|
1661
|
+
), children: totpEnabled ? "On" : "Off" })
|
|
1662
|
+
] }) }),
|
|
1663
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
|
|
1664
|
+
!totpEnabled && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1247
1665
|
"button",
|
|
1248
1666
|
{
|
|
1249
|
-
onClick:
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
showName && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
|
|
1254
|
-
]
|
|
1667
|
+
onClick: startEnroll,
|
|
1668
|
+
disabled: loading,
|
|
1669
|
+
className: "w-full py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 transition-all disabled:opacity-60",
|
|
1670
|
+
children: loading ? "Loading\u2026" : "Enable authenticator app"
|
|
1255
1671
|
}
|
|
1256
1672
|
),
|
|
1257
|
-
|
|
1258
|
-
"
|
|
1673
|
+
totpEnabled && !showDisable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1674
|
+
"button",
|
|
1259
1675
|
{
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1676
|
+
onClick: () => setShowDisable(true),
|
|
1677
|
+
className: "w-full py-2 rounded-xl text-xs font-medium text-red-400 border border-red-500/20 hover:bg-red-500/8 transition-colors",
|
|
1678
|
+
children: "Disable authenticator app"
|
|
1679
|
+
}
|
|
1680
|
+
),
|
|
1681
|
+
totpEnabled && showDisable && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: disableTotp, className: "space-y-3", children: [
|
|
1682
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-[#475569]", children: "Enter your current 6-digit code (or a backup code) to confirm." }),
|
|
1683
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1684
|
+
"input",
|
|
1685
|
+
{
|
|
1686
|
+
type: "text",
|
|
1687
|
+
placeholder: "000000 or backup code",
|
|
1688
|
+
value: disableCode,
|
|
1689
|
+
onChange: (e) => setDisableCode(e.target.value),
|
|
1690
|
+
autoFocus: true,
|
|
1691
|
+
required: true,
|
|
1692
|
+
className: "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5 text-sm text-white/90 placeholder:text-[#475569] focus:outline-none focus:border-red-500/60 transition-colors"
|
|
1693
|
+
}
|
|
1694
|
+
),
|
|
1695
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex gap-2", children: [
|
|
1696
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1697
|
+
"button",
|
|
1698
|
+
{
|
|
1699
|
+
type: "submit",
|
|
1700
|
+
disabled: loading || !disableCode,
|
|
1701
|
+
className: "flex-1 py-2 rounded-xl text-xs font-semibold text-white bg-red-600 hover:bg-red-500 transition-colors disabled:opacity-60",
|
|
1702
|
+
children: loading ? "Disabling\u2026" : "Confirm disable"
|
|
1703
|
+
}
|
|
1263
1704
|
),
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
(
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1296
|
-
DropdownItem,
|
|
1297
|
-
{
|
|
1298
|
-
label: signingOut ? "Signing out\u2026" : "Sign out",
|
|
1299
|
-
icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SignOutIcon, {}),
|
|
1300
|
-
onClick: handleSignOut,
|
|
1301
|
-
destructive: true
|
|
1302
|
-
}
|
|
1303
|
-
)
|
|
1304
|
-
] })
|
|
1305
|
-
]
|
|
1705
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1706
|
+
"button",
|
|
1707
|
+
{
|
|
1708
|
+
type: "button",
|
|
1709
|
+
onClick: () => {
|
|
1710
|
+
setShowDisable(false);
|
|
1711
|
+
setError(null);
|
|
1712
|
+
setDisableCode("");
|
|
1713
|
+
},
|
|
1714
|
+
className: "px-4 py-2 rounded-xl text-xs text-[#475569] hover:text-white hover:bg-white/8 border border-white/8 transition-colors",
|
|
1715
|
+
children: "Cancel"
|
|
1716
|
+
}
|
|
1717
|
+
)
|
|
1718
|
+
] })
|
|
1719
|
+
] })
|
|
1720
|
+
] });
|
|
1721
|
+
}
|
|
1722
|
+
function ModalField({ label, value, onChange, placeholder, type = "text" }) {
|
|
1723
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
|
|
1724
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: label }),
|
|
1725
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1726
|
+
"input",
|
|
1727
|
+
{
|
|
1728
|
+
type,
|
|
1729
|
+
value,
|
|
1730
|
+
onChange: (e) => onChange(e.target.value),
|
|
1731
|
+
placeholder,
|
|
1732
|
+
className: (0, import_clsx3.clsx)(
|
|
1733
|
+
"w-full bg-white/5 border border-white/8 rounded-xl px-3 py-2 text-sm text-white/90",
|
|
1734
|
+
"placeholder:text-[#475569] focus:outline-none focus:border-purple-500/60 transition-colors"
|
|
1735
|
+
)
|
|
1306
1736
|
}
|
|
1307
1737
|
)
|
|
1308
1738
|
] });
|
|
1309
1739
|
}
|
|
1310
|
-
function
|
|
1311
|
-
const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
|
|
1312
|
-
if (imageUrl) {
|
|
1313
|
-
return (
|
|
1314
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
1315
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1316
|
-
"img",
|
|
1317
|
-
{
|
|
1318
|
-
src: imageUrl,
|
|
1319
|
-
alt: "",
|
|
1320
|
-
className: (0, import_clsx3.clsx)(dim, "rounded-full object-cover ring-2 ring-white/10")
|
|
1321
|
-
}
|
|
1322
|
-
)
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1740
|
+
function SaveButton({ saving, label = "Save changes" }) {
|
|
1325
1741
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1326
|
-
"
|
|
1742
|
+
"button",
|
|
1327
1743
|
{
|
|
1744
|
+
type: "submit",
|
|
1745
|
+
disabled: saving,
|
|
1328
1746
|
className: (0, import_clsx3.clsx)(
|
|
1329
|
-
|
|
1330
|
-
"
|
|
1331
|
-
"
|
|
1747
|
+
"px-4 py-2 rounded-xl text-sm font-semibold text-white transition-all",
|
|
1748
|
+
"bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500",
|
|
1749
|
+
"shadow-lg shadow-purple-500/20 disabled:opacity-60"
|
|
1332
1750
|
),
|
|
1333
|
-
children:
|
|
1751
|
+
children: saving ? "Saving\u2026" : label
|
|
1334
1752
|
}
|
|
1335
1753
|
);
|
|
1336
1754
|
}
|
|
1755
|
+
function Avatar({ initials, imageUrl, size = "sm" }) {
|
|
1756
|
+
const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
|
|
1757
|
+
if (imageUrl) {
|
|
1758
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: imageUrl, alt: "", className: (0, import_clsx3.clsx)(dim, "rounded-full object-cover ring-2 ring-white/10") });
|
|
1759
|
+
}
|
|
1760
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: (0, import_clsx3.clsx)(
|
|
1761
|
+
dim,
|
|
1762
|
+
"rounded-full flex items-center justify-center font-semibold text-white",
|
|
1763
|
+
"bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
|
|
1764
|
+
), children: initials });
|
|
1765
|
+
}
|
|
1337
1766
|
function AvatarSkeleton() {
|
|
1338
1767
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-8 h-8 rounded-full bg-white/5 animate-pulse" });
|
|
1339
1768
|
}
|
|
@@ -1344,16 +1773,10 @@ function RiskBadge({ level }) {
|
|
|
1344
1773
|
high: "bg-orange-500/15 text-orange-400 border-orange-500/30",
|
|
1345
1774
|
critical: "bg-red-500/15 text-red-400 border-red-500/30"
|
|
1346
1775
|
};
|
|
1347
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1348
|
-
"
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
"inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
|
|
1352
|
-
styles[level] ?? styles.low
|
|
1353
|
-
),
|
|
1354
|
-
children: level
|
|
1355
|
-
}
|
|
1356
|
-
);
|
|
1776
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_clsx3.clsx)(
|
|
1777
|
+
"inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
|
|
1778
|
+
styles[level] ?? styles.low
|
|
1779
|
+
), children: level });
|
|
1357
1780
|
}
|
|
1358
1781
|
function DropdownItem({ label, icon, onClick, destructive }) {
|
|
1359
1782
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
@@ -1377,9 +1800,6 @@ function UserIcon() {
|
|
|
1377
1800
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "12", cy: "7", r: "4" })
|
|
1378
1801
|
] });
|
|
1379
1802
|
}
|
|
1380
|
-
function ShieldIcon() {
|
|
1381
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }) });
|
|
1382
|
-
}
|
|
1383
1803
|
function SignOutIcon() {
|
|
1384
1804
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1385
1805
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
|
|
@@ -1387,6 +1807,12 @@ function SignOutIcon() {
|
|
|
1387
1807
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
|
|
1388
1808
|
] });
|
|
1389
1809
|
}
|
|
1810
|
+
function CloseIcon() {
|
|
1811
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1812
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1813
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
1814
|
+
] });
|
|
1815
|
+
}
|
|
1390
1816
|
|
|
1391
1817
|
// src/components/OrganizationSwitcher.tsx
|
|
1392
1818
|
var import_clsx4 = require("clsx");
|
|
@@ -1582,17 +2008,19 @@ function MiniSpinner() {
|
|
|
1582
2008
|
}
|
|
1583
2009
|
|
|
1584
2010
|
// src/index.ts
|
|
1585
|
-
var STORAGE_KEY2 = "vaultix_jwt";
|
|
1586
2011
|
function getStoredToken() {
|
|
2012
|
+
if (typeof document === "undefined") return null;
|
|
1587
2013
|
try {
|
|
1588
|
-
|
|
2014
|
+
const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
|
|
2015
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
1589
2016
|
} catch {
|
|
1590
2017
|
return null;
|
|
1591
2018
|
}
|
|
1592
2019
|
}
|
|
1593
2020
|
function clearStoredToken() {
|
|
2021
|
+
if (typeof document === "undefined") return;
|
|
1594
2022
|
try {
|
|
1595
|
-
|
|
2023
|
+
document.cookie = "vaultix-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
|
|
1596
2024
|
} catch {
|
|
1597
2025
|
}
|
|
1598
2026
|
}
|