@kyro-cms/admin 0.12.20 → 0.12.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +24 -24
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +19 -19
- package/package.json +6 -6
- package/src/components/ApiKeysManager.tsx +177 -227
- package/src/components/AuditLogsPage.tsx +7 -7
- package/src/components/AuthorDashboard.tsx +235 -0
- package/src/components/AutoForm.tsx +14 -4
- package/src/components/CustomerDashboard.tsx +213 -0
- package/src/components/Dashboard.tsx +1 -1
- package/src/components/DashboardMetrics.tsx +74 -9
- package/src/components/DashboardQuickSections.tsx +237 -0
- package/src/components/ListView.tsx +3 -3
- package/src/components/MediaGallery.tsx +2 -2
- package/src/components/PluginsManager.tsx +1 -1
- package/src/components/SessionsManager.tsx +15 -15
- package/src/components/Sidebar.astro +564 -116
- package/src/components/UserManagement.tsx +43 -9
- package/src/components/UserMenu.tsx +123 -61
- package/src/components/autoform/AutoFormEditView.tsx +4 -4
- package/src/components/autoform/AutoFormHeader.tsx +2 -2
- package/src/components/ui/CommandPalette.tsx +1 -1
- package/src/components/ui/Dropdown.tsx +71 -16
- package/src/components/ui/PageHeader.tsx +1 -1
- package/src/components/ui/Pagination.tsx +1 -1
- package/src/components/ui/Toast.tsx +3 -2
- package/src/components/ui/Toaster.tsx +1 -1
- package/src/components/users/UserDetail.tsx +74 -29
- package/src/components/users/UserForm.tsx +1 -1
- package/src/components/users/UsersList.tsx +2 -2
- package/src/integration.ts +4 -0
- package/src/layouts/AdminLayout.astro +26 -34
- package/src/layouts/AuthLayout.astro +18 -9
- package/src/lib/auth-server.ts +99 -0
- package/src/pages/403.astro +54 -35
- package/src/pages/[collection]/[id].astro +8 -0
- package/src/pages/[collection]/index.astro +7 -0
- package/src/pages/audit/index.astro +4 -1
- package/src/pages/auth/check-email.astro +146 -0
- package/src/pages/auth/forgot-password.astro +95 -0
- package/src/pages/auth/login.astro +29 -12
- package/src/pages/auth/register.astro +38 -12
- package/src/pages/auth/reset-password.astro +125 -0
- package/src/pages/auth/verify-email.astro +87 -0
- package/src/pages/index.astro +220 -121
- package/src/pages/keys.astro +3 -1
- package/src/pages/media.astro +3 -1
- package/src/pages/preview/[collection]/[id].astro +23 -73
- package/src/pages/roles/index.astro +134 -101
- package/src/pages/settings/[slug].astro +43 -3
- package/src/pages/users/[id].astro +5 -1
- package/src/pages/users/index.astro +4 -0
- package/src/pages/users/new.astro +4 -0
- package/src/pages/webhooks.astro +3 -1
- package/src/routes.ts +16 -0
|
@@ -153,13 +153,39 @@ function CustomTooltip({ active, payload, label, currency }: any) {
|
|
|
153
153
|
export const RevenueChart: React.FC = () => {
|
|
154
154
|
const [data, setData] = useState<any>(null);
|
|
155
155
|
const [loading, setLoading] = useState(true);
|
|
156
|
+
const [auth, setAuth] = useState<any>(null);
|
|
157
|
+
const [mounted, setMounted] = useState(false);
|
|
156
158
|
|
|
157
159
|
useEffect(() => {
|
|
160
|
+
setMounted(true);
|
|
161
|
+
if (typeof window !== "undefined") {
|
|
162
|
+
setAuth((window as any).__kyroAuth || null);
|
|
163
|
+
}
|
|
164
|
+
const handleAuth = (e: any) => {
|
|
165
|
+
if (e?.detail) setAuth(e.detail);
|
|
166
|
+
else if (typeof window !== "undefined") setAuth((window as any).__kyroAuth || null);
|
|
167
|
+
};
|
|
168
|
+
window.addEventListener("kyro:auth-ready", handleAuth);
|
|
169
|
+
return () => window.removeEventListener("kyro:auth-ready", handleAuth);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const userRole = auth?.user?.role || "";
|
|
173
|
+
const isAdmin = userRole === "admin" || userRole === "super_admin";
|
|
174
|
+
const canReadOrders = isAdmin || auth?.permissions?.collections?.orders?.read === true;
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!mounted) return;
|
|
178
|
+
if (!canReadOrders) {
|
|
179
|
+
setLoading(false);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
158
182
|
apiGet<any>("/api/analytics", { autoToast: false })
|
|
159
183
|
.then(setData)
|
|
160
184
|
.catch(console.error)
|
|
161
185
|
.finally(() => setLoading(false));
|
|
162
|
-
}, []);
|
|
186
|
+
}, [mounted, canReadOrders]);
|
|
187
|
+
|
|
188
|
+
if (!mounted || !canReadOrders) return null;
|
|
163
189
|
|
|
164
190
|
if (loading)
|
|
165
191
|
return (
|
|
@@ -396,6 +422,27 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
396
422
|
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
|
397
423
|
const [loading, setLoading] = useState(true);
|
|
398
424
|
const [error, setError] = useState<string | null>(null);
|
|
425
|
+
const [auth, setAuth] = useState<any>(null);
|
|
426
|
+
const [mounted, setMounted] = useState(false);
|
|
427
|
+
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
setMounted(true);
|
|
430
|
+
if (typeof window !== "undefined") {
|
|
431
|
+
setAuth((window as any).__kyroAuth || null);
|
|
432
|
+
}
|
|
433
|
+
const handleAuth = (e: any) => {
|
|
434
|
+
if (e?.detail) setAuth(e.detail);
|
|
435
|
+
else if (typeof window !== "undefined") setAuth((window as any).__kyroAuth || null);
|
|
436
|
+
};
|
|
437
|
+
window.addEventListener("kyro:auth-ready", handleAuth);
|
|
438
|
+
return () => window.removeEventListener("kyro:auth-ready", handleAuth);
|
|
439
|
+
}, []);
|
|
440
|
+
|
|
441
|
+
const user = auth?.user;
|
|
442
|
+
const permissions = auth?.permissions;
|
|
443
|
+
const userRole = user?.role || "";
|
|
444
|
+
const isSuperAdmin = userRole === "super_admin";
|
|
445
|
+
const isAdmin = userRole === "admin" || isSuperAdmin;
|
|
399
446
|
|
|
400
447
|
useEffect(() => {
|
|
401
448
|
Promise.all([
|
|
@@ -418,7 +465,7 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
418
465
|
|
|
419
466
|
if (error) {
|
|
420
467
|
return (
|
|
421
|
-
<div className="surface-tile overflow-hidden mb-6">
|
|
468
|
+
<div className="surface-tile overflow-hidden rounded-lg mb-6">
|
|
422
469
|
<div className="p-6 border-b" style={{ borderColor: "var(--kyro-border)" }}>
|
|
423
470
|
<h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>Metrics</h2>
|
|
424
471
|
</div>
|
|
@@ -438,6 +485,7 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
438
485
|
|
|
439
486
|
const ecommerceCards = [
|
|
440
487
|
{
|
|
488
|
+
id: "revenue",
|
|
441
489
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>,
|
|
442
490
|
gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
443
491
|
value: analyticsData?.totalRevenue !== undefined
|
|
@@ -445,48 +493,59 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
445
493
|
: "$0",
|
|
446
494
|
label: "Total Revenue",
|
|
447
495
|
subtext: "From system orders",
|
|
496
|
+
visible: isAdmin || permissions?.collections?.orders?.read === true,
|
|
448
497
|
},
|
|
449
498
|
{
|
|
499
|
+
id: "products",
|
|
450
500
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>,
|
|
451
501
|
gradient: "linear-gradient(135deg, #ec4899 0%, #f43f5e 100%)",
|
|
452
502
|
value: productCount,
|
|
453
503
|
label: "Products / Menu Items",
|
|
454
504
|
subtext: "Active inventory & menu items",
|
|
505
|
+
visible: isAdmin || permissions?.collections?.products?.read === true,
|
|
455
506
|
},
|
|
456
507
|
{
|
|
508
|
+
id: "orders",
|
|
457
509
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>,
|
|
458
510
|
gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
459
511
|
value: analyticsData?.totalOrders !== undefined ? analyticsData.totalOrders : (data?.collectionCounts?.["orders"] || 0),
|
|
460
512
|
label: "Orders",
|
|
461
513
|
subtext: "Total orders placed",
|
|
514
|
+
visible: isAdmin || permissions?.collections?.orders?.read === true,
|
|
462
515
|
},
|
|
463
516
|
{
|
|
517
|
+
id: "customers",
|
|
464
518
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>,
|
|
465
519
|
gradient: "linear-gradient(135deg, #3b82f6 0%, #6366f1 100%)",
|
|
466
520
|
value: data?.collectionCounts?.["customers"] || 0,
|
|
467
521
|
label: "Customers",
|
|
468
522
|
subtext: "Registered shoppers",
|
|
523
|
+
visible: isAdmin || permissions?.collections?.customers?.read === true || permissions?.collections?.users?.read === true,
|
|
469
524
|
},
|
|
470
525
|
{
|
|
526
|
+
id: "reviews",
|
|
471
527
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>,
|
|
472
528
|
gradient: "linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)",
|
|
473
529
|
value: data?.collectionCounts?.["reviews"] || 0,
|
|
474
530
|
label: "Reviews",
|
|
475
531
|
subtext: "Customer feedback",
|
|
532
|
+
visible: isAdmin || permissions?.collections?.reviews?.read === true,
|
|
476
533
|
},
|
|
477
|
-
];
|
|
534
|
+
].filter((c) => c.visible);
|
|
478
535
|
|
|
479
|
-
// Dynamically extract top active custom collections
|
|
536
|
+
// Dynamically extract top active custom collections permitted for user
|
|
480
537
|
const activeCustomCollections = data?.collectionCounts
|
|
481
538
|
? Object.entries(data.collectionCounts)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
539
|
+
.filter(([slug, count]) => count > 0 && !["users", "audit_logs", "media"].includes(slug))
|
|
540
|
+
.filter(([slug]) => isAdmin || permissions?.collections?.[slug]?.read === true)
|
|
541
|
+
.sort((a, b) => b[1] - a[1])
|
|
542
|
+
.slice(0, 3)
|
|
485
543
|
: [];
|
|
486
544
|
|
|
487
545
|
const dynamicCollectionCards = activeCustomCollections.map(([slug, count]) => {
|
|
488
546
|
const formattedLabel = slug.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
489
547
|
return {
|
|
548
|
+
id: `col-${slug}`,
|
|
490
549
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>,
|
|
491
550
|
gradient: "linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)",
|
|
492
551
|
value: count,
|
|
@@ -497,30 +556,36 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
497
556
|
|
|
498
557
|
const baseCmsCards = [
|
|
499
558
|
{
|
|
559
|
+
id: "documents",
|
|
500
560
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>,
|
|
501
561
|
gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
502
562
|
value: data?.totalDocuments || 0,
|
|
503
563
|
label: "Documents",
|
|
504
564
|
subtext: `Across ${data?.collections || 0} collection${data?.collections !== 1 ? "s" : ""}`,
|
|
565
|
+
visible: true,
|
|
505
566
|
},
|
|
506
567
|
{
|
|
568
|
+
id: "media",
|
|
507
569
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>,
|
|
508
570
|
gradient: "linear-gradient(135deg, #a855f7 0%, #ec4899 100%)",
|
|
509
571
|
value: data?.totalMedia || 0,
|
|
510
572
|
label: "Media Files",
|
|
511
573
|
subtext: "Images, videos & docs",
|
|
574
|
+
visible: isAdmin || permissions?.collections?.media?.read === true,
|
|
512
575
|
},
|
|
513
|
-
];
|
|
576
|
+
].filter((c) => c.visible);
|
|
514
577
|
|
|
515
578
|
const trailingCmsCards = [
|
|
516
579
|
{
|
|
580
|
+
id: "team",
|
|
517
581
|
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>,
|
|
518
582
|
gradient: "linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%)",
|
|
519
583
|
value: data?.totalUsers || 0,
|
|
520
584
|
label: "Team Members",
|
|
521
585
|
subtext: "Active user accounts",
|
|
586
|
+
visible: isAdmin || permissions?.collections?.users?.read === true,
|
|
522
587
|
},
|
|
523
|
-
];
|
|
588
|
+
].filter((c) => c.visible);
|
|
524
589
|
|
|
525
590
|
const cmsCards = [
|
|
526
591
|
...baseCmsCards,
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { adminPath } from "../lib/paths";
|
|
3
|
+
import { useAuthStore } from "../lib/stores";
|
|
4
|
+
|
|
5
|
+
interface DashboardQuickSectionsProps {
|
|
6
|
+
collections: Record<string, any>;
|
|
7
|
+
serverUserRole?: string;
|
|
8
|
+
serverPermissions?: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DashboardQuickSections({
|
|
12
|
+
collections,
|
|
13
|
+
serverUserRole,
|
|
14
|
+
serverPermissions,
|
|
15
|
+
}: DashboardQuickSectionsProps) {
|
|
16
|
+
const storeAuth = useAuthStore();
|
|
17
|
+
const [windowAuth, setWindowAuth] = useState<any>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (typeof window !== "undefined" && (window as any).__kyroAuth) {
|
|
21
|
+
setWindowAuth((window as any).__kyroAuth);
|
|
22
|
+
}
|
|
23
|
+
const handleAuth = (e: any) => {
|
|
24
|
+
if (e?.detail) {
|
|
25
|
+
setWindowAuth(e.detail);
|
|
26
|
+
} else if (typeof window !== "undefined") {
|
|
27
|
+
setWindowAuth((window as any).__kyroAuth);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
window.addEventListener("kyro:auth-ready", handleAuth);
|
|
31
|
+
return () => window.removeEventListener("kyro:auth-ready", handleAuth);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const user = storeAuth.user || windowAuth?.user || null;
|
|
35
|
+
const permissions = storeAuth.permissions || windowAuth?.permissions || serverPermissions || null;
|
|
36
|
+
const role = user?.role || serverUserRole || "";
|
|
37
|
+
|
|
38
|
+
const isSuperAdmin = role === "super_admin";
|
|
39
|
+
const isAdmin = role === "admin" || isSuperAdmin;
|
|
40
|
+
const isEditor = role === "editor";
|
|
41
|
+
const isAuthor = role === "author";
|
|
42
|
+
const isCustomer = role === "customer";
|
|
43
|
+
|
|
44
|
+
// Helpers to check collection read/create permission
|
|
45
|
+
const canReadCollection = (slug: string): boolean => {
|
|
46
|
+
if (isAdmin) return true;
|
|
47
|
+
if (permissions && permissions.collections && permissions.collections[slug]) {
|
|
48
|
+
return permissions.collections[slug].read === true;
|
|
49
|
+
}
|
|
50
|
+
if (isEditor) {
|
|
51
|
+
return !["users", "audit_logs", "roles", "plugins", "keys", "webhooks", "settings"].includes(slug);
|
|
52
|
+
}
|
|
53
|
+
if (isAuthor) {
|
|
54
|
+
return ["posts", "categories", "orders"].includes(slug);
|
|
55
|
+
}
|
|
56
|
+
if (isCustomer) {
|
|
57
|
+
return ["orders"].includes(slug);
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const canCreateCollection = (slug: string): boolean => {
|
|
63
|
+
if (isAdmin) return true;
|
|
64
|
+
if (permissions && permissions.collections && permissions.collections[slug]) {
|
|
65
|
+
return permissions.collections[slug].create === true;
|
|
66
|
+
}
|
|
67
|
+
if (isEditor) {
|
|
68
|
+
return !["users", "audit_logs", "roles", "plugins", "keys", "webhooks", "settings"].includes(slug);
|
|
69
|
+
}
|
|
70
|
+
if (isAuthor) {
|
|
71
|
+
return ["posts", "categories"].includes(slug);
|
|
72
|
+
}
|
|
73
|
+
if (isCustomer) {
|
|
74
|
+
return ["orders"].includes(slug);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── Quick Actions RBAC List ──
|
|
80
|
+
const allQuickActions = [
|
|
81
|
+
{
|
|
82
|
+
id: "profile",
|
|
83
|
+
label: "Edit Account Profile",
|
|
84
|
+
description: "Manage your personal profile and account details",
|
|
85
|
+
href: `${adminPath}/profile`,
|
|
86
|
+
visible: true, // Everyone logged in can view their profile
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "media",
|
|
90
|
+
label: "Media Assets Library",
|
|
91
|
+
description: "Upload and manage images, documents and files",
|
|
92
|
+
href: `${adminPath}/media`,
|
|
93
|
+
visible: !isCustomer && (isAdmin || isAuthor || isEditor || canReadCollection("media")),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "orders",
|
|
97
|
+
label: "Order History",
|
|
98
|
+
description: "View past orders and purchases",
|
|
99
|
+
href: `${adminPath}/orders`,
|
|
100
|
+
visible: !!collections["orders"] && (isAdmin || canReadCollection("orders")),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "users",
|
|
104
|
+
label: "User Management",
|
|
105
|
+
description: "Manage team accounts, roles & customer profiles",
|
|
106
|
+
href: `${adminPath}/users`,
|
|
107
|
+
visible: isAdmin || canReadCollection("users"),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "roles",
|
|
111
|
+
label: "Roles & Permissions",
|
|
112
|
+
description: "RBAC inheritance rules and granted permission matrices",
|
|
113
|
+
href: `${adminPath}/roles`,
|
|
114
|
+
visible: isSuperAdmin,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "audit",
|
|
118
|
+
label: "Security Audit Logs",
|
|
119
|
+
description: "Monitor user actions, auth attempts and system logs",
|
|
120
|
+
href: `${adminPath}/audit`,
|
|
121
|
+
visible: isAdmin || canReadCollection("audit_logs"),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "health",
|
|
125
|
+
label: "API System Health",
|
|
126
|
+
description: "Real-time API status, memory and database health",
|
|
127
|
+
href: `${adminPath}/health`,
|
|
128
|
+
visible: isAdmin,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "settings",
|
|
132
|
+
label: "System Settings",
|
|
133
|
+
description: "Global site preferences, SEO & API configuration",
|
|
134
|
+
href: `${adminPath}/settings`,
|
|
135
|
+
visible: isAdmin,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const visibleQuickActions = allQuickActions.filter((action) => action.visible);
|
|
140
|
+
|
|
141
|
+
// ── Explore Content RBAC List ──
|
|
142
|
+
const exploreCollections = Object.entries(collections)
|
|
143
|
+
.filter(([slug]) => !["users", "audit_logs", "media"].includes(slug))
|
|
144
|
+
.filter(([slug]) => canReadCollection(slug))
|
|
145
|
+
.map(([slug, config]: [string, any]) => {
|
|
146
|
+
const label = config.label || slug.split("-").map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
147
|
+
const canCreate = canCreateCollection(slug);
|
|
148
|
+
return {
|
|
149
|
+
slug,
|
|
150
|
+
label,
|
|
151
|
+
canCreate,
|
|
152
|
+
href: `${adminPath}/${slug}`,
|
|
153
|
+
newHref: `${adminPath}/${slug}/new`,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
|
159
|
+
{/* ── Quick Actions ── */}
|
|
160
|
+
<div className="surface-tile p-5 rounded-lg border border-[var(--kyro-border)] flex flex-col justify-between">
|
|
161
|
+
<div>
|
|
162
|
+
<div className="flex items-center justify-between mb-3">
|
|
163
|
+
<div>
|
|
164
|
+
<h2 className="text-sm font-semibold text-[var(--kyro-text-primary)]">Quick Actions</h2>
|
|
165
|
+
<p className="text-[11px] text-[var(--kyro-text-secondary)] mt-0.5">Shortcuts available for your role</p>
|
|
166
|
+
</div>
|
|
167
|
+
<span className="text-[10px] font-mono px-2 py-0.5 rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)]">
|
|
168
|
+
{visibleQuickActions.length} Actions
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="space-y-1">
|
|
173
|
+
{visibleQuickActions.map((action) => (
|
|
174
|
+
<a
|
|
175
|
+
key={action.id}
|
|
176
|
+
href={action.href}
|
|
177
|
+
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-xs text-[var(--kyro-text-primary)] transition-colors group"
|
|
178
|
+
>
|
|
179
|
+
<div>
|
|
180
|
+
<p className="font-medium text-[var(--kyro-text-primary)]">{action.label}</p>
|
|
181
|
+
</div>
|
|
182
|
+
<span className="text-[var(--kyro-text-muted)] group-hover:text-[var(--kyro-text-primary)] transition-colors">
|
|
183
|
+
→
|
|
184
|
+
</span>
|
|
185
|
+
</a>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* ── Explore Content ── */}
|
|
192
|
+
<div className="surface-tile p-5 rounded-lg border border-[var(--kyro-border)] flex flex-col justify-between">
|
|
193
|
+
<div>
|
|
194
|
+
<div className="flex items-center justify-between mb-3">
|
|
195
|
+
<div>
|
|
196
|
+
<h2 className="text-sm font-semibold text-[var(--kyro-text-primary)]">Explore Content</h2>
|
|
197
|
+
<p className="text-[11px] text-[var(--kyro-text-secondary)] mt-0.5">Collections accessible to your account</p>
|
|
198
|
+
</div>
|
|
199
|
+
<span className="text-[10px] font-mono px-2 py-0.5 rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)]">
|
|
200
|
+
{exploreCollections.length} Collections
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{exploreCollections.length === 0 ? (
|
|
205
|
+
<div className="py-8 text-center text-xs text-[var(--kyro-text-muted)]">No accessible collections</div>
|
|
206
|
+
) : (
|
|
207
|
+
<div className="space-y-1">
|
|
208
|
+
{exploreCollections.map((col) => (
|
|
209
|
+
<div
|
|
210
|
+
key={col.slug}
|
|
211
|
+
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-[var(--kyro-surface-accent)] text-xs transition-colors group"
|
|
212
|
+
>
|
|
213
|
+
<a href={col.href} className="font-medium text-[var(--kyro-text-primary)] hover:underline flex-1">
|
|
214
|
+
{col.label}
|
|
215
|
+
</a>
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
{col.canCreate && (
|
|
218
|
+
<a
|
|
219
|
+
href={col.newHref}
|
|
220
|
+
className="text-[10px] font-medium px-2 py-0.5 rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
221
|
+
>
|
|
222
|
+
+ New
|
|
223
|
+
</a>
|
|
224
|
+
)}
|
|
225
|
+
<a href={col.href} className="text-[var(--kyro-text-muted)] group-hover:text-[var(--kyro-text-primary)] transition-colors">
|
|
226
|
+
→
|
|
227
|
+
</a>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -382,7 +382,7 @@ export function ListView({
|
|
|
382
382
|
|
|
383
383
|
|
|
384
384
|
{/* Toolbar */}
|
|
385
|
-
<div className="surface-tile p-4 flex flex-col lg:flex-row gap-4 items-start lg:items-center">
|
|
385
|
+
<div className="surface-tile p-4 flex flex-col lg:flex-row gap-4 items-start lg:items-center rounded-lg">
|
|
386
386
|
{/* Search */}
|
|
387
387
|
<div className="relative flex-1 w-full lg:max-w-md">
|
|
388
388
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
@@ -471,7 +471,7 @@ export function ListView({
|
|
|
471
471
|
|
|
472
472
|
{/* Filter Panel */}
|
|
473
473
|
{showFilters && (
|
|
474
|
-
<div className="surface-tile p-4 border-l-4 border-[var(--kyro-sidebar-active)]">
|
|
474
|
+
<div className="surface-tile p-4 border-l-4 border-[var(--kyro-sidebar-active)] rounded-lg">
|
|
475
475
|
<div className="flex items-center justify-between mb-4">
|
|
476
476
|
<h3 className="font-medium text-[var(--kyro-text-primary)]">
|
|
477
477
|
{t("listView.advancedFilters", { defaultValue: "Advanced Filters" })}
|
|
@@ -573,7 +573,7 @@ export function ListView({
|
|
|
573
573
|
)}
|
|
574
574
|
|
|
575
575
|
{/* Data Table */}
|
|
576
|
-
<div className="surface-tile overflow-hidden">
|
|
576
|
+
<div className="surface-tile overflow-hidden rounded-lg">
|
|
577
577
|
{loading ? (
|
|
578
578
|
<div className="space-y-2 p-4">
|
|
579
579
|
<Shimmer variant="table-row" count={8} />
|
|
@@ -143,7 +143,7 @@ export function MediaGallery({
|
|
|
143
143
|
multiple?: boolean;
|
|
144
144
|
pickerMode?: boolean;
|
|
145
145
|
}) {
|
|
146
|
-
|
|
146
|
+
const { t } = useTranslation();
|
|
147
147
|
const { permissions } = useAuthStore();
|
|
148
148
|
const canUpload = permissions?.media?.create !== false;
|
|
149
149
|
const canDelete = permissions?.media?.delete !== false;
|
|
@@ -678,7 +678,7 @@ export function MediaGallery({
|
|
|
678
678
|
))}
|
|
679
679
|
</div>
|
|
680
680
|
) : (
|
|
681
|
-
<div className="surface-tile overflow-hidden animate-in fade-in duration-500">
|
|
681
|
+
<div className="surface-tile overflow-hidden rounded-lg animate-in fade-in duration-500">
|
|
682
682
|
<table className="w-full text-left">
|
|
683
683
|
<thead>
|
|
684
684
|
<tr className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 border-b border-[var(--kyro-border)]">
|
|
@@ -118,7 +118,7 @@ export function PluginsManager() {
|
|
|
118
118
|
</div>
|
|
119
119
|
)}
|
|
120
120
|
|
|
121
|
-
<div className="flex flex-col gap-8 surface-tile p-8">
|
|
121
|
+
<div className="flex flex-col gap-8 surface-tile p-8 rounded-lg">
|
|
122
122
|
{/* Info Banner */}
|
|
123
123
|
<div className="p-4 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-start gap-3">
|
|
124
124
|
<ShieldCheck className="w-4 h-4 text-blue-500 shrink-0 mt-0.5" />
|
|
@@ -34,7 +34,7 @@ function timeAgo(ts: number): string {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function SessionsManager() {
|
|
37
|
-
|
|
37
|
+
const { t } = useTranslation();
|
|
38
38
|
const [sessions, setSessions] = useState<Session[]>([]);
|
|
39
39
|
const [loading, setLoading] = useState(true);
|
|
40
40
|
const [error, setError] = useState("");
|
|
@@ -43,9 +43,9 @@ export function SessionsManager() {
|
|
|
43
43
|
|
|
44
44
|
useEffect(() => {
|
|
45
45
|
apiGet<{ sessions: Session[] }>("/api/auth/sessions")
|
|
46
|
-
.then((r) => {
|
|
47
|
-
setSessions(Array.isArray(r.sessions) ? r.sessions : []);
|
|
48
|
-
setError("");
|
|
46
|
+
.then((r) => {
|
|
47
|
+
setSessions(Array.isArray(r.sessions) ? r.sessions : []);
|
|
48
|
+
setError("");
|
|
49
49
|
})
|
|
50
50
|
.catch(() => setError("Failed to load sessions"))
|
|
51
51
|
.finally(() => setLoading(false));
|
|
@@ -57,11 +57,11 @@ export function SessionsManager() {
|
|
|
57
57
|
await apiDelete(`/api/auth/sessions/${id}`);
|
|
58
58
|
setSessions((p) => p.filter((s) => s.id !== id));
|
|
59
59
|
toast.success("Session revoked");
|
|
60
|
-
} catch {
|
|
61
|
-
setError("Failed to revoke session");
|
|
60
|
+
} catch {
|
|
61
|
+
setError("Failed to revoke session");
|
|
62
62
|
toast.error("Failed to revoke session");
|
|
63
|
-
} finally {
|
|
64
|
-
setRevokingId(null);
|
|
63
|
+
} finally {
|
|
64
|
+
setRevokingId(null);
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
|
|
@@ -71,11 +71,11 @@ export function SessionsManager() {
|
|
|
71
71
|
await apiDelete("/api/auth/sessions");
|
|
72
72
|
setSessions((p) => p.filter((s) => s.currentSession));
|
|
73
73
|
toast.success("All other sessions revoked");
|
|
74
|
-
} catch {
|
|
75
|
-
setError("Failed to revoke sessions");
|
|
74
|
+
} catch {
|
|
75
|
+
setError("Failed to revoke sessions");
|
|
76
76
|
toast.error("Failed to revoke sessions");
|
|
77
|
-
} finally {
|
|
78
|
-
setRevokingAll(false);
|
|
77
|
+
} finally {
|
|
78
|
+
setRevokingAll(false);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
@@ -98,7 +98,7 @@ export function SessionsManager() {
|
|
|
98
98
|
] : undefined}
|
|
99
99
|
/>
|
|
100
100
|
|
|
101
|
-
<div className="flex flex-col gap-8 surface-tile p-8">
|
|
101
|
+
<div className="flex flex-col gap-8 surface-tile p-8 rounded-lg">
|
|
102
102
|
{/* Security & Summary Row */}
|
|
103
103
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
104
104
|
<div className="lg:col-span-2 p-5 rounded-2xl bg-amber-500/5 border border-amber-500/10 flex items-center gap-4">
|
|
@@ -159,8 +159,8 @@ export function SessionsManager() {
|
|
|
159
159
|
<div className="flex items-start justify-between gap-4 relative z-10">
|
|
160
160
|
<div className="flex items-center gap-3">
|
|
161
161
|
<div className={`kyro-btn-primary p-2.5 rounded-xl transition-colors shadow-sm ${s.currentSession ? "" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] border border-[var(--kyro-border)]"}`}>
|
|
162
|
-
{s.deviceInfo?.platform?.toLowerCase().includes("android") || s.deviceInfo?.platform?.toLowerCase().includes("ios")
|
|
163
|
-
? <Smartphone className="w-4 h-4" />
|
|
162
|
+
{s.deviceInfo?.platform?.toLowerCase().includes("android") || s.deviceInfo?.platform?.toLowerCase().includes("ios")
|
|
163
|
+
? <Smartphone className="w-4 h-4" />
|
|
164
164
|
: <Laptop className="w-4 h-4" />}
|
|
165
165
|
</div>
|
|
166
166
|
<div>
|