@arch-cadre/panel 1.0.7 → 1.0.10
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/ui/activity-log/pages/log-list.cjs +2 -2
- package/dist/ui/components/profile/components.cjs +2 -2
- package/dist/ui/components/profile/components.d.ts +1 -1
- package/dist/ui/components/profile/components.mjs +1 -1
- package/dist/ui/rbac/pages/rbac-admin.cjs +14 -14
- package/dist/ui/session-manager/components/sessions-list.cjs +5 -5
- package/dist/ui/session-manager/components/sessions-list.mjs +1 -1
- package/dist/ui/session-manager/pages/sessions-page.cjs +3 -3
- package/dist/ui/session-manager/pages/sessions-page.mjs +1 -1
- package/package.json +7 -6
- package/src/actions/actions.ts +17 -0
- package/src/actions/activity-log/index.ts +17 -0
- package/src/actions/index.ts +2 -0
- package/src/actions/manager.ts +168 -0
- package/src/actions/profile.ts +173 -0
- package/src/actions/rbac/index.ts +131 -0
- package/src/actions/session-manager/index.ts +87 -0
- package/src/actions/settings.ts +34 -0
- package/src/index.ts +135 -0
- package/src/intl.d.ts +9 -0
- package/src/navigation.ts +57 -0
- package/src/routes.ts +107 -0
- package/src/schema/activity-log.ts +16 -0
- package/src/schema.ts +1 -0
- package/src/types.ts +18 -0
- package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
- package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
- package/src/ui/activity-log/pages/log-list.tsx +91 -0
- package/src/ui/components/app-content.tsx +51 -0
- package/src/ui/components/app-header.tsx +65 -0
- package/src/ui/components/app-sidebar.tsx +249 -0
- package/src/ui/components/app-user.tsx +126 -0
- package/src/ui/components/breadcrumb-slot.tsx +52 -0
- package/src/ui/components/manager/module-card.tsx +327 -0
- package/src/ui/components/manager/module-list.tsx +59 -0
- package/src/ui/components/manager/module-upload.tsx +84 -0
- package/src/ui/components/profile/components.tsx +311 -0
- package/src/ui/components/profile/link.tsx +36 -0
- package/src/ui/components/profile/page.tsx +45 -0
- package/src/ui/components/sidebar-slot.tsx +47 -0
- package/src/ui/dashboard/page.tsx +17 -0
- package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
- package/src/ui/error.tsx +82 -0
- package/src/ui/layout.tsx +54 -0
- package/src/ui/modules/docs/page.tsx +105 -0
- package/src/ui/modules/page.tsx +30 -0
- package/src/ui/page.tsx +15 -0
- package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
- package/src/ui/router.tsx +69 -0
- package/src/ui/session-manager/components/sessions-list.tsx +303 -0
- package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
- package/src/ui/settings/page.tsx +73 -0
- package/src/ui/settings-page.tsx +97 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
2
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "@arch-cadre/ui/components/card";
|
|
10
|
+
import {
|
|
11
|
+
Tabs,
|
|
12
|
+
TabsContent,
|
|
13
|
+
TabsList,
|
|
14
|
+
TabsTrigger,
|
|
15
|
+
} from "@arch-cadre/ui/components/tabs";
|
|
16
|
+
import { Icon } from "@iconify/react";
|
|
17
|
+
import Link from "next/link";
|
|
18
|
+
import { notFound } from "next/navigation";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { getModuleDocumentation } from "../../../actions/manager";
|
|
21
|
+
|
|
22
|
+
export default async function ModuleDocsPage({
|
|
23
|
+
params,
|
|
24
|
+
}: {
|
|
25
|
+
params: { slug: string };
|
|
26
|
+
}) {
|
|
27
|
+
const { slug: moduleId } = await params;
|
|
28
|
+
if (!moduleId) return notFound();
|
|
29
|
+
|
|
30
|
+
const { t } = await getTranslation();
|
|
31
|
+
|
|
32
|
+
const docs = await getModuleDocumentation(moduleId);
|
|
33
|
+
if (!docs) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-col items-center justify-center py-20 space-y-4">
|
|
36
|
+
<Icon
|
|
37
|
+
icon="solar:document-text-broken"
|
|
38
|
+
className="size-16 text-muted-foreground"
|
|
39
|
+
/>
|
|
40
|
+
<h2 className="text-xl font-bold">{t("Not Found")}</h2>
|
|
41
|
+
<p className="text-muted-foreground text-center max-w-md">
|
|
42
|
+
{t("Not found documentation for path", { path: "docs/" })}
|
|
43
|
+
</p>
|
|
44
|
+
<Button asChild variant="outline">
|
|
45
|
+
<Link href="/module/module-manager">{t("back")}</Link>
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-6">
|
|
53
|
+
<div className="flex items-center justify-between">
|
|
54
|
+
<div className="space-y-1">
|
|
55
|
+
<h2 className="text-3xl font-black tracking-tight">
|
|
56
|
+
{t("docs.title")}
|
|
57
|
+
</h2>
|
|
58
|
+
<p className="text-muted-foreground text-sm">
|
|
59
|
+
{t("docs.browsing")}{" "}
|
|
60
|
+
<span className="font-bold text-primary">{moduleId}</span>
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
<Button asChild variant="outline" size="sm">
|
|
64
|
+
<Link
|
|
65
|
+
href="/module/module-manager"
|
|
66
|
+
className="flex items-center gap-2"
|
|
67
|
+
>
|
|
68
|
+
<Icon icon="solar:arrow-left-broken" className="size-4" />
|
|
69
|
+
{t("docs.back")}
|
|
70
|
+
</Link>
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<Tabs defaultValue={docs[0].filename} className="w-full">
|
|
75
|
+
<TabsList className="bg-muted/50 mb-8">
|
|
76
|
+
{docs.map((doc) => (
|
|
77
|
+
<TabsTrigger key={doc.filename} value={doc.filename}>
|
|
78
|
+
{doc.title}
|
|
79
|
+
</TabsTrigger>
|
|
80
|
+
))}
|
|
81
|
+
</TabsList>
|
|
82
|
+
|
|
83
|
+
{docs.map((doc) => (
|
|
84
|
+
<TabsContent key={doc.filename} value={doc.filename} className="mt-0">
|
|
85
|
+
<Card className="bg-card shadow-xl border-primary/5">
|
|
86
|
+
<CardHeader className="border-b bg-muted/10">
|
|
87
|
+
<CardTitle className="text-xl font-bold">{doc.title}</CardTitle>
|
|
88
|
+
<CardDescription className="font-mono text-[10px]">
|
|
89
|
+
{t("docs.file")} {doc.filename}
|
|
90
|
+
</CardDescription>
|
|
91
|
+
</CardHeader>
|
|
92
|
+
<CardContent className="pt-8">
|
|
93
|
+
<div className="prose prose-slate dark:prose-invert max-w-none">
|
|
94
|
+
<pre className="whitespace-pre-wrap font-sans text-base leading-relaxed bg-transparent border-none p-0 text-foreground">
|
|
95
|
+
{doc.content}
|
|
96
|
+
</pre>
|
|
97
|
+
</div>
|
|
98
|
+
</CardContent>
|
|
99
|
+
</Card>
|
|
100
|
+
</TabsContent>
|
|
101
|
+
))}
|
|
102
|
+
</Tabs>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
2
|
+
|
|
3
|
+
import { Icon } from "@iconify/react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { getModulesAction } from "../../actions/manager";
|
|
6
|
+
import { ModuleMarketplaceList } from "../components/manager/module-list";
|
|
7
|
+
import { ModuleUpload } from "../components/manager/module-upload";
|
|
8
|
+
|
|
9
|
+
export default async function ModuleAdminPage() {
|
|
10
|
+
const modules = await getModulesAction();
|
|
11
|
+
const { t } = await getTranslation();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-6">
|
|
15
|
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
16
|
+
<div>
|
|
17
|
+
<h1 className="text-3xl font-black tracking-tight flex items-center gap-2">
|
|
18
|
+
{t("Modules Marketplace")}
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="text-muted-foreground">
|
|
21
|
+
{t("Manage your installed modules")}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
<ModuleUpload />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<ModuleMarketplaceList initialModules={modules} />
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/ui/page.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getCurrentSession } from "@arch-cadre/core/server";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
export default async function Page() {
|
|
6
|
+
const { session, user } = await getCurrentSession();
|
|
7
|
+
|
|
8
|
+
// Redirect unauthenticated users to signin
|
|
9
|
+
if (!session || !user) {
|
|
10
|
+
redirect("/signin");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Redirect authenticated users to dashboard
|
|
14
|
+
redirect("/kryo/dashboard");
|
|
15
|
+
}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <explanation> */
|
|
2
|
+
"use client";
|
|
3
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
4
|
+
import { Icon, toast } from "@arch-cadre/ui";
|
|
5
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from "@arch-cadre/ui/components/card";
|
|
13
|
+
import {
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogDescription,
|
|
17
|
+
DialogHeader,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
DialogTrigger,
|
|
20
|
+
} from "@arch-cadre/ui/components/dialog";
|
|
21
|
+
import { Input } from "@arch-cadre/ui/components/input";
|
|
22
|
+
import {
|
|
23
|
+
Table,
|
|
24
|
+
TableBody,
|
|
25
|
+
TableCell,
|
|
26
|
+
TableRow,
|
|
27
|
+
} from "@arch-cadre/ui/components/table";
|
|
28
|
+
import * as React from "react";
|
|
29
|
+
import { useCallback, useEffect, useState } from "react";
|
|
30
|
+
import {
|
|
31
|
+
assignPermissionToRole,
|
|
32
|
+
assignPermissionToUser,
|
|
33
|
+
assignRoleToUser,
|
|
34
|
+
createPermission,
|
|
35
|
+
createRole,
|
|
36
|
+
deletePermission,
|
|
37
|
+
deleteRole,
|
|
38
|
+
getPermissions,
|
|
39
|
+
getRolePermissions,
|
|
40
|
+
getRoles,
|
|
41
|
+
getUserRbacData,
|
|
42
|
+
getUsers,
|
|
43
|
+
revokePermissionFromRole,
|
|
44
|
+
revokePermissionFromUser,
|
|
45
|
+
revokeRoleFromUser,
|
|
46
|
+
} from "../../../actions/rbac/index";
|
|
47
|
+
|
|
48
|
+
export default function RbacAdminPage() {
|
|
49
|
+
const [roles, setRoles] = useState<any[]>([]);
|
|
50
|
+
const [permissions, setPermissions] = useState<any[]>([]);
|
|
51
|
+
const [users, setUsers] = useState<any[]>([]);
|
|
52
|
+
const [loading, setLoading] = useState(true);
|
|
53
|
+
|
|
54
|
+
const { t } = useTranslation();
|
|
55
|
+
// Form states
|
|
56
|
+
const [newRoleName, setNewRoleName] = useState("");
|
|
57
|
+
const [newPermissionName, setNewPermissionName] = useState("");
|
|
58
|
+
|
|
59
|
+
// Role-Permission mapping state
|
|
60
|
+
const [selectedRole, setSelectedRole] = useState<any | null>(null);
|
|
61
|
+
const [rolePermissions, setRolePermissions] = useState<any[]>([]);
|
|
62
|
+
|
|
63
|
+
// User-Role/Permission mapping state
|
|
64
|
+
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
|
65
|
+
const [userRbacData, setUserRbacData] = useState<{
|
|
66
|
+
roles: any[];
|
|
67
|
+
directPermissions: any[];
|
|
68
|
+
effectivePermissions: any[];
|
|
69
|
+
}>({
|
|
70
|
+
roles: [],
|
|
71
|
+
directPermissions: [],
|
|
72
|
+
effectivePermissions: [],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const loadData = useCallback(async () => {
|
|
76
|
+
setLoading(true);
|
|
77
|
+
try {
|
|
78
|
+
const [r, p, u] = await Promise.all([
|
|
79
|
+
getRoles(),
|
|
80
|
+
getPermissions(),
|
|
81
|
+
getUsers(),
|
|
82
|
+
]);
|
|
83
|
+
setRoles(r);
|
|
84
|
+
setPermissions(p);
|
|
85
|
+
setUsers(u);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error("[RBAC] Failed to load initial data:", e);
|
|
88
|
+
toast.error(t("Failed to load RBAC data."));
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const loadRolePermissions = useCallback(async (roleId: string) => {
|
|
95
|
+
try {
|
|
96
|
+
const p = await getRolePermissions(roleId);
|
|
97
|
+
setRolePermissions(p);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(`[RBAC] Failed to load permissions for role ${roleId}:`, e);
|
|
100
|
+
toast.error(t("Failed to load permissions for role."));
|
|
101
|
+
}
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const loadUserRbacData = useCallback(async (userId: string) => {
|
|
105
|
+
try {
|
|
106
|
+
const data = await getUserRbacData(userId);
|
|
107
|
+
setUserRbacData(data);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`[RBAC] Failed to load RBAC data for user ${userId}:`, e);
|
|
110
|
+
toast.error(t("Failed to load user RBAC data."));
|
|
111
|
+
}
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
loadData();
|
|
116
|
+
}, [loadData]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (selectedRole?.id) {
|
|
120
|
+
loadRolePermissions(selectedRole.id);
|
|
121
|
+
}
|
|
122
|
+
}, [selectedRole?.id, loadRolePermissions]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (selectedUser?.id) {
|
|
126
|
+
loadUserRbacData(selectedUser.id);
|
|
127
|
+
}
|
|
128
|
+
}, [selectedUser?.id, loadUserRbacData]);
|
|
129
|
+
|
|
130
|
+
const handleCreateRole = useCallback(async () => {
|
|
131
|
+
if (!newRoleName) return;
|
|
132
|
+
try {
|
|
133
|
+
await createRole(newRoleName);
|
|
134
|
+
setNewRoleName("");
|
|
135
|
+
toast.success(t("Role created."));
|
|
136
|
+
loadData();
|
|
137
|
+
} catch (_e) {
|
|
138
|
+
toast.error(t("Failed to create role."));
|
|
139
|
+
}
|
|
140
|
+
}, [newRoleName, loadData]);
|
|
141
|
+
|
|
142
|
+
const handleCreatePermission = useCallback(async () => {
|
|
143
|
+
if (!newPermissionName) return;
|
|
144
|
+
try {
|
|
145
|
+
await createPermission(newPermissionName);
|
|
146
|
+
setNewPermissionName("");
|
|
147
|
+
toast.success(t("Permission created."));
|
|
148
|
+
loadData();
|
|
149
|
+
} catch (_e) {
|
|
150
|
+
toast.error(t("Failed to create permission."));
|
|
151
|
+
}
|
|
152
|
+
}, [newPermissionName, loadData]);
|
|
153
|
+
|
|
154
|
+
const handleDeleteRole = useCallback(
|
|
155
|
+
async (id: string) => {
|
|
156
|
+
try {
|
|
157
|
+
await deleteRole(id);
|
|
158
|
+
toast.success(t("Role deleted."));
|
|
159
|
+
if (selectedRole?.id === id) setSelectedRole(null);
|
|
160
|
+
loadData();
|
|
161
|
+
} catch (_e) {
|
|
162
|
+
toast.error(t("Failed to delete role."));
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
[selectedRole, loadData],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const handleDeletePermission = useCallback(
|
|
169
|
+
async (id: string) => {
|
|
170
|
+
try {
|
|
171
|
+
await deletePermission(id);
|
|
172
|
+
toast.success(t("Permission deleted."));
|
|
173
|
+
loadData();
|
|
174
|
+
} catch (_e) {
|
|
175
|
+
toast.error(t("Failed to delete permission."));
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
[loadData],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const togglePermissionForRole = useCallback(
|
|
182
|
+
async (permId: string, hasIt: boolean) => {
|
|
183
|
+
if (!selectedRole) return;
|
|
184
|
+
try {
|
|
185
|
+
if (hasIt) {
|
|
186
|
+
await revokePermissionFromRole(selectedRole.id, permId);
|
|
187
|
+
} else {
|
|
188
|
+
await assignPermissionToRole(selectedRole.id, permId);
|
|
189
|
+
}
|
|
190
|
+
loadRolePermissions(selectedRole.id);
|
|
191
|
+
} catch (_e) {
|
|
192
|
+
toast.error(t("Action failed."));
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
[selectedRole, loadRolePermissions],
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const toggleRoleForUser = useCallback(
|
|
199
|
+
async (roleId: string, hasIt: boolean) => {
|
|
200
|
+
if (!selectedUser) return;
|
|
201
|
+
try {
|
|
202
|
+
if (hasIt) {
|
|
203
|
+
await revokeRoleFromUser(selectedUser.id, roleId);
|
|
204
|
+
} else {
|
|
205
|
+
await assignRoleToUser(selectedUser.id, roleId);
|
|
206
|
+
}
|
|
207
|
+
loadUserRbacData(selectedUser.id);
|
|
208
|
+
} catch (_e) {
|
|
209
|
+
toast.error(t("Action failed."));
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
[selectedUser, loadUserRbacData],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const togglePermissionForUser = useCallback(
|
|
216
|
+
async (permId: string, hasIt: boolean) => {
|
|
217
|
+
if (!selectedUser) return;
|
|
218
|
+
try {
|
|
219
|
+
if (hasIt) {
|
|
220
|
+
await revokePermissionFromUser(selectedUser.id, permId);
|
|
221
|
+
} else {
|
|
222
|
+
await assignPermissionToUser(selectedUser.id, permId);
|
|
223
|
+
}
|
|
224
|
+
loadUserRbacData(selectedUser.id);
|
|
225
|
+
} catch (_e) {
|
|
226
|
+
toast.error(t("Action failed."));
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
[selectedUser, loadUserRbacData],
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className="space-y-6">
|
|
234
|
+
<div className="space-y-1">
|
|
235
|
+
<h1 className="text-3xl font-black tracking-tight flex items-center gap-2">
|
|
236
|
+
{/* <Icon
|
|
237
|
+
icon="solar:shield-keyhole-bold-duotone"
|
|
238
|
+
className="size-10 text-primary"
|
|
239
|
+
/> */}
|
|
240
|
+
{t("Role-Based Access Control Manager")}
|
|
241
|
+
</h1>
|
|
242
|
+
<p className="text-muted-foreground">
|
|
243
|
+
{t("Manage roles, permissions, and their relationships.")}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
248
|
+
{/* Roles Section */}
|
|
249
|
+
<Card className="border-primary/5 shadow-sm">
|
|
250
|
+
<CardHeader>
|
|
251
|
+
<CardTitle>{t("Roles")}</CardTitle>
|
|
252
|
+
<CardDescription>{t("System roles.")}</CardDescription>
|
|
253
|
+
</CardHeader>
|
|
254
|
+
<CardContent className="space-y-4">
|
|
255
|
+
<div className="flex gap-2">
|
|
256
|
+
<Input
|
|
257
|
+
placeholder={t("New role...")}
|
|
258
|
+
value={newRoleName}
|
|
259
|
+
onChange={(e) => setNewRoleName(e.target.value)}
|
|
260
|
+
/>
|
|
261
|
+
<Button onClick={handleCreateRole}>{t("Add")}</Button>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="border rounded-md max-h-[300px] overflow-auto">
|
|
264
|
+
<Table>
|
|
265
|
+
<TableBody>
|
|
266
|
+
{roles.map((role) => (
|
|
267
|
+
<TableRow
|
|
268
|
+
key={role.id}
|
|
269
|
+
className={
|
|
270
|
+
selectedRole?.id === role.id ? "bg-primary/5" : ""
|
|
271
|
+
}
|
|
272
|
+
>
|
|
273
|
+
<TableCell
|
|
274
|
+
className="font-medium cursor-pointer"
|
|
275
|
+
onClick={() => setSelectedRole(role)}
|
|
276
|
+
>
|
|
277
|
+
{role.name}
|
|
278
|
+
</TableCell>
|
|
279
|
+
<TableCell className="text-right">
|
|
280
|
+
<Button
|
|
281
|
+
variant="ghost"
|
|
282
|
+
size="icon"
|
|
283
|
+
onClick={() => handleDeleteRole(role.id)}
|
|
284
|
+
>
|
|
285
|
+
<Icon
|
|
286
|
+
icon="solar:trash-bin-trash-bold"
|
|
287
|
+
className="size-4 text-destructive"
|
|
288
|
+
/>
|
|
289
|
+
</Button>
|
|
290
|
+
</TableCell>
|
|
291
|
+
</TableRow>
|
|
292
|
+
))}
|
|
293
|
+
</TableBody>
|
|
294
|
+
</Table>
|
|
295
|
+
</div>
|
|
296
|
+
</CardContent>
|
|
297
|
+
</Card>
|
|
298
|
+
|
|
299
|
+
{/* Permissions Section */}
|
|
300
|
+
<Card className="border-primary/5 shadow-sm">
|
|
301
|
+
<CardHeader>
|
|
302
|
+
<CardTitle>{t("Permissions")}</CardTitle>
|
|
303
|
+
<CardDescription>{t("System permissions.")}</CardDescription>
|
|
304
|
+
</CardHeader>
|
|
305
|
+
<CardContent className="space-y-4">
|
|
306
|
+
<div className="flex gap-2">
|
|
307
|
+
<Input
|
|
308
|
+
placeholder={t("New permission...")}
|
|
309
|
+
value={newPermissionName}
|
|
310
|
+
onChange={(e) => setNewPermissionName(e.target.value)}
|
|
311
|
+
/>
|
|
312
|
+
<Button onClick={handleCreatePermission}>{t("Add")}</Button>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="border rounded-md max-h-[300px] overflow-auto">
|
|
315
|
+
<Table>
|
|
316
|
+
<TableBody>
|
|
317
|
+
{permissions.map((perm) => (
|
|
318
|
+
<TableRow key={perm.id}>
|
|
319
|
+
<TableCell className="font-medium">{perm.name}</TableCell>
|
|
320
|
+
<TableCell className="text-right">
|
|
321
|
+
<Button
|
|
322
|
+
variant="ghost"
|
|
323
|
+
size="icon"
|
|
324
|
+
onClick={() => handleDeletePermission(perm.id)}
|
|
325
|
+
>
|
|
326
|
+
<Icon
|
|
327
|
+
icon="solar:trash-bin-trash-bold"
|
|
328
|
+
className="size-4 text-destructive"
|
|
329
|
+
/>
|
|
330
|
+
</Button>
|
|
331
|
+
</TableCell>
|
|
332
|
+
</TableRow>
|
|
333
|
+
))}
|
|
334
|
+
</TableBody>
|
|
335
|
+
</Table>
|
|
336
|
+
</div>
|
|
337
|
+
</CardContent>
|
|
338
|
+
</Card>
|
|
339
|
+
|
|
340
|
+
{/* Users Section */}
|
|
341
|
+
<Card className="border-primary/5 shadow-sm">
|
|
342
|
+
<CardHeader>
|
|
343
|
+
<CardTitle>{t("Users")}</CardTitle>
|
|
344
|
+
<CardDescription>{t("Manage user roles.")}</CardDescription>
|
|
345
|
+
</CardHeader>
|
|
346
|
+
<CardContent className="space-y-4">
|
|
347
|
+
<div className="border rounded-md max-h-[358px] overflow-auto">
|
|
348
|
+
<Table>
|
|
349
|
+
<TableBody>
|
|
350
|
+
{users.map((user) => (
|
|
351
|
+
<TableRow
|
|
352
|
+
key={user.id}
|
|
353
|
+
className={
|
|
354
|
+
selectedUser?.id === user.id ? "bg-primary/5" : ""
|
|
355
|
+
}
|
|
356
|
+
>
|
|
357
|
+
<TableCell
|
|
358
|
+
className="font-medium cursor-pointer"
|
|
359
|
+
onClick={() => setSelectedUser(user)}
|
|
360
|
+
>
|
|
361
|
+
<div>{user.name}</div>
|
|
362
|
+
<div className="text-[10px] text-muted-foreground">
|
|
363
|
+
{user.email}
|
|
364
|
+
</div>
|
|
365
|
+
</TableCell>
|
|
366
|
+
</TableRow>
|
|
367
|
+
))}
|
|
368
|
+
</TableBody>
|
|
369
|
+
</Table>
|
|
370
|
+
</div>
|
|
371
|
+
</CardContent>
|
|
372
|
+
</Card>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{/* Role-Permission Assignment */}
|
|
376
|
+
<Dialog
|
|
377
|
+
open={!!selectedRole}
|
|
378
|
+
onOpenChange={(open) => !open && setSelectedRole(null)}
|
|
379
|
+
>
|
|
380
|
+
<DialogContent>
|
|
381
|
+
<DialogHeader>
|
|
382
|
+
<DialogTitle>
|
|
383
|
+
{t("Edit Role: {roleName}", { roleName: selectedRole?.name })}
|
|
384
|
+
</DialogTitle>
|
|
385
|
+
<DialogDescription>
|
|
386
|
+
{t("Manage permissions for role {roleName}", {
|
|
387
|
+
roleName: selectedRole?.name,
|
|
388
|
+
})}
|
|
389
|
+
</DialogDescription>
|
|
390
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
391
|
+
{permissions.map((perm) => {
|
|
392
|
+
const hasIt = rolePermissions.some((rp) => rp.id === perm.id);
|
|
393
|
+
return (
|
|
394
|
+
<Button
|
|
395
|
+
key={perm.id}
|
|
396
|
+
variant={hasIt ? "default" : "outline"}
|
|
397
|
+
size="sm"
|
|
398
|
+
className="justify-start gap-2 h-auto py-2 px-3 overflow-hidden"
|
|
399
|
+
onClick={() => togglePermissionForRole(perm.id, hasIt)}
|
|
400
|
+
>
|
|
401
|
+
<Icon
|
|
402
|
+
icon={
|
|
403
|
+
hasIt
|
|
404
|
+
? "solar:check-circle-bold"
|
|
405
|
+
: "solar:circle-linear"
|
|
406
|
+
}
|
|
407
|
+
className={
|
|
408
|
+
hasIt
|
|
409
|
+
? "text-primary-foreground"
|
|
410
|
+
: "text-muted-foreground"
|
|
411
|
+
}
|
|
412
|
+
/>
|
|
413
|
+
<span className="truncate text-xs">{perm.name}</span>
|
|
414
|
+
</Button>
|
|
415
|
+
);
|
|
416
|
+
})}
|
|
417
|
+
</div>
|
|
418
|
+
</DialogHeader>
|
|
419
|
+
</DialogContent>
|
|
420
|
+
</Dialog>
|
|
421
|
+
|
|
422
|
+
{/* User-Role Assignment */}
|
|
423
|
+
<Dialog
|
|
424
|
+
open={!!selectedUser}
|
|
425
|
+
onOpenChange={(open) => !open && setSelectedUser(null)}
|
|
426
|
+
>
|
|
427
|
+
<DialogContent>
|
|
428
|
+
<DialogHeader>
|
|
429
|
+
<DialogTitle>
|
|
430
|
+
{t("Management for {selectedUser}", {
|
|
431
|
+
selectedUser: selectedUser?.name,
|
|
432
|
+
})}
|
|
433
|
+
</DialogTitle>
|
|
434
|
+
<DialogDescription>
|
|
435
|
+
{t("Manage roles and permissions for {selectedUser}", {
|
|
436
|
+
selectedUser: selectedUser?.name,
|
|
437
|
+
})}
|
|
438
|
+
</DialogDescription>
|
|
439
|
+
<div className="space-y-6">
|
|
440
|
+
<div>
|
|
441
|
+
<p className="text-[10px] font-black uppercase text-muted-foreground tracking-widest mb-3">
|
|
442
|
+
{t("User Roles")}
|
|
443
|
+
</p>
|
|
444
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
445
|
+
{roles.map((role) => {
|
|
446
|
+
const hasIt = userRbacData.roles.some(
|
|
447
|
+
(ur) => ur.id === role.id,
|
|
448
|
+
);
|
|
449
|
+
return (
|
|
450
|
+
<Button
|
|
451
|
+
key={role.id}
|
|
452
|
+
variant={hasIt ? "default" : "outline"}
|
|
453
|
+
size="sm"
|
|
454
|
+
className="justify-start gap-2 h-auto py-2 px-3 overflow-hidden"
|
|
455
|
+
onClick={() => toggleRoleForUser(role.id, hasIt)}
|
|
456
|
+
>
|
|
457
|
+
<Icon
|
|
458
|
+
icon={
|
|
459
|
+
hasIt
|
|
460
|
+
? "solar:check-circle-bold"
|
|
461
|
+
: "solar:circle-linear"
|
|
462
|
+
}
|
|
463
|
+
className={
|
|
464
|
+
hasIt
|
|
465
|
+
? "text-primary-foreground"
|
|
466
|
+
: "text-muted-foreground"
|
|
467
|
+
}
|
|
468
|
+
/>
|
|
469
|
+
<span className="truncate text-xs">{role.name}</span>
|
|
470
|
+
</Button>
|
|
471
|
+
);
|
|
472
|
+
})}
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<div>
|
|
477
|
+
<p className="text-[10px] font-black uppercase text-muted-foreground tracking-widest mb-3">
|
|
478
|
+
{t("Direct Permissions (User-specific)")}
|
|
479
|
+
</p>
|
|
480
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
481
|
+
{permissions.map((perm) => {
|
|
482
|
+
// Check if permission is directly assigned to user
|
|
483
|
+
// We need directPermissions from the action
|
|
484
|
+
const isDirect = userRbacData.directPermissions.some(
|
|
485
|
+
(up) => up.id === perm.id,
|
|
486
|
+
);
|
|
487
|
+
return (
|
|
488
|
+
<Button
|
|
489
|
+
key={perm.id}
|
|
490
|
+
variant={isDirect ? "default" : "outline"}
|
|
491
|
+
size="sm"
|
|
492
|
+
className="justify-start gap-2 h-auto py-2 px-3 overflow-hidden"
|
|
493
|
+
onClick={() =>
|
|
494
|
+
togglePermissionForUser(perm.id, isDirect)
|
|
495
|
+
}
|
|
496
|
+
>
|
|
497
|
+
<Icon
|
|
498
|
+
icon={
|
|
499
|
+
isDirect
|
|
500
|
+
? "solar:check-circle-bold"
|
|
501
|
+
: "solar:circle-linear"
|
|
502
|
+
}
|
|
503
|
+
className={
|
|
504
|
+
isDirect
|
|
505
|
+
? "text-primary-foreground"
|
|
506
|
+
: "text-muted-foreground"
|
|
507
|
+
}
|
|
508
|
+
/>
|
|
509
|
+
<span className="truncate text-xs">{perm.name}</span>
|
|
510
|
+
</Button>
|
|
511
|
+
);
|
|
512
|
+
})}
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<div className="pt-4 border-t">
|
|
517
|
+
<p className="text-[10px] font-black uppercase text-muted-foreground tracking-widest mb-2">
|
|
518
|
+
{t("Effective Permissions (inherited + direct)")}
|
|
519
|
+
</p>
|
|
520
|
+
<div className="flex flex-wrap gap-1">
|
|
521
|
+
{userRbacData.effectivePermissions.map((p) => (
|
|
522
|
+
<div
|
|
523
|
+
key={p.id}
|
|
524
|
+
className="bg-muted px-2 py-0.5 rounded text-[9px] font-mono"
|
|
525
|
+
>
|
|
526
|
+
{p.name}
|
|
527
|
+
</div>
|
|
528
|
+
))}
|
|
529
|
+
{userRbacData.effectivePermissions.length === 0 && (
|
|
530
|
+
<span className="text-[10px] italic">
|
|
531
|
+
{t("No permissions")}
|
|
532
|
+
</span>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</DialogHeader>
|
|
538
|
+
</DialogContent>
|
|
539
|
+
</Dialog>
|
|
540
|
+
|
|
541
|
+
{loading && (
|
|
542
|
+
<div className="fixed inset-0 bg-background/50 backdrop-blur-sm flex items-center justify-center z-50">
|
|
543
|
+
<Icon
|
|
544
|
+
icon="svg-spinners:18-dots-indicator"
|
|
545
|
+
className="size-12 text-primary"
|
|
546
|
+
/>
|
|
547
|
+
</div>
|
|
548
|
+
)}
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
}
|