@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.mjs
CHANGED
|
@@ -23,28 +23,35 @@ function resolveApiOrigin(key, apiUrlProp) {
|
|
|
23
23
|
throw new Error(`Unknown publishable key prefix "${parts[0]}".`);
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
return atob(parts.slice(3).join("_")).replace(/\/$/, "");
|
|
26
|
+
return atob(parts.slice(3).join("_")).replace(/\|.+$/, "").replace(/\/$/, "");
|
|
27
27
|
} catch {
|
|
28
28
|
throw new Error("Publishable key has an unreadable API origin payload.");
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
var
|
|
31
|
+
var TOKEN_COOKIE = "vaultix-token";
|
|
32
|
+
var COOKIE_DAYS = 30;
|
|
32
33
|
function storeJwt(jwt) {
|
|
34
|
+
if (typeof document === "undefined") return;
|
|
33
35
|
try {
|
|
34
|
-
|
|
36
|
+
const expires = new Date(Date.now() + COOKIE_DAYS * 864e5).toUTCString();
|
|
37
|
+
const secure = location.protocol === "https:" ? "; Secure" : "";
|
|
38
|
+
document.cookie = `${TOKEN_COOKIE}=${encodeURIComponent(jwt)}; path=/; expires=${expires}; SameSite=Lax${secure}`;
|
|
35
39
|
} catch {
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
function loadJwt() {
|
|
43
|
+
if (typeof document === "undefined") return null;
|
|
39
44
|
try {
|
|
40
|
-
|
|
45
|
+
const m = document.cookie.match(new RegExp(`(?:^|; )${TOKEN_COOKIE}=([^;]+)`));
|
|
46
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
41
47
|
} catch {
|
|
42
48
|
return null;
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
function clearJwt() {
|
|
52
|
+
if (typeof document === "undefined") return;
|
|
46
53
|
try {
|
|
47
|
-
|
|
54
|
+
document.cookie = `${TOKEN_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`;
|
|
48
55
|
} catch {
|
|
49
56
|
}
|
|
50
57
|
}
|
|
@@ -115,26 +122,27 @@ function VaultixProvider({
|
|
|
115
122
|
async function hydrate() {
|
|
116
123
|
let jwt = null;
|
|
117
124
|
if (typeof window !== "undefined") {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
url.searchParams.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
jwt = loadJwt();
|
|
126
|
+
if (!jwt) {
|
|
127
|
+
const url = new URL(window.location.href);
|
|
128
|
+
const handshakeToken = url.searchParams.get("__vaultix_handshake");
|
|
129
|
+
if (handshakeToken) {
|
|
130
|
+
url.searchParams.delete("__vaultix_handshake");
|
|
131
|
+
window.history.replaceState({}, "", url.toString());
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
body: JSON.stringify({ handshake_token: handshakeToken })
|
|
137
|
+
});
|
|
138
|
+
if (res.ok) {
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
jwt = data.session_jwt;
|
|
141
|
+
storeJwt(jwt);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
133
144
|
}
|
|
134
|
-
} catch {
|
|
135
145
|
}
|
|
136
|
-
} else {
|
|
137
|
-
jwt = loadJwt();
|
|
138
146
|
}
|
|
139
147
|
jwtRef.current = jwt;
|
|
140
148
|
}
|
|
@@ -175,7 +183,7 @@ function VaultixProvider({
|
|
|
175
183
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
176
184
|
};
|
|
177
185
|
}, [apiOrigin, publishableKey, scheduleRefresh]);
|
|
178
|
-
const signOut = useCallback(async () => {
|
|
186
|
+
const signOut = useCallback(async (redirectUrl) => {
|
|
179
187
|
const jwt = jwtRef.current;
|
|
180
188
|
clearAuth();
|
|
181
189
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
@@ -187,8 +195,29 @@ function VaultixProvider({
|
|
|
187
195
|
headers
|
|
188
196
|
}).catch(() => {
|
|
189
197
|
});
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
const dest = redirectUrl ?? afterSignUpUrl ?? "/";
|
|
199
|
+
if (typeof window !== "undefined") window.location.href = dest;
|
|
200
|
+
}, [apiOrigin, afterSignUpUrl, clearAuth]);
|
|
201
|
+
const updateUser = useCallback(async (params) => {
|
|
202
|
+
const userId = user?.id;
|
|
203
|
+
if (!userId) throw new Error("No active session");
|
|
204
|
+
const jwt = jwtRef.current;
|
|
205
|
+
const headers = { "Content-Type": "application/json" };
|
|
206
|
+
if (jwt) headers["Authorization"] = `Bearer ${jwt}`;
|
|
207
|
+
const res = await fetch(`${apiOrigin}/api/v1/users/${userId}`, {
|
|
208
|
+
method: "PATCH",
|
|
209
|
+
credentials: jwt ? "omit" : "include",
|
|
210
|
+
headers,
|
|
211
|
+
body: JSON.stringify(params)
|
|
212
|
+
});
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
const err = await res.json().catch(() => ({ error: "Update failed" }));
|
|
215
|
+
throw new Error(err.error ?? "Update failed");
|
|
216
|
+
}
|
|
217
|
+
const updated = await res.json();
|
|
218
|
+
setUser(updated);
|
|
219
|
+
return updated;
|
|
220
|
+
}, [apiOrigin, user?.id]);
|
|
192
221
|
const connectPlatform = useCallback((provider, options = {}) => {
|
|
193
222
|
const params = new URLSearchParams({ mode: "connect" });
|
|
194
223
|
params.set("redirect_url", options.redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "/"));
|
|
@@ -206,7 +235,9 @@ function VaultixProvider({
|
|
|
206
235
|
organization,
|
|
207
236
|
isLoaded,
|
|
208
237
|
isSignedIn: !!session,
|
|
238
|
+
apiOrigin,
|
|
209
239
|
signOut,
|
|
240
|
+
updateUser,
|
|
210
241
|
connectPlatform
|
|
211
242
|
};
|
|
212
243
|
return /* @__PURE__ */ jsx(VaultixContext.Provider, { value, children: /* @__PURE__ */ jsx(
|
|
@@ -232,8 +263,8 @@ function useSession() {
|
|
|
232
263
|
return { session, isLoaded, isSignedIn };
|
|
233
264
|
}
|
|
234
265
|
function useUser() {
|
|
235
|
-
const { user, isLoaded, isSignedIn } = useVaultixContext();
|
|
236
|
-
return { user, isLoaded, isSignedIn };
|
|
266
|
+
const { user, isLoaded, isSignedIn, updateUser } = useVaultixContext();
|
|
267
|
+
return { user, isLoaded, isSignedIn, update: updateUser };
|
|
237
268
|
}
|
|
238
269
|
function useOrganization() {
|
|
239
270
|
const { organization, isLoaded } = useVaultixContext();
|
|
@@ -244,7 +275,8 @@ function useAuth() {
|
|
|
244
275
|
const getToken = useCallback2(async () => {
|
|
245
276
|
if (!isSignedIn) return null;
|
|
246
277
|
try {
|
|
247
|
-
|
|
278
|
+
const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
|
|
279
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
248
280
|
} catch {
|
|
249
281
|
return null;
|
|
250
282
|
}
|
|
@@ -358,6 +390,23 @@ function SignIn({
|
|
|
358
390
|
setLoading(false);
|
|
359
391
|
}
|
|
360
392
|
}
|
|
393
|
+
async function handleMagicLinkRequest() {
|
|
394
|
+
setError(null);
|
|
395
|
+
setLoading(true);
|
|
396
|
+
try {
|
|
397
|
+
const target = resolveAfterSignInUrl(redirectUrl);
|
|
398
|
+
await fetch(`${apiOrigin}/api/v1/auth/magic-link/send`, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: { "Content-Type": "application/json" },
|
|
401
|
+
body: JSON.stringify({ email, redirect_url: target })
|
|
402
|
+
});
|
|
403
|
+
setStep("magic-link-sent");
|
|
404
|
+
} catch {
|
|
405
|
+
setErr("Network error. Please try again.");
|
|
406
|
+
} finally {
|
|
407
|
+
setLoading(false);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
361
410
|
async function handlePasswordSubmit(e) {
|
|
362
411
|
e.preventDefault();
|
|
363
412
|
setError(null);
|
|
@@ -526,6 +575,7 @@ function SignIn({
|
|
|
526
575
|
password: "Enter password",
|
|
527
576
|
passkey: "Passkey sign-in",
|
|
528
577
|
totp: "Two-factor code",
|
|
578
|
+
"magic-link-sent": "Check your inbox",
|
|
529
579
|
forgot: "Forgot password",
|
|
530
580
|
"forgot-verify": "Enter reset code",
|
|
531
581
|
"forgot-reset": "New password"
|
|
@@ -535,6 +585,7 @@ function SignIn({
|
|
|
535
585
|
password: email,
|
|
536
586
|
passkey: email,
|
|
537
587
|
totp: "Enter the 6-digit code from your authenticator app",
|
|
588
|
+
"magic-link-sent": `We sent a sign-in link to ${email}`,
|
|
538
589
|
forgot: "We'll send a reset code to your email",
|
|
539
590
|
"forgot-verify": `Code sent to ${email}`,
|
|
540
591
|
"forgot-reset": "Choose a new password"
|
|
@@ -592,12 +643,31 @@ function SignIn({
|
|
|
592
643
|
}
|
|
593
644
|
),
|
|
594
645
|
/* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Sign in" }),
|
|
646
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
647
|
+
/* @__PURE__ */ jsx2("div", { className: "flex-1 h-px bg-white/8" }),
|
|
648
|
+
/* @__PURE__ */ jsx2("span", { className: "text-[10px] text-[#475569]", children: "or" }),
|
|
649
|
+
/* @__PURE__ */ jsx2("div", { className: "flex-1 h-px bg-white/8" })
|
|
650
|
+
] }),
|
|
651
|
+
/* @__PURE__ */ jsx2(GhostButton, { onClick: handleMagicLinkRequest, children: "\u2709 Email me a sign-in link" }),
|
|
595
652
|
/* @__PURE__ */ jsx2(GhostButton, { onClick: () => {
|
|
596
653
|
setStep("forgot");
|
|
597
654
|
setError(null);
|
|
598
655
|
}, children: "Forgot password?" }),
|
|
599
656
|
/* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
|
|
600
657
|
] }),
|
|
658
|
+
step === "magic-link-sent" && /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-center", children: [
|
|
659
|
+
/* @__PURE__ */ jsx2("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__ */ jsx2(EnvelopeIcon, {}) }),
|
|
660
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-[#94a3b8] leading-relaxed", children: [
|
|
661
|
+
"The link expires in ",
|
|
662
|
+
/* @__PURE__ */ jsx2("span", { className: "text-white/80 font-medium", children: "15 minutes" }),
|
|
663
|
+
" and can only be used once."
|
|
664
|
+
] }),
|
|
665
|
+
/* @__PURE__ */ jsx2(GhostButton, { onClick: handleMagicLinkRequest, children: "Resend link" }),
|
|
666
|
+
/* @__PURE__ */ jsx2(GhostButton, { onClick: () => {
|
|
667
|
+
setStep("password");
|
|
668
|
+
setError(null);
|
|
669
|
+
}, children: "Use password instead" })
|
|
670
|
+
] }),
|
|
601
671
|
step === "passkey" && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
602
672
|
/* @__PURE__ */ jsxs(PrimaryButton, { loading, onClick: handlePasskeySignIn, children: [
|
|
603
673
|
/* @__PURE__ */ jsx2(FingerprintIcon, {}),
|
|
@@ -840,6 +910,12 @@ function GitHubIcon() {
|
|
|
840
910
|
function XIcon() {
|
|
841
911
|
return /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ jsx2("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" }) });
|
|
842
912
|
}
|
|
913
|
+
function EnvelopeIcon() {
|
|
914
|
+
return /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#a78bfa", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
915
|
+
/* @__PURE__ */ jsx2("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
|
|
916
|
+
/* @__PURE__ */ jsx2("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })
|
|
917
|
+
] });
|
|
918
|
+
}
|
|
843
919
|
|
|
844
920
|
// src/components/SignUp.tsx
|
|
845
921
|
import { clsx as clsx2 } from "clsx";
|
|
@@ -1180,22 +1256,17 @@ function RedirectToSignUp({ redirectUrl = "/sign-up" }) {
|
|
|
1180
1256
|
|
|
1181
1257
|
// src/components/UserButton.tsx
|
|
1182
1258
|
import { clsx as clsx3 } from "clsx";
|
|
1183
|
-
import { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
|
|
1184
|
-
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1185
|
-
function UserButton({
|
|
1186
|
-
showName = false,
|
|
1187
|
-
afterSignOutUrl,
|
|
1188
|
-
className
|
|
1189
|
-
}) {
|
|
1259
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
|
|
1260
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1261
|
+
function UserButton({ showName = false, afterSignOutUrl, className }) {
|
|
1190
1262
|
const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
|
|
1191
1263
|
const [open, setOpen] = useState4(false);
|
|
1192
1264
|
const [signingOut, setSigningOut] = useState4(false);
|
|
1265
|
+
const [showProfile, setShowProfile] = useState4(false);
|
|
1193
1266
|
const containerRef = useRef3(null);
|
|
1194
1267
|
useEffect3(() => {
|
|
1195
1268
|
function onPointerDown(e) {
|
|
1196
|
-
if (containerRef.current && !containerRef.current.contains(e.target))
|
|
1197
|
-
setOpen(false);
|
|
1198
|
-
}
|
|
1269
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
|
|
1199
1270
|
}
|
|
1200
1271
|
document.addEventListener("pointerdown", onPointerDown);
|
|
1201
1272
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
@@ -1208,98 +1279,456 @@ function UserButton({
|
|
|
1208
1279
|
await signOut();
|
|
1209
1280
|
if (afterSignOutUrl) window.location.href = afterSignOutUrl;
|
|
1210
1281
|
}
|
|
1211
|
-
return /* @__PURE__ */ jsxs3(
|
|
1212
|
-
/* @__PURE__ */ jsxs3(
|
|
1282
|
+
return /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1283
|
+
/* @__PURE__ */ jsxs3("div", { ref: containerRef, className: clsx3("relative", className), children: [
|
|
1284
|
+
/* @__PURE__ */ jsxs3(
|
|
1285
|
+
"button",
|
|
1286
|
+
{
|
|
1287
|
+
onClick: () => setOpen((o) => !o),
|
|
1288
|
+
className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
|
|
1289
|
+
children: [
|
|
1290
|
+
/* @__PURE__ */ jsx5(Avatar, { initials, imageUrl: user.imageUrl }),
|
|
1291
|
+
showName && /* @__PURE__ */ jsx5("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
|
|
1292
|
+
]
|
|
1293
|
+
}
|
|
1294
|
+
),
|
|
1295
|
+
open && /* @__PURE__ */ jsxs3(
|
|
1296
|
+
"div",
|
|
1297
|
+
{
|
|
1298
|
+
className: clsx3("absolute right-0 mt-2 w-64 rounded-2xl border border-white/8", "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"),
|
|
1299
|
+
style: { background: "rgba(22,27,45,0.96)" },
|
|
1300
|
+
children: [
|
|
1301
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
|
|
1302
|
+
/* @__PURE__ */ jsx5(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
|
|
1303
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
|
|
1304
|
+
(user.firstName || user.lastName) && /* @__PURE__ */ jsx5("p", { className: "text-sm font-semibold text-white/90 truncate", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
|
|
1305
|
+
/* @__PURE__ */ jsx5("p", { className: "text-xs text-[#475569] truncate", children: user.email })
|
|
1306
|
+
] })
|
|
1307
|
+
] }),
|
|
1308
|
+
session && /* @__PURE__ */ jsx5("div", { className: "px-4 py-2 border-b border-white/8", children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
|
|
1309
|
+
/* @__PURE__ */ jsx5("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
|
|
1310
|
+
/* @__PURE__ */ jsx5(RiskBadge, { level: session.riskLevel })
|
|
1311
|
+
] }) }),
|
|
1312
|
+
/* @__PURE__ */ jsxs3("div", { className: "p-2", children: [
|
|
1313
|
+
/* @__PURE__ */ jsx5(DropdownItem, { label: "Manage account", icon: /* @__PURE__ */ jsx5(UserIcon, {}), onClick: () => {
|
|
1314
|
+
setOpen(false);
|
|
1315
|
+
setShowProfile(true);
|
|
1316
|
+
} }),
|
|
1317
|
+
/* @__PURE__ */ jsx5("div", { className: "h-px bg-white/8 my-1" }),
|
|
1318
|
+
/* @__PURE__ */ jsx5(DropdownItem, { label: signingOut ? "Signing out\u2026" : "Sign out", icon: /* @__PURE__ */ jsx5(SignOutIcon, {}), onClick: handleSignOut, destructive: true })
|
|
1319
|
+
] })
|
|
1320
|
+
]
|
|
1321
|
+
}
|
|
1322
|
+
)
|
|
1323
|
+
] }),
|
|
1324
|
+
showProfile && /* @__PURE__ */ jsx5(ProfileModal, { onClose: () => setShowProfile(false) })
|
|
1325
|
+
] });
|
|
1326
|
+
}
|
|
1327
|
+
function ProfileModal({ onClose }) {
|
|
1328
|
+
const { user, updateUser } = useVaultixContext();
|
|
1329
|
+
const [tab, setTab] = useState4("profile");
|
|
1330
|
+
const [firstName, setFirstName] = useState4(user?.firstName ?? "");
|
|
1331
|
+
const [lastName, setLastName] = useState4(user?.lastName ?? "");
|
|
1332
|
+
const [imageUrl, setImageUrl] = useState4(user?.imageUrl ?? "");
|
|
1333
|
+
const [currentPassword, setCurrentPassword] = useState4("");
|
|
1334
|
+
const [newPassword, setNewPassword] = useState4("");
|
|
1335
|
+
const [confirmPassword, setConfirmPassword] = useState4("");
|
|
1336
|
+
const [saving, setSaving] = useState4(false);
|
|
1337
|
+
const [error, setError] = useState4(null);
|
|
1338
|
+
const [success, setSuccess] = useState4(null);
|
|
1339
|
+
useEffect3(() => {
|
|
1340
|
+
function onKey(e) {
|
|
1341
|
+
if (e.key === "Escape") onClose();
|
|
1342
|
+
}
|
|
1343
|
+
document.addEventListener("keydown", onKey);
|
|
1344
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1345
|
+
}, [onClose]);
|
|
1346
|
+
async function saveProfile(e) {
|
|
1347
|
+
e.preventDefault();
|
|
1348
|
+
setError(null);
|
|
1349
|
+
setSuccess(null);
|
|
1350
|
+
setSaving(true);
|
|
1351
|
+
try {
|
|
1352
|
+
await updateUser({ firstName: firstName.trim() || null, lastName: lastName.trim() || null, imageUrl: imageUrl.trim() || null });
|
|
1353
|
+
setSuccess("Profile updated.");
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
setError(err instanceof Error ? err.message : "Update failed");
|
|
1356
|
+
} finally {
|
|
1357
|
+
setSaving(false);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async function savePassword(e) {
|
|
1361
|
+
e.preventDefault();
|
|
1362
|
+
setError(null);
|
|
1363
|
+
setSuccess(null);
|
|
1364
|
+
if (newPassword !== confirmPassword) {
|
|
1365
|
+
setError("Passwords do not match.");
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (newPassword.length < 8) {
|
|
1369
|
+
setError("Password must be at least 8 characters.");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
setSaving(true);
|
|
1373
|
+
try {
|
|
1374
|
+
await updateUser({ currentPassword, newPassword });
|
|
1375
|
+
setSuccess("Password changed.");
|
|
1376
|
+
setCurrentPassword("");
|
|
1377
|
+
setNewPassword("");
|
|
1378
|
+
setConfirmPassword("");
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
setError(err instanceof Error ? err.message : "Update failed");
|
|
1381
|
+
} finally {
|
|
1382
|
+
setSaving(false);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const TABS = [
|
|
1386
|
+
{ id: "profile", label: "Profile" },
|
|
1387
|
+
{ id: "password", label: "Change password" },
|
|
1388
|
+
{ id: "security", label: "Security" }
|
|
1389
|
+
];
|
|
1390
|
+
return /* @__PURE__ */ jsx5(
|
|
1391
|
+
"div",
|
|
1392
|
+
{
|
|
1393
|
+
className: "fixed inset-0 z-[9999] flex items-center justify-center p-4",
|
|
1394
|
+
style: { background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" },
|
|
1395
|
+
onPointerDown: (e) => {
|
|
1396
|
+
if (e.target === e.currentTarget) onClose();
|
|
1397
|
+
},
|
|
1398
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "w-full max-w-md rounded-2xl border border-white/8 shadow-2xl", style: { background: "rgba(22,27,45,0.98)" }, children: [
|
|
1399
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/8", children: [
|
|
1400
|
+
/* @__PURE__ */ jsx5("h2", { className: "text-sm font-semibold text-white/90", children: "Manage account" }),
|
|
1401
|
+
/* @__PURE__ */ jsx5("button", { onClick: onClose, className: "text-[#475569] hover:text-white transition-colors", children: /* @__PURE__ */ jsx5(CloseIcon, {}) })
|
|
1402
|
+
] }),
|
|
1403
|
+
/* @__PURE__ */ jsx5("div", { className: "flex gap-1 px-6 pt-4 flex-wrap", children: TABS.map(({ id, label }) => /* @__PURE__ */ jsx5(
|
|
1404
|
+
"button",
|
|
1405
|
+
{
|
|
1406
|
+
onClick: () => {
|
|
1407
|
+
setTab(id);
|
|
1408
|
+
setError(null);
|
|
1409
|
+
setSuccess(null);
|
|
1410
|
+
},
|
|
1411
|
+
className: clsx3(
|
|
1412
|
+
"px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",
|
|
1413
|
+
tab === id ? "bg-purple-500/15 text-purple-300 border border-purple-500/30" : "text-[#475569] hover:text-white/70"
|
|
1414
|
+
),
|
|
1415
|
+
children: label
|
|
1416
|
+
},
|
|
1417
|
+
id
|
|
1418
|
+
)) }),
|
|
1419
|
+
/* @__PURE__ */ jsxs3("div", { className: "px-6 pt-3", children: [
|
|
1420
|
+
error && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
|
|
1421
|
+
success && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-emerald-500/10 border border-emerald-500/30 px-3 py-2 text-xs text-emerald-400", children: success })
|
|
1422
|
+
] }),
|
|
1423
|
+
tab === "profile" && /* @__PURE__ */ jsxs3("form", { onSubmit: saveProfile, className: "px-6 py-4 space-y-4", children: [
|
|
1424
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
1425
|
+
/* @__PURE__ */ jsx5(ModalField, { label: "First name", value: firstName, onChange: setFirstName, placeholder: "Jane" }),
|
|
1426
|
+
/* @__PURE__ */ jsx5(ModalField, { label: "Last name", value: lastName, onChange: setLastName, placeholder: "Smith" })
|
|
1427
|
+
] }),
|
|
1428
|
+
/* @__PURE__ */ jsx5(ModalField, { label: "Avatar URL", value: imageUrl, onChange: setImageUrl, placeholder: "https://\u2026", type: "url" }),
|
|
1429
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 pt-1", children: [
|
|
1430
|
+
/* @__PURE__ */ jsx5(SaveButton, { saving }),
|
|
1431
|
+
/* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
|
|
1432
|
+
] })
|
|
1433
|
+
] }),
|
|
1434
|
+
tab === "password" && /* @__PURE__ */ jsxs3("form", { onSubmit: savePassword, className: "px-6 py-4 space-y-4", children: [
|
|
1435
|
+
/* @__PURE__ */ jsx5(ModalField, { label: "Current password", value: currentPassword, onChange: setCurrentPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
|
|
1436
|
+
/* @__PURE__ */ jsx5(ModalField, { label: "New password", value: newPassword, onChange: setNewPassword, type: "password", placeholder: "Min 8 characters" }),
|
|
1437
|
+
/* @__PURE__ */ jsx5(ModalField, { label: "Confirm new password", value: confirmPassword, onChange: setConfirmPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
|
|
1438
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 pt-1", children: [
|
|
1439
|
+
/* @__PURE__ */ jsx5(SaveButton, { saving, label: "Change password" }),
|
|
1440
|
+
/* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
|
|
1441
|
+
] })
|
|
1442
|
+
] }),
|
|
1443
|
+
tab === "security" && /* @__PURE__ */ jsx5(SecurityTab, {})
|
|
1444
|
+
] })
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
function SecurityTab() {
|
|
1449
|
+
const { apiOrigin } = useVaultixContext();
|
|
1450
|
+
const [totpEnabled, setTotpEnabled] = useState4(null);
|
|
1451
|
+
const [backupCodesLeft, setBackupCodesLeft] = useState4(null);
|
|
1452
|
+
const [enrollStep, setEnrollStep] = useState4("idle");
|
|
1453
|
+
const [qrDataUrl, setQrDataUrl] = useState4("");
|
|
1454
|
+
const [manualKey, setManualKey] = useState4("");
|
|
1455
|
+
const [enrollCode, setEnrollCode] = useState4("");
|
|
1456
|
+
const [disableCode, setDisableCode] = useState4("");
|
|
1457
|
+
const [backupCodes, setBackupCodes] = useState4([]);
|
|
1458
|
+
const [loading, setLoading] = useState4(false);
|
|
1459
|
+
const [error, setError] = useState4(null);
|
|
1460
|
+
const [showDisable, setShowDisable] = useState4(false);
|
|
1461
|
+
const fetchStatus = useCallback3(async () => {
|
|
1462
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp/status`, { credentials: "include" });
|
|
1463
|
+
if (res.ok) {
|
|
1464
|
+
const d = await res.json();
|
|
1465
|
+
setTotpEnabled(d.enabled);
|
|
1466
|
+
setBackupCodesLeft(d.backup_codes_remaining);
|
|
1467
|
+
}
|
|
1468
|
+
}, [apiOrigin]);
|
|
1469
|
+
useEffect3(() => {
|
|
1470
|
+
void fetchStatus();
|
|
1471
|
+
}, [fetchStatus]);
|
|
1472
|
+
async function startEnroll() {
|
|
1473
|
+
setError(null);
|
|
1474
|
+
setLoading(true);
|
|
1475
|
+
try {
|
|
1476
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp/enroll`, { method: "POST", credentials: "include" });
|
|
1477
|
+
const d = await res.json();
|
|
1478
|
+
if (!res.ok) {
|
|
1479
|
+
setError(d.error ?? "Failed to start enrollment.");
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
setQrDataUrl(d.qr_data_url);
|
|
1483
|
+
setManualKey(d.manual_entry_key);
|
|
1484
|
+
setEnrollStep("qr");
|
|
1485
|
+
} finally {
|
|
1486
|
+
setLoading(false);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function verifyEnroll(e) {
|
|
1490
|
+
e.preventDefault();
|
|
1491
|
+
setError(null);
|
|
1492
|
+
setLoading(true);
|
|
1493
|
+
try {
|
|
1494
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp/verify-enroll`, {
|
|
1495
|
+
method: "POST",
|
|
1496
|
+
credentials: "include",
|
|
1497
|
+
headers: { "Content-Type": "application/json" },
|
|
1498
|
+
body: JSON.stringify({ code: enrollCode })
|
|
1499
|
+
});
|
|
1500
|
+
const d = await res.json();
|
|
1501
|
+
if (!res.ok) {
|
|
1502
|
+
setError(d.error ?? "Invalid code. Try again.");
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
setBackupCodes(d.backup_codes);
|
|
1506
|
+
setEnrollStep("backup-codes");
|
|
1507
|
+
setTotpEnabled(true);
|
|
1508
|
+
setBackupCodesLeft(d.backup_codes.length);
|
|
1509
|
+
} finally {
|
|
1510
|
+
setLoading(false);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
async function disableTotp(e) {
|
|
1514
|
+
e.preventDefault();
|
|
1515
|
+
setError(null);
|
|
1516
|
+
setLoading(true);
|
|
1517
|
+
try {
|
|
1518
|
+
const res = await fetch(`${apiOrigin}/api/v1/auth/totp`, {
|
|
1519
|
+
method: "DELETE",
|
|
1520
|
+
credentials: "include",
|
|
1521
|
+
headers: { "Content-Type": "application/json" },
|
|
1522
|
+
body: JSON.stringify({ code: disableCode })
|
|
1523
|
+
});
|
|
1524
|
+
const d = await res.json();
|
|
1525
|
+
if (!res.ok) {
|
|
1526
|
+
setError(d.error ?? "Invalid code.");
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
setTotpEnabled(false);
|
|
1530
|
+
setBackupCodesLeft(null);
|
|
1531
|
+
setShowDisable(false);
|
|
1532
|
+
setDisableCode("");
|
|
1533
|
+
} finally {
|
|
1534
|
+
setLoading(false);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (totpEnabled === null) {
|
|
1538
|
+
return /* @__PURE__ */ jsx5("div", { className: "px-6 py-8 text-center text-[#475569] text-sm", children: "Loading\u2026" });
|
|
1539
|
+
}
|
|
1540
|
+
if (enrollStep === "backup-codes") {
|
|
1541
|
+
return /* @__PURE__ */ jsxs3("div", { className: "px-6 py-4 space-y-4", children: [
|
|
1542
|
+
/* @__PURE__ */ jsxs3("div", { className: "rounded-xl bg-emerald-500/10 border border-emerald-500/30 px-4 py-3", children: [
|
|
1543
|
+
/* @__PURE__ */ jsx5("p", { className: "text-xs font-semibold text-emerald-400 mb-1", children: "Authenticator app enabled!" }),
|
|
1544
|
+
/* @__PURE__ */ jsx5("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." })
|
|
1545
|
+
] }),
|
|
1546
|
+
/* @__PURE__ */ jsx5("div", { className: "grid grid-cols-2 gap-1.5", children: backupCodes.map((c) => /* @__PURE__ */ jsx5("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)) }),
|
|
1547
|
+
/* @__PURE__ */ jsx5(
|
|
1548
|
+
"button",
|
|
1549
|
+
{
|
|
1550
|
+
onClick: () => {
|
|
1551
|
+
const t = backupCodes.join("\n");
|
|
1552
|
+
navigator.clipboard.writeText(t).catch(() => {
|
|
1553
|
+
});
|
|
1554
|
+
},
|
|
1555
|
+
className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1 border border-white/8 rounded-lg",
|
|
1556
|
+
children: "Copy all codes"
|
|
1557
|
+
}
|
|
1558
|
+
),
|
|
1559
|
+
/* @__PURE__ */ jsx5(
|
|
1560
|
+
"button",
|
|
1561
|
+
{
|
|
1562
|
+
onClick: () => setEnrollStep("idle"),
|
|
1563
|
+
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",
|
|
1564
|
+
children: "Done"
|
|
1565
|
+
}
|
|
1566
|
+
)
|
|
1567
|
+
] });
|
|
1568
|
+
}
|
|
1569
|
+
if (enrollStep === "qr") {
|
|
1570
|
+
return /* @__PURE__ */ jsxs3("div", { className: "px-6 py-4 space-y-4", children: [
|
|
1571
|
+
/* @__PURE__ */ jsx5("p", { className: "text-xs text-[#475569]", children: "Scan this QR code with Google Authenticator, Authy, or any TOTP app." }),
|
|
1572
|
+
qrDataUrl && /* @__PURE__ */ jsx5("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx5("img", { src: qrDataUrl, alt: "TOTP QR code", className: "rounded-xl", width: 180, height: 180 }) }),
|
|
1573
|
+
/* @__PURE__ */ jsxs3("details", { className: "text-[11px] text-[#475569]", children: [
|
|
1574
|
+
/* @__PURE__ */ jsx5("summary", { className: "cursor-pointer hover:text-white/60", children: "Can't scan? Enter manually" }),
|
|
1575
|
+
/* @__PURE__ */ jsx5("p", { className: "mt-1 font-mono break-all bg-white/5 rounded px-2 py-1 text-white/60 select-all", children: manualKey })
|
|
1576
|
+
] }),
|
|
1577
|
+
error && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
|
|
1578
|
+
/* @__PURE__ */ jsxs3("form", { onSubmit: verifyEnroll, className: "space-y-3", children: [
|
|
1579
|
+
/* @__PURE__ */ jsx5(
|
|
1580
|
+
"input",
|
|
1581
|
+
{
|
|
1582
|
+
type: "text",
|
|
1583
|
+
inputMode: "numeric",
|
|
1584
|
+
pattern: "[0-9]{6}",
|
|
1585
|
+
maxLength: 6,
|
|
1586
|
+
placeholder: "000000",
|
|
1587
|
+
value: enrollCode,
|
|
1588
|
+
onChange: (e) => setEnrollCode(e.target.value),
|
|
1589
|
+
autoFocus: true,
|
|
1590
|
+
required: true,
|
|
1591
|
+
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]"
|
|
1592
|
+
}
|
|
1593
|
+
),
|
|
1594
|
+
/* @__PURE__ */ jsx5(
|
|
1595
|
+
"button",
|
|
1596
|
+
{
|
|
1597
|
+
type: "submit",
|
|
1598
|
+
disabled: loading || enrollCode.length !== 6,
|
|
1599
|
+
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",
|
|
1600
|
+
children: loading ? "Verifying\u2026" : "Verify and enable"
|
|
1601
|
+
}
|
|
1602
|
+
),
|
|
1603
|
+
/* @__PURE__ */ jsx5(
|
|
1604
|
+
"button",
|
|
1605
|
+
{
|
|
1606
|
+
type: "button",
|
|
1607
|
+
onClick: () => {
|
|
1608
|
+
setEnrollStep("idle");
|
|
1609
|
+
setError(null);
|
|
1610
|
+
},
|
|
1611
|
+
className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
|
|
1612
|
+
children: "\u2190 Cancel"
|
|
1613
|
+
}
|
|
1614
|
+
)
|
|
1615
|
+
] })
|
|
1616
|
+
] });
|
|
1617
|
+
}
|
|
1618
|
+
return /* @__PURE__ */ jsxs3("div", { className: "px-6 py-4 space-y-4", children: [
|
|
1619
|
+
/* @__PURE__ */ jsx5("div", { className: "rounded-xl border border-white/8 p-4", style: { background: "rgba(255,255,255,0.03)" }, children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
|
|
1620
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
1621
|
+
/* @__PURE__ */ jsx5("p", { className: "text-sm font-medium text-white/90", children: "Authenticator app" }),
|
|
1622
|
+
/* @__PURE__ */ jsx5("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" })
|
|
1623
|
+
] }),
|
|
1624
|
+
/* @__PURE__ */ jsx5("span", { className: clsx3(
|
|
1625
|
+
"text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full border",
|
|
1626
|
+
totpEnabled ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" : "bg-white/5 text-[#475569] border-white/8"
|
|
1627
|
+
), children: totpEnabled ? "On" : "Off" })
|
|
1628
|
+
] }) }),
|
|
1629
|
+
error && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
|
|
1630
|
+
!totpEnabled && /* @__PURE__ */ jsx5(
|
|
1213
1631
|
"button",
|
|
1214
1632
|
{
|
|
1215
|
-
onClick:
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
showName && /* @__PURE__ */ jsx5("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
|
|
1220
|
-
]
|
|
1633
|
+
onClick: startEnroll,
|
|
1634
|
+
disabled: loading,
|
|
1635
|
+
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",
|
|
1636
|
+
children: loading ? "Loading\u2026" : "Enable authenticator app"
|
|
1221
1637
|
}
|
|
1222
1638
|
),
|
|
1223
|
-
|
|
1224
|
-
"
|
|
1639
|
+
totpEnabled && !showDisable && /* @__PURE__ */ jsx5(
|
|
1640
|
+
"button",
|
|
1225
1641
|
{
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1642
|
+
onClick: () => setShowDisable(true),
|
|
1643
|
+
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",
|
|
1644
|
+
children: "Disable authenticator app"
|
|
1645
|
+
}
|
|
1646
|
+
),
|
|
1647
|
+
totpEnabled && showDisable && /* @__PURE__ */ jsxs3("form", { onSubmit: disableTotp, className: "space-y-3", children: [
|
|
1648
|
+
/* @__PURE__ */ jsx5("p", { className: "text-[11px] text-[#475569]", children: "Enter your current 6-digit code (or a backup code) to confirm." }),
|
|
1649
|
+
/* @__PURE__ */ jsx5(
|
|
1650
|
+
"input",
|
|
1651
|
+
{
|
|
1652
|
+
type: "text",
|
|
1653
|
+
placeholder: "000000 or backup code",
|
|
1654
|
+
value: disableCode,
|
|
1655
|
+
onChange: (e) => setDisableCode(e.target.value),
|
|
1656
|
+
autoFocus: true,
|
|
1657
|
+
required: true,
|
|
1658
|
+
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"
|
|
1659
|
+
}
|
|
1660
|
+
),
|
|
1661
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
|
|
1662
|
+
/* @__PURE__ */ jsx5(
|
|
1663
|
+
"button",
|
|
1664
|
+
{
|
|
1665
|
+
type: "submit",
|
|
1666
|
+
disabled: loading || !disableCode,
|
|
1667
|
+
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",
|
|
1668
|
+
children: loading ? "Disabling\u2026" : "Confirm disable"
|
|
1669
|
+
}
|
|
1229
1670
|
),
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
(
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
/* @__PURE__ */ jsx5(
|
|
1262
|
-
DropdownItem,
|
|
1263
|
-
{
|
|
1264
|
-
label: signingOut ? "Signing out\u2026" : "Sign out",
|
|
1265
|
-
icon: /* @__PURE__ */ jsx5(SignOutIcon, {}),
|
|
1266
|
-
onClick: handleSignOut,
|
|
1267
|
-
destructive: true
|
|
1268
|
-
}
|
|
1269
|
-
)
|
|
1270
|
-
] })
|
|
1271
|
-
]
|
|
1671
|
+
/* @__PURE__ */ jsx5(
|
|
1672
|
+
"button",
|
|
1673
|
+
{
|
|
1674
|
+
type: "button",
|
|
1675
|
+
onClick: () => {
|
|
1676
|
+
setShowDisable(false);
|
|
1677
|
+
setError(null);
|
|
1678
|
+
setDisableCode("");
|
|
1679
|
+
},
|
|
1680
|
+
className: "px-4 py-2 rounded-xl text-xs text-[#475569] hover:text-white hover:bg-white/8 border border-white/8 transition-colors",
|
|
1681
|
+
children: "Cancel"
|
|
1682
|
+
}
|
|
1683
|
+
)
|
|
1684
|
+
] })
|
|
1685
|
+
] })
|
|
1686
|
+
] });
|
|
1687
|
+
}
|
|
1688
|
+
function ModalField({ label, value, onChange, placeholder, type = "text" }) {
|
|
1689
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
|
|
1690
|
+
/* @__PURE__ */ jsx5("label", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: label }),
|
|
1691
|
+
/* @__PURE__ */ jsx5(
|
|
1692
|
+
"input",
|
|
1693
|
+
{
|
|
1694
|
+
type,
|
|
1695
|
+
value,
|
|
1696
|
+
onChange: (e) => onChange(e.target.value),
|
|
1697
|
+
placeholder,
|
|
1698
|
+
className: clsx3(
|
|
1699
|
+
"w-full bg-white/5 border border-white/8 rounded-xl px-3 py-2 text-sm text-white/90",
|
|
1700
|
+
"placeholder:text-[#475569] focus:outline-none focus:border-purple-500/60 transition-colors"
|
|
1701
|
+
)
|
|
1272
1702
|
}
|
|
1273
1703
|
)
|
|
1274
1704
|
] });
|
|
1275
1705
|
}
|
|
1276
|
-
function
|
|
1277
|
-
const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
|
|
1278
|
-
if (imageUrl) {
|
|
1279
|
-
return (
|
|
1280
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
1281
|
-
/* @__PURE__ */ jsx5(
|
|
1282
|
-
"img",
|
|
1283
|
-
{
|
|
1284
|
-
src: imageUrl,
|
|
1285
|
-
alt: "",
|
|
1286
|
-
className: clsx3(dim, "rounded-full object-cover ring-2 ring-white/10")
|
|
1287
|
-
}
|
|
1288
|
-
)
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1706
|
+
function SaveButton({ saving, label = "Save changes" }) {
|
|
1291
1707
|
return /* @__PURE__ */ jsx5(
|
|
1292
|
-
"
|
|
1708
|
+
"button",
|
|
1293
1709
|
{
|
|
1710
|
+
type: "submit",
|
|
1711
|
+
disabled: saving,
|
|
1294
1712
|
className: clsx3(
|
|
1295
|
-
|
|
1296
|
-
"
|
|
1297
|
-
"
|
|
1713
|
+
"px-4 py-2 rounded-xl text-sm font-semibold text-white transition-all",
|
|
1714
|
+
"bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500",
|
|
1715
|
+
"shadow-lg shadow-purple-500/20 disabled:opacity-60"
|
|
1298
1716
|
),
|
|
1299
|
-
children:
|
|
1717
|
+
children: saving ? "Saving\u2026" : label
|
|
1300
1718
|
}
|
|
1301
1719
|
);
|
|
1302
1720
|
}
|
|
1721
|
+
function Avatar({ initials, imageUrl, size = "sm" }) {
|
|
1722
|
+
const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
|
|
1723
|
+
if (imageUrl) {
|
|
1724
|
+
return /* @__PURE__ */ jsx5("img", { src: imageUrl, alt: "", className: clsx3(dim, "rounded-full object-cover ring-2 ring-white/10") });
|
|
1725
|
+
}
|
|
1726
|
+
return /* @__PURE__ */ jsx5("div", { className: clsx3(
|
|
1727
|
+
dim,
|
|
1728
|
+
"rounded-full flex items-center justify-center font-semibold text-white",
|
|
1729
|
+
"bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
|
|
1730
|
+
), children: initials });
|
|
1731
|
+
}
|
|
1303
1732
|
function AvatarSkeleton() {
|
|
1304
1733
|
return /* @__PURE__ */ jsx5("div", { className: "w-8 h-8 rounded-full bg-white/5 animate-pulse" });
|
|
1305
1734
|
}
|
|
@@ -1310,16 +1739,10 @@ function RiskBadge({ level }) {
|
|
|
1310
1739
|
high: "bg-orange-500/15 text-orange-400 border-orange-500/30",
|
|
1311
1740
|
critical: "bg-red-500/15 text-red-400 border-red-500/30"
|
|
1312
1741
|
};
|
|
1313
|
-
return /* @__PURE__ */ jsx5(
|
|
1314
|
-
"
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
"inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
|
|
1318
|
-
styles[level] ?? styles.low
|
|
1319
|
-
),
|
|
1320
|
-
children: level
|
|
1321
|
-
}
|
|
1322
|
-
);
|
|
1742
|
+
return /* @__PURE__ */ jsx5("span", { className: clsx3(
|
|
1743
|
+
"inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
|
|
1744
|
+
styles[level] ?? styles.low
|
|
1745
|
+
), children: level });
|
|
1323
1746
|
}
|
|
1324
1747
|
function DropdownItem({ label, icon, onClick, destructive }) {
|
|
1325
1748
|
return /* @__PURE__ */ jsxs3(
|
|
@@ -1343,9 +1766,6 @@ function UserIcon() {
|
|
|
1343
1766
|
/* @__PURE__ */ jsx5("circle", { cx: "12", cy: "7", r: "4" })
|
|
1344
1767
|
] });
|
|
1345
1768
|
}
|
|
1346
|
-
function ShieldIcon() {
|
|
1347
|
-
return /* @__PURE__ */ jsx5("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx5("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }) });
|
|
1348
|
-
}
|
|
1349
1769
|
function SignOutIcon() {
|
|
1350
1770
|
return /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1351
1771
|
/* @__PURE__ */ jsx5("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
|
|
@@ -1353,6 +1773,12 @@ function SignOutIcon() {
|
|
|
1353
1773
|
/* @__PURE__ */ jsx5("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
|
|
1354
1774
|
] });
|
|
1355
1775
|
}
|
|
1776
|
+
function CloseIcon() {
|
|
1777
|
+
return /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1778
|
+
/* @__PURE__ */ jsx5("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1779
|
+
/* @__PURE__ */ jsx5("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
1780
|
+
] });
|
|
1781
|
+
}
|
|
1356
1782
|
|
|
1357
1783
|
// src/components/OrganizationSwitcher.tsx
|
|
1358
1784
|
import { clsx as clsx4 } from "clsx";
|
|
@@ -1548,17 +1974,19 @@ function MiniSpinner() {
|
|
|
1548
1974
|
}
|
|
1549
1975
|
|
|
1550
1976
|
// src/index.ts
|
|
1551
|
-
var STORAGE_KEY2 = "vaultix_jwt";
|
|
1552
1977
|
function getStoredToken() {
|
|
1978
|
+
if (typeof document === "undefined") return null;
|
|
1553
1979
|
try {
|
|
1554
|
-
|
|
1980
|
+
const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
|
|
1981
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
1555
1982
|
} catch {
|
|
1556
1983
|
return null;
|
|
1557
1984
|
}
|
|
1558
1985
|
}
|
|
1559
1986
|
function clearStoredToken() {
|
|
1987
|
+
if (typeof document === "undefined") return;
|
|
1560
1988
|
try {
|
|
1561
|
-
|
|
1989
|
+
document.cookie = "vaultix-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
|
|
1562
1990
|
} catch {
|
|
1563
1991
|
}
|
|
1564
1992
|
}
|