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