rbac-manager 0.0.0-stage → 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.
Files changed (67) hide show
  1. package/.github/workflows/publish.yml +29 -0
  2. package/README.md +317 -2
  3. package/package.json +45 -4
  4. package/scripts/postbuild.js +17 -0
  5. package/src/config/rbac.config.ts +206 -0
  6. package/src/constants/actions.constants.ts +21 -0
  7. package/src/constants/apiKeys.constants.ts +25 -0
  8. package/src/constants/auth.constants.ts +39 -0
  9. package/src/constants/endpoints.constants.ts +66 -0
  10. package/src/constants/i18n.constants.ts +82 -0
  11. package/src/constants/index.ts +70 -0
  12. package/src/constants/masters.constants.ts +68 -0
  13. package/src/constants/permissions/index.ts +44 -0
  14. package/src/constants/permissions/masterPermissions.constants.ts +44 -0
  15. package/src/constants/permissions/rolePermissions.constants.ts +18 -0
  16. package/src/constants/permissions.constants.ts +34 -0
  17. package/src/constants/roles.constants.ts +34 -0
  18. package/src/core/adapters/authAdapter.ts +89 -0
  19. package/src/core/adapters/index.ts +17 -0
  20. package/src/core/adapters/mastersAdapter.ts +132 -0
  21. package/src/core/adapters/permissionsAdapter.ts +90 -0
  22. package/src/core/adapters/rolesAdapter.ts +115 -0
  23. package/src/core/api/httpClient.ts +296 -0
  24. package/src/core/context/PermissionContext.tsx +232 -0
  25. package/src/core/context/RBACConfigContext.tsx +52 -0
  26. package/src/core/guards/Can.tsx +98 -0
  27. package/src/core/guards/RouteGuard.tsx +42 -0
  28. package/src/core/hooks/useMaster.ts +99 -0
  29. package/src/core/hooks/usePermissions.ts +114 -0
  30. package/src/core/hooks/useRoles.ts +120 -0
  31. package/src/core/types/adapter.types.ts +54 -0
  32. package/src/core/types/auth.types.ts +44 -0
  33. package/src/core/types/index.ts +7 -0
  34. package/src/core/types/masters.types.ts +63 -0
  35. package/src/core/types/pagination.types.ts +37 -0
  36. package/src/core/types/rbac.types.ts +69 -0
  37. package/src/core/types/router.types.ts +43 -0
  38. package/src/core/types/style.types.ts +53 -0
  39. package/src/core/utils/permissionResolver.ts +44 -0
  40. package/src/index.ts +44 -0
  41. package/src/modules/masters/DepartmentMasterView.tsx +126 -0
  42. package/src/modules/masters/DesignationMasterView.tsx +97 -0
  43. package/src/modules/masters/EmployeeMasterView.tsx +107 -0
  44. package/src/modules/masters/EntityMasterView.tsx +102 -0
  45. package/src/modules/masters/ModuleMasterView.tsx +104 -0
  46. package/src/modules/masters/SubModuleMasterView.tsx +106 -0
  47. package/src/modules/masters/components/MasterDataTable.tsx +684 -0
  48. package/src/modules/masters/index.ts +7 -0
  49. package/src/modules/roles-and-permissions/components/PermissionMatrix.tsx +289 -0
  50. package/src/modules/roles-and-permissions/components/PermissionRegistry.tsx +108 -0
  51. package/src/modules/roles-and-permissions/components/RoleListTable.tsx +568 -0
  52. package/src/modules/roles-and-permissions/components/RoleModal.tsx +213 -0
  53. package/src/modules/roles-and-permissions/index.ts +4 -0
  54. package/src/shared/components/Badge.tsx +24 -0
  55. package/src/shared/components/Button.tsx +75 -0
  56. package/src/shared/components/DynamicSidebar.tsx +181 -0
  57. package/src/shared/components/Input.tsx +48 -0
  58. package/src/shared/components/Modal.tsx +70 -0
  59. package/src/shared/components/Table.tsx +345 -0
  60. package/src/styles/standalone.css +51 -0
  61. package/src/theme/ThemeContext.tsx +74 -0
  62. package/src/theme/defaultTheme.ts +81 -0
  63. package/src/theme/index.ts +3 -0
  64. package/src/theme/theme.types.ts +106 -0
  65. package/tsconfig.cjs.json +10 -0
  66. package/tsconfig.esm.json +10 -0
  67. package/tsconfig.json +28 -0
@@ -0,0 +1,232 @@
1
+ import React, { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react';
2
+ import { useRBACConfig } from './RBACConfigContext';
3
+ import { StandardUser, StandardNavigationItem, LoginCredentials } from '../types';
4
+
5
+ export interface PermissionContextValue {
6
+ user: StandardUser | null;
7
+ permissions: Set<string>;
8
+ navigation: StandardNavigationItem[];
9
+ isLoading: boolean;
10
+ isAuthenticated: boolean;
11
+ error: Error | null;
12
+ activeTenantId: string | number | null;
13
+ setTenantId: (id: string | number | null) => void;
14
+ hasPermission: (permissionKey: string) => boolean;
15
+ canNavigate: (routePath: string) => boolean;
16
+ login: (credentials: LoginCredentials) => Promise<any>;
17
+ logout: () => Promise<void>;
18
+ refresh: () => Promise<void>;
19
+ }
20
+
21
+ export interface PermissionProviderProps {
22
+ children: React.ReactNode;
23
+ autoFetch?: boolean;
24
+ // SSR Hydration Props to eliminate FOUC (Flash of Unauthorized Content)
25
+ initialUser?: StandardUser | null;
26
+ initialPermissions?: string[] | Set<string>;
27
+ initialNavigation?: StandardNavigationItem[];
28
+ initialTenantId?: string | number | null;
29
+ }
30
+
31
+ const PermissionContext = createContext<PermissionContextValue | null>(null);
32
+
33
+ export const PermissionProvider: React.FC<PermissionProviderProps> = ({
34
+ children,
35
+ autoFetch = true,
36
+ initialUser = null,
37
+ initialPermissions,
38
+ initialNavigation = [],
39
+ initialTenantId = null,
40
+ }) => {
41
+ const { config, http } = useRBACConfig();
42
+
43
+ const [user, setUser] = useState<StandardUser | null>(initialUser);
44
+ const [permissions, setPermissions] = useState<Set<string>>(() => {
45
+ if (!initialPermissions) return new Set();
46
+ return initialPermissions instanceof Set ? initialPermissions : new Set(initialPermissions);
47
+ });
48
+ const [routePaths, setRoutePaths] = useState<Set<string>>(new Set());
49
+ const [navigation, setNavigation] = useState<StandardNavigationItem[]>(initialNavigation);
50
+ const [activeTenantId, setActiveTenantId] = useState<string | number | null>(initialTenantId);
51
+
52
+ // If initial user or permissions are provided by SSR, start with isLoading = false!
53
+ const [isLoading, setIsLoading] = useState<boolean>(() => !initialUser && !initialPermissions && autoFetch);
54
+ const [error, setError] = useState<Error | null>(null);
55
+
56
+ const fetchAuthMe = useCallback(async () => {
57
+ setIsLoading(true);
58
+ setError(null);
59
+ try {
60
+ const raw = await http.get(config.endpoints.AUTH_ME);
61
+ const transformed = config.adapters.auth.transformAuthMeResponse(raw);
62
+
63
+ setUser(transformed.user);
64
+
65
+ const permKeys = new Set<string>();
66
+ const routes = new Set<string>();
67
+
68
+ transformed.permissions.forEach((p) => {
69
+ if (p.permissionKey) permKeys.add(p.permissionKey);
70
+ if (p.routePath) routes.add(p.routePath);
71
+ });
72
+
73
+ setPermissions(permKeys);
74
+ setRoutePaths(routes);
75
+ setNavigation(transformed.navigation);
76
+ } catch (err: any) {
77
+ setUser(null);
78
+ setPermissions(new Set());
79
+ setRoutePaths(new Set());
80
+ setNavigation([]);
81
+ setError(err instanceof Error ? err : new Error(String(err)));
82
+ } finally {
83
+ setIsLoading(false);
84
+ }
85
+ }, [config.adapters.auth, config.endpoints.AUTH_ME, http]);
86
+
87
+ useEffect(() => {
88
+ if (autoFetch && !initialUser) {
89
+ fetchAuthMe();
90
+ }
91
+ }, [autoFetch, fetchAuthMe, initialUser]);
92
+
93
+ const setTenantId = useCallback(
94
+ (newTenantId: string | number | null) => {
95
+ setActiveTenantId(newTenantId);
96
+ // Re-fetch permissions for the newly switched tenant
97
+ fetchAuthMe();
98
+ },
99
+ [fetchAuthMe]
100
+ );
101
+
102
+ const hasPermission = useCallback(
103
+ (permissionKey: string): boolean => {
104
+ if (!permissionKey) return true;
105
+ if (permissions.has('*') || permissions.has('all')) return true;
106
+
107
+ // 1. Direct match
108
+ if (permissions.has(permissionKey)) return true;
109
+
110
+ // 2. Check alias map from config (e.g. 'roles.add' -> 'roles.addition')
111
+ const aliasedKey = config.permissionAliases[permissionKey];
112
+ if (aliasedKey && permissions.has(aliasedKey)) return true;
113
+
114
+ // 3. Reverse alias check (if user passed 'roles.addition', check if 'roles.add' is in set)
115
+ for (const [standardKey, mappedKey] of Object.entries(config.permissionAliases)) {
116
+ if (mappedKey === permissionKey && permissions.has(standardKey)) return true;
117
+ }
118
+
119
+ // 4. Wildcard matching (e.g. "roles.*" or "department_master.*")
120
+ const parts = permissionKey.split(/[.:_]/);
121
+ if (parts.length > 1) {
122
+ const wildcardDot = `${parts[0]}.*`;
123
+ const wildcardColon = `${parts[0]}:*`;
124
+ if (permissions.has(wildcardDot) || permissions.has(wildcardColon)) return true;
125
+ }
126
+
127
+ return false;
128
+ },
129
+ [config.permissionAliases, permissions]
130
+ );
131
+
132
+ const canNavigate = useCallback(
133
+ (routePath: string): boolean => {
134
+ if (!routePath) return true;
135
+ if (permissions.has('*')) return true;
136
+ if (routePaths.has(routePath)) return true;
137
+
138
+ // Allow matching paths with dynamic parameters e.g. /masters/entity-master/edit/[id]
139
+ for (const allowedRoute of routePaths) {
140
+ if (allowedRoute === routePath) return true;
141
+ if (allowedRoute.includes('[') || allowedRoute.includes(':')) {
142
+ const regexStr = '^' + allowedRoute.replace(/\[\w+\]|:\w+/g, '[^/]+') + '$';
143
+ if (new RegExp(regexStr).test(routePath)) return true;
144
+ }
145
+ }
146
+ return false;
147
+ },
148
+ [permissions, routePaths]
149
+ );
150
+
151
+ const login = useCallback(
152
+ async (credentials: LoginCredentials) => {
153
+ setIsLoading(true);
154
+ try {
155
+ const payload = config.adapters.auth.transformLoginPayload(credentials);
156
+ const raw = await http.post(config.endpoints.AUTH_LOGIN, payload, { skipAuthRedirect: true });
157
+ const res = config.adapters.auth.transformLoginResponse(raw);
158
+
159
+ if (res.token && typeof window !== 'undefined') {
160
+ localStorage.setItem('token', res.token);
161
+ }
162
+
163
+ await fetchAuthMe();
164
+ return res;
165
+ } catch (err: any) {
166
+ setError(err);
167
+ throw err;
168
+ } finally {
169
+ setIsLoading(false);
170
+ }
171
+ },
172
+ [config.adapters.auth, config.endpoints.AUTH_LOGIN, fetchAuthMe, http]
173
+ );
174
+
175
+ const logout = useCallback(async () => {
176
+ setIsLoading(true);
177
+ try {
178
+ await http.delete(config.endpoints.AUTH_LOGOUT).catch(() => {});
179
+ if (typeof window !== 'undefined') {
180
+ localStorage.removeItem('token');
181
+ }
182
+ setUser(null);
183
+ setPermissions(new Set());
184
+ setRoutePaths(new Set());
185
+ setNavigation([]);
186
+ } finally {
187
+ setIsLoading(false);
188
+ }
189
+ }, [config.endpoints.AUTH_LOGOUT, http]);
190
+
191
+ const value = useMemo<PermissionContextValue>(
192
+ () => ({
193
+ user,
194
+ permissions,
195
+ navigation,
196
+ isLoading,
197
+ isAuthenticated: Boolean(user),
198
+ error,
199
+ activeTenantId,
200
+ setTenantId,
201
+ hasPermission,
202
+ canNavigate,
203
+ login,
204
+ logout,
205
+ refresh: fetchAuthMe,
206
+ }),
207
+ [
208
+ user,
209
+ permissions,
210
+ navigation,
211
+ isLoading,
212
+ error,
213
+ activeTenantId,
214
+ setTenantId,
215
+ hasPermission,
216
+ canNavigate,
217
+ login,
218
+ logout,
219
+ fetchAuthMe,
220
+ ]
221
+ );
222
+
223
+ return <PermissionContext.Provider value={value}>{children}</PermissionContext.Provider>;
224
+ };
225
+
226
+ export function usePermission(): PermissionContextValue {
227
+ const context = useContext(PermissionContext);
228
+ if (!context) {
229
+ throw new Error('usePermission must be used within a PermissionProvider');
230
+ }
231
+ return context;
232
+ }
@@ -0,0 +1,52 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { RBACConfig, defaultRBACConfig } from '../../config/rbac.config';
3
+ import { HttpClient } from '../api/httpClient';
4
+ import { ThemeProvider } from '../../theme/ThemeContext';
5
+
6
+ interface RBACConfigContextValue {
7
+ config: RBACConfig;
8
+ http: HttpClient;
9
+ }
10
+
11
+ const RBACConfigContext = createContext<RBACConfigContextValue | null>(null);
12
+
13
+ export interface RBACConfigProviderProps {
14
+ config?: RBACConfig;
15
+ children: React.ReactNode;
16
+ }
17
+
18
+ export const RBACConfigProvider: React.FC<RBACConfigProviderProps> = ({ config = defaultRBACConfig, children }) => {
19
+ const http = useMemo(() => {
20
+ return new HttpClient({
21
+ baseUrl: config.apiBaseUrl,
22
+ getToken: config.getToken,
23
+ getTenantId: config.getTenantId,
24
+ getCustomHeaders: config.getCustomHeaders,
25
+ refreshToken: config.refreshToken,
26
+ onUnauthorized: config.onUnauthorized,
27
+ });
28
+ }, [
29
+ config.apiBaseUrl,
30
+ config.getToken,
31
+ config.getTenantId,
32
+ config.getCustomHeaders,
33
+ config.refreshToken,
34
+ config.onUnauthorized,
35
+ ]);
36
+
37
+ const value = useMemo(() => ({ config, http }), [config, http]);
38
+
39
+ return (
40
+ <RBACConfigContext.Provider value={value}>
41
+ <ThemeProvider theme={config.theme}>{children}</ThemeProvider>
42
+ </RBACConfigContext.Provider>
43
+ );
44
+ };
45
+
46
+ export function useRBACConfig(): RBACConfigContextValue {
47
+ const context = useContext(RBACConfigContext);
48
+ if (!context) {
49
+ throw new Error('useRBACConfig must be used within an RBACConfigProvider');
50
+ }
51
+ return context;
52
+ }
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import { usePermission } from '../context/PermissionContext';
3
+ import { useRBACConfig } from '../context/RBACConfigContext';
4
+ import { resolvePermission } from '../utils/permissionResolver';
5
+
6
+ export interface CanProps {
7
+ /**
8
+ * The permission key to check e.g. "roles.add", "entity_master.edit"
9
+ */
10
+ do?: string;
11
+ /**
12
+ * Alternatively specify module and action without hardcoding the slug format:
13
+ * e.g. <Can module="roles" action="add">
14
+ */
15
+ module?: string;
16
+ action?: string;
17
+ /**
18
+ * Check route permission
19
+ */
20
+ route?: string;
21
+ /**
22
+ * Multiple permissions check (all must be present)
23
+ */
24
+ all?: string[];
25
+ /**
26
+ * Multiple permissions check (any one is sufficient)
27
+ */
28
+ any?: string[];
29
+ /**
30
+ * Invert the check (render if user DOES NOT have permission)
31
+ */
32
+ not?: boolean;
33
+ /**
34
+ * Fallback UI if access denied
35
+ */
36
+ fallback?: React.ReactNode;
37
+ children: React.ReactNode;
38
+ }
39
+
40
+ export const Can: React.FC<CanProps> = ({
41
+ do: actionKey,
42
+ module: moduleKey,
43
+ action: moduleAction,
44
+ route,
45
+ all,
46
+ any,
47
+ not = false,
48
+ fallback = null,
49
+ children,
50
+ }) => {
51
+ const { hasPermission, canNavigate } = usePermission();
52
+ const { config } = useRBACConfig();
53
+
54
+ let isAllowed = true;
55
+
56
+ // Resolve permission key if module and action are supplied or if 'do' has aliases
57
+ const targetPermission = resolvePermission(
58
+ { permissionKey: actionKey, module: moduleKey, action: moduleAction },
59
+ config.permissionResolver,
60
+ config.permissionAliases
61
+ );
62
+
63
+ if (targetPermission) {
64
+ isAllowed = isAllowed && hasPermission(targetPermission);
65
+ }
66
+
67
+ if (route) {
68
+ isAllowed = isAllowed && canNavigate(route);
69
+ }
70
+
71
+ if (all && all.length > 0) {
72
+ isAllowed =
73
+ isAllowed &&
74
+ all.every((k) =>
75
+ hasPermission(
76
+ resolvePermission({ permissionKey: k }, config.permissionResolver, config.permissionAliases)
77
+ )
78
+ );
79
+ }
80
+
81
+ if (any && any.length > 0) {
82
+ isAllowed =
83
+ isAllowed &&
84
+ any.some((k) =>
85
+ hasPermission(
86
+ resolvePermission({ permissionKey: k }, config.permissionResolver, config.permissionAliases)
87
+ )
88
+ );
89
+ }
90
+
91
+ const result = not ? !isAllowed : isAllowed;
92
+
93
+ if (!result) {
94
+ return <>{fallback}</>;
95
+ }
96
+
97
+ return <>{children}</>;
98
+ };
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { usePermission } from '../context/PermissionContext';
3
+
4
+ export interface RouteGuardProps {
5
+ routePath: string;
6
+ fallback?: React.ReactNode;
7
+ loadingFallback?: React.ReactNode;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export const RouteGuard: React.FC<RouteGuardProps> = ({
12
+ routePath,
13
+ fallback = (
14
+ <div className="flex flex-col items-center justify-center min-h-[400px] p-6 text-center">
15
+ <div className="w-12 h-12 mb-4 text-red-500 rounded-full bg-red-50 flex items-center justify-center font-bold text-xl">
16
+ ✕
17
+ </div>
18
+ <h3 className="text-lg font-semibold text-gray-900">Access Denied</h3>
19
+ <p className="mt-1 text-sm text-gray-500 max-w-sm">
20
+ You do not have permission to access the route <code>{routePath}</code>.
21
+ </p>
22
+ </div>
23
+ ),
24
+ loadingFallback = (
25
+ <div className="flex min-h-[200px] items-center justify-center">
26
+ <div className="text-sm text-gray-500 animate-pulse">Checking permissions...</div>
27
+ </div>
28
+ ),
29
+ children,
30
+ }) => {
31
+ const { canNavigate, isLoading } = usePermission();
32
+
33
+ if (isLoading) {
34
+ return <>{loadingFallback}</>;
35
+ }
36
+
37
+ if (!canNavigate(routePath)) {
38
+ return <>{fallback}</>;
39
+ }
40
+
41
+ return <>{children}</>;
42
+ };
@@ -0,0 +1,99 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { useRBACConfig } from '../context/RBACConfigContext';
3
+ import { PaginatedResult } from '../types';
4
+ import { buildPaginationQueryParams } from '../types/pagination.types';
5
+
6
+ export interface UseMasterOptions<T> {
7
+ listEndpoint: string;
8
+ createEndpoint?: string;
9
+ editEndpoint?: string;
10
+ deleteEndpoint?: string;
11
+ transformer: (raw: any) => PaginatedResult<T>;
12
+ defaultLimit?: number;
13
+ }
14
+
15
+ export function useMaster<T extends { id: string | number }>(options: UseMasterOptions<T>) {
16
+ const { config, http } = useRBACConfig();
17
+ const [items, setItems] = useState<T[]>([]);
18
+ const [total, setTotal] = useState<number>(0);
19
+ const [page, setPage] = useState<number>(1);
20
+ const [limit, setLimit] = useState<number>(options.defaultLimit ?? 10);
21
+ const [isLoading, setIsLoading] = useState<boolean>(false);
22
+ const [error, setError] = useState<Error | null>(null);
23
+
24
+ const fetchItems = useCallback(
25
+ async (params?: { page?: number; limit?: number; search?: string; [key: string]: any }) => {
26
+ setIsLoading(true);
27
+ setError(null);
28
+ try {
29
+ const targetPage = params?.page ?? page;
30
+ const targetLimit = params?.limit ?? limit;
31
+
32
+ const pagParams = buildPaginationQueryParams(targetPage, targetLimit, config.pagination);
33
+ const query = {
34
+ ...pagParams,
35
+ ...(params || {}),
36
+ };
37
+ const raw = await http.get(options.listEndpoint, query);
38
+ const result = options.transformer(raw);
39
+
40
+ setItems(result.items);
41
+ setTotal(result.total);
42
+ setPage(result.page);
43
+ setLimit(result.limit);
44
+ return result;
45
+ } catch (err: any) {
46
+ setError(err);
47
+ throw err;
48
+ } finally {
49
+ setIsLoading(false);
50
+ }
51
+ },
52
+ [config.pagination, http, limit, options, page]
53
+ );
54
+
55
+ const createItem = useCallback(
56
+ async (payload: any) => {
57
+ if (!options.createEndpoint) throw new Error('Create endpoint not configured for this master');
58
+ const raw = await http.post(options.createEndpoint, payload);
59
+ await fetchItems();
60
+ return raw;
61
+ },
62
+ [fetchItems, http, options.createEndpoint]
63
+ );
64
+
65
+ const editItem = useCallback(
66
+ async (id: string | number, payload: any) => {
67
+ if (!options.editEndpoint) throw new Error('Edit endpoint not configured for this master');
68
+ const url = options.editEndpoint.replace(':id', String(id));
69
+ const raw = await http.patch(url, payload);
70
+ await fetchItems();
71
+ return raw;
72
+ },
73
+ [fetchItems, http, options.editEndpoint]
74
+ );
75
+
76
+ const deleteItem = useCallback(
77
+ async (id: string | number) => {
78
+ if (!options.deleteEndpoint) throw new Error('Delete endpoint not configured for this master');
79
+ const url = options.deleteEndpoint.replace(':id', String(id));
80
+ const raw = await http.delete(url);
81
+ await fetchItems();
82
+ return raw;
83
+ },
84
+ [fetchItems, http, options.deleteEndpoint]
85
+ );
86
+
87
+ return {
88
+ items,
89
+ total,
90
+ page,
91
+ limit,
92
+ isLoading,
93
+ error,
94
+ fetchItems,
95
+ createItem,
96
+ editItem,
97
+ deleteItem,
98
+ };
99
+ }
@@ -0,0 +1,114 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { useRBACConfig } from '../context/RBACConfigContext';
3
+ import { StandardAction, StandardModuleWithActions, RolePermissionMatrixRow, StandardPermissionActionMapping } from '../types';
4
+
5
+ export function usePermissions() {
6
+ const { config, http } = useRBACConfig();
7
+ const [actions, setActions] = useState<StandardAction[]>([]);
8
+ const [registryModules, setRegistryModules] = useState<StandardModuleWithActions[]>([]);
9
+ const [total, setTotal] = useState<number>(0);
10
+ const [page] = useState<number>(1);
11
+ const [limit] = useState<number>(50);
12
+ const [isLoading, setIsLoading] = useState<boolean>(false);
13
+ const [error, setError] = useState<Error | null>(null);
14
+
15
+ const fetchActions = useCallback(async () => {
16
+ try {
17
+ const raw = await http.get(config.endpoints.PERMISSIONS_ACTIONS);
18
+ const items = config.adapters.permissions.transformActionsListResponse(raw);
19
+ setActions(items);
20
+ return items;
21
+ } catch (err: any) {
22
+ console.warn('Failed to fetch actions list, using configured defaults', err);
23
+ return config.adapters.permissions.transformActionsListResponse([]);
24
+ }
25
+ }, [config.adapters.permissions, config.endpoints.PERMISSIONS_ACTIONS, http]);
26
+
27
+ const fetchRegistry = useCallback(
28
+ async (params?: { page?: number; limit?: number }) => {
29
+ setIsLoading(true);
30
+ setError(null);
31
+ try {
32
+ const queryParams = {
33
+ page: params?.page ?? page,
34
+ limit: params?.limit ?? limit,
35
+ };
36
+ const raw = await http.get(config.endpoints.PERMISSIONS_REGISTRY, queryParams);
37
+ const result = config.adapters.permissions.transformRegistryResponse(raw);
38
+ setRegistryModules(result.items);
39
+ setTotal(result.total);
40
+ return result;
41
+ } catch (err: any) {
42
+ setError(err);
43
+ throw err;
44
+ } finally {
45
+ setIsLoading(false);
46
+ }
47
+ },
48
+ [config.adapters.permissions, config.endpoints.PERMISSIONS_REGISTRY, http, limit, page]
49
+ );
50
+
51
+ /**
52
+ * Transforms hierarchical modules into a flat matrix structure for the UI table
53
+ */
54
+ const buildMatrixRows = useCallback(
55
+ (
56
+ modules: StandardModuleWithActions[],
57
+ assignedActionIds: Set<string | number>,
58
+ availableActions: StandardAction[],
59
+ level = 0
60
+ ): RolePermissionMatrixRow[] => {
61
+ const rows: RolePermissionMatrixRow[] = [];
62
+
63
+ modules.forEach((mod) => {
64
+ // Map each available action to whether this module has it registered and assigned
65
+ const actionRow = availableActions.map((action) => {
66
+ const registered = mod.actions.find(
67
+ (pa: StandardPermissionActionMapping) =>
68
+ String(pa.actionId) === String(action.id) || pa.actionDetails?.key === action.key
69
+ );
70
+
71
+ const isAssigned = registered ? assignedActionIds.has(registered.permissionActionId) : false;
72
+
73
+ return {
74
+ actionKey: action.key,
75
+ actionName: action.name,
76
+ actionId: action.id,
77
+ permissionActionId: registered ? registered.permissionActionId : '',
78
+ permissionKey: registered ? registered.permissionKey : `${mod.slug}.${action.key}`,
79
+ isAssigned,
80
+ };
81
+ });
82
+
83
+ rows.push({
84
+ moduleId: mod.id,
85
+ moduleName: mod.name,
86
+ slug: mod.slug,
87
+ parentId: mod.parentId,
88
+ level,
89
+ actions: actionRow,
90
+ });
91
+
92
+ if (mod.children && mod.children.length > 0) {
93
+ rows.push(...buildMatrixRows(mod.children, assignedActionIds, availableActions, level + 1));
94
+ }
95
+ });
96
+
97
+ return rows;
98
+ },
99
+ []
100
+ );
101
+
102
+ return {
103
+ actions,
104
+ registryModules,
105
+ total,
106
+ page,
107
+ limit,
108
+ isLoading,
109
+ error,
110
+ fetchActions,
111
+ fetchRegistry,
112
+ buildMatrixRows,
113
+ };
114
+ }