@invect/user-auth 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +81 -72
  2. package/dist/backend/index.cjs +410 -54
  3. package/dist/backend/index.cjs.map +1 -1
  4. package/dist/backend/index.d.cts +456 -0
  5. package/dist/backend/index.d.cts.map +1 -0
  6. package/dist/backend/index.d.mts +456 -0
  7. package/dist/backend/index.d.mts.map +1 -0
  8. package/dist/backend/index.d.ts +28 -18
  9. package/dist/backend/index.d.ts.map +1 -1
  10. package/dist/backend/index.mjs +408 -53
  11. package/dist/backend/index.mjs.map +1 -1
  12. package/dist/backend/plugin.d.ts +15 -15
  13. package/dist/backend/plugin.d.ts.map +1 -1
  14. package/dist/backend/types.d.ts +85 -9
  15. package/dist/backend/types.d.ts.map +1 -1
  16. package/dist/frontend/components/ApiKeysDialog.d.ts +17 -0
  17. package/dist/frontend/components/ApiKeysDialog.d.ts.map +1 -0
  18. package/dist/frontend/components/AuthenticatedInvect.d.ts +10 -10
  19. package/dist/frontend/components/SignInForm.d.ts.map +1 -1
  20. package/dist/frontend/components/SignInPage.d.ts.map +1 -1
  21. package/dist/frontend/components/UserManagement.d.ts.map +1 -1
  22. package/dist/frontend/index.cjs +434 -58
  23. package/dist/frontend/index.cjs.map +1 -1
  24. package/dist/frontend/index.d.cts +317 -0
  25. package/dist/frontend/index.d.cts.map +1 -0
  26. package/dist/frontend/index.d.mts +317 -0
  27. package/dist/frontend/index.d.mts.map +1 -0
  28. package/dist/frontend/index.d.ts +3 -1
  29. package/dist/frontend/index.d.ts.map +1 -1
  30. package/dist/frontend/index.mjs +418 -43
  31. package/dist/frontend/index.mjs.map +1 -1
  32. package/dist/frontend/plugins/authFrontendPlugin.d.ts +2 -2
  33. package/dist/frontend/plugins/authFrontendPlugin.d.ts.map +1 -1
  34. package/dist/shared/types.d.cts +49 -0
  35. package/dist/shared/types.d.cts.map +1 -0
  36. package/dist/shared/types.d.mts +49 -0
  37. package/dist/shared/types.d.mts.map +1 -0
  38. package/package.json +68 -66
@@ -2,8 +2,8 @@ import { a as formatAuthRoleLabel, o as isAuthAssignableRole, r as AUTH_DEFAULT_
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
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";
5
+ import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronsUpDown, Copy, Key, Loader2, LogOut, Mail, Plus, 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/ui";
7
7
  import { Link, useLocation } from "react-router";
8
8
  //#region src/frontend/providers/AuthProvider.tsx
9
9
  /**
@@ -231,14 +231,14 @@ function SignInForm({ onSuccess, className }) {
231
231
  })]
232
232
  }),
233
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",
234
+ className: "rounded-md border border-imp-destructive/30 bg-imp-destructive/10 px-3 py-2 text-sm text-imp-destructive",
235
235
  children: displayError
236
236
  }),
237
237
  /* @__PURE__ */ jsx("button", {
238
238
  type: "submit",
239
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"
240
+ className: "inline-flex h-9 w-full items-center justify-center gap-2 whitespace-nowrap rounded-md bg-imp-primary px-4 py-2 text-sm font-medium text-imp-primary-foreground shadow transition-colors hover:bg-imp-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-imp-ring disabled:pointer-events-none disabled:opacity-50",
241
+ children: isSigningIn ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { className: "w-4 h-4 animate-spin" }), "Signing in…"] }) : "Sign in"
242
242
  })
243
243
  ]
244
244
  })
@@ -255,16 +255,16 @@ function SignInForm({ onSuccess, className }) {
255
255
  */
256
256
  function SignInPage({ onSuccess, onNavigateToSignUp, title = "Welcome back", subtitle = "Sign in to your account to continue" }) {
257
257
  return /* @__PURE__ */ jsx("div", {
258
- className: "flex min-h-screen items-center justify-center bg-imp-background p-4 text-imp-foreground",
258
+ className: "flex items-center justify-center min-h-screen p-4 bg-imp-background text-imp-foreground",
259
259
  children: /* @__PURE__ */ jsxs("div", {
260
- className: "flex w-full max-w-sm flex-col gap-6",
260
+ className: "flex flex-col w-full max-w-sm gap-6",
261
261
  children: [
262
262
  /* @__PURE__ */ jsxs("div", {
263
263
  className: "flex flex-col items-center gap-2 text-center",
264
264
  children: [
265
265
  /* @__PURE__ */ jsx("div", {
266
266
  className: "flex items-center justify-center",
267
- children: /* @__PURE__ */ jsx(ImpLogo, { className: "h-10 w-auto" })
267
+ children: /* @__PURE__ */ jsx(ImpLogo, { className: "w-auto h-24" })
268
268
  }),
269
269
  /* @__PURE__ */ jsx("h1", {
270
270
  className: "text-xl font-bold",
@@ -278,24 +278,20 @@ function SignInPage({ onSuccess, onNavigateToSignUp, title = "Welcome back", sub
278
278
  }),
279
279
  /* @__PURE__ */ jsx(SignInForm, { onSuccess }),
280
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",
281
+ className: "relative text-sm text-center",
282
+ children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 border-t top-1/2 border-imp-border" }), /* @__PURE__ */ jsx("span", {
283
+ className: "relative px-2 bg-imp-background text-imp-muted-foreground",
284
284
  children: "Admin-managed accounts"
285
285
  })]
286
286
  }),
287
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
- ]
288
+ className: "px-6 text-xs text-center text-imp-muted-foreground",
289
+ children: [onNavigateToSignUp ? " Or " : " Contact your admin if you need access.", onNavigateToSignUp && /* @__PURE__ */ jsx("button", {
290
+ type: "button",
291
+ onClick: onNavigateToSignUp,
292
+ className: "font-medium underline underline-offset-4 hover:text-imp-foreground",
293
+ children: "Sign up"
294
+ })]
299
295
  })
300
296
  ]
301
297
  })
@@ -400,6 +396,359 @@ function AuthGate({ children, fallback = null, loading = null }) {
400
396
  return /* @__PURE__ */ jsx(Fragment, { children });
401
397
  }
402
398
  //#endregion
399
+ //#region src/frontend/components/ApiKeysDialog.tsx
400
+ /**
401
+ * ApiKeysDialog — Admin dialog for managing API keys.
402
+ *
403
+ * Displays a list of API keys with the ability to:
404
+ * - Create new API keys (name, optional expiry)
405
+ * - Copy newly created keys
406
+ * - Delete existing keys
407
+ *
408
+ * Only functional when the auth plugin has API keys enabled.
409
+ */
410
+ function formatDate$1(iso) {
411
+ try {
412
+ return new Intl.DateTimeFormat(void 0, {
413
+ dateStyle: "medium",
414
+ timeStyle: "short"
415
+ }).format(new Date(iso));
416
+ } catch {
417
+ return iso;
418
+ }
419
+ }
420
+ function isExpired(expiresAt) {
421
+ if (!expiresAt) return false;
422
+ return new Date(expiresAt) < /* @__PURE__ */ new Date();
423
+ }
424
+ function ApiKeysDialog({ open, onOpenChange, apiBaseUrl }) {
425
+ const [apiKeys, setApiKeys] = useState([]);
426
+ const [isLoading, setIsLoading] = useState(false);
427
+ const [error, setError] = useState(null);
428
+ const [hasFetched, setHasFetched] = useState(false);
429
+ const [showCreateForm, setShowCreateForm] = useState(false);
430
+ const [newlyCreatedKey, setNewlyCreatedKey] = useState(null);
431
+ const [copiedKeyId, setCopiedKeyId] = useState(null);
432
+ const [pendingDeleteId, setPendingDeleteId] = useState(null);
433
+ const authApiBase = `${apiBaseUrl}/plugins/auth`;
434
+ const fetchApiKeys = useCallback(async () => {
435
+ setIsLoading(true);
436
+ setError(null);
437
+ try {
438
+ const res = await fetch(`${authApiBase}/api-keys`, { credentials: "include" });
439
+ if (!res.ok) {
440
+ const data = await res.json().catch(() => ({ error: "Failed to fetch API keys" }));
441
+ throw new Error(data.error || data.message || `HTTP ${res.status}`);
442
+ }
443
+ setApiKeys((await res.json()).apiKeys ?? []);
444
+ setHasFetched(true);
445
+ } catch (err) {
446
+ setError(err instanceof Error ? err.message : "Failed to fetch API keys");
447
+ } finally {
448
+ setIsLoading(false);
449
+ }
450
+ }, [authApiBase]);
451
+ const deleteApiKey = useCallback(async (keyId) => {
452
+ try {
453
+ const res = await fetch(`${authApiBase}/api-keys/${keyId}`, {
454
+ method: "DELETE",
455
+ credentials: "include"
456
+ });
457
+ if (!res.ok) {
458
+ const data = await res.json().catch(() => ({}));
459
+ throw new Error(data.error || `HTTP ${res.status}`);
460
+ }
461
+ setApiKeys((prev) => prev.filter((k) => k.id !== keyId));
462
+ setPendingDeleteId(null);
463
+ } catch (err) {
464
+ setError(err instanceof Error ? err.message : "Failed to delete API key");
465
+ setPendingDeleteId(null);
466
+ }
467
+ }, [authApiBase]);
468
+ const copyToClipboard = useCallback(async (text, id) => {
469
+ try {
470
+ await navigator.clipboard.writeText(text);
471
+ setCopiedKeyId(id);
472
+ setTimeout(() => setCopiedKeyId(null), 2e3);
473
+ } catch {
474
+ const textArea = document.createElement("textarea");
475
+ textArea.value = text;
476
+ textArea.style.position = "fixed";
477
+ textArea.style.opacity = "0";
478
+ document.body.appendChild(textArea);
479
+ textArea.select();
480
+ document.execCommand("copy");
481
+ document.body.removeChild(textArea);
482
+ setCopiedKeyId(id);
483
+ setTimeout(() => setCopiedKeyId(null), 2e3);
484
+ }
485
+ }, []);
486
+ if (open && !hasFetched && !isLoading) fetchApiKeys();
487
+ const handleOpenChange = (nextOpen) => {
488
+ if (!nextOpen) {
489
+ setShowCreateForm(false);
490
+ setNewlyCreatedKey(null);
491
+ setError(null);
492
+ setPendingDeleteId(null);
493
+ setHasFetched(false);
494
+ }
495
+ onOpenChange(nextOpen);
496
+ };
497
+ return /* @__PURE__ */ jsx(Dialog, {
498
+ open,
499
+ onOpenChange: handleOpenChange,
500
+ children: /* @__PURE__ */ jsxs(DialogContent, {
501
+ className: "max-w-lg border-imp-border bg-imp-background text-imp-foreground sm:max-w-lg",
502
+ children: [
503
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsxs(DialogTitle, {
504
+ className: "flex items-center gap-2 text-sm font-semibold",
505
+ children: [/* @__PURE__ */ jsx(Key, { className: "h-4 w-4" }), "API Keys"]
506
+ }) }),
507
+ /* @__PURE__ */ jsxs("div", {
508
+ className: "space-y-3",
509
+ children: [
510
+ newlyCreatedKey && /* @__PURE__ */ jsxs("div", {
511
+ className: "rounded-md border border-green-500/30 bg-green-50 p-3 dark:bg-green-950/20",
512
+ children: [/* @__PURE__ */ jsx("p", {
513
+ className: "mb-1.5 text-xs font-medium text-green-700 dark:text-green-400",
514
+ children: "API key created! Copy it now — it won't be shown again."
515
+ }), /* @__PURE__ */ jsxs("div", {
516
+ className: "flex items-center gap-2",
517
+ children: [/* @__PURE__ */ jsx("code", {
518
+ className: "flex-1 rounded bg-green-100 px-2 py-1 text-xs font-mono text-green-800 dark:bg-green-900/30 dark:text-green-300 break-all",
519
+ children: newlyCreatedKey
520
+ }), /* @__PURE__ */ jsx("button", {
521
+ type: "button",
522
+ onClick: () => copyToClipboard(newlyCreatedKey, "new-key"),
523
+ className: "shrink-0 rounded-md border border-green-300 p-1.5 text-green-700 hover:bg-green-100 dark:border-green-700 dark:text-green-400 dark:hover:bg-green-900/40",
524
+ children: copiedKeyId === "new-key" ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
525
+ })]
526
+ })]
527
+ }),
528
+ error && /* @__PURE__ */ jsxs("div", {
529
+ className: "rounded-md bg-red-50 p-2 text-xs text-red-600 dark:bg-red-950/20 dark:text-red-400",
530
+ children: [error, /* @__PURE__ */ jsx("button", {
531
+ type: "button",
532
+ onClick: () => setError(null),
533
+ className: "ml-2 underline hover:no-underline",
534
+ children: "Dismiss"
535
+ })]
536
+ }),
537
+ !showCreateForm && /* @__PURE__ */ jsxs("button", {
538
+ type: "button",
539
+ onClick: () => setShowCreateForm(true),
540
+ className: "flex items-center gap-1.5 rounded-lg border border-imp-border px-3 py-1.5 text-xs font-medium text-imp-muted-foreground transition-colors hover:border-imp-primary/50 hover:text-imp-foreground",
541
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }), " Create API Key"]
542
+ }),
543
+ showCreateForm && /* @__PURE__ */ jsx(CreateApiKeyForm, {
544
+ apiBaseUrl: authApiBase,
545
+ onCreated: (key, fullKey) => {
546
+ setApiKeys((prev) => [key, ...prev]);
547
+ setNewlyCreatedKey(fullKey);
548
+ setShowCreateForm(false);
549
+ },
550
+ onCancel: () => setShowCreateForm(false)
551
+ }),
552
+ /* @__PURE__ */ jsxs("div", {
553
+ className: "max-h-[300px] space-y-1.5 overflow-y-auto",
554
+ children: [
555
+ isLoading && !hasFetched && /* @__PURE__ */ jsxs("div", {
556
+ className: "flex items-center justify-center py-8 text-xs text-imp-muted-foreground",
557
+ children: [/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Loading…"]
558
+ }),
559
+ hasFetched && apiKeys.length === 0 && /* @__PURE__ */ jsx("div", {
560
+ className: "py-8 text-center text-xs text-imp-muted-foreground",
561
+ children: "No API keys yet. Create one to get started."
562
+ }),
563
+ apiKeys.map((key) => {
564
+ const expired = isExpired(key.expiresAt);
565
+ return /* @__PURE__ */ jsxs("div", {
566
+ className: `flex items-center justify-between rounded-lg border px-3 py-2.5 ${expired ? "border-red-300/50 bg-red-50/50 dark:border-red-800/30 dark:bg-red-950/10" : "border-imp-border bg-imp-muted/10"}`,
567
+ children: [/* @__PURE__ */ jsxs("div", {
568
+ className: "min-w-0 flex-1",
569
+ children: [/* @__PURE__ */ jsxs("div", {
570
+ className: "flex items-center gap-2",
571
+ children: [
572
+ /* @__PURE__ */ jsx("span", {
573
+ className: "text-sm font-medium text-imp-foreground truncate",
574
+ children: key.name || "Unnamed key"
575
+ }),
576
+ expired && /* @__PURE__ */ jsx("span", {
577
+ className: "shrink-0 rounded-full bg-red-100 px-1.5 py-0.5 text-[10px] font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400",
578
+ children: "Expired"
579
+ }),
580
+ key.enabled === false && /* @__PURE__ */ jsx("span", {
581
+ className: "shrink-0 rounded-full bg-yellow-100 px-1.5 py-0.5 text-[10px] font-medium text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
582
+ children: "Disabled"
583
+ })
584
+ ]
585
+ }), /* @__PURE__ */ jsxs("div", {
586
+ className: "mt-0.5 flex items-center gap-2 text-[11px] text-imp-muted-foreground",
587
+ children: [
588
+ key.start && /* @__PURE__ */ jsxs("span", {
589
+ className: "font-mono",
590
+ children: [
591
+ key.prefix ? `${key.prefix}_` : "",
592
+ key.start,
593
+ "…"
594
+ ]
595
+ }),
596
+ key.createdAt && /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$1(key.createdAt)] }),
597
+ key.expiresAt && !expired && /* @__PURE__ */ jsxs("span", { children: ["Expires ", formatDate$1(key.expiresAt)] })
598
+ ]
599
+ })]
600
+ }), pendingDeleteId === key.id ? /* @__PURE__ */ jsxs("div", {
601
+ className: "flex items-center gap-1.5 shrink-0 ml-2",
602
+ children: [/* @__PURE__ */ jsx("button", {
603
+ type: "button",
604
+ onClick: () => deleteApiKey(key.id),
605
+ className: "rounded-md bg-red-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-red-700",
606
+ children: "Confirm"
607
+ }), /* @__PURE__ */ jsx("button", {
608
+ type: "button",
609
+ onClick: () => setPendingDeleteId(null),
610
+ className: "rounded-md border border-imp-border px-2 py-1 text-[11px] hover:bg-imp-muted",
611
+ children: "Cancel"
612
+ })]
613
+ }) : /* @__PURE__ */ jsx("button", {
614
+ type: "button",
615
+ onClick: () => setPendingDeleteId(key.id),
616
+ className: "shrink-0 ml-2 rounded-md p-1.5 text-imp-muted-foreground transition-colors hover:bg-imp-destructive/10 hover:text-imp-destructive",
617
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
618
+ })]
619
+ }, key.id);
620
+ })
621
+ ]
622
+ })
623
+ ]
624
+ }),
625
+ /* @__PURE__ */ jsx(DialogFooter, { children: /* @__PURE__ */ jsx("button", {
626
+ type: "button",
627
+ onClick: () => handleOpenChange(false),
628
+ className: "rounded-md border border-imp-border px-3 py-1.5 text-sm hover:bg-imp-muted",
629
+ children: "Close"
630
+ }) })
631
+ ]
632
+ })
633
+ });
634
+ }
635
+ function CreateApiKeyForm({ apiBaseUrl, onCreated, onCancel }) {
636
+ const [name, setName] = useState("");
637
+ const [expiresIn, setExpiresIn] = useState("");
638
+ const [isSubmitting, setIsSubmitting] = useState(false);
639
+ const [error, setError] = useState(null);
640
+ const EXPIRY_OPTIONS = [
641
+ {
642
+ value: "",
643
+ label: "No expiry"
644
+ },
645
+ {
646
+ value: "86400",
647
+ label: "1 day"
648
+ },
649
+ {
650
+ value: "604800",
651
+ label: "7 days"
652
+ },
653
+ {
654
+ value: "2592000",
655
+ label: "30 days"
656
+ },
657
+ {
658
+ value: "7776000",
659
+ label: "90 days"
660
+ },
661
+ {
662
+ value: "31536000",
663
+ label: "1 year"
664
+ }
665
+ ];
666
+ const handleSubmit = async (e) => {
667
+ e.preventDefault();
668
+ setError(null);
669
+ setIsSubmitting(true);
670
+ try {
671
+ const body = {};
672
+ if (name.trim()) body.name = name.trim();
673
+ if (expiresIn) body.expiresIn = parseInt(expiresIn, 10);
674
+ const res = await fetch(`${apiBaseUrl}/api-keys`, {
675
+ method: "POST",
676
+ credentials: "include",
677
+ headers: { "content-type": "application/json" },
678
+ body: JSON.stringify(body)
679
+ });
680
+ const data = await res.json();
681
+ if (!res.ok) throw new Error(data.error || data.message || `HTTP ${res.status}`);
682
+ const fullKey = data.key ?? data.apiKey?.key ?? "";
683
+ onCreated({
684
+ id: data.id ?? data.apiKey?.id ?? "",
685
+ name: data.name ?? data.apiKey?.name ?? (name.trim() || null),
686
+ start: data.start ?? data.apiKey?.start ?? null,
687
+ prefix: data.prefix ?? data.apiKey?.prefix ?? null,
688
+ enabled: data.enabled ?? data.apiKey?.enabled ?? true,
689
+ expiresAt: data.expiresAt ?? data.apiKey?.expiresAt ?? null,
690
+ createdAt: data.createdAt ?? data.apiKey?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
691
+ }, fullKey);
692
+ } catch (err) {
693
+ setError(err instanceof Error ? err.message : "Failed to create API key");
694
+ } finally {
695
+ setIsSubmitting(false);
696
+ }
697
+ };
698
+ return /* @__PURE__ */ jsxs("form", {
699
+ onSubmit: handleSubmit,
700
+ className: "space-y-3 rounded-lg border border-imp-border bg-imp-muted/10 p-3",
701
+ children: [
702
+ /* @__PURE__ */ jsx("div", {
703
+ className: "text-xs font-medium text-imp-foreground",
704
+ children: "Create API Key"
705
+ }),
706
+ /* @__PURE__ */ jsxs("div", {
707
+ className: "grid grid-cols-2 gap-3",
708
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
709
+ className: "mb-1 block text-xs font-medium text-imp-foreground",
710
+ children: "Name"
711
+ }), /* @__PURE__ */ jsx("input", {
712
+ type: "text",
713
+ value: name,
714
+ onChange: (e) => setName(e.target.value),
715
+ placeholder: "e.g. Production API",
716
+ 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:border-imp-primary/50 focus:outline-none"
717
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
718
+ className: "mb-1 block text-xs font-medium text-imp-foreground",
719
+ children: "Expiry"
720
+ }), /* @__PURE__ */ jsx("select", {
721
+ value: expiresIn,
722
+ onChange: (e) => setExpiresIn(e.target.value),
723
+ className: "w-full rounded-md border border-imp-border bg-imp-background px-3 py-1.5 text-sm focus:border-imp-primary/50 focus:outline-none",
724
+ children: EXPIRY_OPTIONS.map((option) => /* @__PURE__ */ jsx("option", {
725
+ value: option.value,
726
+ children: option.label
727
+ }, option.value))
728
+ })] })]
729
+ }),
730
+ error && /* @__PURE__ */ jsx("div", {
731
+ className: "rounded-md bg-red-50 p-2 text-xs text-red-600 dark:bg-red-950/20 dark:text-red-400",
732
+ children: error
733
+ }),
734
+ /* @__PURE__ */ jsxs("div", {
735
+ className: "flex justify-end gap-2",
736
+ children: [/* @__PURE__ */ jsx("button", {
737
+ type: "button",
738
+ onClick: onCancel,
739
+ className: "rounded-md border border-imp-border px-3 py-1.5 text-xs hover:bg-imp-muted",
740
+ children: "Cancel"
741
+ }), /* @__PURE__ */ jsx("button", {
742
+ type: "submit",
743
+ disabled: isSubmitting,
744
+ className: "rounded-md bg-imp-primary px-3 py-1.5 text-xs font-semibold text-imp-primary-foreground hover:bg-imp-primary/90 disabled:opacity-50",
745
+ children: isSubmitting ? "Creating…" : "Create"
746
+ })]
747
+ })
748
+ ]
749
+ });
750
+ }
751
+ //#endregion
403
752
  //#region src/frontend/components/UserManagement.tsx
404
753
  /**
405
754
  * UserManagement — Admin panel for managing users.
@@ -518,6 +867,9 @@ function UserManagement({ apiBaseUrl, className }) {
518
867
  const [currentPage, setCurrentPage] = useState(1);
519
868
  const [sortField, setSortField] = useState("name");
520
869
  const [sortDir, setSortDir] = useState("asc");
870
+ const [apiKeysEnabled, setApiKeysEnabled] = useState(false);
871
+ const [showApiKeysDialog, setShowApiKeysDialog] = useState(false);
872
+ const [hasCheckedApiKeys, setHasCheckedApiKeys] = useState(false);
521
873
  const PAGE_SIZE = 10;
522
874
  const handleSort = useCallback((field) => {
523
875
  setSortField((prev) => {
@@ -561,6 +913,14 @@ function UserManagement({ apiBaseUrl, className }) {
561
913
  const totalPages = Math.max(1, Math.ceil(filteredUsers.length / PAGE_SIZE));
562
914
  const paginatedUsers = filteredUsers.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
563
915
  const authApiBase = `${apiBaseUrl}/plugins/auth`;
916
+ const checkApiKeys = useCallback(async () => {
917
+ try {
918
+ const res = await fetch(`${authApiBase}/info`, { credentials: "include" });
919
+ if (res.ok) setApiKeysEnabled(!!(await res.json()).apiKeysEnabled);
920
+ } catch {} finally {
921
+ setHasCheckedApiKeys(true);
922
+ }
923
+ }, [authApiBase]);
564
924
  const fetchUsers = useCallback(async () => {
565
925
  setIsLoading(true);
566
926
  setError(null);
@@ -617,29 +977,39 @@ function UserManagement({ apiBaseUrl, className }) {
617
977
  }, [authApiBase]);
618
978
  if (!isAuthenticated || user?.role !== "admin") return null;
619
979
  if (!hasFetched && !isLoading) fetchUsers();
980
+ if (!hasCheckedApiKeys) checkApiKeys();
620
981
  return /* @__PURE__ */ jsxs("div", {
621
982
  className: `space-y-4 ${className ?? ""}`,
622
983
  children: [
623
984
  /* @__PURE__ */ jsxs("div", {
624
985
  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
- })]
986
+ children: [
987
+ /* @__PURE__ */ jsxs("div", {
988
+ className: "relative flex-1 max-w-sm",
989
+ 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", {
990
+ type: "text",
991
+ value: searchQuery,
992
+ onChange: (e) => {
993
+ setSearchQuery(e.target.value);
994
+ setCurrentPage(1);
995
+ },
996
+ placeholder: "Search users…",
997
+ 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"
998
+ })]
999
+ }),
1000
+ apiKeysEnabled && /* @__PURE__ */ jsxs("button", {
1001
+ type: "button",
1002
+ onClick: () => setShowApiKeysDialog(true),
1003
+ 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",
1004
+ children: [/* @__PURE__ */ jsx(Key, { className: "w-4 h-4" }), " API Keys"]
1005
+ }),
1006
+ /* @__PURE__ */ jsxs("button", {
1007
+ type: "button",
1008
+ onClick: () => setShowCreateDialog(true),
1009
+ 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",
1010
+ children: [/* @__PURE__ */ jsx(UserPlus, { className: "w-4 h-4" }), " Create User"]
1011
+ })
1012
+ ]
643
1013
  }),
644
1014
  error && /* @__PURE__ */ jsxs("div", {
645
1015
  className: "p-3 text-sm text-red-600 rounded-md bg-red-50 dark:bg-red-950/20 dark:text-red-400",
@@ -741,7 +1111,7 @@ function UserManagement({ apiBaseUrl, className }) {
741
1111
  }) : u.id !== user?.id ? /* @__PURE__ */ jsx("button", {
742
1112
  type: "button",
743
1113
  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",
1114
+ className: "rounded-md p-1.5 text-imp-muted-foreground opacity-0 transition-opacity hover:bg-imp-destructive/10 hover:text-imp-destructive group-hover:opacity-100",
745
1115
  children: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" })
746
1116
  }) : null
747
1117
  })
@@ -789,6 +1159,11 @@ function UserManagement({ apiBaseUrl, className }) {
789
1159
  ]
790
1160
  })]
791
1161
  }),
1162
+ apiKeysEnabled && /* @__PURE__ */ jsx(ApiKeysDialog, {
1163
+ open: showApiKeysDialog,
1164
+ onOpenChange: setShowApiKeysDialog,
1165
+ apiBaseUrl
1166
+ }),
792
1167
  /* @__PURE__ */ jsx(Dialog, {
793
1168
  open: showCreateDialog,
794
1169
  onOpenChange: (open) => !open && setShowCreateDialog(false),
@@ -1225,7 +1600,7 @@ function SidebarUserMenu({ collapsed = false, basePath = "" }) {
1225
1600
  * AuthenticatedInvect already wraps the tree with it. This plugin
1226
1601
  * only adds the user management UI.
1227
1602
  */
1228
- const authFrontendPlugin = {
1603
+ const authFrontend = {
1229
1604
  id: "user-auth",
1230
1605
  name: "User Authentication",
1231
1606
  sidebar: [{
@@ -1245,6 +1620,6 @@ const authFrontendPlugin = {
1245
1620
  }]
1246
1621
  };
1247
1622
  //#endregion
1248
- export { AuthGate, AuthProvider, AuthenticatedInvect, ProfilePage, SidebarUserMenu, SignInForm, SignInPage, UserButton, UserManagement, UserManagementPage, authFrontendPlugin, useAuth };
1623
+ export { ApiKeysDialog, AuthGate, AuthProvider, AuthenticatedInvect, ProfilePage, SidebarUserMenu, SignInForm, SignInPage, UserButton, UserManagement, UserManagementPage, authFrontend, useAuth };
1249
1624
 
1250
1625
  //# sourceMappingURL=index.mjs.map