hazo_auth 0.1.2 → 1.0.0
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/hazo_auth_config.example.ini +75 -0
- package/instrumentation.ts +1 -1
- package/next.config.mjs +1 -1
- package/package.json +4 -1
- package/src/app/api/{auth → hazo_auth/auth}/upload_profile_picture/route.ts +2 -2
- package/src/app/api/{auth → hazo_auth}/change_password/route.ts +23 -0
- package/src/app/api/hazo_auth/get_auth/route.ts +89 -0
- package/src/app/api/hazo_auth/invalidate_cache/route.ts +139 -0
- package/src/app/api/{auth → hazo_auth}/library_photos/route.ts +3 -0
- package/src/app/api/{auth → hazo_auth}/logout/route.ts +27 -0
- package/src/app/api/hazo_auth/upload_profile_picture/route.ts +268 -0
- package/src/app/api/hazo_auth/user_management/permissions/route.ts +367 -0
- package/src/app/api/hazo_auth/user_management/roles/route.ts +442 -0
- package/src/app/api/hazo_auth/user_management/users/roles/route.ts +367 -0
- package/src/app/api/hazo_auth/user_management/users/route.ts +239 -0
- package/src/app/api/{auth → hazo_auth}/validate_reset_token/route.ts +3 -0
- package/src/app/api/{auth → hazo_auth}/verify_email/route.ts +3 -0
- package/src/app/globals.css +1 -1
- package/src/app/hazo_auth/user_management/page.tsx +14 -0
- package/src/app/hazo_auth/user_management/user_management_page_client.tsx +16 -0
- package/src/app/hazo_connect/api/sqlite/data/route.ts +7 -1
- package/src/app/hazo_connect/api/sqlite/schema/route.ts +14 -4
- package/src/app/hazo_connect/api/sqlite/tables/route.ts +14 -4
- package/src/app/hazo_connect/sqlite_admin/sqlite-admin-client.tsx +40 -3
- package/src/app/layout.tsx +1 -1
- package/src/app/page.tsx +4 -4
- package/src/components/layouts/email_verification/hooks/use_email_verification.ts +4 -4
- package/src/components/layouts/email_verification/index.tsx +1 -1
- package/src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts +1 -1
- package/src/components/layouts/login/hooks/use_login_form.ts +2 -2
- package/src/components/layouts/my_settings/components/profile_picture_dialog.tsx +1 -1
- package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +35 -6
- package/src/components/layouts/my_settings/hooks/use_my_settings.ts +5 -5
- package/src/components/layouts/my_settings/index.tsx +1 -1
- package/src/components/layouts/register/hooks/use_register_form.ts +1 -1
- package/src/components/layouts/reset_password/hooks/use_reset_password_form.ts +3 -3
- package/src/components/layouts/reset_password/index.tsx +2 -2
- package/src/components/layouts/shared/components/logout_button.tsx +1 -1
- package/src/components/layouts/shared/components/profile_pic_menu.tsx +321 -0
- package/src/components/layouts/shared/components/profile_pic_menu_wrapper.tsx +40 -0
- package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +22 -72
- package/src/components/layouts/shared/components/unauthorized_guard.tsx +1 -1
- package/src/components/layouts/shared/hooks/use_auth_status.ts +1 -1
- package/src/components/layouts/shared/hooks/use_hazo_auth.ts +158 -0
- package/src/components/layouts/user_management/components/roles_matrix.tsx +607 -0
- package/src/components/layouts/user_management/index.tsx +1295 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/lib/auth/auth_cache.ts +220 -0
- package/src/lib/auth/auth_rate_limiter.ts +121 -0
- package/src/lib/auth/auth_types.ts +65 -0
- package/src/lib/auth/hazo_get_auth.server.ts +333 -0
- package/src/lib/auth_utility_config.server.ts +136 -0
- package/src/lib/hazo_connect_setup.server.ts +2 -3
- package/src/lib/my_settings_config.server.ts +1 -1
- package/src/lib/profile_pic_menu_config.server.ts +138 -0
- package/src/lib/reset_password_config.server.ts +5 -5
- package/src/lib/services/email_service.ts +2 -2
- package/src/lib/services/profile_picture_remove_service.ts +1 -1
- package/src/lib/services/token_service.ts +2 -2
- package/src/lib/user_management_config.server.ts +40 -0
- package/src/lib/utils.ts +1 -1
- package/src/middleware.ts +15 -13
- package/src/server/types/express.d.ts +1 -0
- package/src/stories/project_overview.stories.tsx +1 -1
- package/tailwind.config.ts +1 -1
- /package/src/app/api/{auth → hazo_auth}/forgot_password/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/login/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/me/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/profile_picture/[filename]/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/register/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/remove_profile_picture/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/resend_verification/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/reset_password/route.ts +0 -0
- /package/src/app/api/{auth → hazo_auth}/update_user/route.ts +0 -0
- /package/src/app/{forgot_password → hazo_auth/forgot_password}/forgot_password_page_client.tsx +0 -0
- /package/src/app/{forgot_password → hazo_auth/forgot_password}/page.tsx +0 -0
- /package/src/app/{login → hazo_auth/login}/login_page_client.tsx +0 -0
- /package/src/app/{login → hazo_auth/login}/page.tsx +0 -0
- /package/src/app/{my_settings → hazo_auth/my_settings}/my_settings_page_client.tsx +0 -0
- /package/src/app/{my_settings → hazo_auth/my_settings}/page.tsx +0 -0
- /package/src/app/{register → hazo_auth/register}/page.tsx +0 -0
- /package/src/app/{register → hazo_auth/register}/register_page_client.tsx +0 -0
- /package/src/app/{reset_password → hazo_auth/reset_password}/page.tsx +0 -0
- /package/src/app/{reset_password → hazo_auth/reset_password}/reset_password_page_client.tsx +0 -0
- /package/src/app/{verify_email → hazo_auth/verify_email}/page.tsx +0 -0
- /package/src/app/{verify_email → hazo_auth/verify_email}/verify_email_page_client.tsx +0 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
// file_description: User Management layout component with three tabs for managing users, roles, and permissions
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
|
|
5
|
+
// section: imports
|
|
6
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
8
|
+
import { use_hazo_auth } from "@/components/layouts/shared/hooks/use_hazo_auth";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
} from "@/components/ui/table";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
|
19
|
+
import {
|
|
20
|
+
AlertDialog,
|
|
21
|
+
AlertDialogAction,
|
|
22
|
+
AlertDialogCancel,
|
|
23
|
+
AlertDialogContent,
|
|
24
|
+
AlertDialogDescription,
|
|
25
|
+
AlertDialogFooter,
|
|
26
|
+
AlertDialogHeader,
|
|
27
|
+
AlertDialogTitle,
|
|
28
|
+
} from "@/components/ui/alert-dialog";
|
|
29
|
+
import {
|
|
30
|
+
Dialog,
|
|
31
|
+
DialogContent,
|
|
32
|
+
DialogDescription,
|
|
33
|
+
DialogFooter,
|
|
34
|
+
DialogHeader,
|
|
35
|
+
DialogTitle,
|
|
36
|
+
} from "@/components/ui/dialog";
|
|
37
|
+
import { Input } from "@/components/ui/input";
|
|
38
|
+
import { Label } from "@/components/ui/label";
|
|
39
|
+
import { RolesMatrix } from "./components/roles_matrix";
|
|
40
|
+
import { UserX, KeyRound, Edit, Trash2, Loader2, CircleCheck, CircleX, Plus, UserPlus } from "lucide-react";
|
|
41
|
+
import { toast } from "sonner";
|
|
42
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
43
|
+
|
|
44
|
+
// section: types
|
|
45
|
+
export type UserManagementLayoutProps = {
|
|
46
|
+
className?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type User = {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string | null;
|
|
52
|
+
email_address: string;
|
|
53
|
+
email_verified: boolean;
|
|
54
|
+
is_active: boolean;
|
|
55
|
+
last_logon: string | null;
|
|
56
|
+
created_at: string | null;
|
|
57
|
+
profile_picture_url: string | null;
|
|
58
|
+
profile_source: string | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type Permission = {
|
|
62
|
+
id: number;
|
|
63
|
+
permission_name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
source: "db" | "config";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// section: component
|
|
69
|
+
/**
|
|
70
|
+
* User Management layout component with three tabs
|
|
71
|
+
* Tab 1: Manage Users - data table with user details and actions
|
|
72
|
+
* Tab 2: Roles - roles-permissions matrix
|
|
73
|
+
* Tab 3: Permissions - manage permissions from DB and config
|
|
74
|
+
* @param props - Component props
|
|
75
|
+
* @returns User Management layout component
|
|
76
|
+
*/
|
|
77
|
+
export function UserManagementLayout({ className }: UserManagementLayoutProps) {
|
|
78
|
+
// Permission checks
|
|
79
|
+
const authResult = use_hazo_auth();
|
|
80
|
+
const hasUserManagementPermission = authResult.authenticated &&
|
|
81
|
+
authResult.permissions.includes("admin_user_management");
|
|
82
|
+
const hasRoleManagementPermission = authResult.authenticated &&
|
|
83
|
+
authResult.permissions.includes("admin_role_management");
|
|
84
|
+
const hasPermissionManagementPermission = authResult.authenticated &&
|
|
85
|
+
authResult.permissions.includes("admin_permission_management");
|
|
86
|
+
|
|
87
|
+
// Determine which tabs to show
|
|
88
|
+
const showUsersTab = hasUserManagementPermission;
|
|
89
|
+
const showRolesTab = hasRoleManagementPermission;
|
|
90
|
+
const showPermissionsTab = hasPermissionManagementPermission;
|
|
91
|
+
const hasAnyPermission = showUsersTab || showRolesTab || showPermissionsTab;
|
|
92
|
+
|
|
93
|
+
// Tab 1: Users state
|
|
94
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
95
|
+
const [usersLoading, setUsersLoading] = useState(true);
|
|
96
|
+
const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false);
|
|
97
|
+
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
|
|
98
|
+
const [userDetailDialogOpen, setUserDetailDialogOpen] = useState(false);
|
|
99
|
+
const [assignRolesDialogOpen, setAssignRolesDialogOpen] = useState(false);
|
|
100
|
+
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
101
|
+
const [usersActionLoading, setUsersActionLoading] = useState(false);
|
|
102
|
+
|
|
103
|
+
// Tab 3: Permissions state
|
|
104
|
+
const [permissions, setPermissions] = useState<Permission[]>([]);
|
|
105
|
+
const [permissionsLoading, setPermissionsLoading] = useState(true);
|
|
106
|
+
const [editPermissionDialogOpen, setEditPermissionDialogOpen] = useState(false);
|
|
107
|
+
const [addPermissionDialogOpen, setAddPermissionDialogOpen] = useState(false);
|
|
108
|
+
const [editingPermission, setEditingPermission] = useState<Permission | null>(null);
|
|
109
|
+
const [editDescription, setEditDescription] = useState("");
|
|
110
|
+
const [newPermissionName, setNewPermissionName] = useState("");
|
|
111
|
+
const [newPermissionDescription, setNewPermissionDescription] = useState("");
|
|
112
|
+
const [permissionsActionLoading, setPermissionsActionLoading] = useState(false);
|
|
113
|
+
const [migrateLoading, setMigrateLoading] = useState(false);
|
|
114
|
+
|
|
115
|
+
// Load users function (reusable)
|
|
116
|
+
const loadUsers = useCallback(async () => {
|
|
117
|
+
setUsersLoading(true);
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch("/api/hazo_auth/user_management/users");
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
|
|
122
|
+
if (data.success) {
|
|
123
|
+
setUsers(data.users);
|
|
124
|
+
} else {
|
|
125
|
+
toast.error("Failed to load users");
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
toast.error("Failed to load users");
|
|
129
|
+
} finally {
|
|
130
|
+
setUsersLoading(false);
|
|
131
|
+
}
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
// Load users (only if user has permission)
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!showUsersTab) {
|
|
137
|
+
setUsersLoading(false);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void loadUsers();
|
|
142
|
+
}, [showUsersTab, loadUsers]);
|
|
143
|
+
|
|
144
|
+
// Load permissions (only if user has permission)
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!showPermissionsTab) {
|
|
147
|
+
setPermissionsLoading(false);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const loadPermissions = async () => {
|
|
152
|
+
setPermissionsLoading(true);
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch("/api/hazo_auth/user_management/permissions");
|
|
155
|
+
const data = await response.json();
|
|
156
|
+
|
|
157
|
+
if (data.success) {
|
|
158
|
+
const db_perms: Permission[] = data.db_permissions.map((p: {
|
|
159
|
+
id: number;
|
|
160
|
+
permission_name: string;
|
|
161
|
+
description: string;
|
|
162
|
+
}) => ({
|
|
163
|
+
id: p.id,
|
|
164
|
+
permission_name: p.permission_name,
|
|
165
|
+
description: p.description,
|
|
166
|
+
source: "db" as const,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const config_perms: Permission[] = data.config_permissions.map((name: string) => ({
|
|
170
|
+
id: 0, // Temporary ID for config permissions
|
|
171
|
+
permission_name: name,
|
|
172
|
+
description: "",
|
|
173
|
+
source: "config" as const,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
setPermissions([...db_perms, ...config_perms]);
|
|
177
|
+
} else {
|
|
178
|
+
toast.error("Failed to load permissions");
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
toast.error("Failed to load permissions");
|
|
182
|
+
} finally {
|
|
183
|
+
setPermissionsLoading(false);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
void loadPermissions();
|
|
188
|
+
}, [showPermissionsTab]);
|
|
189
|
+
|
|
190
|
+
// Handle deactivate user
|
|
191
|
+
const handleDeactivateUser = async () => {
|
|
192
|
+
if (!selectedUser) return;
|
|
193
|
+
|
|
194
|
+
setUsersActionLoading(true);
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch("/api/user_management/users", {
|
|
197
|
+
method: "PATCH",
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
user_id: selectedUser.id,
|
|
203
|
+
is_active: false,
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const data = await response.json();
|
|
208
|
+
|
|
209
|
+
if (data.success) {
|
|
210
|
+
toast.success("User deactivated successfully");
|
|
211
|
+
setDeactivateDialogOpen(false);
|
|
212
|
+
setSelectedUser(null);
|
|
213
|
+
|
|
214
|
+
// Reload users
|
|
215
|
+
const reload_response = await fetch("/api/user_management/users");
|
|
216
|
+
const reload_data = await reload_response.json();
|
|
217
|
+
if (reload_data.success) {
|
|
218
|
+
setUsers(reload_data.users);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
toast.error(data.error || "Failed to deactivate user");
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
toast.error("Failed to deactivate user");
|
|
225
|
+
} finally {
|
|
226
|
+
setUsersActionLoading(false);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Handle reset password
|
|
231
|
+
const handleResetPassword = async () => {
|
|
232
|
+
if (!selectedUser) return;
|
|
233
|
+
|
|
234
|
+
setUsersActionLoading(true);
|
|
235
|
+
try {
|
|
236
|
+
const response = await fetch("/api/user_management/users", {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
},
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
user_id: selectedUser.id,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
|
|
248
|
+
if (data.success) {
|
|
249
|
+
toast.success("Password reset email sent successfully");
|
|
250
|
+
setResetPasswordDialogOpen(false);
|
|
251
|
+
setSelectedUser(null);
|
|
252
|
+
} else {
|
|
253
|
+
toast.error(data.error || "Failed to send password reset email");
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
toast.error("Failed to send password reset email");
|
|
257
|
+
} finally {
|
|
258
|
+
setUsersActionLoading(false);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
// Handle migrate permissions
|
|
264
|
+
const handleMigratePermissions = async () => {
|
|
265
|
+
setMigrateLoading(true);
|
|
266
|
+
try {
|
|
267
|
+
const response = await fetch("/api/hazo_auth/user_management/permissions?action=migrate", {
|
|
268
|
+
method: "POST",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
|
|
273
|
+
if (data.success) {
|
|
274
|
+
const created_count = data.created?.length || 0;
|
|
275
|
+
const skipped_count = data.skipped?.length || 0;
|
|
276
|
+
|
|
277
|
+
if (created_count > 0) {
|
|
278
|
+
toast.success(
|
|
279
|
+
`Migrated ${created_count} permission(s) to database. ${skipped_count} already existed.`
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
toast.info(`All permissions already exist in database. ${skipped_count} skipped.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Show detailed list in toast if there are changes
|
|
286
|
+
if (data.created && data.created.length > 0) {
|
|
287
|
+
toast.info(`Created: ${data.created.join(", ")}`);
|
|
288
|
+
}
|
|
289
|
+
if (data.skipped && data.skipped.length > 0) {
|
|
290
|
+
toast.info(`Skipped: ${data.skipped.join(", ")}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Reload permissions
|
|
294
|
+
const reload_response = await fetch("/api/user_management/permissions");
|
|
295
|
+
const reload_data = await reload_response.json();
|
|
296
|
+
if (reload_data.success) {
|
|
297
|
+
const db_perms: Permission[] = reload_data.db_permissions.map((p: {
|
|
298
|
+
id: number;
|
|
299
|
+
permission_name: string;
|
|
300
|
+
description: string;
|
|
301
|
+
}) => ({
|
|
302
|
+
id: p.id,
|
|
303
|
+
permission_name: p.permission_name,
|
|
304
|
+
description: p.description,
|
|
305
|
+
source: "db" as const,
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
const config_perms: Permission[] = reload_data.config_permissions.map((name: string) => ({
|
|
309
|
+
id: 0,
|
|
310
|
+
permission_name: name,
|
|
311
|
+
description: "",
|
|
312
|
+
source: "config" as const,
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
setPermissions([...db_perms, ...config_perms]);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
toast.error(data.error || "Failed to migrate permissions");
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
toast.error("Failed to migrate permissions");
|
|
322
|
+
} finally {
|
|
323
|
+
setMigrateLoading(false);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Handle edit permission
|
|
328
|
+
const handleEditPermission = async () => {
|
|
329
|
+
if (!editingPermission || editingPermission.source === "config") return;
|
|
330
|
+
|
|
331
|
+
setPermissionsActionLoading(true);
|
|
332
|
+
try {
|
|
333
|
+
const response = await fetch("/api/user_management/permissions", {
|
|
334
|
+
method: "PUT",
|
|
335
|
+
headers: {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
},
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
permission_id: editingPermission.id,
|
|
340
|
+
description: editDescription,
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const data = await response.json();
|
|
345
|
+
|
|
346
|
+
if (data.success) {
|
|
347
|
+
toast.success("Permission updated successfully");
|
|
348
|
+
setEditPermissionDialogOpen(false);
|
|
349
|
+
setEditingPermission(null);
|
|
350
|
+
setEditDescription("");
|
|
351
|
+
|
|
352
|
+
// Reload permissions
|
|
353
|
+
const reload_response = await fetch("/api/user_management/permissions");
|
|
354
|
+
const reload_data = await reload_response.json();
|
|
355
|
+
if (reload_data.success) {
|
|
356
|
+
const db_perms: Permission[] = reload_data.db_permissions.map((p: {
|
|
357
|
+
id: number;
|
|
358
|
+
permission_name: string;
|
|
359
|
+
description: string;
|
|
360
|
+
}) => ({
|
|
361
|
+
id: p.id,
|
|
362
|
+
permission_name: p.permission_name,
|
|
363
|
+
description: p.description,
|
|
364
|
+
source: "db" as const,
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
const config_perms: Permission[] = reload_data.config_permissions.map((name: string) => ({
|
|
368
|
+
id: 0,
|
|
369
|
+
permission_name: name,
|
|
370
|
+
description: "",
|
|
371
|
+
source: "config" as const,
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
setPermissions([...db_perms, ...config_perms]);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
toast.error(data.error || "Failed to update permission");
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
toast.error("Failed to update permission");
|
|
381
|
+
} finally {
|
|
382
|
+
setPermissionsActionLoading(false);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Handle add permission
|
|
387
|
+
const handleAddPermission = async () => {
|
|
388
|
+
if (!newPermissionName.trim()) {
|
|
389
|
+
toast.error("Permission name is required");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
setPermissionsActionLoading(true);
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch("/api/user_management/permissions", {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: {
|
|
398
|
+
"Content-Type": "application/json",
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
permission_name: newPermissionName.trim(),
|
|
402
|
+
description: newPermissionDescription.trim(),
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const data = await response.json();
|
|
407
|
+
|
|
408
|
+
if (data.success) {
|
|
409
|
+
toast.success("Permission created successfully");
|
|
410
|
+
setAddPermissionDialogOpen(false);
|
|
411
|
+
setNewPermissionName("");
|
|
412
|
+
setNewPermissionDescription("");
|
|
413
|
+
|
|
414
|
+
// Reload permissions
|
|
415
|
+
const reload_response = await fetch("/api/user_management/permissions");
|
|
416
|
+
const reload_data = await reload_response.json();
|
|
417
|
+
if (reload_data.success) {
|
|
418
|
+
const db_perms: Permission[] = reload_data.db_permissions.map((p: {
|
|
419
|
+
id: number;
|
|
420
|
+
permission_name: string;
|
|
421
|
+
description: string;
|
|
422
|
+
}) => ({
|
|
423
|
+
id: p.id,
|
|
424
|
+
permission_name: p.permission_name,
|
|
425
|
+
description: p.description,
|
|
426
|
+
source: "db" as const,
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
const config_perms: Permission[] = reload_data.config_permissions.map((name: string) => ({
|
|
430
|
+
id: 0,
|
|
431
|
+
permission_name: name,
|
|
432
|
+
description: "",
|
|
433
|
+
source: "config" as const,
|
|
434
|
+
}));
|
|
435
|
+
|
|
436
|
+
setPermissions([...db_perms, ...config_perms]);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
toast.error(data.error || "Failed to create permission");
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
toast.error("Failed to create permission");
|
|
443
|
+
} finally {
|
|
444
|
+
setPermissionsActionLoading(false);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Handle delete permission
|
|
449
|
+
const handleDeletePermission = async (permission: Permission) => {
|
|
450
|
+
if (permission.source === "config") return;
|
|
451
|
+
|
|
452
|
+
setPermissionsActionLoading(true);
|
|
453
|
+
try {
|
|
454
|
+
const response = await fetch(
|
|
455
|
+
`/api/hazo_auth/user_management/permissions?permission_id=${permission.id}`,
|
|
456
|
+
{
|
|
457
|
+
method: "DELETE",
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const data = await response.json();
|
|
462
|
+
|
|
463
|
+
if (data.success) {
|
|
464
|
+
toast.success("Permission deleted successfully");
|
|
465
|
+
|
|
466
|
+
// Reload permissions
|
|
467
|
+
const reload_response = await fetch("/api/user_management/permissions");
|
|
468
|
+
const reload_data = await reload_response.json();
|
|
469
|
+
if (reload_data.success) {
|
|
470
|
+
const db_perms: Permission[] = reload_data.db_permissions.map((p: {
|
|
471
|
+
id: number;
|
|
472
|
+
permission_name: string;
|
|
473
|
+
description: string;
|
|
474
|
+
}) => ({
|
|
475
|
+
id: p.id,
|
|
476
|
+
permission_name: p.permission_name,
|
|
477
|
+
description: p.description,
|
|
478
|
+
source: "db" as const,
|
|
479
|
+
}));
|
|
480
|
+
|
|
481
|
+
const config_perms: Permission[] = reload_data.config_permissions.map((name: string) => ({
|
|
482
|
+
id: 0,
|
|
483
|
+
permission_name: name,
|
|
484
|
+
description: "",
|
|
485
|
+
source: "config" as const,
|
|
486
|
+
}));
|
|
487
|
+
|
|
488
|
+
setPermissions([...db_perms, ...config_perms]);
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
toast.error(data.error || "Failed to delete permission");
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
toast.error("Failed to delete permission");
|
|
495
|
+
} finally {
|
|
496
|
+
setPermissionsActionLoading(false);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Get user initials for avatar fallback
|
|
501
|
+
const getUserInitials = (user: User): string => {
|
|
502
|
+
if (user.name) {
|
|
503
|
+
const parts = user.name.trim().split(" ");
|
|
504
|
+
if (parts.length >= 2) {
|
|
505
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
|
506
|
+
}
|
|
507
|
+
return user.name[0]?.toUpperCase() || "";
|
|
508
|
+
}
|
|
509
|
+
if (user.email_address) {
|
|
510
|
+
return user.email_address[0]?.toUpperCase() || "";
|
|
511
|
+
}
|
|
512
|
+
return "?";
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<div className={`cls_user_management_layout flex flex-col gap-4 w-full ${className || ""}`}>
|
|
517
|
+
{/* Show loading spinner while checking permissions */}
|
|
518
|
+
{authResult.loading ? (
|
|
519
|
+
<div className="cls_user_management_permissions_loading flex items-center justify-center p-8">
|
|
520
|
+
<Loader2 className="h-6 w-6 animate-spin text-slate-400" />
|
|
521
|
+
</div>
|
|
522
|
+
) : !hasAnyPermission ? (
|
|
523
|
+
<div className="cls_user_management_no_permissions flex flex-col items-center justify-center p-8 gap-4">
|
|
524
|
+
<p className="text-lg font-semibold text-slate-700">
|
|
525
|
+
Access Denied
|
|
526
|
+
</p>
|
|
527
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
528
|
+
You don't have permission to access User Management. Please contact your administrator.
|
|
529
|
+
</p>
|
|
530
|
+
</div>
|
|
531
|
+
) : (
|
|
532
|
+
<Tabs
|
|
533
|
+
defaultValue={
|
|
534
|
+
showUsersTab ? "users" :
|
|
535
|
+
showRolesTab ? "roles" :
|
|
536
|
+
showPermissionsTab ? "permissions" : "users"
|
|
537
|
+
}
|
|
538
|
+
className="cls_user_management_tabs w-full"
|
|
539
|
+
>
|
|
540
|
+
<TabsList className="cls_user_management_tabs_list flex w-full">
|
|
541
|
+
{showUsersTab && (
|
|
542
|
+
<TabsTrigger value="users" className="cls_user_management_tabs_trigger flex-1">
|
|
543
|
+
Manage Users
|
|
544
|
+
</TabsTrigger>
|
|
545
|
+
)}
|
|
546
|
+
{showRolesTab && (
|
|
547
|
+
<TabsTrigger value="roles" className="cls_user_management_tabs_trigger flex-1">
|
|
548
|
+
Roles
|
|
549
|
+
</TabsTrigger>
|
|
550
|
+
)}
|
|
551
|
+
{showPermissionsTab && (
|
|
552
|
+
<TabsTrigger value="permissions" className="cls_user_management_tabs_trigger flex-1">
|
|
553
|
+
Permissions
|
|
554
|
+
</TabsTrigger>
|
|
555
|
+
)}
|
|
556
|
+
</TabsList>
|
|
557
|
+
|
|
558
|
+
{/* Tab 1: Manage Users */}
|
|
559
|
+
{showUsersTab && (
|
|
560
|
+
<TabsContent value="users" className="cls_user_management_tab_users w-full">
|
|
561
|
+
{usersLoading ? (
|
|
562
|
+
<div className="cls_user_management_users_loading flex items-center justify-center p-8">
|
|
563
|
+
<Loader2 className="h-6 w-6 animate-spin text-slate-400" />
|
|
564
|
+
</div>
|
|
565
|
+
) : (
|
|
566
|
+
<div className="cls_user_management_users_table_container border rounded-lg overflow-auto w-full">
|
|
567
|
+
<Table className="cls_user_management_users_table w-full">
|
|
568
|
+
<TableHeader className="cls_user_management_users_table_header">
|
|
569
|
+
<TableRow className="cls_user_management_users_table_header_row">
|
|
570
|
+
<TableHead className="cls_user_management_users_table_header_profile_pic w-16">
|
|
571
|
+
Photo
|
|
572
|
+
</TableHead>
|
|
573
|
+
<TableHead className="cls_user_management_users_table_header_id">ID</TableHead>
|
|
574
|
+
<TableHead className="cls_user_management_users_table_header_name">
|
|
575
|
+
Name
|
|
576
|
+
</TableHead>
|
|
577
|
+
<TableHead className="cls_user_management_users_table_header_email">
|
|
578
|
+
Email
|
|
579
|
+
</TableHead>
|
|
580
|
+
<TableHead className="cls_user_management_users_table_header_email_verified">
|
|
581
|
+
Email Verified
|
|
582
|
+
</TableHead>
|
|
583
|
+
<TableHead className="cls_user_management_users_table_header_is_active">
|
|
584
|
+
Active
|
|
585
|
+
</TableHead>
|
|
586
|
+
<TableHead className="cls_user_management_users_table_header_last_logon">
|
|
587
|
+
Last Logon
|
|
588
|
+
</TableHead>
|
|
589
|
+
<TableHead className="cls_user_management_users_table_header_created_at">
|
|
590
|
+
Created At
|
|
591
|
+
</TableHead>
|
|
592
|
+
<TableHead className="cls_user_management_users_table_header_actions text-right">
|
|
593
|
+
Actions
|
|
594
|
+
</TableHead>
|
|
595
|
+
</TableRow>
|
|
596
|
+
</TableHeader>
|
|
597
|
+
<TableBody className="cls_user_management_users_table_body">
|
|
598
|
+
{users.length === 0 ? (
|
|
599
|
+
<TableRow className="cls_user_management_users_table_row_empty">
|
|
600
|
+
<TableCell colSpan={9} className="text-center text-muted-foreground py-8">
|
|
601
|
+
No users found.
|
|
602
|
+
</TableCell>
|
|
603
|
+
</TableRow>
|
|
604
|
+
) : (
|
|
605
|
+
users.map((user) => (
|
|
606
|
+
<TableRow
|
|
607
|
+
key={user.id}
|
|
608
|
+
className="cls_user_management_users_table_row cursor-pointer hover:bg-muted/50"
|
|
609
|
+
onClick={() => {
|
|
610
|
+
setSelectedUser(user);
|
|
611
|
+
setUserDetailDialogOpen(true);
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<TableCell className="cls_user_management_users_table_cell_profile_pic">
|
|
615
|
+
<Avatar className="cls_user_management_users_table_avatar h-8 w-8">
|
|
616
|
+
<AvatarImage
|
|
617
|
+
src={user.profile_picture_url || undefined}
|
|
618
|
+
alt={user.name ? `Profile picture of ${user.name}` : "Profile picture"}
|
|
619
|
+
className="cls_user_management_users_table_avatar_image"
|
|
620
|
+
/>
|
|
621
|
+
<AvatarFallback className="cls_user_management_users_table_avatar_fallback bg-slate-200 text-slate-600 text-xs">
|
|
622
|
+
{getUserInitials(user)}
|
|
623
|
+
</AvatarFallback>
|
|
624
|
+
</Avatar>
|
|
625
|
+
</TableCell>
|
|
626
|
+
<TableCell className="cls_user_management_users_table_cell_id font-mono text-xs">
|
|
627
|
+
{user.id.substring(0, 8)}...
|
|
628
|
+
</TableCell>
|
|
629
|
+
<TableCell className="cls_user_management_users_table_cell_name">
|
|
630
|
+
{user.name || "-"}
|
|
631
|
+
</TableCell>
|
|
632
|
+
<TableCell className="cls_user_management_users_table_cell_email">
|
|
633
|
+
{user.email_address}
|
|
634
|
+
</TableCell>
|
|
635
|
+
<TableCell className="cls_user_management_users_table_cell_email_verified">
|
|
636
|
+
{user.email_verified ? (
|
|
637
|
+
<span className="text-green-600">Yes</span>
|
|
638
|
+
) : (
|
|
639
|
+
<span className="text-red-600">No</span>
|
|
640
|
+
)}
|
|
641
|
+
</TableCell>
|
|
642
|
+
<TableCell className="cls_user_management_users_table_cell_is_active">
|
|
643
|
+
{user.is_active ? (
|
|
644
|
+
<span className="text-green-600">Active</span>
|
|
645
|
+
) : (
|
|
646
|
+
<span className="text-red-600">Inactive</span>
|
|
647
|
+
)}
|
|
648
|
+
</TableCell>
|
|
649
|
+
<TableCell className="cls_user_management_users_table_cell_last_logon">
|
|
650
|
+
{user.last_logon
|
|
651
|
+
? new Date(user.last_logon).toLocaleDateString()
|
|
652
|
+
: "-"}
|
|
653
|
+
</TableCell>
|
|
654
|
+
<TableCell className="cls_user_management_users_table_cell_created_at">
|
|
655
|
+
{user.created_at
|
|
656
|
+
? new Date(user.created_at).toLocaleDateString()
|
|
657
|
+
: "-"}
|
|
658
|
+
</TableCell>
|
|
659
|
+
<TableCell className="cls_user_management_users_table_cell_actions text-right">
|
|
660
|
+
<TooltipProvider>
|
|
661
|
+
<div
|
|
662
|
+
className="cls_user_management_users_table_actions flex items-center justify-end gap-2"
|
|
663
|
+
onClick={(e) => e.stopPropagation()}
|
|
664
|
+
>
|
|
665
|
+
<Tooltip>
|
|
666
|
+
<TooltipTrigger asChild>
|
|
667
|
+
<Button
|
|
668
|
+
onClick={() => {
|
|
669
|
+
setSelectedUser(user);
|
|
670
|
+
setAssignRolesDialogOpen(true);
|
|
671
|
+
}}
|
|
672
|
+
variant="outline"
|
|
673
|
+
size="sm"
|
|
674
|
+
className="cls_user_management_users_table_action_assign_roles"
|
|
675
|
+
>
|
|
676
|
+
<UserPlus className="h-4 w-4" />
|
|
677
|
+
</Button>
|
|
678
|
+
</TooltipTrigger>
|
|
679
|
+
<TooltipContent>
|
|
680
|
+
<p>Assign Roles</p>
|
|
681
|
+
</TooltipContent>
|
|
682
|
+
</Tooltip>
|
|
683
|
+
{user.is_active && (
|
|
684
|
+
<Tooltip>
|
|
685
|
+
<TooltipTrigger asChild>
|
|
686
|
+
<Button
|
|
687
|
+
onClick={() => {
|
|
688
|
+
setSelectedUser(user);
|
|
689
|
+
setDeactivateDialogOpen(true);
|
|
690
|
+
}}
|
|
691
|
+
variant="outline"
|
|
692
|
+
size="sm"
|
|
693
|
+
className="cls_user_management_users_table_action_deactivate"
|
|
694
|
+
>
|
|
695
|
+
<UserX className="h-4 w-4" />
|
|
696
|
+
</Button>
|
|
697
|
+
</TooltipTrigger>
|
|
698
|
+
<TooltipContent>
|
|
699
|
+
<p>Deactivate</p>
|
|
700
|
+
</TooltipContent>
|
|
701
|
+
</Tooltip>
|
|
702
|
+
)}
|
|
703
|
+
<Tooltip>
|
|
704
|
+
<TooltipTrigger asChild>
|
|
705
|
+
<Button
|
|
706
|
+
onClick={() => {
|
|
707
|
+
setSelectedUser(user);
|
|
708
|
+
setResetPasswordDialogOpen(true);
|
|
709
|
+
}}
|
|
710
|
+
variant="outline"
|
|
711
|
+
size="sm"
|
|
712
|
+
className="cls_user_management_users_table_action_reset_password"
|
|
713
|
+
>
|
|
714
|
+
<KeyRound className="h-4 w-4" />
|
|
715
|
+
</Button>
|
|
716
|
+
</TooltipTrigger>
|
|
717
|
+
<TooltipContent>
|
|
718
|
+
<p>Reset Password</p>
|
|
719
|
+
</TooltipContent>
|
|
720
|
+
</Tooltip>
|
|
721
|
+
</div>
|
|
722
|
+
</TooltipProvider>
|
|
723
|
+
</TableCell>
|
|
724
|
+
</TableRow>
|
|
725
|
+
))
|
|
726
|
+
)}
|
|
727
|
+
</TableBody>
|
|
728
|
+
</Table>
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
</TabsContent>
|
|
732
|
+
)}
|
|
733
|
+
|
|
734
|
+
{/* Tab 2: Roles */}
|
|
735
|
+
{showRolesTab && (
|
|
736
|
+
<TabsContent value="roles" className="cls_user_management_tab_roles w-full">
|
|
737
|
+
<RolesMatrix
|
|
738
|
+
add_button_enabled={true}
|
|
739
|
+
role_name_selection_enabled={false}
|
|
740
|
+
onSave={(data) => {
|
|
741
|
+
// Data is already saved by RolesMatrix component
|
|
742
|
+
console.log("Roles saved:", data);
|
|
743
|
+
}}
|
|
744
|
+
/>
|
|
745
|
+
</TabsContent>
|
|
746
|
+
)}
|
|
747
|
+
|
|
748
|
+
{/* Tab 3: Permissions */}
|
|
749
|
+
{showPermissionsTab && (
|
|
750
|
+
<TabsContent value="permissions" className="cls_user_management_tab_permissions w-full">
|
|
751
|
+
<div className="cls_user_management_permissions_container flex flex-col gap-4 w-full">
|
|
752
|
+
{/* Header buttons */}
|
|
753
|
+
<div className="cls_user_management_permissions_header flex items-center justify-between">
|
|
754
|
+
<div className="cls_user_management_permissions_header_left flex items-center gap-2">
|
|
755
|
+
<Button
|
|
756
|
+
onClick={() => setAddPermissionDialogOpen(true)}
|
|
757
|
+
variant="default"
|
|
758
|
+
size="sm"
|
|
759
|
+
className="cls_user_management_permissions_add_button"
|
|
760
|
+
>
|
|
761
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
762
|
+
Add Permission
|
|
763
|
+
</Button>
|
|
764
|
+
</div>
|
|
765
|
+
<div className="cls_user_management_permissions_header_right">
|
|
766
|
+
<Button
|
|
767
|
+
onClick={handleMigratePermissions}
|
|
768
|
+
disabled={migrateLoading}
|
|
769
|
+
variant="default"
|
|
770
|
+
size="sm"
|
|
771
|
+
className="cls_user_management_permissions_migrate_button"
|
|
772
|
+
>
|
|
773
|
+
{migrateLoading ? (
|
|
774
|
+
<>
|
|
775
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
776
|
+
Migrating...
|
|
777
|
+
</>
|
|
778
|
+
) : (
|
|
779
|
+
"Migrate config to database"
|
|
780
|
+
)}
|
|
781
|
+
</Button>
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
|
|
785
|
+
{/* Permissions table */}
|
|
786
|
+
{permissionsLoading ? (
|
|
787
|
+
<div className="cls_user_management_permissions_loading flex items-center justify-center p-8">
|
|
788
|
+
<Loader2 className="h-6 w-6 animate-spin text-slate-400" />
|
|
789
|
+
</div>
|
|
790
|
+
) : (
|
|
791
|
+
<div className="cls_user_management_permissions_table_container border rounded-lg overflow-auto w-full">
|
|
792
|
+
<Table className="cls_user_management_permissions_table w-full">
|
|
793
|
+
<TableHeader className="cls_user_management_permissions_table_header">
|
|
794
|
+
<TableRow className="cls_user_management_permissions_table_header_row">
|
|
795
|
+
<TableHead className="cls_user_management_permissions_table_header_name">
|
|
796
|
+
Permission Name
|
|
797
|
+
</TableHead>
|
|
798
|
+
<TableHead className="cls_user_management_permissions_table_header_description">
|
|
799
|
+
Description
|
|
800
|
+
</TableHead>
|
|
801
|
+
<TableHead className="cls_user_management_permissions_table_header_source">
|
|
802
|
+
Source
|
|
803
|
+
</TableHead>
|
|
804
|
+
<TableHead className="cls_user_management_permissions_table_header_actions text-right">
|
|
805
|
+
Actions
|
|
806
|
+
</TableHead>
|
|
807
|
+
</TableRow>
|
|
808
|
+
</TableHeader>
|
|
809
|
+
<TableBody className="cls_user_management_permissions_table_body">
|
|
810
|
+
{permissions.length === 0 ? (
|
|
811
|
+
<TableRow className="cls_user_management_permissions_table_row_empty">
|
|
812
|
+
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
|
813
|
+
No permissions found.
|
|
814
|
+
</TableCell>
|
|
815
|
+
</TableRow>
|
|
816
|
+
) : (
|
|
817
|
+
permissions.map((permission) => (
|
|
818
|
+
<TableRow
|
|
819
|
+
key={`${permission.source}-${permission.id}-${permission.permission_name}`}
|
|
820
|
+
className="cls_user_management_permissions_table_row"
|
|
821
|
+
>
|
|
822
|
+
<TableCell
|
|
823
|
+
className={`cls_user_management_permissions_table_cell_name font-medium ${
|
|
824
|
+
permission.source === "db" ? "text-blue-600" : "text-purple-600"
|
|
825
|
+
}`}
|
|
826
|
+
>
|
|
827
|
+
{permission.permission_name}
|
|
828
|
+
</TableCell>
|
|
829
|
+
<TableCell className="cls_user_management_permissions_table_cell_description">
|
|
830
|
+
{permission.description || "-"}
|
|
831
|
+
</TableCell>
|
|
832
|
+
<TableCell className="cls_user_management_permissions_table_cell_source">
|
|
833
|
+
<span
|
|
834
|
+
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
835
|
+
permission.source === "db"
|
|
836
|
+
? "bg-blue-100 text-blue-700"
|
|
837
|
+
: "bg-purple-100 text-purple-700"
|
|
838
|
+
}`}
|
|
839
|
+
>
|
|
840
|
+
{permission.source === "db" ? "Database" : "Config"}
|
|
841
|
+
</span>
|
|
842
|
+
</TableCell>
|
|
843
|
+
<TableCell className="cls_user_management_permissions_table_cell_actions text-right">
|
|
844
|
+
<div className="cls_user_management_permissions_table_actions flex items-center justify-end gap-2">
|
|
845
|
+
{permission.source === "db" && (
|
|
846
|
+
<>
|
|
847
|
+
<Button
|
|
848
|
+
onClick={() => {
|
|
849
|
+
setEditingPermission(permission);
|
|
850
|
+
setEditDescription(permission.description);
|
|
851
|
+
setEditPermissionDialogOpen(true);
|
|
852
|
+
}}
|
|
853
|
+
variant="outline"
|
|
854
|
+
size="sm"
|
|
855
|
+
className="cls_user_management_permissions_table_action_edit"
|
|
856
|
+
>
|
|
857
|
+
<Edit className="h-4 w-4 mr-1" />
|
|
858
|
+
Edit
|
|
859
|
+
</Button>
|
|
860
|
+
<Button
|
|
861
|
+
onClick={() => handleDeletePermission(permission)}
|
|
862
|
+
disabled={permissionsActionLoading}
|
|
863
|
+
variant="outline"
|
|
864
|
+
size="sm"
|
|
865
|
+
className="cls_user_management_permissions_table_action_delete text-destructive"
|
|
866
|
+
>
|
|
867
|
+
<Trash2 className="h-4 w-4 mr-1" />
|
|
868
|
+
Delete
|
|
869
|
+
</Button>
|
|
870
|
+
</>
|
|
871
|
+
)}
|
|
872
|
+
</div>
|
|
873
|
+
</TableCell>
|
|
874
|
+
</TableRow>
|
|
875
|
+
))
|
|
876
|
+
)}
|
|
877
|
+
</TableBody>
|
|
878
|
+
</Table>
|
|
879
|
+
</div>
|
|
880
|
+
)}
|
|
881
|
+
</div>
|
|
882
|
+
</TabsContent>
|
|
883
|
+
)}
|
|
884
|
+
</Tabs>
|
|
885
|
+
)}
|
|
886
|
+
|
|
887
|
+
{/* Deactivate User Dialog */}
|
|
888
|
+
<AlertDialog open={deactivateDialogOpen} onOpenChange={setDeactivateDialogOpen}>
|
|
889
|
+
<AlertDialogContent className="cls_user_management_deactivate_dialog">
|
|
890
|
+
<AlertDialogHeader>
|
|
891
|
+
<AlertDialogTitle>Deactivate User</AlertDialogTitle>
|
|
892
|
+
<AlertDialogDescription>
|
|
893
|
+
Are you sure you want to deactivate {selectedUser?.name || selectedUser?.email_address}? They will not be able to log in until reactivated.
|
|
894
|
+
</AlertDialogDescription>
|
|
895
|
+
</AlertDialogHeader>
|
|
896
|
+
<AlertDialogFooter className="cls_user_management_deactivate_dialog_footer">
|
|
897
|
+
<AlertDialogAction
|
|
898
|
+
onClick={handleDeactivateUser}
|
|
899
|
+
disabled={usersActionLoading}
|
|
900
|
+
className="cls_user_management_deactivate_dialog_confirm"
|
|
901
|
+
>
|
|
902
|
+
{usersActionLoading ? (
|
|
903
|
+
<>
|
|
904
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
905
|
+
Deactivating...
|
|
906
|
+
</>
|
|
907
|
+
) : (
|
|
908
|
+
"Deactivate"
|
|
909
|
+
)}
|
|
910
|
+
</AlertDialogAction>
|
|
911
|
+
<AlertDialogCancel
|
|
912
|
+
onClick={() => {
|
|
913
|
+
setDeactivateDialogOpen(false);
|
|
914
|
+
setSelectedUser(null);
|
|
915
|
+
}}
|
|
916
|
+
className="cls_user_management_deactivate_dialog_cancel"
|
|
917
|
+
>
|
|
918
|
+
Cancel
|
|
919
|
+
</AlertDialogCancel>
|
|
920
|
+
</AlertDialogFooter>
|
|
921
|
+
</AlertDialogContent>
|
|
922
|
+
</AlertDialog>
|
|
923
|
+
|
|
924
|
+
{/* Reset Password Dialog */}
|
|
925
|
+
<AlertDialog open={resetPasswordDialogOpen} onOpenChange={setResetPasswordDialogOpen}>
|
|
926
|
+
<AlertDialogContent className="cls_user_management_reset_password_dialog">
|
|
927
|
+
<AlertDialogHeader>
|
|
928
|
+
<AlertDialogTitle>Reset Password</AlertDialogTitle>
|
|
929
|
+
<AlertDialogDescription>
|
|
930
|
+
Send a password reset email to {selectedUser?.email_address}? They will receive a link to reset their password.
|
|
931
|
+
</AlertDialogDescription>
|
|
932
|
+
</AlertDialogHeader>
|
|
933
|
+
<AlertDialogFooter className="cls_user_management_reset_password_dialog_footer">
|
|
934
|
+
<AlertDialogAction
|
|
935
|
+
onClick={handleResetPassword}
|
|
936
|
+
disabled={usersActionLoading}
|
|
937
|
+
className="cls_user_management_reset_password_dialog_confirm"
|
|
938
|
+
>
|
|
939
|
+
{usersActionLoading ? (
|
|
940
|
+
<>
|
|
941
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
942
|
+
Sending...
|
|
943
|
+
</>
|
|
944
|
+
) : (
|
|
945
|
+
"Send Reset Email"
|
|
946
|
+
)}
|
|
947
|
+
</AlertDialogAction>
|
|
948
|
+
<AlertDialogCancel
|
|
949
|
+
onClick={() => {
|
|
950
|
+
setResetPasswordDialogOpen(false);
|
|
951
|
+
setSelectedUser(null);
|
|
952
|
+
}}
|
|
953
|
+
className="cls_user_management_reset_password_dialog_cancel"
|
|
954
|
+
>
|
|
955
|
+
Cancel
|
|
956
|
+
</AlertDialogCancel>
|
|
957
|
+
</AlertDialogFooter>
|
|
958
|
+
</AlertDialogContent>
|
|
959
|
+
</AlertDialog>
|
|
960
|
+
|
|
961
|
+
{/* Edit Permission Dialog */}
|
|
962
|
+
<Dialog open={editPermissionDialogOpen} onOpenChange={setEditPermissionDialogOpen}>
|
|
963
|
+
<DialogContent className="cls_user_management_edit_permission_dialog">
|
|
964
|
+
<DialogHeader>
|
|
965
|
+
<DialogTitle>Edit Permission</DialogTitle>
|
|
966
|
+
<DialogDescription>
|
|
967
|
+
Update the description for permission: {editingPermission?.permission_name}
|
|
968
|
+
</DialogDescription>
|
|
969
|
+
</DialogHeader>
|
|
970
|
+
<div className="cls_user_management_edit_permission_dialog_content flex flex-col gap-4 py-4">
|
|
971
|
+
<div className="cls_user_management_edit_permission_dialog_field flex flex-col gap-2">
|
|
972
|
+
<Label htmlFor="permission_description" className="cls_user_management_edit_permission_dialog_label">
|
|
973
|
+
Description
|
|
974
|
+
</Label>
|
|
975
|
+
<Input
|
|
976
|
+
id="permission_description"
|
|
977
|
+
value={editDescription}
|
|
978
|
+
onChange={(e) => setEditDescription(e.target.value)}
|
|
979
|
+
placeholder="Enter permission description"
|
|
980
|
+
className="cls_user_management_edit_permission_dialog_input"
|
|
981
|
+
/>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
<DialogFooter className="cls_user_management_edit_permission_dialog_footer">
|
|
985
|
+
<Button
|
|
986
|
+
onClick={handleEditPermission}
|
|
987
|
+
disabled={permissionsActionLoading}
|
|
988
|
+
variant="default"
|
|
989
|
+
className="cls_user_management_edit_permission_dialog_save"
|
|
990
|
+
>
|
|
991
|
+
{permissionsActionLoading ? (
|
|
992
|
+
<>
|
|
993
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
994
|
+
Saving...
|
|
995
|
+
</>
|
|
996
|
+
) : (
|
|
997
|
+
<>
|
|
998
|
+
<CircleCheck className="h-4 w-4 mr-2" />
|
|
999
|
+
Save
|
|
1000
|
+
</>
|
|
1001
|
+
)}
|
|
1002
|
+
</Button>
|
|
1003
|
+
<Button
|
|
1004
|
+
onClick={() => {
|
|
1005
|
+
setEditPermissionDialogOpen(false);
|
|
1006
|
+
setEditingPermission(null);
|
|
1007
|
+
setEditDescription("");
|
|
1008
|
+
}}
|
|
1009
|
+
variant="outline"
|
|
1010
|
+
className="cls_user_management_edit_permission_dialog_cancel"
|
|
1011
|
+
>
|
|
1012
|
+
<CircleX className="h-4 w-4 mr-2" />
|
|
1013
|
+
Cancel
|
|
1014
|
+
</Button>
|
|
1015
|
+
</DialogFooter>
|
|
1016
|
+
</DialogContent>
|
|
1017
|
+
</Dialog>
|
|
1018
|
+
|
|
1019
|
+
{/* Add Permission Dialog */}
|
|
1020
|
+
<Dialog open={addPermissionDialogOpen} onOpenChange={setAddPermissionDialogOpen}>
|
|
1021
|
+
<DialogContent className="cls_user_management_add_permission_dialog">
|
|
1022
|
+
<DialogHeader>
|
|
1023
|
+
<DialogTitle>Add New Permission</DialogTitle>
|
|
1024
|
+
<DialogDescription>
|
|
1025
|
+
Create a new permission that can be assigned to roles.
|
|
1026
|
+
</DialogDescription>
|
|
1027
|
+
</DialogHeader>
|
|
1028
|
+
<div className="cls_user_management_add_permission_dialog_content flex flex-col gap-4 py-4">
|
|
1029
|
+
<div className="cls_user_management_add_permission_dialog_field flex flex-col gap-2">
|
|
1030
|
+
<Label htmlFor="new_permission_name" className="cls_user_management_add_permission_dialog_label">
|
|
1031
|
+
Permission Name *
|
|
1032
|
+
</Label>
|
|
1033
|
+
<Input
|
|
1034
|
+
id="new_permission_name"
|
|
1035
|
+
value={newPermissionName}
|
|
1036
|
+
onChange={(e) => setNewPermissionName(e.target.value)}
|
|
1037
|
+
placeholder="Enter permission name (e.g., READ_USERS)"
|
|
1038
|
+
className="cls_user_management_add_permission_dialog_input"
|
|
1039
|
+
onKeyDown={(e) => {
|
|
1040
|
+
if (e.key === "Enter") {
|
|
1041
|
+
handleAddPermission();
|
|
1042
|
+
}
|
|
1043
|
+
}}
|
|
1044
|
+
/>
|
|
1045
|
+
</div>
|
|
1046
|
+
<div className="cls_user_management_add_permission_dialog_field flex flex-col gap-2">
|
|
1047
|
+
<Label htmlFor="new_permission_description" className="cls_user_management_add_permission_dialog_label">
|
|
1048
|
+
Description
|
|
1049
|
+
</Label>
|
|
1050
|
+
<Input
|
|
1051
|
+
id="new_permission_description"
|
|
1052
|
+
value={newPermissionDescription}
|
|
1053
|
+
onChange={(e) => setNewPermissionDescription(e.target.value)}
|
|
1054
|
+
placeholder="Enter permission description (optional)"
|
|
1055
|
+
className="cls_user_management_add_permission_dialog_input"
|
|
1056
|
+
onKeyDown={(e) => {
|
|
1057
|
+
if (e.key === "Enter") {
|
|
1058
|
+
handleAddPermission();
|
|
1059
|
+
}
|
|
1060
|
+
}}
|
|
1061
|
+
/>
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
<DialogFooter className="cls_user_management_add_permission_dialog_footer">
|
|
1065
|
+
<Button
|
|
1066
|
+
onClick={handleAddPermission}
|
|
1067
|
+
disabled={permissionsActionLoading || !newPermissionName.trim()}
|
|
1068
|
+
variant="default"
|
|
1069
|
+
className="cls_user_management_add_permission_dialog_save"
|
|
1070
|
+
>
|
|
1071
|
+
{permissionsActionLoading ? (
|
|
1072
|
+
<>
|
|
1073
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
1074
|
+
Creating...
|
|
1075
|
+
</>
|
|
1076
|
+
) : (
|
|
1077
|
+
<>
|
|
1078
|
+
<CircleCheck className="h-4 w-4 mr-2" />
|
|
1079
|
+
Create Permission
|
|
1080
|
+
</>
|
|
1081
|
+
)}
|
|
1082
|
+
</Button>
|
|
1083
|
+
<Button
|
|
1084
|
+
onClick={() => {
|
|
1085
|
+
setAddPermissionDialogOpen(false);
|
|
1086
|
+
setNewPermissionName("");
|
|
1087
|
+
setNewPermissionDescription("");
|
|
1088
|
+
}}
|
|
1089
|
+
variant="outline"
|
|
1090
|
+
className="cls_user_management_add_permission_dialog_cancel"
|
|
1091
|
+
>
|
|
1092
|
+
<CircleX className="h-4 w-4 mr-2" />
|
|
1093
|
+
Cancel
|
|
1094
|
+
</Button>
|
|
1095
|
+
</DialogFooter>
|
|
1096
|
+
</DialogContent>
|
|
1097
|
+
</Dialog>
|
|
1098
|
+
|
|
1099
|
+
{/* User Detail Dialog */}
|
|
1100
|
+
<Dialog open={userDetailDialogOpen} onOpenChange={setUserDetailDialogOpen}>
|
|
1101
|
+
<DialogContent className="cls_user_management_user_detail_dialog max-w-2xl">
|
|
1102
|
+
<DialogHeader>
|
|
1103
|
+
<DialogTitle>User Details</DialogTitle>
|
|
1104
|
+
<DialogDescription>
|
|
1105
|
+
Complete information for {selectedUser?.name || selectedUser?.email_address}
|
|
1106
|
+
</DialogDescription>
|
|
1107
|
+
</DialogHeader>
|
|
1108
|
+
<div className="cls_user_management_user_detail_dialog_content flex flex-col gap-4 py-4">
|
|
1109
|
+
{selectedUser && (
|
|
1110
|
+
<div className="cls_user_management_user_detail_fields grid grid-cols-1 gap-4">
|
|
1111
|
+
{/* Profile Picture */}
|
|
1112
|
+
<div className="cls_user_management_user_detail_field_profile_pic flex flex-col gap-2">
|
|
1113
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1114
|
+
Profile Picture
|
|
1115
|
+
</Label>
|
|
1116
|
+
<div className="cls_user_management_user_detail_profile_pic_container flex items-center gap-4">
|
|
1117
|
+
<Avatar className="cls_user_management_user_detail_avatar h-16 w-16">
|
|
1118
|
+
<AvatarImage
|
|
1119
|
+
src={selectedUser.profile_picture_url || undefined}
|
|
1120
|
+
alt={selectedUser.name ? `Profile picture of ${selectedUser.name}` : "Profile picture"}
|
|
1121
|
+
className="cls_user_management_user_detail_avatar_image"
|
|
1122
|
+
/>
|
|
1123
|
+
<AvatarFallback className="cls_user_management_user_detail_avatar_fallback bg-slate-200 text-slate-600 text-lg">
|
|
1124
|
+
{getUserInitials(selectedUser)}
|
|
1125
|
+
</AvatarFallback>
|
|
1126
|
+
</Avatar>
|
|
1127
|
+
<div className="cls_user_management_user_detail_profile_pic_info flex flex-col gap-1">
|
|
1128
|
+
<span className="cls_user_management_user_detail_profile_pic_url text-sm text-muted-foreground">
|
|
1129
|
+
{selectedUser.profile_picture_url || "No profile picture"}
|
|
1130
|
+
</span>
|
|
1131
|
+
<span className="cls_user_management_user_detail_profile_pic_source text-xs text-muted-foreground">
|
|
1132
|
+
Source: {selectedUser.profile_source || "N/A"}
|
|
1133
|
+
</span>
|
|
1134
|
+
</div>
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
|
|
1138
|
+
{/* User ID */}
|
|
1139
|
+
<div className="cls_user_management_user_detail_field_id flex flex-col gap-2">
|
|
1140
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1141
|
+
User ID
|
|
1142
|
+
</Label>
|
|
1143
|
+
<div className="cls_user_management_user_detail_id_value font-mono text-sm bg-muted p-2 rounded">
|
|
1144
|
+
{selectedUser.id}
|
|
1145
|
+
</div>
|
|
1146
|
+
</div>
|
|
1147
|
+
|
|
1148
|
+
{/* Name */}
|
|
1149
|
+
<div className="cls_user_management_user_detail_field_name flex flex-col gap-2">
|
|
1150
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1151
|
+
Name
|
|
1152
|
+
</Label>
|
|
1153
|
+
<div className="cls_user_management_user_detail_name_value text-sm">
|
|
1154
|
+
{selectedUser.name || "-"}
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
{/* Email */}
|
|
1159
|
+
<div className="cls_user_management_user_detail_field_email flex flex-col gap-2">
|
|
1160
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1161
|
+
Email Address
|
|
1162
|
+
</Label>
|
|
1163
|
+
<div className="cls_user_management_user_detail_email_value text-sm">
|
|
1164
|
+
{selectedUser.email_address}
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
{/* Email Verified */}
|
|
1169
|
+
<div className="cls_user_management_user_detail_field_email_verified flex flex-col gap-2">
|
|
1170
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1171
|
+
Email Verified
|
|
1172
|
+
</Label>
|
|
1173
|
+
<div className="cls_user_management_user_detail_email_verified_value">
|
|
1174
|
+
{selectedUser.email_verified ? (
|
|
1175
|
+
<span className="text-green-600 font-medium">Yes</span>
|
|
1176
|
+
) : (
|
|
1177
|
+
<span className="text-red-600 font-medium">No</span>
|
|
1178
|
+
)}
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
|
|
1182
|
+
{/* Active Status */}
|
|
1183
|
+
<div className="cls_user_management_user_detail_field_is_active flex flex-col gap-2">
|
|
1184
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1185
|
+
Active Status
|
|
1186
|
+
</Label>
|
|
1187
|
+
<div className="cls_user_management_user_detail_is_active_value">
|
|
1188
|
+
{selectedUser.is_active ? (
|
|
1189
|
+
<span className="text-green-600 font-medium">Active</span>
|
|
1190
|
+
) : (
|
|
1191
|
+
<span className="text-red-600 font-medium">Inactive</span>
|
|
1192
|
+
)}
|
|
1193
|
+
</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
|
|
1196
|
+
{/* Last Logon */}
|
|
1197
|
+
<div className="cls_user_management_user_detail_field_last_logon flex flex-col gap-2">
|
|
1198
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1199
|
+
Last Logon
|
|
1200
|
+
</Label>
|
|
1201
|
+
<div className="cls_user_management_user_detail_last_logon_value text-sm">
|
|
1202
|
+
{selectedUser.last_logon ? (
|
|
1203
|
+
<span className="font-medium">
|
|
1204
|
+
{new Date(selectedUser.last_logon).toLocaleString(undefined, {
|
|
1205
|
+
year: "numeric",
|
|
1206
|
+
month: "long",
|
|
1207
|
+
day: "numeric",
|
|
1208
|
+
hour: "2-digit",
|
|
1209
|
+
minute: "2-digit",
|
|
1210
|
+
second: "2-digit",
|
|
1211
|
+
timeZoneName: "short",
|
|
1212
|
+
})}
|
|
1213
|
+
</span>
|
|
1214
|
+
) : (
|
|
1215
|
+
<span className="text-muted-foreground">Never</span>
|
|
1216
|
+
)}
|
|
1217
|
+
</div>
|
|
1218
|
+
</div>
|
|
1219
|
+
|
|
1220
|
+
{/* Created At */}
|
|
1221
|
+
<div className="cls_user_management_user_detail_field_created_at flex flex-col gap-2">
|
|
1222
|
+
<Label className="cls_user_management_user_detail_label font-semibold">
|
|
1223
|
+
Created At
|
|
1224
|
+
</Label>
|
|
1225
|
+
<div className="cls_user_management_user_detail_created_at_value text-sm">
|
|
1226
|
+
{selectedUser.created_at ? (
|
|
1227
|
+
<span className="font-medium">
|
|
1228
|
+
{new Date(selectedUser.created_at).toLocaleString(undefined, {
|
|
1229
|
+
year: "numeric",
|
|
1230
|
+
month: "long",
|
|
1231
|
+
day: "numeric",
|
|
1232
|
+
hour: "2-digit",
|
|
1233
|
+
minute: "2-digit",
|
|
1234
|
+
second: "2-digit",
|
|
1235
|
+
timeZoneName: "short",
|
|
1236
|
+
})}
|
|
1237
|
+
</span>
|
|
1238
|
+
) : (
|
|
1239
|
+
<span className="text-muted-foreground">-</span>
|
|
1240
|
+
)}
|
|
1241
|
+
</div>
|
|
1242
|
+
</div>
|
|
1243
|
+
</div>
|
|
1244
|
+
)}
|
|
1245
|
+
</div>
|
|
1246
|
+
<DialogFooter className="cls_user_management_user_detail_dialog_footer">
|
|
1247
|
+
<Button
|
|
1248
|
+
onClick={() => setUserDetailDialogOpen(false)}
|
|
1249
|
+
variant="outline"
|
|
1250
|
+
className="cls_user_management_user_detail_dialog_close"
|
|
1251
|
+
>
|
|
1252
|
+
Close
|
|
1253
|
+
</Button>
|
|
1254
|
+
</DialogFooter>
|
|
1255
|
+
</DialogContent>
|
|
1256
|
+
</Dialog>
|
|
1257
|
+
|
|
1258
|
+
{/* Assign Roles Dialog */}
|
|
1259
|
+
<Dialog open={assignRolesDialogOpen} onOpenChange={setAssignRolesDialogOpen}>
|
|
1260
|
+
<DialogContent className="cls_user_management_assign_roles_dialog max-w-4xl max-h-[80vh] overflow-y-auto">
|
|
1261
|
+
<DialogHeader>
|
|
1262
|
+
<DialogTitle>Assign Roles to User</DialogTitle>
|
|
1263
|
+
<DialogDescription>
|
|
1264
|
+
Select roles to assign to {selectedUser?.name || selectedUser?.email_address}.
|
|
1265
|
+
Check the roles you want to assign, then click Save.
|
|
1266
|
+
</DialogDescription>
|
|
1267
|
+
</DialogHeader>
|
|
1268
|
+
<div className="cls_user_management_assign_roles_dialog_content py-4">
|
|
1269
|
+
<RolesMatrix
|
|
1270
|
+
add_button_enabled={false}
|
|
1271
|
+
role_name_selection_enabled={true}
|
|
1272
|
+
permissions_read_only={true}
|
|
1273
|
+
show_save_cancel={true}
|
|
1274
|
+
user_id={selectedUser?.id}
|
|
1275
|
+
onSave={(data) => {
|
|
1276
|
+
// Data is already saved by RolesMatrix component
|
|
1277
|
+
console.log("User roles saved:", data);
|
|
1278
|
+
// Refresh users list to show updated roles
|
|
1279
|
+
void loadUsers();
|
|
1280
|
+
setAssignRolesDialogOpen(false);
|
|
1281
|
+
setSelectedUser(null);
|
|
1282
|
+
}}
|
|
1283
|
+
onCancel={() => {
|
|
1284
|
+
setAssignRolesDialogOpen(false);
|
|
1285
|
+
setSelectedUser(null);
|
|
1286
|
+
}}
|
|
1287
|
+
className="cls_user_management_assign_roles_matrix"
|
|
1288
|
+
/>
|
|
1289
|
+
</div>
|
|
1290
|
+
</DialogContent>
|
|
1291
|
+
</Dialog>
|
|
1292
|
+
</div>
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
|