hazo_auth 0.3.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +628 -1
- package/hazo_auth_config.example.ini +39 -0
- package/instrumentation.ts +1 -1
- package/next.config.mjs +1 -1
- package/package.json +3 -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}/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 +2 -2
- 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 +4 -4
- package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +19 -7
- 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/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 +4 -4
- 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}/library_photos/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,607 @@
|
|
|
1
|
+
// file_description: internal reusable component for roles-permissions matrix with data table
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
|
|
5
|
+
// section: imports
|
|
6
|
+
import { useState, useEffect, useMemo } from "react";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
} from "@/components/ui/table";
|
|
17
|
+
import {
|
|
18
|
+
Dialog,
|
|
19
|
+
DialogContent,
|
|
20
|
+
DialogDescription,
|
|
21
|
+
DialogFooter,
|
|
22
|
+
DialogHeader,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
} from "@/components/ui/dialog";
|
|
25
|
+
import { Input } from "@/components/ui/input";
|
|
26
|
+
import { Label } from "@/components/ui/label";
|
|
27
|
+
import { Plus, Save, Loader2, CircleCheck, CircleX } from "lucide-react";
|
|
28
|
+
import { toast } from "sonner";
|
|
29
|
+
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
|
30
|
+
|
|
31
|
+
// section: types
|
|
32
|
+
export type RolesMatrixData = {
|
|
33
|
+
roles: Array<{
|
|
34
|
+
role_id?: number; // undefined for new roles
|
|
35
|
+
role_name: string;
|
|
36
|
+
selected: boolean; // if role_name_selection_enabled
|
|
37
|
+
permissions: string[]; // permission names
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type RolesMatrixProps = {
|
|
42
|
+
add_button_enabled?: boolean;
|
|
43
|
+
role_name_selection_enabled?: boolean;
|
|
44
|
+
permissions_read_only?: boolean; // If true, permission checkboxes are disabled/read-only
|
|
45
|
+
show_save_cancel?: boolean; // If true, show Save and Cancel buttons
|
|
46
|
+
user_id?: string; // If provided, show user info and pre-check roles assigned to user
|
|
47
|
+
onSave?: (data: RolesMatrixData) => void;
|
|
48
|
+
onCancel?: () => void; // Callback when Cancel button is pressed
|
|
49
|
+
onRoleSelection?: (role_id: number, role_name: string) => void; // Callback when a role is selected (for assignment mode)
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// section: component
|
|
54
|
+
/**
|
|
55
|
+
* Roles matrix component - reusable internal component for roles-permissions matrix
|
|
56
|
+
* Shows data table with permissions as columns and roles as rows
|
|
57
|
+
* Checkboxes in cells indicate role-permission mappings
|
|
58
|
+
* Changes are stored locally and only saved when Save button is pressed
|
|
59
|
+
* @param props - Component props including button enable flags and save callback
|
|
60
|
+
* @returns Roles matrix component
|
|
61
|
+
*/
|
|
62
|
+
export function RolesMatrix({
|
|
63
|
+
add_button_enabled = true,
|
|
64
|
+
role_name_selection_enabled = true,
|
|
65
|
+
permissions_read_only = false,
|
|
66
|
+
show_save_cancel = true,
|
|
67
|
+
user_id,
|
|
68
|
+
onSave,
|
|
69
|
+
onCancel,
|
|
70
|
+
onRoleSelection,
|
|
71
|
+
className,
|
|
72
|
+
}: RolesMatrixProps) {
|
|
73
|
+
const [roles, setRoles] = useState<Array<{
|
|
74
|
+
role_id?: number;
|
|
75
|
+
role_name: string;
|
|
76
|
+
selected: boolean;
|
|
77
|
+
permissions: Set<string>;
|
|
78
|
+
}>>([]);
|
|
79
|
+
const [originalRoles, setOriginalRoles] = useState<Array<{
|
|
80
|
+
role_id?: number;
|
|
81
|
+
role_name: string;
|
|
82
|
+
selected: boolean;
|
|
83
|
+
permissions: Set<string>;
|
|
84
|
+
}>>([]);
|
|
85
|
+
const [permissions, setPermissions] = useState<string[]>([]);
|
|
86
|
+
const [loading, setLoading] = useState(true);
|
|
87
|
+
const [saving, setSaving] = useState(false);
|
|
88
|
+
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
|
89
|
+
const [newRoleName, setNewRoleName] = useState("");
|
|
90
|
+
const [userInfo, setUserInfo] = useState<{
|
|
91
|
+
name: string | null;
|
|
92
|
+
email_address: string;
|
|
93
|
+
profile_picture_url: string | null;
|
|
94
|
+
} | null>(null);
|
|
95
|
+
const [userRoleIds, setUserRoleIds] = useState<number[]>([]);
|
|
96
|
+
|
|
97
|
+
// Load roles and permissions on mount
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const loadData = async () => {
|
|
100
|
+
setLoading(true);
|
|
101
|
+
try {
|
|
102
|
+
// Load roles and permissions
|
|
103
|
+
const roles_response = await fetch("/api/hazo_auth/user_management/roles");
|
|
104
|
+
const roles_data = await roles_response.json();
|
|
105
|
+
|
|
106
|
+
if (!roles_data.success) {
|
|
107
|
+
toast.error("Failed to load roles and permissions");
|
|
108
|
+
setLoading(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setPermissions(roles_data.permissions.map((p: { permission_name: string }) => p.permission_name));
|
|
113
|
+
|
|
114
|
+
// Initialize roles with permissions as Sets
|
|
115
|
+
const roles_with_permissions = roles_data.roles.map((role: {
|
|
116
|
+
role_id: number;
|
|
117
|
+
role_name: string;
|
|
118
|
+
permissions: string[];
|
|
119
|
+
}) => ({
|
|
120
|
+
role_id: role.role_id,
|
|
121
|
+
role_name: role.role_name,
|
|
122
|
+
selected: false,
|
|
123
|
+
permissions: new Set(role.permissions),
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// Store original state for cancel functionality
|
|
127
|
+
const original_roles = roles_data.roles.map((role: {
|
|
128
|
+
role_id: number;
|
|
129
|
+
role_name: string;
|
|
130
|
+
permissions: string[];
|
|
131
|
+
}) => ({
|
|
132
|
+
role_id: role.role_id,
|
|
133
|
+
role_name: role.role_name,
|
|
134
|
+
selected: false,
|
|
135
|
+
permissions: new Set(role.permissions),
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
// If user_id is provided, load user info and user roles
|
|
139
|
+
if (user_id) {
|
|
140
|
+
// Load user info
|
|
141
|
+
const user_response = await fetch(`/api/hazo_auth/user_management/users?id=${user_id}`);
|
|
142
|
+
const user_data = await user_response.json();
|
|
143
|
+
|
|
144
|
+
if (user_data.success && Array.isArray(user_data.users) && user_data.users.length > 0) {
|
|
145
|
+
const user = user_data.users[0];
|
|
146
|
+
setUserInfo({
|
|
147
|
+
name: user.name || null,
|
|
148
|
+
email_address: user.email_address,
|
|
149
|
+
profile_picture_url: user.profile_picture_url || null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Load user roles
|
|
154
|
+
const user_roles_response = await fetch(`/api/hazo_auth/user_management/users/roles?user_id=${user_id}`);
|
|
155
|
+
const user_roles_data = await user_roles_response.json();
|
|
156
|
+
|
|
157
|
+
if (user_roles_data.success && Array.isArray(user_roles_data.role_ids)) {
|
|
158
|
+
setUserRoleIds(user_roles_data.role_ids);
|
|
159
|
+
|
|
160
|
+
// Pre-check roles that are assigned to the user
|
|
161
|
+
roles_with_permissions.forEach((role: {
|
|
162
|
+
role_id?: number;
|
|
163
|
+
role_name: string;
|
|
164
|
+
selected: boolean;
|
|
165
|
+
permissions: Set<string>;
|
|
166
|
+
}) => {
|
|
167
|
+
if (role.role_id !== undefined && user_roles_data.role_ids.includes(role.role_id)) {
|
|
168
|
+
role.selected = true;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Also update original roles
|
|
173
|
+
original_roles.forEach((role: {
|
|
174
|
+
role_id?: number;
|
|
175
|
+
role_name: string;
|
|
176
|
+
selected: boolean;
|
|
177
|
+
permissions: Set<string>;
|
|
178
|
+
}) => {
|
|
179
|
+
if (role.role_id !== undefined && user_roles_data.role_ids.includes(role.role_id)) {
|
|
180
|
+
role.selected = true;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setRoles(roles_with_permissions);
|
|
187
|
+
setOriginalRoles(original_roles);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
toast.error("Failed to load roles and permissions");
|
|
190
|
+
} finally {
|
|
191
|
+
setLoading(false);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
void loadData();
|
|
196
|
+
}, [user_id]);
|
|
197
|
+
|
|
198
|
+
// Handle checkbox change for role-permission mapping
|
|
199
|
+
const handlePermissionToggle = (role_index: number, permission_name: string) => {
|
|
200
|
+
setRoles((prev) => {
|
|
201
|
+
const updated = [...prev];
|
|
202
|
+
const role = { ...updated[role_index] };
|
|
203
|
+
const new_permissions = new Set(role.permissions);
|
|
204
|
+
|
|
205
|
+
if (new_permissions.has(permission_name)) {
|
|
206
|
+
new_permissions.delete(permission_name);
|
|
207
|
+
} else {
|
|
208
|
+
new_permissions.add(permission_name);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
role.permissions = new_permissions;
|
|
212
|
+
updated[role_index] = role;
|
|
213
|
+
return updated;
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Handle role name checkbox toggle
|
|
218
|
+
const handleRoleSelectionToggle = (role_index: number) => {
|
|
219
|
+
// Toggle selection state
|
|
220
|
+
setRoles((prev) => {
|
|
221
|
+
const updated = [...prev];
|
|
222
|
+
const updated_role = { ...updated[role_index] };
|
|
223
|
+
updated_role.selected = !updated_role.selected;
|
|
224
|
+
updated[role_index] = updated_role;
|
|
225
|
+
return updated;
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Handle add role
|
|
230
|
+
const handleAddRole = () => {
|
|
231
|
+
if (!newRoleName.trim()) {
|
|
232
|
+
toast.error("Role name is required");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if role name already exists
|
|
237
|
+
if (roles.some((r) => r.role_name.toLowerCase() === newRoleName.trim().toLowerCase())) {
|
|
238
|
+
toast.error("Role with this name already exists");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setRoles((prev) => [
|
|
243
|
+
...prev,
|
|
244
|
+
{
|
|
245
|
+
role_name: newRoleName.trim(),
|
|
246
|
+
selected: false,
|
|
247
|
+
permissions: new Set<string>(),
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
setNewRoleName("");
|
|
252
|
+
setIsAddDialogOpen(false);
|
|
253
|
+
toast.success("Role added. Don't forget to save changes.");
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Handle cancel - reset to original database state
|
|
257
|
+
const handleCancel = () => {
|
|
258
|
+
// Deep clone original roles to reset state (this removes any newly added roles without role_id)
|
|
259
|
+
const reset_roles = originalRoles.map((role) => ({
|
|
260
|
+
role_id: role.role_id,
|
|
261
|
+
role_name: role.role_name,
|
|
262
|
+
selected: role.selected,
|
|
263
|
+
permissions: new Set(role.permissions),
|
|
264
|
+
}));
|
|
265
|
+
setRoles(reset_roles);
|
|
266
|
+
toast.info("Changes cancelled. State reset to database values.");
|
|
267
|
+
|
|
268
|
+
// Call onCancel callback if provided (e.g., to close dialog)
|
|
269
|
+
if (onCancel) {
|
|
270
|
+
onCancel();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Handle save
|
|
275
|
+
const handleSave = async () => {
|
|
276
|
+
setSaving(true);
|
|
277
|
+
try {
|
|
278
|
+
// Convert Sets to arrays for JSON serialization
|
|
279
|
+
const roles_data: RolesMatrixData = {
|
|
280
|
+
roles: roles.map((role) => ({
|
|
281
|
+
role_id: role.role_id,
|
|
282
|
+
role_name: role.role_name,
|
|
283
|
+
selected: role.selected,
|
|
284
|
+
permissions: Array.from(role.permissions),
|
|
285
|
+
})),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// If user_id is provided, save user roles instead of role-permission mappings
|
|
289
|
+
if (user_id) {
|
|
290
|
+
// Get selected role IDs
|
|
291
|
+
const selected_role_ids = roles
|
|
292
|
+
.filter((role) => role.selected && role.role_id !== undefined)
|
|
293
|
+
.map((role) => role.role_id as number);
|
|
294
|
+
|
|
295
|
+
// Update user roles via API
|
|
296
|
+
const response = await fetch("/api/hazo_auth/user_management/users/roles", {
|
|
297
|
+
method: "PUT",
|
|
298
|
+
headers: {
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
},
|
|
301
|
+
body: JSON.stringify({
|
|
302
|
+
user_id,
|
|
303
|
+
role_ids: selected_role_ids,
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const data = await response.json();
|
|
308
|
+
|
|
309
|
+
if (data.success) {
|
|
310
|
+
toast.success("User roles updated successfully");
|
|
311
|
+
|
|
312
|
+
// Update original state to reflect saved changes
|
|
313
|
+
const updated_original_roles = originalRoles.map((role) => ({
|
|
314
|
+
...role,
|
|
315
|
+
selected: role.role_id !== undefined && selected_role_ids.includes(role.role_id),
|
|
316
|
+
}));
|
|
317
|
+
setOriginalRoles(updated_original_roles);
|
|
318
|
+
setUserRoleIds(selected_role_ids);
|
|
319
|
+
} else {
|
|
320
|
+
toast.error(data.error || "Failed to update user roles");
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Save role-permission mappings (original behavior)
|
|
324
|
+
// Call onSave callback if provided
|
|
325
|
+
if (onSave) {
|
|
326
|
+
onSave(roles_data);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Save to API
|
|
330
|
+
const response = await fetch("/api/hazo_auth/user_management/roles", {
|
|
331
|
+
method: "PUT",
|
|
332
|
+
headers: {
|
|
333
|
+
"Content-Type": "application/json",
|
|
334
|
+
},
|
|
335
|
+
body: JSON.stringify(roles_data),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const data = await response.json();
|
|
339
|
+
|
|
340
|
+
if (data.success) {
|
|
341
|
+
toast.success("Roles and permissions saved successfully");
|
|
342
|
+
|
|
343
|
+
// Reload data to get updated role IDs
|
|
344
|
+
const reload_response = await fetch("/api/hazo_auth/user_management/roles");
|
|
345
|
+
const reload_data = await reload_response.json();
|
|
346
|
+
|
|
347
|
+
if (reload_data.success) {
|
|
348
|
+
const updated_roles = reload_data.roles.map((role: {
|
|
349
|
+
role_id: number;
|
|
350
|
+
role_name: string;
|
|
351
|
+
permissions: string[];
|
|
352
|
+
}) => ({
|
|
353
|
+
role_id: role.role_id,
|
|
354
|
+
role_name: role.role_name,
|
|
355
|
+
selected: false,
|
|
356
|
+
permissions: new Set(role.permissions),
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
// Update both current and original state after save
|
|
360
|
+
setRoles(updated_roles);
|
|
361
|
+
setOriginalRoles(updated_roles.map((r: {
|
|
362
|
+
role_id?: number;
|
|
363
|
+
role_name: string;
|
|
364
|
+
selected: boolean;
|
|
365
|
+
permissions: Set<string>;
|
|
366
|
+
}) => ({
|
|
367
|
+
role_id: r.role_id,
|
|
368
|
+
role_name: r.role_name,
|
|
369
|
+
selected: r.selected,
|
|
370
|
+
permissions: new Set(r.permissions),
|
|
371
|
+
})));
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
toast.error(data.error || "Failed to save roles and permissions");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
toast.error(user_id ? "Failed to update user roles" : "Failed to save roles and permissions");
|
|
379
|
+
} finally {
|
|
380
|
+
setSaving(false);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (loading) {
|
|
385
|
+
return (
|
|
386
|
+
<div className={`cls_roles_matrix flex items-center justify-center p-8 ${className || ""}`}>
|
|
387
|
+
<Loader2 className="h-6 w-6 animate-spin text-slate-400" />
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Helper function to get user initials
|
|
393
|
+
const getUserInitials = (name: string | null, email: string): string => {
|
|
394
|
+
if (name) {
|
|
395
|
+
const parts = name.trim().split(/\s+/);
|
|
396
|
+
if (parts.length >= 2) {
|
|
397
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
398
|
+
}
|
|
399
|
+
return name.substring(0, 2).toUpperCase();
|
|
400
|
+
}
|
|
401
|
+
return email.substring(0, 2).toUpperCase();
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div className={`cls_roles_matrix flex flex-col gap-4 w-full ${className || ""}`}>
|
|
406
|
+
{/* User Info Section - only show when user_id is provided */}
|
|
407
|
+
{user_id && userInfo && (
|
|
408
|
+
<div className="cls_roles_matrix_user_info flex items-center gap-4 p-4 border rounded-lg bg-muted/50">
|
|
409
|
+
<Avatar className="cls_roles_matrix_user_avatar h-12 w-12">
|
|
410
|
+
<AvatarImage
|
|
411
|
+
src={userInfo.profile_picture_url || undefined}
|
|
412
|
+
alt={userInfo.name ? `Profile picture of ${userInfo.name}` : "Profile picture"}
|
|
413
|
+
className="cls_roles_matrix_user_avatar_image"
|
|
414
|
+
/>
|
|
415
|
+
<AvatarFallback className="cls_roles_matrix_user_avatar_fallback bg-slate-200 text-slate-600">
|
|
416
|
+
{getUserInitials(userInfo.name, userInfo.email_address)}
|
|
417
|
+
</AvatarFallback>
|
|
418
|
+
</Avatar>
|
|
419
|
+
<div className="cls_roles_matrix_user_info_details flex flex-col">
|
|
420
|
+
<span className="cls_roles_matrix_user_name font-semibold text-lg">
|
|
421
|
+
{userInfo.name || userInfo.email_address}
|
|
422
|
+
</span>
|
|
423
|
+
{userInfo.name && (
|
|
424
|
+
<span className="cls_roles_matrix_user_email text-sm text-muted-foreground">
|
|
425
|
+
{userInfo.email_address}
|
|
426
|
+
</span>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{/* Header with Add Role button */}
|
|
433
|
+
<div className="cls_roles_matrix_header flex items-center justify-between">
|
|
434
|
+
<div className="cls_roles_matrix_header_left">
|
|
435
|
+
{add_button_enabled && (
|
|
436
|
+
<Button
|
|
437
|
+
onClick={() => setIsAddDialogOpen(true)}
|
|
438
|
+
variant="default"
|
|
439
|
+
size="sm"
|
|
440
|
+
className="cls_roles_matrix_add_button"
|
|
441
|
+
>
|
|
442
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
443
|
+
Add Role
|
|
444
|
+
</Button>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{/* Data table */}
|
|
450
|
+
<div className="cls_roles_matrix_table_container border rounded-lg overflow-auto w-full">
|
|
451
|
+
<Table className="cls_roles_matrix_table w-full">
|
|
452
|
+
<TableHeader className="cls_roles_matrix_table_header">
|
|
453
|
+
<TableRow className="cls_roles_matrix_table_header_row">
|
|
454
|
+
{role_name_selection_enabled && (
|
|
455
|
+
<TableHead className="cls_roles_matrix_table_header_role_checkbox w-12">
|
|
456
|
+
{/* Empty header for role checkbox column */}
|
|
457
|
+
</TableHead>
|
|
458
|
+
)}
|
|
459
|
+
<TableHead className="cls_roles_matrix_table_header_role_name">
|
|
460
|
+
Role Name
|
|
461
|
+
</TableHead>
|
|
462
|
+
{permissions.map((permission_name) => (
|
|
463
|
+
<TableHead
|
|
464
|
+
key={permission_name}
|
|
465
|
+
className="cls_roles_matrix_table_header_permission text-center"
|
|
466
|
+
>
|
|
467
|
+
{permission_name}
|
|
468
|
+
</TableHead>
|
|
469
|
+
))}
|
|
470
|
+
</TableRow>
|
|
471
|
+
</TableHeader>
|
|
472
|
+
<TableBody className="cls_roles_matrix_table_body">
|
|
473
|
+
{roles.length === 0 ? (
|
|
474
|
+
<TableRow className="cls_roles_matrix_table_row_empty">
|
|
475
|
+
<TableCell
|
|
476
|
+
colSpan={permissions.length + (role_name_selection_enabled ? 2 : 1)}
|
|
477
|
+
className="text-center text-muted-foreground py-8"
|
|
478
|
+
>
|
|
479
|
+
No roles found. Add a role to get started.
|
|
480
|
+
</TableCell>
|
|
481
|
+
</TableRow>
|
|
482
|
+
) : (
|
|
483
|
+
roles.map((role, role_index) => (
|
|
484
|
+
<TableRow key={role_index} className="cls_roles_matrix_table_row">
|
|
485
|
+
{role_name_selection_enabled && (
|
|
486
|
+
<TableCell className="cls_roles_matrix_table_cell_role_checkbox text-center">
|
|
487
|
+
<div className="cls_roles_matrix_role_checkbox_wrapper flex items-center justify-center">
|
|
488
|
+
<Checkbox
|
|
489
|
+
checked={role.selected}
|
|
490
|
+
onCheckedChange={() => handleRoleSelectionToggle(role_index)}
|
|
491
|
+
className="cls_roles_matrix_role_checkbox"
|
|
492
|
+
/>
|
|
493
|
+
</div>
|
|
494
|
+
</TableCell>
|
|
495
|
+
)}
|
|
496
|
+
<TableCell className="cls_roles_matrix_table_cell_role_name font-medium">
|
|
497
|
+
{role.role_name}
|
|
498
|
+
</TableCell>
|
|
499
|
+
{permissions.map((permission_name) => (
|
|
500
|
+
<TableCell
|
|
501
|
+
key={permission_name}
|
|
502
|
+
className="cls_roles_matrix_table_cell_permission text-center"
|
|
503
|
+
>
|
|
504
|
+
<div className="cls_roles_matrix_permission_checkbox_wrapper flex items-center justify-center">
|
|
505
|
+
<Checkbox
|
|
506
|
+
checked={role.permissions.has(permission_name)}
|
|
507
|
+
onCheckedChange={() => handlePermissionToggle(role_index, permission_name)}
|
|
508
|
+
disabled={permissions_read_only}
|
|
509
|
+
className="cls_roles_matrix_permission_checkbox"
|
|
510
|
+
/>
|
|
511
|
+
</div>
|
|
512
|
+
</TableCell>
|
|
513
|
+
))}
|
|
514
|
+
</TableRow>
|
|
515
|
+
))
|
|
516
|
+
)}
|
|
517
|
+
</TableBody>
|
|
518
|
+
</Table>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
{/* Footer with Save and Cancel buttons - show based on show_save_cancel prop */}
|
|
522
|
+
{show_save_cancel && (
|
|
523
|
+
<div className="cls_roles_matrix_footer flex items-center justify-end gap-2">
|
|
524
|
+
<Button
|
|
525
|
+
onClick={handleSave}
|
|
526
|
+
disabled={saving}
|
|
527
|
+
variant="default"
|
|
528
|
+
size="sm"
|
|
529
|
+
className="cls_roles_matrix_save_button"
|
|
530
|
+
>
|
|
531
|
+
{saving ? (
|
|
532
|
+
<>
|
|
533
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
534
|
+
Saving...
|
|
535
|
+
</>
|
|
536
|
+
) : (
|
|
537
|
+
<>
|
|
538
|
+
<CircleCheck className="h-4 w-4 mr-2" />
|
|
539
|
+
Save
|
|
540
|
+
</>
|
|
541
|
+
)}
|
|
542
|
+
</Button>
|
|
543
|
+
<Button
|
|
544
|
+
onClick={handleCancel}
|
|
545
|
+
variant="outline"
|
|
546
|
+
size="sm"
|
|
547
|
+
className="cls_roles_matrix_cancel_button"
|
|
548
|
+
>
|
|
549
|
+
<CircleX className="h-4 w-4 mr-2" />
|
|
550
|
+
Cancel
|
|
551
|
+
</Button>
|
|
552
|
+
</div>
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
{/* Add Role Dialog */}
|
|
556
|
+
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
|
557
|
+
<DialogContent className="cls_roles_matrix_add_dialog">
|
|
558
|
+
<DialogHeader>
|
|
559
|
+
<DialogTitle>Add New Role</DialogTitle>
|
|
560
|
+
<DialogDescription>
|
|
561
|
+
Enter a name for the new role. You can assign permissions after creating the role.
|
|
562
|
+
</DialogDescription>
|
|
563
|
+
</DialogHeader>
|
|
564
|
+
<div className="cls_roles_matrix_add_dialog_content flex flex-col gap-4 py-4">
|
|
565
|
+
<div className="cls_roles_matrix_add_dialog_field flex flex-col gap-2">
|
|
566
|
+
<Label htmlFor="role_name" className="cls_roles_matrix_add_dialog_label">
|
|
567
|
+
Role Name
|
|
568
|
+
</Label>
|
|
569
|
+
<Input
|
|
570
|
+
id="role_name"
|
|
571
|
+
value={newRoleName}
|
|
572
|
+
onChange={(e) => setNewRoleName(e.target.value)}
|
|
573
|
+
placeholder="Enter role name"
|
|
574
|
+
className="cls_roles_matrix_add_dialog_input"
|
|
575
|
+
onKeyDown={(e) => {
|
|
576
|
+
if (e.key === "Enter") {
|
|
577
|
+
handleAddRole();
|
|
578
|
+
}
|
|
579
|
+
}}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
<DialogFooter className="cls_roles_matrix_add_dialog_footer">
|
|
584
|
+
<Button
|
|
585
|
+
onClick={handleAddRole}
|
|
586
|
+
variant="default"
|
|
587
|
+
className="cls_roles_matrix_add_dialog_save"
|
|
588
|
+
>
|
|
589
|
+
Add Role
|
|
590
|
+
</Button>
|
|
591
|
+
<Button
|
|
592
|
+
onClick={() => {
|
|
593
|
+
setIsAddDialogOpen(false);
|
|
594
|
+
setNewRoleName("");
|
|
595
|
+
}}
|
|
596
|
+
variant="outline"
|
|
597
|
+
className="cls_roles_matrix_add_dialog_cancel"
|
|
598
|
+
>
|
|
599
|
+
Cancel
|
|
600
|
+
</Button>
|
|
601
|
+
</DialogFooter>
|
|
602
|
+
</DialogContent>
|
|
603
|
+
</Dialog>
|
|
604
|
+
</div>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|