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.
- package/.github/workflows/publish.yml +29 -0
- package/README.md +317 -2
- package/package.json +45 -4
- package/scripts/postbuild.js +17 -0
- package/src/config/rbac.config.ts +206 -0
- package/src/constants/actions.constants.ts +21 -0
- package/src/constants/apiKeys.constants.ts +25 -0
- package/src/constants/auth.constants.ts +39 -0
- package/src/constants/endpoints.constants.ts +66 -0
- package/src/constants/i18n.constants.ts +82 -0
- package/src/constants/index.ts +70 -0
- package/src/constants/masters.constants.ts +68 -0
- package/src/constants/permissions/index.ts +44 -0
- package/src/constants/permissions/masterPermissions.constants.ts +44 -0
- package/src/constants/permissions/rolePermissions.constants.ts +18 -0
- package/src/constants/permissions.constants.ts +34 -0
- package/src/constants/roles.constants.ts +34 -0
- package/src/core/adapters/authAdapter.ts +89 -0
- package/src/core/adapters/index.ts +17 -0
- package/src/core/adapters/mastersAdapter.ts +132 -0
- package/src/core/adapters/permissionsAdapter.ts +90 -0
- package/src/core/adapters/rolesAdapter.ts +115 -0
- package/src/core/api/httpClient.ts +296 -0
- package/src/core/context/PermissionContext.tsx +232 -0
- package/src/core/context/RBACConfigContext.tsx +52 -0
- package/src/core/guards/Can.tsx +98 -0
- package/src/core/guards/RouteGuard.tsx +42 -0
- package/src/core/hooks/useMaster.ts +99 -0
- package/src/core/hooks/usePermissions.ts +114 -0
- package/src/core/hooks/useRoles.ts +120 -0
- package/src/core/types/adapter.types.ts +54 -0
- package/src/core/types/auth.types.ts +44 -0
- package/src/core/types/index.ts +7 -0
- package/src/core/types/masters.types.ts +63 -0
- package/src/core/types/pagination.types.ts +37 -0
- package/src/core/types/rbac.types.ts +69 -0
- package/src/core/types/router.types.ts +43 -0
- package/src/core/types/style.types.ts +53 -0
- package/src/core/utils/permissionResolver.ts +44 -0
- package/src/index.ts +44 -0
- package/src/modules/masters/DepartmentMasterView.tsx +126 -0
- package/src/modules/masters/DesignationMasterView.tsx +97 -0
- package/src/modules/masters/EmployeeMasterView.tsx +107 -0
- package/src/modules/masters/EntityMasterView.tsx +102 -0
- package/src/modules/masters/ModuleMasterView.tsx +104 -0
- package/src/modules/masters/SubModuleMasterView.tsx +106 -0
- package/src/modules/masters/components/MasterDataTable.tsx +684 -0
- package/src/modules/masters/index.ts +7 -0
- package/src/modules/roles-and-permissions/components/PermissionMatrix.tsx +289 -0
- package/src/modules/roles-and-permissions/components/PermissionRegistry.tsx +108 -0
- package/src/modules/roles-and-permissions/components/RoleListTable.tsx +568 -0
- package/src/modules/roles-and-permissions/components/RoleModal.tsx +213 -0
- package/src/modules/roles-and-permissions/index.ts +4 -0
- package/src/shared/components/Badge.tsx +24 -0
- package/src/shared/components/Button.tsx +75 -0
- package/src/shared/components/DynamicSidebar.tsx +181 -0
- package/src/shared/components/Input.tsx +48 -0
- package/src/shared/components/Modal.tsx +70 -0
- package/src/shared/components/Table.tsx +345 -0
- package/src/styles/standalone.css +51 -0
- package/src/theme/ThemeContext.tsx +74 -0
- package/src/theme/defaultTheme.ts +81 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/theme.types.ts +106 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { RBAC_CONSTANTS } from '../../constants';
|
|
2
|
+
import {
|
|
3
|
+
MastersAdapter,
|
|
4
|
+
StandardEntityMaster,
|
|
5
|
+
StandardDepartmentMaster,
|
|
6
|
+
StandardDesignationMaster,
|
|
7
|
+
StandardEmployeeMaster,
|
|
8
|
+
StandardModuleMasterItem,
|
|
9
|
+
PaginatedResult,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
function extractListAndMeta(raw: any): { itemsRaw: any[]; total: number; page: number; limit: number } {
|
|
13
|
+
const dataKey = RBAC_CONSTANTS.api.DATA;
|
|
14
|
+
const pagKeys = RBAC_CONSTANTS.pagination;
|
|
15
|
+
const root = raw?.[dataKey] ?? raw;
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(root)) {
|
|
18
|
+
return { itemsRaw: root, total: root.length, page: 1, limit: 10 };
|
|
19
|
+
} else if (root && typeof root === 'object') {
|
|
20
|
+
const itemsRaw = root[pagKeys.ITEMS] || root.items || root.rows || [];
|
|
21
|
+
const total = root[pagKeys.TOTAL] ?? itemsRaw.length;
|
|
22
|
+
const page = root[pagKeys.PAGE] ?? 1;
|
|
23
|
+
const limit = root[pagKeys.LIMIT] ?? 10;
|
|
24
|
+
return { itemsRaw, total, page, limit };
|
|
25
|
+
}
|
|
26
|
+
return { itemsRaw: [], total: 0, page: 1, limit: 10 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const defaultMastersAdapter: MastersAdapter = {
|
|
30
|
+
transformEntityListResponse: (raw: any): PaginatedResult<StandardEntityMaster> => {
|
|
31
|
+
const k = RBAC_CONSTANTS.masters.ENTITY;
|
|
32
|
+
const { itemsRaw, total, page, limit } = extractListAndMeta(raw);
|
|
33
|
+
|
|
34
|
+
const items = itemsRaw.map((e: any) => ({
|
|
35
|
+
id: e[k.ID] ?? e.id,
|
|
36
|
+
name: e[k.NAME] ?? e.name,
|
|
37
|
+
code: e[k.CODE],
|
|
38
|
+
state: e[k.STATE],
|
|
39
|
+
city: e[k.CITY],
|
|
40
|
+
pincode: e[k.PINCODE],
|
|
41
|
+
isActive: Boolean(e[RBAC_CONSTANTS.masters.IS_ACTIVE] ?? e.is_active ?? true),
|
|
42
|
+
createdAt: e[RBAC_CONSTANTS.masters.CREATED_AT],
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
return { items, total, page, limit };
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
transformDepartmentListResponse: (raw: any): PaginatedResult<StandardDepartmentMaster> => {
|
|
49
|
+
const k = RBAC_CONSTANTS.masters.DEPARTMENT;
|
|
50
|
+
const { itemsRaw, total, page, limit } = extractListAndMeta(raw);
|
|
51
|
+
|
|
52
|
+
const items = itemsRaw.map((d: any) => ({
|
|
53
|
+
id: d[k.ID] ?? d.id,
|
|
54
|
+
name: d[k.NAME] ?? d.name,
|
|
55
|
+
code: d[k.CODE],
|
|
56
|
+
entityId: d[k.ENTITY_ID] ?? d.entity?.entityId,
|
|
57
|
+
entityName: d.entity?.entity_name ?? d.entityName,
|
|
58
|
+
isActive: Boolean(d[RBAC_CONSTANTS.masters.IS_ACTIVE] ?? d.is_active ?? true),
|
|
59
|
+
createdAt: d[RBAC_CONSTANTS.masters.CREATED_AT],
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
return { items, total, page, limit };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
transformDesignationListResponse: (raw: any): PaginatedResult<StandardDesignationMaster> => {
|
|
66
|
+
const k = RBAC_CONSTANTS.masters.DESIGNATION;
|
|
67
|
+
const { itemsRaw, total, page, limit } = extractListAndMeta(raw);
|
|
68
|
+
|
|
69
|
+
const items = itemsRaw.map((des: any) => ({
|
|
70
|
+
id: des[k.ID] ?? des.id,
|
|
71
|
+
name: des[k.NAME] ?? des.name,
|
|
72
|
+
code: des[k.CODE],
|
|
73
|
+
departmentId: des[k.DEPARTMENT_ID] ?? des.department?.departmentId,
|
|
74
|
+
departmentName: des.department?.department_name ?? des.departmentName,
|
|
75
|
+
isActive: Boolean(des[RBAC_CONSTANTS.masters.IS_ACTIVE] ?? des.is_active ?? true),
|
|
76
|
+
createdAt: des[RBAC_CONSTANTS.masters.CREATED_AT],
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
return { items, total, page, limit };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
transformEmployeeListResponse: (raw: any): PaginatedResult<StandardEmployeeMaster> => {
|
|
83
|
+
const k = RBAC_CONSTANTS.masters.EMPLOYEE;
|
|
84
|
+
const { itemsRaw, total, page, limit } = extractListAndMeta(raw);
|
|
85
|
+
|
|
86
|
+
const items = itemsRaw.map((emp: any) => {
|
|
87
|
+
const fn = emp[k.FIRST_NAME] || '';
|
|
88
|
+
const ln = emp[k.LAST_NAME] || '';
|
|
89
|
+
return {
|
|
90
|
+
id: emp[k.ID] ?? emp.id,
|
|
91
|
+
employeeCode: emp[k.EMPLOYEE_CODE],
|
|
92
|
+
firstName: fn,
|
|
93
|
+
lastName: ln,
|
|
94
|
+
fullName: `${fn} ${ln}`.trim() || emp.name || emp[k.EMAIL] || '',
|
|
95
|
+
email: emp[k.EMAIL] || '',
|
|
96
|
+
mobileNumber: emp[k.MOBILE_NUMBER],
|
|
97
|
+
entityId: emp[k.ENTITY_ID],
|
|
98
|
+
entityName: emp.entity?.entity_name,
|
|
99
|
+
departmentId: emp[k.DEPARTMENT_ID],
|
|
100
|
+
departmentName: emp.department?.department_name,
|
|
101
|
+
designationId: emp[k.DESIGNATION_ID],
|
|
102
|
+
designationName: emp.designation?.designation_name,
|
|
103
|
+
reportingManagerId: emp[k.REPORTING_MANAGER_ID],
|
|
104
|
+
roleId: emp[k.ROLE_ID] ?? emp.role?.id,
|
|
105
|
+
roleName: emp.role?.role_name,
|
|
106
|
+
isActive: Boolean(emp[RBAC_CONSTANTS.masters.IS_ACTIVE] ?? emp.is_active ?? true),
|
|
107
|
+
createdAt: emp[RBAC_CONSTANTS.masters.CREATED_AT],
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return { items, total, page, limit };
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
transformModuleMasterListResponse: (raw: any): PaginatedResult<StandardModuleMasterItem> => {
|
|
115
|
+
const k = RBAC_CONSTANTS.masters.MODULE;
|
|
116
|
+
const { itemsRaw, total, page, limit } = extractListAndMeta(raw);
|
|
117
|
+
|
|
118
|
+
const items = itemsRaw.map((m: any) => ({
|
|
119
|
+
id: m[k.ID] ?? m.id,
|
|
120
|
+
name: m[k.NAME] ?? m.name,
|
|
121
|
+
slug: m[k.SLUG_NAME] ?? m.slug,
|
|
122
|
+
routePath: m[k.ROUTE_PATH],
|
|
123
|
+
sortOrder: m[k.SORT_ORDER] || 0,
|
|
124
|
+
parentId: m[k.PARENT_ID] || 0,
|
|
125
|
+
parentName: m.parent?.name,
|
|
126
|
+
isActive: Boolean(m[RBAC_CONSTANTS.masters.IS_ACTIVE] ?? m.is_active ?? true),
|
|
127
|
+
createdAt: m[RBAC_CONSTANTS.masters.CREATED_AT],
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return { items, total, page, limit };
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { RBAC_CONSTANTS } from '../../constants';
|
|
2
|
+
import { PermissionsAdapter, StandardAction, StandardModuleWithActions, PaginatedResult } from '../types';
|
|
3
|
+
|
|
4
|
+
export const defaultPermissionsAdapter: PermissionsAdapter = {
|
|
5
|
+
transformActionsListResponse: (raw: any): StandardAction[] => {
|
|
6
|
+
const dataKey = RBAC_CONSTANTS.api.DATA;
|
|
7
|
+
const root = raw?.[dataKey] || raw || [];
|
|
8
|
+
const items = Array.isArray(root) ? root : root.list || [];
|
|
9
|
+
|
|
10
|
+
return items.map((a: any) => ({
|
|
11
|
+
id: a[RBAC_CONSTANTS.permissions.ACTION_ID] ?? a.id,
|
|
12
|
+
name: a[RBAC_CONSTANTS.permissions.ACTION_NAME] ?? a.name,
|
|
13
|
+
key: a[RBAC_CONSTANTS.permissions.ACTION_KEY] ?? a.key,
|
|
14
|
+
isActive: Boolean(a[RBAC_CONSTANTS.permissions.IS_ACTIVE] ?? true),
|
|
15
|
+
sortOrder: a.sort_order ?? 0,
|
|
16
|
+
}));
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
transformRegistryResponse: (raw: any): PaginatedResult<StandardModuleWithActions> => {
|
|
20
|
+
const permKeys = RBAC_CONSTANTS.permissions;
|
|
21
|
+
const dataKey = RBAC_CONSTANTS.api.DATA;
|
|
22
|
+
const pagKeys = RBAC_CONSTANTS.pagination;
|
|
23
|
+
|
|
24
|
+
let itemsRaw: any[] = [];
|
|
25
|
+
let total = 0;
|
|
26
|
+
let page = 1;
|
|
27
|
+
let limit = 10;
|
|
28
|
+
|
|
29
|
+
const root = raw?.[dataKey] ?? raw;
|
|
30
|
+
if (Array.isArray(root)) {
|
|
31
|
+
itemsRaw = root;
|
|
32
|
+
total = root.length;
|
|
33
|
+
} else if (root && typeof root === 'object') {
|
|
34
|
+
itemsRaw = root[pagKeys.ITEMS] || root.items || [];
|
|
35
|
+
total = root[pagKeys.TOTAL] ?? itemsRaw.length;
|
|
36
|
+
page = root[pagKeys.PAGE] ?? 1;
|
|
37
|
+
limit = root[pagKeys.LIMIT] ?? 10;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mapModule = (m: any): StandardModuleWithActions => {
|
|
41
|
+
const rawActions = m[permKeys.ACTIONS_ARRAY] || [];
|
|
42
|
+
const actions = Array.isArray(rawActions)
|
|
43
|
+
? rawActions.map((pa: any) => {
|
|
44
|
+
const ad = pa[permKeys.ACTION_DETAILS] || {};
|
|
45
|
+
return {
|
|
46
|
+
permissionActionId: pa[permKeys.PERMISSION_ACTION_ID] ?? pa.id,
|
|
47
|
+
moduleId: pa[permKeys.PERMISSION_MASTER_ID] ?? m[permKeys.PERMISSION_MASTER_ID],
|
|
48
|
+
actionId: pa[permKeys.ACTION_ID] ?? ad[permKeys.ACTION_ID],
|
|
49
|
+
permissionKey: pa[permKeys.PERMISSION_KEY] || '',
|
|
50
|
+
routePath: pa[permKeys.ROUTE_PATH],
|
|
51
|
+
description: pa.description,
|
|
52
|
+
sortOrder: pa[permKeys.SORT_ORDER] || 0,
|
|
53
|
+
actionDetails: {
|
|
54
|
+
id: ad[permKeys.ACTION_ID] ?? pa[permKeys.ACTION_ID] ?? 0,
|
|
55
|
+
name: ad[permKeys.ACTION_NAME] || pa[permKeys.ACTION_NAME] || '',
|
|
56
|
+
key: ad[permKeys.ACTION_KEY] || pa[permKeys.ACTION_KEY] || '',
|
|
57
|
+
isActive: Boolean(ad[permKeys.IS_ACTIVE] ?? true),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
})
|
|
61
|
+
: [];
|
|
62
|
+
|
|
63
|
+
const rawChildren = m[permKeys.CHILDREN_ARRAY] || m[permKeys.SUBMODULES_ARRAY] || [];
|
|
64
|
+
const children = Array.isArray(rawChildren) ? rawChildren.map(mapModule) : [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id: m[permKeys.PERMISSION_MASTER_ID] ?? m.id,
|
|
68
|
+
name: m[permKeys.MODULE_NAME] || '',
|
|
69
|
+
slug: m[permKeys.SLUG_NAME] || '',
|
|
70
|
+
routePath: m[permKeys.ROUTE_PATH],
|
|
71
|
+
sortOrder: m[permKeys.SORT_ORDER] || 0,
|
|
72
|
+
parentId: m[permKeys.PARENT_ID] || 0,
|
|
73
|
+
isActive: Boolean(m[permKeys.IS_ACTIVE] ?? true),
|
|
74
|
+
actions,
|
|
75
|
+
children,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const items = itemsRaw.map(mapModule);
|
|
80
|
+
return { items, total, page, limit };
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
transformModuleParentsResponse: (raw: any): PaginatedResult<StandardModuleWithActions> => {
|
|
84
|
+
return defaultPermissionsAdapter.transformRegistryResponse(raw);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
transformModuleSubmodulesResponse: (raw: any): PaginatedResult<StandardModuleWithActions> => {
|
|
88
|
+
return defaultPermissionsAdapter.transformRegistryResponse(raw);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { RBAC_CONSTANTS } from '../../constants';
|
|
2
|
+
import { RolesAdapter, StandardRole, PaginatedResult, RoleFormData } from '../types';
|
|
3
|
+
|
|
4
|
+
export const defaultRolesAdapter: RolesAdapter = {
|
|
5
|
+
transformRoleListResponse: (raw: any): PaginatedResult<StandardRole> => {
|
|
6
|
+
const roleKeys = RBAC_CONSTANTS.roles;
|
|
7
|
+
const dataKey = RBAC_CONSTANTS.api.DATA;
|
|
8
|
+
const pagKeys = RBAC_CONSTANTS.pagination;
|
|
9
|
+
|
|
10
|
+
let itemsRaw: any[] = [];
|
|
11
|
+
let total = 0;
|
|
12
|
+
let page = 1;
|
|
13
|
+
let limit = 10;
|
|
14
|
+
|
|
15
|
+
const root = raw?.[dataKey] ?? raw;
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(root)) {
|
|
18
|
+
itemsRaw = root;
|
|
19
|
+
total = root.length;
|
|
20
|
+
} else if (root && typeof root === 'object') {
|
|
21
|
+
itemsRaw = root[pagKeys.ITEMS] || root.roles || root.items || root.rows || [];
|
|
22
|
+
total = root[pagKeys.TOTAL] ?? itemsRaw.length;
|
|
23
|
+
page = root[pagKeys.PAGE] ?? 1;
|
|
24
|
+
limit = root[pagKeys.LIMIT] ?? 10;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const items: StandardRole[] = itemsRaw.map((r: any) => {
|
|
28
|
+
const entityObj = r[roleKeys.ENTITY_OBJECT];
|
|
29
|
+
const deptObj = r[roleKeys.DEPARTMENT_OBJECT];
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
id: r[roleKeys.ID] ?? r[roleKeys.ROLE_ID],
|
|
33
|
+
name: r[roleKeys.ROLE_NAME] || '',
|
|
34
|
+
key: r[roleKeys.ROLE_KEY] || '',
|
|
35
|
+
description: r[roleKeys.DESCRIPTION],
|
|
36
|
+
isActive: Boolean(r[roleKeys.IS_ACTIVE] ?? true),
|
|
37
|
+
entityId: r[roleKeys.ENTITY_ID] ?? entityObj?.[RBAC_CONSTANTS.masters.ENTITY.ID],
|
|
38
|
+
entityName: entityObj?.[roleKeys.ENTITY_NAME] ?? entityObj?.name,
|
|
39
|
+
departmentId: r[roleKeys.DEPARTMENT_ID] ?? deptObj?.[RBAC_CONSTANTS.masters.DEPARTMENT.ID],
|
|
40
|
+
departmentName: deptObj?.[roleKeys.DEPARTMENT_NAME] ?? deptObj?.name,
|
|
41
|
+
createdAt: r[roleKeys.CREATED_AT],
|
|
42
|
+
updatedAt: r[roleKeys.UPDATED_AT],
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { items, total, page, limit };
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
transformRoleDetailsResponse: (raw: any): StandardRole => {
|
|
50
|
+
const roleKeys = RBAC_CONSTANTS.roles;
|
|
51
|
+
const dataKey = RBAC_CONSTANTS.api.DATA;
|
|
52
|
+
const r = raw?.[dataKey] || raw || {};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: r[roleKeys.ID] ?? r[roleKeys.ROLE_ID],
|
|
56
|
+
name: r[roleKeys.ROLE_NAME] || '',
|
|
57
|
+
key: r[roleKeys.ROLE_KEY] || '',
|
|
58
|
+
description: r[roleKeys.DESCRIPTION],
|
|
59
|
+
isActive: Boolean(r[roleKeys.IS_ACTIVE] ?? true),
|
|
60
|
+
entityId: r[roleKeys.ENTITY_ID],
|
|
61
|
+
departmentId: r[roleKeys.DEPARTMENT_ID],
|
|
62
|
+
createdAt: r[roleKeys.CREATED_AT],
|
|
63
|
+
updatedAt: r[roleKeys.UPDATED_AT],
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
transformCreateRolePayload: (data: RoleFormData): Record<string, any> => {
|
|
68
|
+
const roleKeys = RBAC_CONSTANTS.roles;
|
|
69
|
+
return {
|
|
70
|
+
[roleKeys.ROLE_NAME]: data.role_name,
|
|
71
|
+
[roleKeys.ROLE_KEY]: data.role_key,
|
|
72
|
+
[roleKeys.DESCRIPTION]: data.description || '',
|
|
73
|
+
...(data.entityId ? { [roleKeys.ENTITY_ID]: data.entityId } : {}),
|
|
74
|
+
...(data.departmentId ? { [roleKeys.DEPARTMENT_ID]: data.departmentId } : {}),
|
|
75
|
+
[roleKeys.IS_ACTIVE]: data.is_active ?? true,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
transformEditRolePayload: (data: Partial<RoleFormData>): Record<string, any> => {
|
|
80
|
+
const roleKeys = RBAC_CONSTANTS.roles;
|
|
81
|
+
const payload: Record<string, any> = {};
|
|
82
|
+
if (data.role_name !== undefined) payload[roleKeys.ROLE_NAME] = data.role_name;
|
|
83
|
+
if (data.role_key !== undefined) payload[roleKeys.ROLE_KEY] = data.role_key;
|
|
84
|
+
if (data.description !== undefined) payload[roleKeys.DESCRIPTION] = data.description;
|
|
85
|
+
if (data.entityId !== undefined) payload[roleKeys.ENTITY_ID] = data.entityId;
|
|
86
|
+
if (data.departmentId !== undefined) payload[roleKeys.DEPARTMENT_ID] = data.departmentId;
|
|
87
|
+
if (data.is_active !== undefined) payload[roleKeys.IS_ACTIVE] = data.is_active;
|
|
88
|
+
return payload;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
transformRolePermissionsResponse: (raw: any): (string | number)[] => {
|
|
92
|
+
const dataKey = RBAC_CONSTANTS.api.DATA;
|
|
93
|
+
const root = raw?.[dataKey] || raw || {};
|
|
94
|
+
const permissions = root[RBAC_CONSTANTS.roles.ASSIGNED_PERMISSIONS_ARRAY] || root.permissionActionIds || root;
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(permissions)) {
|
|
97
|
+
return permissions.map((p: any) => {
|
|
98
|
+
if (typeof p === 'number' || typeof p === 'string') return p;
|
|
99
|
+
return (
|
|
100
|
+
p[RBAC_CONSTANTS.permissions.PERMISSION_ACTION_ID] ??
|
|
101
|
+
p[RBAC_CONSTANTS.permissions.ACTION_ID] ??
|
|
102
|
+
p[RBAC_CONSTANTS.permissions.PERMISSION_KEY_ID] ??
|
|
103
|
+
p.id
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
transformAssignPermissionsPayload: (_roleId: string | number, permissionActionIds: (string | number)[]) => {
|
|
111
|
+
return {
|
|
112
|
+
[RBAC_CONSTANTS.roles.ASSIGNED_ACTION_IDS_PAYLOAD_KEY]: permissionActionIds,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
export interface RequestOptions extends RequestInit {
|
|
2
|
+
params?: Record<string, string | number | boolean | undefined | null>;
|
|
3
|
+
skipAuthRedirect?: boolean;
|
|
4
|
+
skipTokenRefresh?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function extractFieldErrors(data: any): Record<string, string> | undefined {
|
|
8
|
+
if (!data || typeof data !== 'object') return undefined;
|
|
9
|
+
|
|
10
|
+
// Pattern 1: { fieldErrors: { email: "taken" } }
|
|
11
|
+
if (data.fieldErrors && typeof data.fieldErrors === 'object' && !Array.isArray(data.fieldErrors)) {
|
|
12
|
+
return data.fieldErrors;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Pattern 2: { errors: { email: "taken" | ["taken", "invalid"] } }
|
|
16
|
+
if (data.errors && typeof data.errors === 'object' && !Array.isArray(data.errors)) {
|
|
17
|
+
const res: Record<string, string> = {};
|
|
18
|
+
for (const [k, v] of Object.entries(data.errors)) {
|
|
19
|
+
res[k] = Array.isArray(v) ? String(v[0]) : String(v);
|
|
20
|
+
}
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Pattern 3: { errors: [{ field: "email", message: "taken" }] } or [{ path: "email", msg: "taken" }]
|
|
25
|
+
if (Array.isArray(data.errors)) {
|
|
26
|
+
const res: Record<string, string> = {};
|
|
27
|
+
for (const item of data.errors) {
|
|
28
|
+
const key = item.field || item.path || item.param || item.key;
|
|
29
|
+
const msg = item.message || item.msg || item.error;
|
|
30
|
+
if (key && msg) res[key] = String(msg);
|
|
31
|
+
}
|
|
32
|
+
return Object.keys(res).length > 0 ? res : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Pattern 4: { validationErrors: { ... } }
|
|
36
|
+
if (data.validationErrors && typeof data.validationErrors === 'object') {
|
|
37
|
+
const res: Record<string, string> = {};
|
|
38
|
+
for (const [k, v] of Object.entries(data.validationErrors)) {
|
|
39
|
+
res[k] = Array.isArray(v) ? String(v[0]) : String(v);
|
|
40
|
+
}
|
|
41
|
+
return res;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class HttpClient {
|
|
48
|
+
private baseUrl: string;
|
|
49
|
+
private getToken?: () => string | null | undefined;
|
|
50
|
+
private getTenantId?: () => string | number | null | undefined;
|
|
51
|
+
private getCustomHeaders?: () => Record<string, string>;
|
|
52
|
+
private onUnauthorized?: () => void;
|
|
53
|
+
private refreshTokenFn?: () => Promise<string | null | undefined>;
|
|
54
|
+
|
|
55
|
+
private isRefreshing = false;
|
|
56
|
+
private refreshSubscribers: Array<(token: string | null) => void> = [];
|
|
57
|
+
|
|
58
|
+
constructor(options: {
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
getToken?: () => string | null | undefined;
|
|
61
|
+
getTenantId?: () => string | number | null | undefined;
|
|
62
|
+
getCustomHeaders?: () => Record<string, string>;
|
|
63
|
+
onUnauthorized?: () => void;
|
|
64
|
+
refreshToken?: () => Promise<string | null | undefined>;
|
|
65
|
+
}) {
|
|
66
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
67
|
+
this.getToken = options.getToken;
|
|
68
|
+
this.getTenantId = options.getTenantId;
|
|
69
|
+
this.getCustomHeaders = options.getCustomHeaders;
|
|
70
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
71
|
+
this.refreshTokenFn = options.refreshToken;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public setBaseUrl(url: string) {
|
|
75
|
+
this.baseUrl = url.replace(/\/$/, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private subscribeTokenRefresh(cb: (token: string | null) => void) {
|
|
79
|
+
this.refreshSubscribers.push(cb);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private onTokenRefreshed(token: string | null) {
|
|
83
|
+
this.refreshSubscribers.forEach((cb) => cb(token));
|
|
84
|
+
this.refreshSubscribers = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private buildUrl(path: string, params?: Record<string, any>): string {
|
|
88
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
89
|
+
const url = new URL(`${this.baseUrl}${cleanPath}`);
|
|
90
|
+
|
|
91
|
+
if (params) {
|
|
92
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
93
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
94
|
+
url.searchParams.append(key, String(value));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return url.toString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async request<T = any>(
|
|
103
|
+
path: string,
|
|
104
|
+
options: RequestOptions = {},
|
|
105
|
+
): Promise<T> {
|
|
106
|
+
const url = this.buildUrl(path, options.params);
|
|
107
|
+
const headers: Record<string, string> = {
|
|
108
|
+
...((options.headers as Record<string, string>) || {}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const isFormData =
|
|
112
|
+
typeof FormData !== "undefined" && options.body instanceof FormData;
|
|
113
|
+
if (!isFormData && !headers["Content-Type"]) {
|
|
114
|
+
headers["Content-Type"] = "application/json";
|
|
115
|
+
} else if (isFormData) {
|
|
116
|
+
delete headers["Content-Type"]; // Let browser generate boundary
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Inject custom headers if configured
|
|
120
|
+
if (this.getCustomHeaders) {
|
|
121
|
+
Object.assign(headers, this.getCustomHeaders());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Inject tenant/entity identifier if configured
|
|
125
|
+
if (this.getTenantId) {
|
|
126
|
+
const tenantId = this.getTenantId();
|
|
127
|
+
if (tenantId) {
|
|
128
|
+
headers["X-Tenant-Id"] = String(tenantId);
|
|
129
|
+
headers["X-Entity-Id"] = String(tenantId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Inject Bearer token
|
|
134
|
+
const token = this.getToken ? this.getToken() : null;
|
|
135
|
+
if (token && !headers["Authorization"]) {
|
|
136
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let response: Response;
|
|
140
|
+
try {
|
|
141
|
+
response = await fetch(url, {
|
|
142
|
+
...options,
|
|
143
|
+
headers,
|
|
144
|
+
credentials: options.credentials || 'include',
|
|
145
|
+
});
|
|
146
|
+
} catch (networkError: any) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Network error: ${networkError.message || "Failed to fetch"}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle 401 Unauthorized with Silent Token Refresh
|
|
153
|
+
if (response.status === 401 && !options.skipAuthRedirect) {
|
|
154
|
+
if (this.refreshTokenFn && !options.skipTokenRefresh) {
|
|
155
|
+
if (!this.isRefreshing) {
|
|
156
|
+
this.isRefreshing = true;
|
|
157
|
+
try {
|
|
158
|
+
const newToken = await this.refreshTokenFn();
|
|
159
|
+
this.isRefreshing = false;
|
|
160
|
+
this.onTokenRefreshed(newToken || null);
|
|
161
|
+
|
|
162
|
+
if (newToken) {
|
|
163
|
+
// Retry original request with newly refreshed token
|
|
164
|
+
headers["Authorization"] = `Bearer ${newToken}`;
|
|
165
|
+
return this.request<T>(path, {
|
|
166
|
+
...options,
|
|
167
|
+
headers,
|
|
168
|
+
skipTokenRefresh: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
} catch (refreshErr) {
|
|
172
|
+
this.isRefreshing = false;
|
|
173
|
+
this.onTokenRefreshed(null);
|
|
174
|
+
if (this.onUnauthorized) this.onUnauthorized();
|
|
175
|
+
throw refreshErr;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// Another request is already refreshing the token; wait for it
|
|
179
|
+
return new Promise<T>((resolve, reject) => {
|
|
180
|
+
this.subscribeTokenRefresh((newToken) => {
|
|
181
|
+
if (newToken) {
|
|
182
|
+
headers["Authorization"] = `Bearer ${newToken}`;
|
|
183
|
+
resolve(
|
|
184
|
+
this.request<T>(path, {
|
|
185
|
+
...options,
|
|
186
|
+
headers,
|
|
187
|
+
skipTokenRefresh: true,
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
if (this.onUnauthorized) this.onUnauthorized();
|
|
192
|
+
reject(new Error("Session expired"));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If no refreshToken handler, trigger unauthorized callback
|
|
200
|
+
if (this.onUnauthorized) {
|
|
201
|
+
this.onUnauthorized();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const data = await response.json().catch(() => null);
|
|
206
|
+
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
const errorMsg =
|
|
209
|
+
data?.message ||
|
|
210
|
+
data?.error ||
|
|
211
|
+
`Request failed with status ${response.status}`;
|
|
212
|
+
const error: any = new Error(errorMsg);
|
|
213
|
+
error.status = response.status;
|
|
214
|
+
error.data = data;
|
|
215
|
+
// Extract field-level validation errors if returned by backend
|
|
216
|
+
error.fieldErrors = extractFieldErrors(data);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return data as T;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public get<T = any>(
|
|
224
|
+
path: string,
|
|
225
|
+
params?: Record<string, any>,
|
|
226
|
+
options?: RequestOptions,
|
|
227
|
+
): Promise<T> {
|
|
228
|
+
return this.request<T>(path, { ...options, method: "GET", params });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public post<T = any>(
|
|
232
|
+
path: string,
|
|
233
|
+
body?: any,
|
|
234
|
+
options?: RequestOptions,
|
|
235
|
+
): Promise<T> {
|
|
236
|
+
const isFormData =
|
|
237
|
+
typeof FormData !== "undefined" && body instanceof FormData;
|
|
238
|
+
return this.request<T>(path, {
|
|
239
|
+
...options,
|
|
240
|
+
method: "POST",
|
|
241
|
+
body: isFormData
|
|
242
|
+
? body
|
|
243
|
+
: body !== undefined
|
|
244
|
+
? JSON.stringify(body)
|
|
245
|
+
: undefined,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public patch<T = any>(
|
|
250
|
+
path: string,
|
|
251
|
+
body?: any,
|
|
252
|
+
options?: RequestOptions,
|
|
253
|
+
): Promise<T> {
|
|
254
|
+
const isFormData =
|
|
255
|
+
typeof FormData !== "undefined" && body instanceof FormData;
|
|
256
|
+
return this.request<T>(path, {
|
|
257
|
+
...options,
|
|
258
|
+
method: "PATCH",
|
|
259
|
+
body: isFormData
|
|
260
|
+
? body
|
|
261
|
+
: body !== undefined
|
|
262
|
+
? JSON.stringify(body)
|
|
263
|
+
: undefined,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public put<T = any>(
|
|
268
|
+
path: string,
|
|
269
|
+
body?: any,
|
|
270
|
+
options?: RequestOptions,
|
|
271
|
+
): Promise<T> {
|
|
272
|
+
const isFormData =
|
|
273
|
+
typeof FormData !== "undefined" && body instanceof FormData;
|
|
274
|
+
return this.request<T>(path, {
|
|
275
|
+
...options,
|
|
276
|
+
method: "PUT",
|
|
277
|
+
body: isFormData
|
|
278
|
+
? body
|
|
279
|
+
: body !== undefined
|
|
280
|
+
? JSON.stringify(body)
|
|
281
|
+
: undefined,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public delete<T = any>(path: string, options?: RequestOptions): Promise<T> {
|
|
286
|
+
return this.request<T>(path, { ...options, method: "DELETE" });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public upload<T = any>(path: string, formData: FormData, options?: RequestOptions): Promise<T> {
|
|
290
|
+
return this.request<T>(path, {
|
|
291
|
+
...options,
|
|
292
|
+
method: 'POST',
|
|
293
|
+
body: formData,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|