@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.
- package/README.md +81 -72
- package/dist/backend/index.cjs +410 -54
- package/dist/backend/index.cjs.map +1 -1
- package/dist/backend/index.d.cts +456 -0
- package/dist/backend/index.d.cts.map +1 -0
- package/dist/backend/index.d.mts +456 -0
- package/dist/backend/index.d.mts.map +1 -0
- package/dist/backend/index.d.ts +28 -18
- package/dist/backend/index.d.ts.map +1 -1
- package/dist/backend/index.mjs +408 -53
- package/dist/backend/index.mjs.map +1 -1
- package/dist/backend/plugin.d.ts +15 -15
- package/dist/backend/plugin.d.ts.map +1 -1
- package/dist/backend/types.d.ts +85 -9
- package/dist/backend/types.d.ts.map +1 -1
- package/dist/frontend/components/ApiKeysDialog.d.ts +17 -0
- package/dist/frontend/components/ApiKeysDialog.d.ts.map +1 -0
- package/dist/frontend/components/AuthenticatedInvect.d.ts +10 -10
- package/dist/frontend/components/SignInForm.d.ts.map +1 -1
- package/dist/frontend/components/SignInPage.d.ts.map +1 -1
- package/dist/frontend/components/UserManagement.d.ts.map +1 -1
- package/dist/frontend/index.cjs +434 -58
- package/dist/frontend/index.cjs.map +1 -1
- package/dist/frontend/index.d.cts +317 -0
- package/dist/frontend/index.d.cts.map +1 -0
- package/dist/frontend/index.d.mts +317 -0
- package/dist/frontend/index.d.mts.map +1 -0
- package/dist/frontend/index.d.ts +3 -1
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/index.mjs +418 -43
- package/dist/frontend/index.mjs.map +1 -1
- package/dist/frontend/plugins/authFrontendPlugin.d.ts +2 -2
- package/dist/frontend/plugins/authFrontendPlugin.d.ts.map +1 -1
- package/dist/shared/types.d.cts +49 -0
- package/dist/shared/types.d.cts.map +1 -0
- package/dist/shared/types.d.mts +49 -0
- package/dist/shared/types.d.mts.map +1 -0
- package/package.json +68 -66
package/dist/frontend/index.mjs
CHANGED
|
@@ -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/
|
|
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-
|
|
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-
|
|
241
|
-
children: isSigningIn ? "Signing in…" : "
|
|
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
|
|
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
|
|
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: "
|
|
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-
|
|
282
|
-
children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 top-1/2 border-
|
|
283
|
-
className: "relative bg-imp-background
|
|
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-
|
|
289
|
-
children: [
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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: [
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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-
|
|
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
|
|
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,
|
|
1623
|
+
export { ApiKeysDialog, AuthGate, AuthProvider, AuthenticatedInvect, ProfilePage, SidebarUserMenu, SignInForm, SignInPage, UserButton, UserManagement, UserManagementPage, authFrontend, useAuth };
|
|
1249
1624
|
|
|
1250
1625
|
//# sourceMappingURL=index.mjs.map
|