@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.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 STORAGE_KEY = "vaultix_jwt";
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
- sessionStorage.setItem(STORAGE_KEY, jwt);
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
- return sessionStorage.getItem(STORAGE_KEY);
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
- sessionStorage.removeItem(STORAGE_KEY);
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
- const url = new URL(window.location.href);
153
- const handshakeToken = url.searchParams.get("__vaultix_handshake");
154
- if (handshakeToken) {
155
- url.searchParams.delete("__vaultix_handshake");
156
- window.history.replaceState({}, "", url.toString());
157
- try {
158
- const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
159
- method: "POST",
160
- headers: { "Content-Type": "application/json" },
161
- body: JSON.stringify({ handshake_token: handshakeToken })
162
- });
163
- if (res.ok) {
164
- const data = await res.json();
165
- jwt = data.session_jwt;
166
- storeJwt(jwt);
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
- if (afterSignInUrl) window.location.href = afterSignInUrl;
225
- }, [apiOrigin, afterSignInUrl, clearAuth]);
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
- return sessionStorage.getItem("vaultix_jwt");
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)("div", { ref: containerRef, className: (0, import_clsx3.clsx)("relative", className), children: [
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: () => setOpen((o) => !o),
1250
- className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
1251
- children: [
1252
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl }),
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
- open && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1258
- "div",
1673
+ totpEnabled && !showDisable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1674
+ "button",
1259
1675
  {
1260
- className: (0, import_clsx3.clsx)(
1261
- "absolute right-0 mt-2 w-64 rounded-2xl border border-white/8",
1262
- "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"
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
- style: { background: "rgba(22,27,45,0.96)" },
1265
- children: [
1266
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
1267
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
1268
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 min-w-0", children: [
1269
- (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(" ") }),
1270
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-[#475569] truncate", children: user.email })
1271
- ] })
1272
- ] }),
1273
- 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: [
1274
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
1275
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RiskBadge, { level: session.riskLevel })
1276
- ] }) }),
1277
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-2", children: [
1278
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1279
- DropdownItem,
1280
- {
1281
- label: "Manage account",
1282
- icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(UserIcon, {}),
1283
- onClick: () => setOpen(false)
1284
- }
1285
- ),
1286
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1287
- DropdownItem,
1288
- {
1289
- label: "Security settings",
1290
- icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ShieldIcon, {}),
1291
- onClick: () => setOpen(false)
1292
- }
1293
- ),
1294
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-px bg-white/8 my-1" }),
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 Avatar({ initials, imageUrl, size = "sm" }) {
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
- "div",
1742
+ "button",
1327
1743
  {
1744
+ type: "submit",
1745
+ disabled: saving,
1328
1746
  className: (0, import_clsx3.clsx)(
1329
- dim,
1330
- "rounded-full flex items-center justify-center font-semibold text-white",
1331
- "bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
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: initials
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
- "span",
1349
- {
1350
- className: (0, import_clsx3.clsx)(
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
- return (typeof sessionStorage !== "undefined" ? sessionStorage : null)?.getItem(STORAGE_KEY2) ?? null;
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
- if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(STORAGE_KEY2);
2023
+ document.cookie = "vaultix-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
1596
2024
  } catch {
1597
2025
  }
1598
2026
  }