@vaultix.ai/react 0.3.1 → 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 +49 -6
- package/dist/index.d.ts +49 -6
- package/dist/index.js +586 -126
- package/dist/index.mjs +586 -128
- 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,15 +195,50 @@ 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]);
|
|
221
|
+
const connectPlatform = useCallback((provider, options = {}) => {
|
|
222
|
+
const params = new URLSearchParams({ mode: "connect" });
|
|
223
|
+
params.set("redirect_url", options.redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "/"));
|
|
224
|
+
params.set("pk", publishableKey);
|
|
225
|
+
if (options.scopes?.length) params.set("scope", options.scopes.join(" "));
|
|
226
|
+
if (options.configId) params.set("config_id", options.configId);
|
|
227
|
+
if (options.extraParams) Object.entries(options.extraParams).forEach(([k, v]) => params.set(k, v));
|
|
228
|
+
if (typeof window !== "undefined") {
|
|
229
|
+
window.location.href = `${apiOrigin}/api/v1/auth/oauth/${provider}/connect?${params}`;
|
|
230
|
+
}
|
|
231
|
+
}, [apiOrigin, publishableKey]);
|
|
192
232
|
const value = {
|
|
193
233
|
user,
|
|
194
234
|
session,
|
|
195
235
|
organization,
|
|
196
236
|
isLoaded,
|
|
197
237
|
isSignedIn: !!session,
|
|
198
|
-
|
|
238
|
+
apiOrigin,
|
|
239
|
+
signOut,
|
|
240
|
+
updateUser,
|
|
241
|
+
connectPlatform
|
|
199
242
|
};
|
|
200
243
|
return /* @__PURE__ */ jsx(VaultixContext.Provider, { value, children: /* @__PURE__ */ jsx(
|
|
201
244
|
"div",
|
|
@@ -220,8 +263,8 @@ function useSession() {
|
|
|
220
263
|
return { session, isLoaded, isSignedIn };
|
|
221
264
|
}
|
|
222
265
|
function useUser() {
|
|
223
|
-
const { user, isLoaded, isSignedIn } = useVaultixContext();
|
|
224
|
-
return { user, isLoaded, isSignedIn };
|
|
266
|
+
const { user, isLoaded, isSignedIn, updateUser } = useVaultixContext();
|
|
267
|
+
return { user, isLoaded, isSignedIn, update: updateUser };
|
|
225
268
|
}
|
|
226
269
|
function useOrganization() {
|
|
227
270
|
const { organization, isLoaded } = useVaultixContext();
|
|
@@ -232,7 +275,8 @@ function useAuth() {
|
|
|
232
275
|
const getToken = useCallback2(async () => {
|
|
233
276
|
if (!isSignedIn) return null;
|
|
234
277
|
try {
|
|
235
|
-
|
|
278
|
+
const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
|
|
279
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
236
280
|
} catch {
|
|
237
281
|
return null;
|
|
238
282
|
}
|
|
@@ -346,6 +390,23 @@ function SignIn({
|
|
|
346
390
|
setLoading(false);
|
|
347
391
|
}
|
|
348
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
|
+
}
|
|
349
410
|
async function handlePasswordSubmit(e) {
|
|
350
411
|
e.preventDefault();
|
|
351
412
|
setError(null);
|
|
@@ -514,6 +575,7 @@ function SignIn({
|
|
|
514
575
|
password: "Enter password",
|
|
515
576
|
passkey: "Passkey sign-in",
|
|
516
577
|
totp: "Two-factor code",
|
|
578
|
+
"magic-link-sent": "Check your inbox",
|
|
517
579
|
forgot: "Forgot password",
|
|
518
580
|
"forgot-verify": "Enter reset code",
|
|
519
581
|
"forgot-reset": "New password"
|
|
@@ -523,6 +585,7 @@ function SignIn({
|
|
|
523
585
|
password: email,
|
|
524
586
|
passkey: email,
|
|
525
587
|
totp: "Enter the 6-digit code from your authenticator app",
|
|
588
|
+
"magic-link-sent": `We sent a sign-in link to ${email}`,
|
|
526
589
|
forgot: "We'll send a reset code to your email",
|
|
527
590
|
"forgot-verify": `Code sent to ${email}`,
|
|
528
591
|
"forgot-reset": "Choose a new password"
|
|
@@ -580,12 +643,31 @@ function SignIn({
|
|
|
580
643
|
}
|
|
581
644
|
),
|
|
582
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" }),
|
|
583
652
|
/* @__PURE__ */ jsx2(GhostButton, { onClick: () => {
|
|
584
653
|
setStep("forgot");
|
|
585
654
|
setError(null);
|
|
586
655
|
}, children: "Forgot password?" }),
|
|
587
656
|
/* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
|
|
588
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
|
+
] }),
|
|
589
671
|
step === "passkey" && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
590
672
|
/* @__PURE__ */ jsxs(PrimaryButton, { loading, onClick: handlePasskeySignIn, children: [
|
|
591
673
|
/* @__PURE__ */ jsx2(FingerprintIcon, {}),
|
|
@@ -828,6 +910,12 @@ function GitHubIcon() {
|
|
|
828
910
|
function XIcon() {
|
|
829
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" }) });
|
|
830
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
|
+
}
|
|
831
919
|
|
|
832
920
|
// src/components/SignUp.tsx
|
|
833
921
|
import { clsx as clsx2 } from "clsx";
|
|
@@ -1168,22 +1256,17 @@ function RedirectToSignUp({ redirectUrl = "/sign-up" }) {
|
|
|
1168
1256
|
|
|
1169
1257
|
// src/components/UserButton.tsx
|
|
1170
1258
|
import { clsx as clsx3 } from "clsx";
|
|
1171
|
-
import { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
|
|
1172
|
-
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1173
|
-
function UserButton({
|
|
1174
|
-
showName = false,
|
|
1175
|
-
afterSignOutUrl,
|
|
1176
|
-
className
|
|
1177
|
-
}) {
|
|
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 }) {
|
|
1178
1262
|
const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
|
|
1179
1263
|
const [open, setOpen] = useState4(false);
|
|
1180
1264
|
const [signingOut, setSigningOut] = useState4(false);
|
|
1265
|
+
const [showProfile, setShowProfile] = useState4(false);
|
|
1181
1266
|
const containerRef = useRef3(null);
|
|
1182
1267
|
useEffect3(() => {
|
|
1183
1268
|
function onPointerDown(e) {
|
|
1184
|
-
if (containerRef.current && !containerRef.current.contains(e.target))
|
|
1185
|
-
setOpen(false);
|
|
1186
|
-
}
|
|
1269
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
|
|
1187
1270
|
}
|
|
1188
1271
|
document.addEventListener("pointerdown", onPointerDown);
|
|
1189
1272
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
@@ -1196,98 +1279,456 @@ function UserButton({
|
|
|
1196
1279
|
await signOut();
|
|
1197
1280
|
if (afterSignOutUrl) window.location.href = afterSignOutUrl;
|
|
1198
1281
|
}
|
|
1199
|
-
return /* @__PURE__ */ jsxs3(
|
|
1200
|
-
/* @__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(
|
|
1201
1631
|
"button",
|
|
1202
1632
|
{
|
|
1203
|
-
onClick:
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
showName && /* @__PURE__ */ jsx5("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
|
|
1208
|
-
]
|
|
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"
|
|
1209
1637
|
}
|
|
1210
1638
|
),
|
|
1211
|
-
|
|
1212
|
-
"
|
|
1639
|
+
totpEnabled && !showDisable && /* @__PURE__ */ jsx5(
|
|
1640
|
+
"button",
|
|
1213
1641
|
{
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
+
}
|
|
1217
1670
|
),
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
(
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
/* @__PURE__ */ jsx5(
|
|
1250
|
-
DropdownItem,
|
|
1251
|
-
{
|
|
1252
|
-
label: signingOut ? "Signing out\u2026" : "Sign out",
|
|
1253
|
-
icon: /* @__PURE__ */ jsx5(SignOutIcon, {}),
|
|
1254
|
-
onClick: handleSignOut,
|
|
1255
|
-
destructive: true
|
|
1256
|
-
}
|
|
1257
|
-
)
|
|
1258
|
-
] })
|
|
1259
|
-
]
|
|
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
|
+
)
|
|
1260
1702
|
}
|
|
1261
1703
|
)
|
|
1262
1704
|
] });
|
|
1263
1705
|
}
|
|
1264
|
-
function
|
|
1265
|
-
const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
|
|
1266
|
-
if (imageUrl) {
|
|
1267
|
-
return (
|
|
1268
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
1269
|
-
/* @__PURE__ */ jsx5(
|
|
1270
|
-
"img",
|
|
1271
|
-
{
|
|
1272
|
-
src: imageUrl,
|
|
1273
|
-
alt: "",
|
|
1274
|
-
className: clsx3(dim, "rounded-full object-cover ring-2 ring-white/10")
|
|
1275
|
-
}
|
|
1276
|
-
)
|
|
1277
|
-
);
|
|
1278
|
-
}
|
|
1706
|
+
function SaveButton({ saving, label = "Save changes" }) {
|
|
1279
1707
|
return /* @__PURE__ */ jsx5(
|
|
1280
|
-
"
|
|
1708
|
+
"button",
|
|
1281
1709
|
{
|
|
1710
|
+
type: "submit",
|
|
1711
|
+
disabled: saving,
|
|
1282
1712
|
className: clsx3(
|
|
1283
|
-
|
|
1284
|
-
"
|
|
1285
|
-
"
|
|
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"
|
|
1286
1716
|
),
|
|
1287
|
-
children:
|
|
1717
|
+
children: saving ? "Saving\u2026" : label
|
|
1288
1718
|
}
|
|
1289
1719
|
);
|
|
1290
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
|
+
}
|
|
1291
1732
|
function AvatarSkeleton() {
|
|
1292
1733
|
return /* @__PURE__ */ jsx5("div", { className: "w-8 h-8 rounded-full bg-white/5 animate-pulse" });
|
|
1293
1734
|
}
|
|
@@ -1298,16 +1739,10 @@ function RiskBadge({ level }) {
|
|
|
1298
1739
|
high: "bg-orange-500/15 text-orange-400 border-orange-500/30",
|
|
1299
1740
|
critical: "bg-red-500/15 text-red-400 border-red-500/30"
|
|
1300
1741
|
};
|
|
1301
|
-
return /* @__PURE__ */ jsx5(
|
|
1302
|
-
"
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
"inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
|
|
1306
|
-
styles[level] ?? styles.low
|
|
1307
|
-
),
|
|
1308
|
-
children: level
|
|
1309
|
-
}
|
|
1310
|
-
);
|
|
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 });
|
|
1311
1746
|
}
|
|
1312
1747
|
function DropdownItem({ label, icon, onClick, destructive }) {
|
|
1313
1748
|
return /* @__PURE__ */ jsxs3(
|
|
@@ -1331,9 +1766,6 @@ function UserIcon() {
|
|
|
1331
1766
|
/* @__PURE__ */ jsx5("circle", { cx: "12", cy: "7", r: "4" })
|
|
1332
1767
|
] });
|
|
1333
1768
|
}
|
|
1334
|
-
function ShieldIcon() {
|
|
1335
|
-
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" }) });
|
|
1336
|
-
}
|
|
1337
1769
|
function SignOutIcon() {
|
|
1338
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: [
|
|
1339
1771
|
/* @__PURE__ */ jsx5("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
|
|
@@ -1341,6 +1773,12 @@ function SignOutIcon() {
|
|
|
1341
1773
|
/* @__PURE__ */ jsx5("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
|
|
1342
1774
|
] });
|
|
1343
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
|
+
}
|
|
1344
1782
|
|
|
1345
1783
|
// src/components/OrganizationSwitcher.tsx
|
|
1346
1784
|
import { clsx as clsx4 } from "clsx";
|
|
@@ -1534,6 +1972,24 @@ function MiniSpinner() {
|
|
|
1534
1972
|
/* @__PURE__ */ jsx6("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })
|
|
1535
1973
|
] });
|
|
1536
1974
|
}
|
|
1975
|
+
|
|
1976
|
+
// src/index.ts
|
|
1977
|
+
function getStoredToken() {
|
|
1978
|
+
if (typeof document === "undefined") return null;
|
|
1979
|
+
try {
|
|
1980
|
+
const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
|
|
1981
|
+
return m?.[1] ? decodeURIComponent(m[1]) : null;
|
|
1982
|
+
} catch {
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function clearStoredToken() {
|
|
1987
|
+
if (typeof document === "undefined") return;
|
|
1988
|
+
try {
|
|
1989
|
+
document.cookie = "vaultix-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
|
|
1990
|
+
} catch {
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1537
1993
|
export {
|
|
1538
1994
|
OrganizationSwitcher,
|
|
1539
1995
|
RedirectToSignIn,
|
|
@@ -1544,6 +2000,8 @@ export {
|
|
|
1544
2000
|
SignedOut,
|
|
1545
2001
|
UserButton,
|
|
1546
2002
|
VaultixProvider,
|
|
2003
|
+
clearStoredToken,
|
|
2004
|
+
getStoredToken,
|
|
1547
2005
|
useAuth,
|
|
1548
2006
|
useOrganization,
|
|
1549
2007
|
useSession,
|