@invect/user-auth 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/backend/index.cjs +1166 -0
- package/dist/backend/index.cjs.map +1 -0
- package/dist/backend/index.d.ts +42 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.mjs +1164 -0
- package/dist/backend/index.mjs.map +1 -0
- package/dist/backend/plugin.d.ts +62 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/types.d.ts +299 -0
- package/dist/backend/types.d.ts.map +1 -0
- package/dist/frontend/components/AuthGate.d.ts +17 -0
- package/dist/frontend/components/AuthGate.d.ts.map +1 -0
- package/dist/frontend/components/AuthenticatedInvect.d.ts +129 -0
- package/dist/frontend/components/AuthenticatedInvect.d.ts.map +1 -0
- package/dist/frontend/components/ProfilePage.d.ts +10 -0
- package/dist/frontend/components/ProfilePage.d.ts.map +1 -0
- package/dist/frontend/components/SidebarUserMenu.d.ts +12 -0
- package/dist/frontend/components/SidebarUserMenu.d.ts.map +1 -0
- package/dist/frontend/components/SignInForm.d.ts +14 -0
- package/dist/frontend/components/SignInForm.d.ts.map +1 -0
- package/dist/frontend/components/SignInPage.d.ts +19 -0
- package/dist/frontend/components/SignInPage.d.ts.map +1 -0
- package/dist/frontend/components/UserButton.d.ts +15 -0
- package/dist/frontend/components/UserButton.d.ts.map +1 -0
- package/dist/frontend/components/UserManagement.d.ts +19 -0
- package/dist/frontend/components/UserManagement.d.ts.map +1 -0
- package/dist/frontend/components/UserManagementPage.d.ts +9 -0
- package/dist/frontend/components/UserManagementPage.d.ts.map +1 -0
- package/dist/frontend/index.cjs +1262 -0
- package/dist/frontend/index.cjs.map +1 -0
- package/dist/frontend/index.d.ts +29 -0
- package/dist/frontend/index.d.ts.map +1 -0
- package/dist/frontend/index.mjs +1250 -0
- package/dist/frontend/index.mjs.map +1 -0
- package/dist/frontend/plugins/authFrontendPlugin.d.ts +14 -0
- package/dist/frontend/plugins/authFrontendPlugin.d.ts.map +1 -0
- package/dist/frontend/providers/AuthProvider.d.ts +46 -0
- package/dist/frontend/providers/AuthProvider.d.ts.map +1 -0
- package/dist/roles-BOY5N82v.cjs +74 -0
- package/dist/roles-BOY5N82v.cjs.map +1 -0
- package/dist/roles-CZuKFEpJ.mjs +33 -0
- package/dist/roles-CZuKFEpJ.mjs.map +1 -0
- package/dist/shared/roles.d.ts +11 -0
- package/dist/shared/roles.d.ts.map +1 -0
- package/dist/shared/types.cjs +0 -0
- package/dist/shared/types.d.ts +46 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.mjs +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
import { a as formatAuthRoleLabel, o as isAuthAssignableRole, r as AUTH_DEFAULT_ROLE } from "../roles-CZuKFEpJ.mjs";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { ArrowDown, ArrowUp, ChevronDown, ChevronsUpDown, LogOut, Mail, Search, Shield, Trash2, User, UserPlus, Users } from "lucide-react";
|
|
6
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, PageLayout, useApiClient } from "@invect/frontend";
|
|
7
|
+
import { Link, useLocation } from "react-router";
|
|
8
|
+
//#region src/frontend/providers/AuthProvider.tsx
|
|
9
|
+
/**
|
|
10
|
+
* AuthProvider — Context provider for authentication state.
|
|
11
|
+
*
|
|
12
|
+
* Fetches the current session from the better-auth proxy endpoints
|
|
13
|
+
* and caches it via React Query. Provides sign-in, sign-up, and
|
|
14
|
+
* sign-out actions to child components.
|
|
15
|
+
*/
|
|
16
|
+
const AuthContext = createContext(null);
|
|
17
|
+
function AuthProvider({ children, baseUrl }) {
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
const authApiBase = `${baseUrl}/plugins/auth/api/auth`;
|
|
20
|
+
const { data: session, isLoading } = useQuery({
|
|
21
|
+
queryKey: ["auth", "session"],
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
const response = await fetch(`${authApiBase}/get-session`, { credentials: "include" });
|
|
24
|
+
if (!response.ok) return {
|
|
25
|
+
user: null,
|
|
26
|
+
isAuthenticated: false
|
|
27
|
+
};
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
if (!data?.user) return {
|
|
30
|
+
user: null,
|
|
31
|
+
isAuthenticated: false
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
user: {
|
|
35
|
+
id: data.user.id,
|
|
36
|
+
name: data.user.name ?? void 0,
|
|
37
|
+
email: data.user.email ?? void 0,
|
|
38
|
+
image: data.user.image ?? void 0,
|
|
39
|
+
role: data.user.role ?? void 0
|
|
40
|
+
},
|
|
41
|
+
isAuthenticated: true
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
staleTime: 300 * 1e3,
|
|
45
|
+
retry: 1
|
|
46
|
+
});
|
|
47
|
+
const signInMutation = useMutation({
|
|
48
|
+
mutationFn: async (credentials) => {
|
|
49
|
+
const response = await fetch(`${authApiBase}/sign-in/email`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
credentials: "include",
|
|
53
|
+
body: JSON.stringify(credentials)
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const err = await response.json().catch(() => ({ message: "Sign in failed" }));
|
|
57
|
+
throw new Error(err.message || `Sign in failed (${response.status})`);
|
|
58
|
+
}
|
|
59
|
+
return response.json();
|
|
60
|
+
},
|
|
61
|
+
onSuccess: () => {
|
|
62
|
+
queryClient.invalidateQueries({ queryKey: ["auth", "session"] });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const signUpMutation = useMutation({
|
|
66
|
+
mutationFn: async (credentials) => {
|
|
67
|
+
const response = await fetch(`${authApiBase}/sign-up/email`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json" },
|
|
70
|
+
credentials: "include",
|
|
71
|
+
body: JSON.stringify(credentials)
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const err = await response.json().catch(() => ({ message: "Sign up failed" }));
|
|
75
|
+
throw new Error(err.message || `Sign up failed (${response.status})`);
|
|
76
|
+
}
|
|
77
|
+
return response.json();
|
|
78
|
+
},
|
|
79
|
+
onSuccess: () => {
|
|
80
|
+
queryClient.invalidateQueries({ queryKey: ["auth", "session"] });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const signOutMutation = useMutation({
|
|
84
|
+
mutationFn: async () => {
|
|
85
|
+
const response = await fetch(`${authApiBase}/sign-out`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "content-type": "application/json" },
|
|
88
|
+
credentials: "include",
|
|
89
|
+
body: JSON.stringify({})
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const err = await response.json().catch(async () => ({ message: await response.text().catch(() => "") || "Sign out failed" }));
|
|
93
|
+
throw new Error(err.message || `Sign out failed (${response.status})`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onSuccess: () => {
|
|
97
|
+
queryClient.invalidateQueries({ queryKey: ["auth", "session"] });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const signIn = useCallback(async (credentials) => {
|
|
101
|
+
await signInMutation.mutateAsync(credentials);
|
|
102
|
+
}, [signInMutation]);
|
|
103
|
+
const signUp = useCallback(async (credentials) => {
|
|
104
|
+
await signUpMutation.mutateAsync(credentials);
|
|
105
|
+
}, [signUpMutation]);
|
|
106
|
+
const signOut = useCallback(async () => {
|
|
107
|
+
await signOutMutation.mutateAsync();
|
|
108
|
+
}, [signOutMutation]);
|
|
109
|
+
const error = signInMutation.error?.message ?? signUpMutation.error?.message ?? signOutMutation.error?.message ?? null;
|
|
110
|
+
const value = useMemo(() => ({
|
|
111
|
+
user: session?.isAuthenticated ? session.user : null,
|
|
112
|
+
isAuthenticated: session?.isAuthenticated ?? false,
|
|
113
|
+
isLoading,
|
|
114
|
+
signIn,
|
|
115
|
+
signUp,
|
|
116
|
+
signOut,
|
|
117
|
+
isSigningIn: signInMutation.isPending,
|
|
118
|
+
isSigningUp: signUpMutation.isPending,
|
|
119
|
+
error
|
|
120
|
+
}), [
|
|
121
|
+
session,
|
|
122
|
+
isLoading,
|
|
123
|
+
signIn,
|
|
124
|
+
signUp,
|
|
125
|
+
signOut,
|
|
126
|
+
signInMutation.isPending,
|
|
127
|
+
signUpMutation.isPending,
|
|
128
|
+
error
|
|
129
|
+
]);
|
|
130
|
+
return /* @__PURE__ */ jsx(AuthContext.Provider, {
|
|
131
|
+
value,
|
|
132
|
+
children
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Access auth context — current user, sign-in/sign-up/sign-out actions.
|
|
137
|
+
*
|
|
138
|
+
* Must be used within an `<AuthProvider>`.
|
|
139
|
+
* Returns a safe fallback (unauthenticated) if provider is missing.
|
|
140
|
+
*/
|
|
141
|
+
function useAuth() {
|
|
142
|
+
const ctx = useContext(AuthContext);
|
|
143
|
+
if (!ctx) return {
|
|
144
|
+
user: null,
|
|
145
|
+
isAuthenticated: false,
|
|
146
|
+
isLoading: false,
|
|
147
|
+
signIn: async () => {
|
|
148
|
+
throw new Error("AuthProvider not found");
|
|
149
|
+
},
|
|
150
|
+
signUp: async () => {
|
|
151
|
+
throw new Error("AuthProvider not found");
|
|
152
|
+
},
|
|
153
|
+
signOut: async () => {
|
|
154
|
+
throw new Error("AuthProvider not found");
|
|
155
|
+
},
|
|
156
|
+
isSigningIn: false,
|
|
157
|
+
isSigningUp: false,
|
|
158
|
+
error: null
|
|
159
|
+
};
|
|
160
|
+
return ctx;
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/frontend/components/SignInForm.tsx
|
|
164
|
+
/**
|
|
165
|
+
* SignInForm — Email/password sign-in form component.
|
|
166
|
+
*
|
|
167
|
+
* Uses the AuthProvider's signIn action. Styled to match the Invect
|
|
168
|
+
* design system with grouped fields, clean labels, and themed inputs.
|
|
169
|
+
*/
|
|
170
|
+
function SignInForm({ onSuccess, className }) {
|
|
171
|
+
const { signIn, isSigningIn, error } = useAuth();
|
|
172
|
+
const [email, setEmail] = useState("");
|
|
173
|
+
const [password, setPassword] = useState("");
|
|
174
|
+
const [localError, setLocalError] = useState(null);
|
|
175
|
+
const handleSubmit = async (e) => {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
setLocalError(null);
|
|
178
|
+
if (!email.trim() || !password.trim()) {
|
|
179
|
+
setLocalError("Email and password are required");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await signIn({
|
|
184
|
+
email,
|
|
185
|
+
password
|
|
186
|
+
});
|
|
187
|
+
onSuccess?.();
|
|
188
|
+
} catch (err) {
|
|
189
|
+
setLocalError(err instanceof Error ? err.message : "Sign in failed");
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const displayError = localError ?? error;
|
|
193
|
+
return /* @__PURE__ */ jsx("form", {
|
|
194
|
+
onSubmit: handleSubmit,
|
|
195
|
+
className,
|
|
196
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
className: "flex flex-col gap-6",
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsxs("div", {
|
|
200
|
+
className: "grid gap-2",
|
|
201
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
202
|
+
htmlFor: "auth-signin-email",
|
|
203
|
+
className: "text-sm font-medium leading-none",
|
|
204
|
+
children: "Email"
|
|
205
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
206
|
+
id: "auth-signin-email",
|
|
207
|
+
type: "email",
|
|
208
|
+
value: email,
|
|
209
|
+
onChange: (e) => setEmail(e.target.value),
|
|
210
|
+
placeholder: "you@example.com",
|
|
211
|
+
autoComplete: "email",
|
|
212
|
+
required: true,
|
|
213
|
+
className: "flex h-9 w-full rounded-md border border-imp-border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-imp-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-imp-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
214
|
+
})]
|
|
215
|
+
}),
|
|
216
|
+
/* @__PURE__ */ jsxs("div", {
|
|
217
|
+
className: "grid gap-2",
|
|
218
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
219
|
+
htmlFor: "auth-signin-password",
|
|
220
|
+
className: "text-sm font-medium leading-none",
|
|
221
|
+
children: "Password"
|
|
222
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
223
|
+
id: "auth-signin-password",
|
|
224
|
+
type: "password",
|
|
225
|
+
value: password,
|
|
226
|
+
onChange: (e) => setPassword(e.target.value),
|
|
227
|
+
placeholder: "••••••••",
|
|
228
|
+
autoComplete: "current-password",
|
|
229
|
+
required: true,
|
|
230
|
+
className: "flex h-9 w-full rounded-md border border-imp-border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-imp-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-imp-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
231
|
+
})]
|
|
232
|
+
}),
|
|
233
|
+
displayError && /* @__PURE__ */ jsx("div", {
|
|
234
|
+
className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-900/50 dark:bg-red-950/20 dark:text-red-400",
|
|
235
|
+
children: displayError
|
|
236
|
+
}),
|
|
237
|
+
/* @__PURE__ */ jsx("button", {
|
|
238
|
+
type: "submit",
|
|
239
|
+
disabled: isSigningIn,
|
|
240
|
+
className: "inline-flex h-9 w-full items-center justify-center gap-2 whitespace-nowrap rounded-md bg-imp-foreground px-4 py-2 text-sm font-medium text-imp-background shadow transition-colors hover:bg-imp-foreground/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-imp-ring disabled:pointer-events-none disabled:opacity-50",
|
|
241
|
+
children: isSigningIn ? "Signing in…" : "Login"
|
|
242
|
+
})
|
|
243
|
+
]
|
|
244
|
+
})
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/frontend/components/SignInPage.tsx
|
|
249
|
+
/**
|
|
250
|
+
* SignInPage — Full-page sign-in component with layout.
|
|
251
|
+
*
|
|
252
|
+
* Renders the SignInForm centered on the page with a logo, title,
|
|
253
|
+
* and grouped fields matching the Invect design system.
|
|
254
|
+
* Sign-up is not offered — new users are created by admins.
|
|
255
|
+
*/
|
|
256
|
+
function SignInPage({ onSuccess, onNavigateToSignUp, title = "Welcome back", subtitle = "Sign in to your account to continue" }) {
|
|
257
|
+
return /* @__PURE__ */ jsx("div", {
|
|
258
|
+
className: "flex min-h-screen items-center justify-center bg-imp-background p-4 text-imp-foreground",
|
|
259
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
260
|
+
className: "flex w-full max-w-sm flex-col gap-6",
|
|
261
|
+
children: [
|
|
262
|
+
/* @__PURE__ */ jsxs("div", {
|
|
263
|
+
className: "flex flex-col items-center gap-2 text-center",
|
|
264
|
+
children: [
|
|
265
|
+
/* @__PURE__ */ jsx("div", {
|
|
266
|
+
className: "flex items-center justify-center",
|
|
267
|
+
children: /* @__PURE__ */ jsx(ImpLogo, { className: "h-10 w-auto" })
|
|
268
|
+
}),
|
|
269
|
+
/* @__PURE__ */ jsx("h1", {
|
|
270
|
+
className: "text-xl font-bold",
|
|
271
|
+
children: title
|
|
272
|
+
}),
|
|
273
|
+
/* @__PURE__ */ jsx("p", {
|
|
274
|
+
className: "text-sm text-imp-muted-foreground",
|
|
275
|
+
children: subtitle
|
|
276
|
+
})
|
|
277
|
+
]
|
|
278
|
+
}),
|
|
279
|
+
/* @__PURE__ */ jsx(SignInForm, { onSuccess }),
|
|
280
|
+
/* @__PURE__ */ jsxs("div", {
|
|
281
|
+
className: "relative text-center text-sm",
|
|
282
|
+
children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 top-1/2 border-t border-imp-border" }), /* @__PURE__ */ jsx("span", {
|
|
283
|
+
className: "relative bg-imp-background px-2 text-imp-muted-foreground",
|
|
284
|
+
children: "Admin-managed accounts"
|
|
285
|
+
})]
|
|
286
|
+
}),
|
|
287
|
+
/* @__PURE__ */ jsxs("p", {
|
|
288
|
+
className: "px-6 text-center text-xs text-imp-muted-foreground",
|
|
289
|
+
children: [
|
|
290
|
+
"New accounts are created by your administrator.",
|
|
291
|
+
onNavigateToSignUp ? " Or " : " Contact your admin if you need access.",
|
|
292
|
+
onNavigateToSignUp && /* @__PURE__ */ jsx("button", {
|
|
293
|
+
type: "button",
|
|
294
|
+
onClick: onNavigateToSignUp,
|
|
295
|
+
className: "font-medium underline underline-offset-4 hover:text-imp-foreground",
|
|
296
|
+
children: "Sign up"
|
|
297
|
+
})
|
|
298
|
+
]
|
|
299
|
+
})
|
|
300
|
+
]
|
|
301
|
+
})
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function ImpLogo({ className }) {
|
|
305
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
306
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
307
|
+
viewBox: "0 0 109 209",
|
|
308
|
+
fill: "none",
|
|
309
|
+
stroke: "currentColor",
|
|
310
|
+
strokeWidth: "9",
|
|
311
|
+
strokeLinecap: "round",
|
|
312
|
+
strokeLinejoin: "round",
|
|
313
|
+
className,
|
|
314
|
+
children: [
|
|
315
|
+
/* @__PURE__ */ jsx("path", { d: "M31.7735 104.5L4.50073 18.7859L54.5007 33.0716L31.7735 104.5Z" }),
|
|
316
|
+
/* @__PURE__ */ jsx("path", { d: "M54.5007 4.5L104.501 18.7857L54.5007 33.0714L4.50073 18.7857L54.5007 4.5Z" }),
|
|
317
|
+
/* @__PURE__ */ jsx("path", { d: "M4.50073 190.214L54.5007 33.0716L104.501 18.7859L4.50073 190.214Z" }),
|
|
318
|
+
/* @__PURE__ */ jsx("path", { d: "M54.5007 204.5L81.4238 104.5L104.501 18.7859L4.50073 190.214L54.5007 204.5Z" }),
|
|
319
|
+
/* @__PURE__ */ jsx("path", { d: "M54.5007 204.5L81.4238 104.5L104.501 190.214L54.5007 204.5Z" })
|
|
320
|
+
]
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/frontend/components/UserButton.tsx
|
|
325
|
+
/**
|
|
326
|
+
* UserButton — Compact user avatar + dropdown for the authenticated user.
|
|
327
|
+
*
|
|
328
|
+
* Shows the user's avatar/initials when signed in, with a dropdown
|
|
329
|
+
* containing their name, email, and sign-out button.
|
|
330
|
+
* Shows a "Sign In" button when not authenticated.
|
|
331
|
+
*/
|
|
332
|
+
function UserButton({ onSignInClick, className }) {
|
|
333
|
+
const { user, isAuthenticated, isLoading, signOut } = useAuth();
|
|
334
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
335
|
+
const ref = useRef(null);
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
const handleClick = (e) => {
|
|
338
|
+
if (ref.current && !ref.current.contains(e.target)) setIsOpen(false);
|
|
339
|
+
};
|
|
340
|
+
if (isOpen) document.addEventListener("mousedown", handleClick);
|
|
341
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
342
|
+
}, [isOpen]);
|
|
343
|
+
if (isLoading) return /* @__PURE__ */ jsx("div", { className: `h-8 w-8 animate-pulse rounded-full bg-imp-muted ${className ?? ""}` });
|
|
344
|
+
if (!isAuthenticated || !user) return /* @__PURE__ */ jsxs("button", {
|
|
345
|
+
onClick: onSignInClick,
|
|
346
|
+
className: `inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-imp-foreground hover:bg-imp-muted ${className ?? ""}`,
|
|
347
|
+
children: [/* @__PURE__ */ jsx(User, { className: "h-4 w-4" }), "Sign In"]
|
|
348
|
+
});
|
|
349
|
+
const initials = (user.name ?? user.email ?? user.id)[0]?.toUpperCase() ?? "?";
|
|
350
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
351
|
+
ref,
|
|
352
|
+
className: `relative ${className ?? ""}`,
|
|
353
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
354
|
+
onClick: () => setIsOpen(!isOpen),
|
|
355
|
+
className: "flex h-8 w-8 items-center justify-center rounded-full bg-imp-primary/10 text-sm font-medium text-imp-primary hover:bg-imp-primary/20 transition-colors",
|
|
356
|
+
title: user.name ?? user.email ?? user.id,
|
|
357
|
+
children: user.image ? /* @__PURE__ */ jsx("img", {
|
|
358
|
+
src: user.image,
|
|
359
|
+
alt: user.name ?? "",
|
|
360
|
+
className: "h-8 w-8 rounded-full object-cover"
|
|
361
|
+
}) : initials
|
|
362
|
+
}), isOpen && /* @__PURE__ */ jsxs("div", {
|
|
363
|
+
className: "absolute right-0 top-full z-50 mt-2 w-56 rounded-lg border border-imp-border bg-imp-background shadow-lg",
|
|
364
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
365
|
+
className: "border-b border-imp-border px-4 py-3",
|
|
366
|
+
children: [
|
|
367
|
+
/* @__PURE__ */ jsx("p", {
|
|
368
|
+
className: "truncate text-sm font-medium",
|
|
369
|
+
children: user.name ?? "User"
|
|
370
|
+
}),
|
|
371
|
+
user.email && /* @__PURE__ */ jsx("p", {
|
|
372
|
+
className: "truncate text-xs text-imp-muted-foreground",
|
|
373
|
+
children: user.email
|
|
374
|
+
}),
|
|
375
|
+
user.role && /* @__PURE__ */ jsx("span", {
|
|
376
|
+
className: "mt-1 inline-block rounded-full bg-imp-muted px-2 py-0.5 text-xs font-medium",
|
|
377
|
+
children: formatAuthRoleLabel(user.role)
|
|
378
|
+
})
|
|
379
|
+
]
|
|
380
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
381
|
+
className: "p-1",
|
|
382
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
383
|
+
onClick: async () => {
|
|
384
|
+
setIsOpen(false);
|
|
385
|
+
await signOut();
|
|
386
|
+
},
|
|
387
|
+
className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm text-imp-foreground hover:bg-imp-muted",
|
|
388
|
+
children: [/* @__PURE__ */ jsx(LogOut, { className: "h-4 w-4" }), "Sign Out"]
|
|
389
|
+
})
|
|
390
|
+
})]
|
|
391
|
+
})]
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/frontend/components/AuthGate.tsx
|
|
396
|
+
function AuthGate({ children, fallback = null, loading = null }) {
|
|
397
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
398
|
+
if (isLoading) return /* @__PURE__ */ jsx(Fragment, { children: loading });
|
|
399
|
+
if (!isAuthenticated) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
400
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/frontend/components/UserManagement.tsx
|
|
404
|
+
/**
|
|
405
|
+
* UserManagement — Admin panel for managing users.
|
|
406
|
+
*
|
|
407
|
+
* Displays a list of users with the ability to:
|
|
408
|
+
* - Create new users (email/password/role)
|
|
409
|
+
* - Change user roles
|
|
410
|
+
* - Delete users
|
|
411
|
+
*
|
|
412
|
+
* Only visible to admin users. Uses the auth plugin's
|
|
413
|
+
* `/plugins/auth/users` endpoints.
|
|
414
|
+
*/
|
|
415
|
+
const ASSIGNABLE_ROLE_OPTIONS = [
|
|
416
|
+
{
|
|
417
|
+
value: AUTH_DEFAULT_ROLE,
|
|
418
|
+
label: "None",
|
|
419
|
+
description: "No global access; flow access can still be granted via RBAC."
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
value: "owner",
|
|
423
|
+
label: "Owner",
|
|
424
|
+
description: "Can edit and manage sharing for all flows."
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
value: "editor",
|
|
428
|
+
label: "Editor",
|
|
429
|
+
description: "Can inspect, run, and edit flows."
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
value: "operator",
|
|
433
|
+
label: "Operator",
|
|
434
|
+
description: "Can inspect and run flows."
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
value: "viewer",
|
|
438
|
+
label: "Viewer",
|
|
439
|
+
description: "Can inspect flows."
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
const ROLE_BADGE_CLASSES = "border-imp-border bg-imp-muted/50 text-imp-foreground";
|
|
443
|
+
function getInitials(user) {
|
|
444
|
+
if (user.name) {
|
|
445
|
+
const parts = user.name.trim().split(/\s+/);
|
|
446
|
+
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
447
|
+
return parts[0][0].toUpperCase();
|
|
448
|
+
}
|
|
449
|
+
if (user.email) return user.email[0].toUpperCase();
|
|
450
|
+
return "?";
|
|
451
|
+
}
|
|
452
|
+
function formatDate(iso) {
|
|
453
|
+
try {
|
|
454
|
+
return new Intl.DateTimeFormat(void 0, { dateStyle: "medium" }).format(new Date(iso));
|
|
455
|
+
} catch {
|
|
456
|
+
return iso;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function SortHeader({ label, field, sortField, sortDir, onSort, align = "left" }) {
|
|
460
|
+
const active = sortField === field;
|
|
461
|
+
const Icon = active ? sortDir === "asc" ? ArrowUp : ArrowDown : ChevronsUpDown;
|
|
462
|
+
return /* @__PURE__ */ jsx("th", {
|
|
463
|
+
className: `px-4 py-2.5 text-${align} font-medium`,
|
|
464
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
465
|
+
type: "button",
|
|
466
|
+
onClick: () => onSort(field),
|
|
467
|
+
className: `inline-flex items-center gap-1 rounded transition-colors hover:text-imp-foreground ${active ? "text-imp-foreground" : ""}`,
|
|
468
|
+
children: [label, /* @__PURE__ */ jsx(Icon, { className: "w-3 h-3 shrink-0" })]
|
|
469
|
+
})
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function RoleDropdown({ value, userId, disabled, onChange }) {
|
|
473
|
+
const current = isAuthAssignableRole(value) ? value : AUTH_DEFAULT_ROLE;
|
|
474
|
+
const currentLabel = ASSIGNABLE_ROLE_OPTIONS.find((o) => o.value === current)?.label ?? current;
|
|
475
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
476
|
+
asChild: true,
|
|
477
|
+
disabled,
|
|
478
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
479
|
+
type: "button",
|
|
480
|
+
className: `inline-flex w-28 items-center justify-between gap-1.5 rounded-full border px-2.5 py-0.5 text-sm font-medium transition-colors hover:bg-imp-muted disabled:cursor-not-allowed disabled:opacity-50 ${ROLE_BADGE_CLASSES}`,
|
|
481
|
+
children: [currentLabel, !disabled && /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3 shrink-0" })]
|
|
482
|
+
})
|
|
483
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
484
|
+
align: "start",
|
|
485
|
+
className: "w-56",
|
|
486
|
+
children: [
|
|
487
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, {
|
|
488
|
+
className: "text-xs",
|
|
489
|
+
children: "Set role"
|
|
490
|
+
}),
|
|
491
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
492
|
+
ASSIGNABLE_ROLE_OPTIONS.map((option) => /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
493
|
+
onSelect: () => onChange(userId, option.value),
|
|
494
|
+
className: `items-start gap-0 px-2 py-2 ${current === option.value ? "bg-accent text-accent-foreground" : ""}`,
|
|
495
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
496
|
+
className: "min-w-0 text-left",
|
|
497
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
498
|
+
className: "text-sm font-medium",
|
|
499
|
+
children: option.label
|
|
500
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
501
|
+
className: "text-xs text-muted-foreground",
|
|
502
|
+
children: option.description
|
|
503
|
+
})]
|
|
504
|
+
})
|
|
505
|
+
}, option.value))
|
|
506
|
+
]
|
|
507
|
+
})] });
|
|
508
|
+
}
|
|
509
|
+
function UserManagement({ apiBaseUrl, className }) {
|
|
510
|
+
const { user, isAuthenticated } = useAuth();
|
|
511
|
+
const [users, setUsers] = useState([]);
|
|
512
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
513
|
+
const [error, setError] = useState(null);
|
|
514
|
+
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
515
|
+
const [pendingDeleteUser, setPendingDeleteUser] = useState(null);
|
|
516
|
+
const [hasFetched, setHasFetched] = useState(false);
|
|
517
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
518
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
519
|
+
const [sortField, setSortField] = useState("name");
|
|
520
|
+
const [sortDir, setSortDir] = useState("asc");
|
|
521
|
+
const PAGE_SIZE = 10;
|
|
522
|
+
const handleSort = useCallback((field) => {
|
|
523
|
+
setSortField((prev) => {
|
|
524
|
+
if (prev === field) {
|
|
525
|
+
setSortDir((d) => d === "asc" ? "desc" : "asc");
|
|
526
|
+
return prev;
|
|
527
|
+
}
|
|
528
|
+
setSortDir("asc");
|
|
529
|
+
return field;
|
|
530
|
+
});
|
|
531
|
+
setCurrentPage(1);
|
|
532
|
+
}, []);
|
|
533
|
+
const filteredUsers = useMemo(() => {
|
|
534
|
+
let result = users;
|
|
535
|
+
if (searchQuery.trim()) {
|
|
536
|
+
const q = searchQuery.toLowerCase();
|
|
537
|
+
result = result.filter((u) => u.name?.toLowerCase().includes(q) || u.email?.toLowerCase().includes(q) || u.role?.toLowerCase().includes(q));
|
|
538
|
+
}
|
|
539
|
+
return [...result].sort((a, b) => {
|
|
540
|
+
let aVal = "";
|
|
541
|
+
let bVal = "";
|
|
542
|
+
if (sortField === "name") {
|
|
543
|
+
aVal = (a.name || a.email || "").toLowerCase();
|
|
544
|
+
bVal = (b.name || b.email || "").toLowerCase();
|
|
545
|
+
} else if (sortField === "createdAt") {
|
|
546
|
+
aVal = a.createdAt ?? "";
|
|
547
|
+
bVal = b.createdAt ?? "";
|
|
548
|
+
} else if (sortField === "role") {
|
|
549
|
+
aVal = (a.role ?? "").toLowerCase();
|
|
550
|
+
bVal = (b.role ?? "").toLowerCase();
|
|
551
|
+
}
|
|
552
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
553
|
+
return sortDir === "asc" ? cmp : -cmp;
|
|
554
|
+
});
|
|
555
|
+
}, [
|
|
556
|
+
users,
|
|
557
|
+
searchQuery,
|
|
558
|
+
sortField,
|
|
559
|
+
sortDir
|
|
560
|
+
]);
|
|
561
|
+
const totalPages = Math.max(1, Math.ceil(filteredUsers.length / PAGE_SIZE));
|
|
562
|
+
const paginatedUsers = filteredUsers.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
|
563
|
+
const authApiBase = `${apiBaseUrl}/plugins/auth`;
|
|
564
|
+
const fetchUsers = useCallback(async () => {
|
|
565
|
+
setIsLoading(true);
|
|
566
|
+
setError(null);
|
|
567
|
+
try {
|
|
568
|
+
const res = await fetch(`${authApiBase}/users`, { credentials: "include" });
|
|
569
|
+
if (!res.ok) {
|
|
570
|
+
const data = await res.json().catch(() => ({ error: "Failed to fetch users" }));
|
|
571
|
+
throw new Error(data.error || data.message || `HTTP ${res.status}`);
|
|
572
|
+
}
|
|
573
|
+
setUsers((await res.json()).users ?? []);
|
|
574
|
+
setHasFetched(true);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
setError(err instanceof Error ? err.message : "Failed to fetch users");
|
|
577
|
+
} finally {
|
|
578
|
+
setIsLoading(false);
|
|
579
|
+
}
|
|
580
|
+
}, [authApiBase]);
|
|
581
|
+
const deleteUser = useCallback(async (userId) => {
|
|
582
|
+
try {
|
|
583
|
+
const res = await fetch(`${authApiBase}/users/${userId}`, {
|
|
584
|
+
method: "DELETE",
|
|
585
|
+
credentials: "include"
|
|
586
|
+
});
|
|
587
|
+
if (!res.ok) {
|
|
588
|
+
const data = await res.json().catch(() => ({}));
|
|
589
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
590
|
+
}
|
|
591
|
+
setUsers((prev) => prev.filter((u) => u.id !== userId));
|
|
592
|
+
setPendingDeleteUser(null);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
setError(err instanceof Error ? err.message : "Failed to delete user");
|
|
595
|
+
setPendingDeleteUser(null);
|
|
596
|
+
}
|
|
597
|
+
}, [authApiBase]);
|
|
598
|
+
const updateRole = useCallback(async (userId, role) => {
|
|
599
|
+
try {
|
|
600
|
+
const res = await fetch(`${authApiBase}/users/${userId}/role`, {
|
|
601
|
+
method: "PATCH",
|
|
602
|
+
credentials: "include",
|
|
603
|
+
headers: { "content-type": "application/json" },
|
|
604
|
+
body: JSON.stringify({ role })
|
|
605
|
+
});
|
|
606
|
+
if (!res.ok) {
|
|
607
|
+
const data = await res.json().catch(() => ({}));
|
|
608
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
609
|
+
}
|
|
610
|
+
setUsers((prev) => prev.map((u) => u.id === userId ? {
|
|
611
|
+
...u,
|
|
612
|
+
role
|
|
613
|
+
} : u));
|
|
614
|
+
} catch (err) {
|
|
615
|
+
setError(err instanceof Error ? err.message : "Failed to update role");
|
|
616
|
+
}
|
|
617
|
+
}, [authApiBase]);
|
|
618
|
+
if (!isAuthenticated || user?.role !== "admin") return null;
|
|
619
|
+
if (!hasFetched && !isLoading) fetchUsers();
|
|
620
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
621
|
+
className: `space-y-4 ${className ?? ""}`,
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ jsxs("div", {
|
|
624
|
+
className: "flex items-center gap-2",
|
|
625
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
626
|
+
className: "relative flex-1 max-w-sm",
|
|
627
|
+
children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 pointer-events-none text-imp-muted-foreground" }), /* @__PURE__ */ jsx("input", {
|
|
628
|
+
type: "text",
|
|
629
|
+
value: searchQuery,
|
|
630
|
+
onChange: (e) => {
|
|
631
|
+
setSearchQuery(e.target.value);
|
|
632
|
+
setCurrentPage(1);
|
|
633
|
+
},
|
|
634
|
+
placeholder: "Search users…",
|
|
635
|
+
className: "w-full py-2 pr-3 text-sm border rounded-lg outline-none border-imp-border bg-transparent pl-9 placeholder:text-imp-muted-foreground focus:border-imp-primary/50"
|
|
636
|
+
})]
|
|
637
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
638
|
+
type: "button",
|
|
639
|
+
onClick: () => setShowCreateDialog(true),
|
|
640
|
+
className: "flex items-center gap-1.5 rounded-lg border border-imp-border px-3 py-2 text-sm font-medium text-imp-muted-foreground transition-colors hover:border-imp-primary/50 hover:text-imp-foreground",
|
|
641
|
+
children: [/* @__PURE__ */ jsx(UserPlus, { className: "w-4 h-4" }), " Create User"]
|
|
642
|
+
})]
|
|
643
|
+
}),
|
|
644
|
+
error && /* @__PURE__ */ jsxs("div", {
|
|
645
|
+
className: "p-3 text-sm text-red-600 rounded-md bg-red-50 dark:bg-red-950/20 dark:text-red-400",
|
|
646
|
+
children: [error, /* @__PURE__ */ jsx("button", {
|
|
647
|
+
type: "button",
|
|
648
|
+
onClick: () => setError(null),
|
|
649
|
+
className: "ml-2 underline hover:no-underline",
|
|
650
|
+
children: "Dismiss"
|
|
651
|
+
})]
|
|
652
|
+
}),
|
|
653
|
+
/* @__PURE__ */ jsx("div", {
|
|
654
|
+
className: "overflow-hidden border rounded-xl border-imp-border bg-imp-background/40",
|
|
655
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
656
|
+
className: "w-full text-sm table-fixed",
|
|
657
|
+
children: [
|
|
658
|
+
/* @__PURE__ */ jsxs("colgroup", { children: [
|
|
659
|
+
/* @__PURE__ */ jsx("col", { className: "w-[40%]" }),
|
|
660
|
+
/* @__PURE__ */ jsx("col", { className: "w-[20%]" }),
|
|
661
|
+
/* @__PURE__ */ jsx("col", { className: "w-[20%]" }),
|
|
662
|
+
/* @__PURE__ */ jsx("col", { className: "w-[20%]" })
|
|
663
|
+
] }),
|
|
664
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
|
|
665
|
+
className: "text-xs font-medium border-b border-imp-border bg-imp-muted/20 text-imp-muted-foreground",
|
|
666
|
+
children: [
|
|
667
|
+
/* @__PURE__ */ jsx(SortHeader, {
|
|
668
|
+
label: "User",
|
|
669
|
+
field: "name",
|
|
670
|
+
sortField,
|
|
671
|
+
sortDir,
|
|
672
|
+
onSort: handleSort
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ jsx(SortHeader, {
|
|
675
|
+
label: "Global Role",
|
|
676
|
+
field: "role",
|
|
677
|
+
sortField,
|
|
678
|
+
sortDir,
|
|
679
|
+
onSort: handleSort
|
|
680
|
+
}),
|
|
681
|
+
/* @__PURE__ */ jsx(SortHeader, {
|
|
682
|
+
label: "Created",
|
|
683
|
+
field: "createdAt",
|
|
684
|
+
sortField,
|
|
685
|
+
sortDir,
|
|
686
|
+
onSort: handleSort,
|
|
687
|
+
align: "right"
|
|
688
|
+
}),
|
|
689
|
+
/* @__PURE__ */ jsx("th", { className: "px-4 py-2.5 text-right font-medium" })
|
|
690
|
+
]
|
|
691
|
+
}) }),
|
|
692
|
+
/* @__PURE__ */ jsxs("tbody", {
|
|
693
|
+
className: "divide-y divide-imp-border",
|
|
694
|
+
children: [paginatedUsers.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
|
|
695
|
+
colSpan: 4,
|
|
696
|
+
className: "px-4 py-8 text-sm text-center text-imp-muted-foreground",
|
|
697
|
+
children: !hasFetched || isLoading ? "Loading…" : searchQuery ? "No users match your search." : "No users found."
|
|
698
|
+
}) }), paginatedUsers.map((u) => /* @__PURE__ */ jsxs("tr", {
|
|
699
|
+
className: "group hover:bg-imp-muted/20",
|
|
700
|
+
children: [
|
|
701
|
+
/* @__PURE__ */ jsx("td", {
|
|
702
|
+
className: "px-4 py-3",
|
|
703
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
704
|
+
className: "flex items-center gap-3",
|
|
705
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
706
|
+
className: "flex items-center justify-center w-8 h-8 text-xs font-semibold rounded-full shrink-0 bg-imp-primary/10 text-imp-primary",
|
|
707
|
+
children: getInitials(u)
|
|
708
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
709
|
+
className: "min-w-0",
|
|
710
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
711
|
+
className: "font-medium truncate text-imp-foreground",
|
|
712
|
+
children: u.name || "Unnamed"
|
|
713
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
714
|
+
className: "text-xs truncate text-imp-muted-foreground",
|
|
715
|
+
children: u.email
|
|
716
|
+
})]
|
|
717
|
+
})]
|
|
718
|
+
})
|
|
719
|
+
}),
|
|
720
|
+
/* @__PURE__ */ jsx("td", {
|
|
721
|
+
className: "px-4 py-3",
|
|
722
|
+
children: u.role === "admin" ? /* @__PURE__ */ jsx("span", {
|
|
723
|
+
className: `inline-flex rounded-full border px-2.5 py-0.5 text-sm font-medium ${ROLE_BADGE_CLASSES}`,
|
|
724
|
+
children: formatAuthRoleLabel(u.role)
|
|
725
|
+
}) : /* @__PURE__ */ jsx(RoleDropdown, {
|
|
726
|
+
value: u.role ?? "default",
|
|
727
|
+
userId: u.id,
|
|
728
|
+
disabled: u.id === user?.id,
|
|
729
|
+
onChange: updateRole
|
|
730
|
+
})
|
|
731
|
+
}),
|
|
732
|
+
/* @__PURE__ */ jsx("td", {
|
|
733
|
+
className: "px-4 py-3 text-xs text-right text-imp-muted-foreground",
|
|
734
|
+
children: u.createdAt ? formatDate(u.createdAt) : "—"
|
|
735
|
+
}),
|
|
736
|
+
/* @__PURE__ */ jsx("td", {
|
|
737
|
+
className: "px-4 py-3 text-right",
|
|
738
|
+
children: u.role === "admin" ? /* @__PURE__ */ jsx("span", {
|
|
739
|
+
className: "text-xs text-imp-muted-foreground",
|
|
740
|
+
children: "Config managed"
|
|
741
|
+
}) : u.id !== user?.id ? /* @__PURE__ */ jsx("button", {
|
|
742
|
+
type: "button",
|
|
743
|
+
onClick: () => setPendingDeleteUser(u),
|
|
744
|
+
className: "rounded-md p-1.5 text-imp-muted-foreground opacity-0 transition-opacity hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-950/20 dark:hover:text-red-400",
|
|
745
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" })
|
|
746
|
+
}) : null
|
|
747
|
+
})
|
|
748
|
+
]
|
|
749
|
+
}, u.id))]
|
|
750
|
+
})
|
|
751
|
+
]
|
|
752
|
+
})
|
|
753
|
+
}),
|
|
754
|
+
/* @__PURE__ */ jsxs("div", {
|
|
755
|
+
className: "flex items-center justify-between text-sm",
|
|
756
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
757
|
+
className: "text-xs text-imp-muted-foreground",
|
|
758
|
+
children: [
|
|
759
|
+
filteredUsers.length,
|
|
760
|
+
" user",
|
|
761
|
+
filteredUsers.length !== 1 ? "s" : "",
|
|
762
|
+
searchQuery && ` matching "${searchQuery}"`
|
|
763
|
+
]
|
|
764
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
765
|
+
className: "flex items-center gap-2",
|
|
766
|
+
children: [
|
|
767
|
+
/* @__PURE__ */ jsx("button", {
|
|
768
|
+
type: "button",
|
|
769
|
+
onClick: () => setCurrentPage((p) => Math.max(1, p - 1)),
|
|
770
|
+
disabled: currentPage === 1,
|
|
771
|
+
className: "rounded-md border border-imp-border px-2.5 py-1 text-xs hover:bg-imp-muted disabled:opacity-50",
|
|
772
|
+
children: "Previous"
|
|
773
|
+
}),
|
|
774
|
+
/* @__PURE__ */ jsxs("span", {
|
|
775
|
+
className: "text-xs text-imp-muted-foreground",
|
|
776
|
+
children: [
|
|
777
|
+
currentPage,
|
|
778
|
+
" / ",
|
|
779
|
+
totalPages
|
|
780
|
+
]
|
|
781
|
+
}),
|
|
782
|
+
/* @__PURE__ */ jsx("button", {
|
|
783
|
+
type: "button",
|
|
784
|
+
onClick: () => setCurrentPage((p) => Math.min(totalPages, p + 1)),
|
|
785
|
+
disabled: currentPage === totalPages,
|
|
786
|
+
className: "rounded-md border border-imp-border px-2.5 py-1 text-xs hover:bg-imp-muted disabled:opacity-50",
|
|
787
|
+
children: "Next"
|
|
788
|
+
})
|
|
789
|
+
]
|
|
790
|
+
})]
|
|
791
|
+
}),
|
|
792
|
+
/* @__PURE__ */ jsx(Dialog, {
|
|
793
|
+
open: showCreateDialog,
|
|
794
|
+
onOpenChange: (open) => !open && setShowCreateDialog(false),
|
|
795
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
796
|
+
className: "max-w-md border-imp-border bg-imp-background text-imp-foreground sm:max-w-md",
|
|
797
|
+
children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, {
|
|
798
|
+
className: "text-sm font-semibold",
|
|
799
|
+
children: "Create New User"
|
|
800
|
+
}) }), /* @__PURE__ */ jsx(CreateUserForm, {
|
|
801
|
+
apiBaseUrl: authApiBase,
|
|
802
|
+
onCreated: (newUser) => {
|
|
803
|
+
setUsers((prev) => [...prev, newUser]);
|
|
804
|
+
setShowCreateDialog(false);
|
|
805
|
+
},
|
|
806
|
+
onCancel: () => setShowCreateDialog(false)
|
|
807
|
+
})]
|
|
808
|
+
})
|
|
809
|
+
}),
|
|
810
|
+
/* @__PURE__ */ jsx(Dialog, {
|
|
811
|
+
open: pendingDeleteUser !== null,
|
|
812
|
+
onOpenChange: (open) => !open && setPendingDeleteUser(null),
|
|
813
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
814
|
+
className: "max-w-sm border-imp-border bg-imp-background text-imp-foreground sm:max-w-sm",
|
|
815
|
+
children: [
|
|
816
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, {
|
|
817
|
+
className: "text-sm font-semibold",
|
|
818
|
+
children: "Delete user"
|
|
819
|
+
}) }),
|
|
820
|
+
/* @__PURE__ */ jsxs("p", {
|
|
821
|
+
className: "text-sm text-imp-muted-foreground",
|
|
822
|
+
children: [
|
|
823
|
+
"Are you sure you want to delete",
|
|
824
|
+
" ",
|
|
825
|
+
/* @__PURE__ */ jsx("span", {
|
|
826
|
+
className: "font-medium text-imp-foreground",
|
|
827
|
+
children: pendingDeleteUser?.name || pendingDeleteUser?.email || "this user"
|
|
828
|
+
}),
|
|
829
|
+
"? This action cannot be undone."
|
|
830
|
+
]
|
|
831
|
+
}),
|
|
832
|
+
/* @__PURE__ */ jsxs(DialogFooter, {
|
|
833
|
+
className: "gap-2",
|
|
834
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
835
|
+
type: "button",
|
|
836
|
+
onClick: () => setPendingDeleteUser(null),
|
|
837
|
+
className: "rounded-md border border-imp-border px-3 py-1.5 text-sm hover:bg-imp-muted",
|
|
838
|
+
children: "Cancel"
|
|
839
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
840
|
+
type: "button",
|
|
841
|
+
onClick: () => pendingDeleteUser && deleteUser(pendingDeleteUser.id),
|
|
842
|
+
className: "rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700",
|
|
843
|
+
children: "Delete"
|
|
844
|
+
})]
|
|
845
|
+
})
|
|
846
|
+
]
|
|
847
|
+
})
|
|
848
|
+
})
|
|
849
|
+
]
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
function CreateUserForm({ apiBaseUrl, onCreated, onCancel }) {
|
|
853
|
+
const [email, setEmail] = useState("");
|
|
854
|
+
const [password, setPassword] = useState("");
|
|
855
|
+
const [name, setName] = useState("");
|
|
856
|
+
const [role, setRole] = useState(AUTH_DEFAULT_ROLE);
|
|
857
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
858
|
+
const [error, setError] = useState(null);
|
|
859
|
+
const handleSubmit = async (e) => {
|
|
860
|
+
e.preventDefault();
|
|
861
|
+
setError(null);
|
|
862
|
+
if (!email.trim() || !password.trim()) {
|
|
863
|
+
setError("Email and password are required");
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (password.length < 8) {
|
|
867
|
+
setError("Password must be at least 8 characters");
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
setIsSubmitting(true);
|
|
871
|
+
try {
|
|
872
|
+
const res = await fetch(`${apiBaseUrl}/users`, {
|
|
873
|
+
method: "POST",
|
|
874
|
+
credentials: "include",
|
|
875
|
+
headers: { "content-type": "application/json" },
|
|
876
|
+
body: JSON.stringify({
|
|
877
|
+
email,
|
|
878
|
+
password,
|
|
879
|
+
name: name.trim() || void 0,
|
|
880
|
+
role
|
|
881
|
+
})
|
|
882
|
+
});
|
|
883
|
+
const data = await res.json();
|
|
884
|
+
if (!res.ok) throw new Error(data.error || data.message || `HTTP ${res.status}`);
|
|
885
|
+
onCreated(data.user);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
setError(err instanceof Error ? err.message : "Failed to create user");
|
|
888
|
+
} finally {
|
|
889
|
+
setIsSubmitting(false);
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
893
|
+
onSubmit: handleSubmit,
|
|
894
|
+
className: "space-y-3",
|
|
895
|
+
children: [
|
|
896
|
+
/* @__PURE__ */ jsxs("div", {
|
|
897
|
+
className: "grid grid-cols-2 gap-3",
|
|
898
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
899
|
+
className: "block mb-1 text-xs font-medium text-imp-foreground",
|
|
900
|
+
children: "Name"
|
|
901
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
902
|
+
type: "text",
|
|
903
|
+
value: name,
|
|
904
|
+
onChange: (e) => setName(e.target.value),
|
|
905
|
+
placeholder: "User name",
|
|
906
|
+
className: "w-full rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-sm placeholder:text-imp-muted-foreground focus:outline-none focus:border-imp-primary/50"
|
|
907
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [
|
|
908
|
+
/* @__PURE__ */ jsx("label", {
|
|
909
|
+
className: "block mb-1 text-xs font-medium text-imp-foreground",
|
|
910
|
+
children: "Role"
|
|
911
|
+
}),
|
|
912
|
+
/* @__PURE__ */ jsx("select", {
|
|
913
|
+
value: role,
|
|
914
|
+
onChange: (e) => setRole(e.target.value),
|
|
915
|
+
className: "w-full rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-sm focus:outline-none focus:border-imp-primary/50",
|
|
916
|
+
children: ASSIGNABLE_ROLE_OPTIONS.map((option) => /* @__PURE__ */ jsx("option", {
|
|
917
|
+
value: option.value,
|
|
918
|
+
children: option.label
|
|
919
|
+
}, option.value))
|
|
920
|
+
}),
|
|
921
|
+
/* @__PURE__ */ jsx("p", {
|
|
922
|
+
className: "mt-1 text-xs text-imp-muted-foreground",
|
|
923
|
+
children: "Flow access can still be granted via RBAC."
|
|
924
|
+
})
|
|
925
|
+
] })]
|
|
926
|
+
}),
|
|
927
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
|
|
928
|
+
className: "block mb-1 text-xs font-medium text-imp-foreground",
|
|
929
|
+
children: ["Email ", /* @__PURE__ */ jsx("span", {
|
|
930
|
+
className: "text-red-500",
|
|
931
|
+
children: "*"
|
|
932
|
+
})]
|
|
933
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
934
|
+
type: "email",
|
|
935
|
+
value: email,
|
|
936
|
+
onChange: (e) => setEmail(e.target.value),
|
|
937
|
+
placeholder: "user@example.com",
|
|
938
|
+
required: true,
|
|
939
|
+
autoComplete: "off",
|
|
940
|
+
className: "w-full rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-sm placeholder:text-imp-muted-foreground focus:outline-none focus:border-imp-primary/50"
|
|
941
|
+
})] }),
|
|
942
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
|
|
943
|
+
className: "block mb-1 text-xs font-medium text-imp-foreground",
|
|
944
|
+
children: ["Password ", /* @__PURE__ */ jsx("span", {
|
|
945
|
+
className: "text-red-500",
|
|
946
|
+
children: "*"
|
|
947
|
+
})]
|
|
948
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
949
|
+
type: "password",
|
|
950
|
+
value: password,
|
|
951
|
+
onChange: (e) => setPassword(e.target.value),
|
|
952
|
+
placeholder: "Min 8 characters",
|
|
953
|
+
required: true,
|
|
954
|
+
minLength: 8,
|
|
955
|
+
autoComplete: "new-password",
|
|
956
|
+
className: "w-full rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-sm placeholder:text-imp-muted-foreground focus:outline-none focus:border-imp-primary/50"
|
|
957
|
+
})] }),
|
|
958
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
959
|
+
className: "p-2 text-xs text-red-600 rounded-md bg-red-50 dark:bg-red-950/20 dark:text-red-400",
|
|
960
|
+
children: error
|
|
961
|
+
}),
|
|
962
|
+
/* @__PURE__ */ jsxs("div", {
|
|
963
|
+
className: "flex justify-end gap-2 pt-1",
|
|
964
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
965
|
+
type: "button",
|
|
966
|
+
onClick: onCancel,
|
|
967
|
+
className: "rounded-md border border-imp-border px-3 py-1.5 text-sm hover:bg-imp-muted",
|
|
968
|
+
children: "Cancel"
|
|
969
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
970
|
+
type: "submit",
|
|
971
|
+
disabled: isSubmitting,
|
|
972
|
+
className: "px-4 py-2 text-sm font-semibold rounded-md bg-imp-primary text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
|
|
973
|
+
children: isSubmitting ? "Creating…" : "Create User"
|
|
974
|
+
})]
|
|
975
|
+
})
|
|
976
|
+
]
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/frontend/components/AuthenticatedInvect.tsx
|
|
981
|
+
const defaultQueryClient = new QueryClient({ defaultOptions: { queries: {
|
|
982
|
+
staleTime: 300 * 1e3,
|
|
983
|
+
retry: 1
|
|
984
|
+
} } });
|
|
985
|
+
function AuthenticatedInvect({ apiBaseUrl = "http://localhost:3000/invect", basePath = "/invect", InvectComponent, ShellComponent, reactQueryClient, loading, theme = "light", plugins }) {
|
|
986
|
+
const client = reactQueryClient ?? defaultQueryClient;
|
|
987
|
+
const Invect = InvectComponent;
|
|
988
|
+
const Shell = ShellComponent;
|
|
989
|
+
const content = /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
990
|
+
client,
|
|
991
|
+
children: /* @__PURE__ */ jsx(AuthProvider, {
|
|
992
|
+
baseUrl: apiBaseUrl,
|
|
993
|
+
children: /* @__PURE__ */ jsx(AuthGate, {
|
|
994
|
+
loading: loading ?? /* @__PURE__ */ jsx(LoadingSpinner, {}),
|
|
995
|
+
fallback: /* @__PURE__ */ jsx(SignInOnly, {}),
|
|
996
|
+
children: /* @__PURE__ */ jsx(Invect, {
|
|
997
|
+
apiBaseUrl,
|
|
998
|
+
basePath,
|
|
999
|
+
reactQueryClient: client,
|
|
1000
|
+
plugins
|
|
1001
|
+
})
|
|
1002
|
+
})
|
|
1003
|
+
})
|
|
1004
|
+
});
|
|
1005
|
+
if (Shell) return /* @__PURE__ */ jsx(Shell, {
|
|
1006
|
+
theme,
|
|
1007
|
+
className: "h-full",
|
|
1008
|
+
children: content
|
|
1009
|
+
});
|
|
1010
|
+
return content;
|
|
1011
|
+
}
|
|
1012
|
+
function SignInOnly() {
|
|
1013
|
+
return /* @__PURE__ */ jsx(SignInPage, {
|
|
1014
|
+
onSuccess: () => {},
|
|
1015
|
+
subtitle: "Sign in to access Invect"
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
function LoadingSpinner() {
|
|
1019
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1020
|
+
className: "flex min-h-screen items-center justify-center bg-imp-background",
|
|
1021
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-imp-muted border-t-imp-primary" })
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
//#endregion
|
|
1025
|
+
//#region src/frontend/components/UserManagementPage.tsx
|
|
1026
|
+
/**
|
|
1027
|
+
* UserManagementPage — Standalone page for user management.
|
|
1028
|
+
*
|
|
1029
|
+
* Wraps the existing UserManagement component in a page layout
|
|
1030
|
+
* consistent with the Access Control page style. Registered as a
|
|
1031
|
+
* plugin route contribution at '/users'.
|
|
1032
|
+
*/
|
|
1033
|
+
function UserManagementPage() {
|
|
1034
|
+
const api = useApiClient();
|
|
1035
|
+
const { user, isAuthenticated } = useAuth();
|
|
1036
|
+
const apiBaseUrl = api.getBaseURL();
|
|
1037
|
+
if (!isAuthenticated) return /* @__PURE__ */ jsx("div", {
|
|
1038
|
+
className: "imp-page w-full h-full min-h-0 overflow-y-auto bg-imp-background text-imp-foreground flex items-center justify-center",
|
|
1039
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1040
|
+
className: "text-sm text-imp-muted-foreground",
|
|
1041
|
+
children: "Please sign in to access this page."
|
|
1042
|
+
})
|
|
1043
|
+
});
|
|
1044
|
+
if (user?.role !== "admin") return /* @__PURE__ */ jsx(PageLayout, {
|
|
1045
|
+
title: "User Management",
|
|
1046
|
+
subtitle: "Manage users for your Invect instance.",
|
|
1047
|
+
icon: Users,
|
|
1048
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1049
|
+
className: "rounded-md bg-yellow-50 p-3 text-sm text-yellow-800 dark:bg-yellow-950/20 dark:text-yellow-400",
|
|
1050
|
+
children: "Only administrators can manage users. Contact an admin for access."
|
|
1051
|
+
})
|
|
1052
|
+
});
|
|
1053
|
+
return /* @__PURE__ */ jsx(PageLayout, {
|
|
1054
|
+
title: "User Management",
|
|
1055
|
+
subtitle: "Create, manage, and remove users for your Invect instance.",
|
|
1056
|
+
icon: Users,
|
|
1057
|
+
children: /* @__PURE__ */ jsx(UserManagement, { apiBaseUrl })
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region src/frontend/components/ProfilePage.tsx
|
|
1062
|
+
/**
|
|
1063
|
+
* ProfilePage — Standalone page for the current authenticated user.
|
|
1064
|
+
*
|
|
1065
|
+
* Shows basic account information and provides a sign-out action.
|
|
1066
|
+
*/
|
|
1067
|
+
function ProfilePage({ basePath }) {
|
|
1068
|
+
const { user, isAuthenticated, isLoading, signOut } = useAuth();
|
|
1069
|
+
if (isLoading) return /* @__PURE__ */ jsx("div", {
|
|
1070
|
+
className: "imp-page flex h-full min-h-0 w-full items-center justify-center overflow-y-auto bg-imp-background text-imp-foreground",
|
|
1071
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1072
|
+
className: "text-sm text-imp-muted-foreground",
|
|
1073
|
+
children: "Loading profile…"
|
|
1074
|
+
})
|
|
1075
|
+
});
|
|
1076
|
+
if (!isAuthenticated || !user) return /* @__PURE__ */ jsx("div", {
|
|
1077
|
+
className: "imp-page flex h-full min-h-0 w-full items-center justify-center overflow-y-auto bg-imp-background text-imp-foreground",
|
|
1078
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1079
|
+
className: "text-sm text-imp-muted-foreground",
|
|
1080
|
+
children: "Please sign in to view your profile."
|
|
1081
|
+
})
|
|
1082
|
+
});
|
|
1083
|
+
const displayName = user.name ?? user.email ?? user.id;
|
|
1084
|
+
const initials = displayName[0]?.toUpperCase() ?? "?";
|
|
1085
|
+
return /* @__PURE__ */ jsx(PageLayout, {
|
|
1086
|
+
title: "Profile",
|
|
1087
|
+
subtitle: "View your account details and manage your current session.",
|
|
1088
|
+
icon: User,
|
|
1089
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1090
|
+
className: "max-w-2xl rounded-xl border border-imp-border bg-imp-card p-6 shadow-sm",
|
|
1091
|
+
children: [
|
|
1092
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1093
|
+
className: "mb-6 flex items-center gap-4",
|
|
1094
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1095
|
+
className: "flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-full bg-imp-primary/10 text-lg font-semibold text-imp-primary",
|
|
1096
|
+
children: user.image ? /* @__PURE__ */ jsx("img", {
|
|
1097
|
+
src: user.image,
|
|
1098
|
+
alt: displayName,
|
|
1099
|
+
className: "h-16 w-16 object-cover"
|
|
1100
|
+
}) : initials
|
|
1101
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1102
|
+
className: "min-w-0",
|
|
1103
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1104
|
+
className: "truncate text-lg font-semibold text-imp-foreground",
|
|
1105
|
+
children: displayName
|
|
1106
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1107
|
+
className: "truncate text-sm text-imp-muted-foreground",
|
|
1108
|
+
children: user.email ?? user.id
|
|
1109
|
+
})]
|
|
1110
|
+
})]
|
|
1111
|
+
}),
|
|
1112
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1113
|
+
className: "grid gap-4 md:grid-cols-2",
|
|
1114
|
+
children: [
|
|
1115
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1116
|
+
className: "rounded-lg border border-imp-border bg-imp-background p-4",
|
|
1117
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1118
|
+
className: "mb-2 flex items-center gap-2 text-sm font-medium text-imp-foreground",
|
|
1119
|
+
children: [/* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-imp-muted-foreground" }), "Name"]
|
|
1120
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1121
|
+
className: "text-sm text-imp-muted-foreground",
|
|
1122
|
+
children: user.name ?? "Not set"
|
|
1123
|
+
})]
|
|
1124
|
+
}),
|
|
1125
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1126
|
+
className: "rounded-lg border border-imp-border bg-imp-background p-4",
|
|
1127
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1128
|
+
className: "mb-2 flex items-center gap-2 text-sm font-medium text-imp-foreground",
|
|
1129
|
+
children: [/* @__PURE__ */ jsx(Mail, { className: "h-4 w-4 text-imp-muted-foreground" }), "Email"]
|
|
1130
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1131
|
+
className: "text-sm text-imp-muted-foreground",
|
|
1132
|
+
children: user.email ?? "Not available"
|
|
1133
|
+
})]
|
|
1134
|
+
}),
|
|
1135
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1136
|
+
className: "rounded-lg border border-imp-border bg-imp-background p-4",
|
|
1137
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1138
|
+
className: "mb-2 flex items-center gap-2 text-sm font-medium text-imp-foreground",
|
|
1139
|
+
children: [/* @__PURE__ */ jsx(Shield, { className: "h-4 w-4 text-imp-muted-foreground" }), "Role"]
|
|
1140
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1141
|
+
className: "text-sm text-imp-muted-foreground",
|
|
1142
|
+
children: formatAuthRoleLabel(user.role)
|
|
1143
|
+
})]
|
|
1144
|
+
}),
|
|
1145
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1146
|
+
className: "rounded-lg border border-imp-border bg-imp-background p-4",
|
|
1147
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1148
|
+
className: "mb-2 flex items-center gap-2 text-sm font-medium text-imp-foreground",
|
|
1149
|
+
children: [/* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-imp-muted-foreground" }), "User ID"]
|
|
1150
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1151
|
+
className: "break-all text-sm text-imp-muted-foreground",
|
|
1152
|
+
children: user.id
|
|
1153
|
+
})]
|
|
1154
|
+
})
|
|
1155
|
+
]
|
|
1156
|
+
}),
|
|
1157
|
+
/* @__PURE__ */ jsx("div", {
|
|
1158
|
+
className: "mt-6 flex justify-end border-t border-imp-border pt-4",
|
|
1159
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
1160
|
+
onClick: async () => {
|
|
1161
|
+
await signOut();
|
|
1162
|
+
},
|
|
1163
|
+
className: "inline-flex items-center gap-2 rounded-md border border-imp-border px-4 py-2 text-sm font-medium text-imp-foreground transition-colors hover:bg-imp-muted",
|
|
1164
|
+
children: [/* @__PURE__ */ jsx(LogOut, { className: "h-4 w-4" }), "Sign Out"]
|
|
1165
|
+
})
|
|
1166
|
+
})
|
|
1167
|
+
]
|
|
1168
|
+
})
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
//#endregion
|
|
1172
|
+
//#region src/frontend/components/SidebarUserMenu.tsx
|
|
1173
|
+
/**
|
|
1174
|
+
* SidebarUserMenu — User avatar link in the sidebar footer.
|
|
1175
|
+
*
|
|
1176
|
+
* Clicking navigates directly to the profile page.
|
|
1177
|
+
* Sign-out is available on the profile page itself.
|
|
1178
|
+
*/
|
|
1179
|
+
function SidebarUserMenu({ collapsed = false, basePath = "" }) {
|
|
1180
|
+
const { user, isAuthenticated, isLoading } = useAuth();
|
|
1181
|
+
const location = useLocation();
|
|
1182
|
+
if (isLoading || !isAuthenticated || !user) return null;
|
|
1183
|
+
const initials = (user.name ?? user.email ?? user.id)[0]?.toUpperCase() ?? "?";
|
|
1184
|
+
const displayName = user.name ?? user.email ?? "User";
|
|
1185
|
+
const profilePath = `${basePath}/profile`;
|
|
1186
|
+
const isActive = location.pathname === profilePath;
|
|
1187
|
+
return /* @__PURE__ */ jsxs(Link, {
|
|
1188
|
+
to: profilePath,
|
|
1189
|
+
title: `${displayName}${user.role ? ` — ${user.role}` : ""}`,
|
|
1190
|
+
className: [
|
|
1191
|
+
"flex w-full items-center gap-3 rounded-md px-2 py-2 transition-colors",
|
|
1192
|
+
"hover:bg-imp-muted/60",
|
|
1193
|
+
isActive ? "bg-imp-muted/60" : "",
|
|
1194
|
+
collapsed ? "justify-center" : ""
|
|
1195
|
+
].filter(Boolean).join(" "),
|
|
1196
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1197
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-imp-primary/10 text-sm font-medium text-imp-primary",
|
|
1198
|
+
children: user.image ? /* @__PURE__ */ jsx("img", {
|
|
1199
|
+
src: user.image,
|
|
1200
|
+
alt: displayName,
|
|
1201
|
+
className: "h-8 w-8 rounded-full object-cover"
|
|
1202
|
+
}) : initials
|
|
1203
|
+
}), !collapsed && /* @__PURE__ */ jsxs("div", {
|
|
1204
|
+
className: "min-w-0 flex-1",
|
|
1205
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1206
|
+
className: "truncate text-sm font-medium",
|
|
1207
|
+
children: displayName
|
|
1208
|
+
}), user.role && /* @__PURE__ */ jsx("p", {
|
|
1209
|
+
className: "truncate text-xs capitalize text-imp-muted-foreground",
|
|
1210
|
+
children: user.role
|
|
1211
|
+
})]
|
|
1212
|
+
})]
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion
|
|
1216
|
+
//#region src/frontend/plugins/authFrontendPlugin.ts
|
|
1217
|
+
/**
|
|
1218
|
+
* @invect/user-auth — Auth Frontend Plugin Definition
|
|
1219
|
+
*
|
|
1220
|
+
* Registers the auth plugin's frontend contributions:
|
|
1221
|
+
* - Sidebar item: "Users" (admin-only)
|
|
1222
|
+
* - Route: /users → UserManagementPage
|
|
1223
|
+
*
|
|
1224
|
+
* Note: AuthProvider is NOT included as a plugin provider because
|
|
1225
|
+
* AuthenticatedInvect already wraps the tree with it. This plugin
|
|
1226
|
+
* only adds the user management UI.
|
|
1227
|
+
*/
|
|
1228
|
+
const authFrontendPlugin = {
|
|
1229
|
+
id: "user-auth",
|
|
1230
|
+
name: "User Authentication",
|
|
1231
|
+
sidebar: [{
|
|
1232
|
+
label: "Users",
|
|
1233
|
+
icon: Users,
|
|
1234
|
+
path: "/users",
|
|
1235
|
+
position: "top",
|
|
1236
|
+
permission: "admin:*"
|
|
1237
|
+
}],
|
|
1238
|
+
sidebarFooter: SidebarUserMenu,
|
|
1239
|
+
routes: [{
|
|
1240
|
+
path: "/profile",
|
|
1241
|
+
component: ProfilePage
|
|
1242
|
+
}, {
|
|
1243
|
+
path: "/users",
|
|
1244
|
+
component: UserManagementPage
|
|
1245
|
+
}]
|
|
1246
|
+
};
|
|
1247
|
+
//#endregion
|
|
1248
|
+
export { AuthGate, AuthProvider, AuthenticatedInvect, ProfilePage, SidebarUserMenu, SignInForm, SignInPage, UserButton, UserManagement, UserManagementPage, authFrontendPlugin, useAuth };
|
|
1249
|
+
|
|
1250
|
+
//# sourceMappingURL=index.mjs.map
|