@carlonicora/nextjs-jsonapi 1.52.0 → 1.53.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/dist/{AuthComponent-BkK4Sf3q.d.mts → AuthComponent-CK9aRRW2.d.mts} +1 -1
- package/dist/{AuthComponent-DCfP4o32.d.ts → AuthComponent-IqFWLNIU.d.ts} +1 -1
- package/dist/{BlockNoteEditor-KQPSJCYG.js → BlockNoteEditor-AROKR3J6.js} +14 -14
- package/dist/{BlockNoteEditor-KQPSJCYG.js.map → BlockNoteEditor-AROKR3J6.js.map} +1 -1
- package/dist/{BlockNoteEditor-WUVRCTQI.mjs → BlockNoteEditor-CNMSBGCL.mjs} +4 -4
- package/dist/ModulePathsInterface-49EWvbWy.d.mts +31 -0
- package/dist/ModulePathsInterface-wVS5Raa4.d.ts +31 -0
- package/dist/{auth.interface-C4kEZscm.d.ts → auth.interface-C1WjZ0fM.d.ts} +1 -1
- package/dist/{auth.interface-24ID4yhT.d.mts → auth.interface-fBFqIrw4.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-BUCV5VFT.mjs → chunk-FE26PIZK.mjs} +53 -2
- package/dist/chunk-FE26PIZK.mjs.map +1 -0
- package/dist/{chunk-BTLJZIDS.mjs → chunk-G5473JP3.mjs} +869 -40
- package/dist/chunk-G5473JP3.mjs.map +1 -0
- package/dist/{chunk-XNISXVQL.mjs → chunk-J2PYGXVD.mjs} +70 -1
- package/dist/chunk-J2PYGXVD.mjs.map +1 -0
- package/dist/{chunk-YKPIFJOB.js → chunk-PQIXFKHT.js} +1457 -628
- package/dist/chunk-PQIXFKHT.js.map +1 -0
- package/dist/{chunk-QIA5FOQB.js → chunk-QOLVON35.js} +71 -2
- package/dist/chunk-QOLVON35.js.map +1 -0
- package/dist/{chunk-V63TFESU.js → chunk-UJBUJALX.js} +53 -2
- package/dist/chunk-UJBUJALX.js.map +1 -0
- package/dist/client/index.d.mts +25 -7
- package/dist/client/index.d.ts +25 -7
- package/dist/client/index.js +10 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -3
- package/dist/components/index.d.mts +52 -10
- package/dist/components/index.d.ts +52 -10
- package/dist/components/index.js +16 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +15 -3
- package/dist/{config-CPN6QZfo.d.ts → config-DZWAFB7H.d.ts} +1 -1
- package/dist/{config-DaxjKdIo.d.mts → config-ndRJIQsP.d.mts} +1 -1
- package/dist/{content.interface-DvPs_JbX.d.mts → content.interface-B5ySfiOE.d.mts} +1 -1
- package/dist/{content.interface-Czin-YRh.d.ts → content.interface-mmz0uMwm.d.ts} +1 -1
- package/dist/contexts/index.d.mts +2 -2
- package/dist/contexts/index.d.ts +2 -2
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +15 -10
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -1
- package/dist/index.d.mts +47 -10
- package/dist/index.d.ts +47 -10
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -2
- package/dist/{notification.interface-DEW8hR8g.d.ts → notification.interface-COKHDQeE.d.ts} +1 -1
- package/dist/{notification.interface-DKR5WGKH.d.mts → notification.interface-DG7cq9oG.d.mts} +1 -1
- package/dist/{s3.service-BHjcTA0t.d.mts → s3.service-BoRPFx82.d.mts} +4 -4
- package/dist/{s3.service-C_K1VHyx.d.ts → s3.service-ppn9iGJU.d.ts} +4 -4
- package/dist/server/index.d.mts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/useRbacState-DhuYYr0S.d.mts +77 -0
- package/dist/useRbacState-NnzNL2ED.d.ts +77 -0
- package/dist/{useSocket-BW6haECW.d.mts → useSocket-CtfuR5wD.d.mts} +1 -1
- package/dist/{useSocket-C9FmYuRM.d.ts → useSocket-bsV-K4qR.d.ts} +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/components/containers/RoundPageContainer.tsx +1 -1
- package/src/components/containers/RoundPageContainerTitle.tsx +1 -1
- package/src/components/index.ts +6 -0
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/rbac/components/RbacContainer.tsx +82 -0
- package/src/features/rbac/components/RbacFeatureSection.tsx +66 -0
- package/src/features/rbac/components/RbacModuleTable.tsx +121 -0
- package/src/features/rbac/components/RbacPermissionCell.tsx +97 -0
- package/src/features/rbac/components/RbacPermissionPicker.tsx +179 -0
- package/src/features/rbac/components/RbacToolbar.tsx +40 -0
- package/src/features/rbac/data/ModulePaths.ts +25 -0
- package/src/features/rbac/data/ModulePathsInterface.ts +6 -0
- package/src/features/rbac/data/PermissionMapping.ts +43 -0
- package/src/features/rbac/data/PermissionMappingInterface.ts +12 -0
- package/src/features/rbac/data/RbacService.ts +47 -0
- package/src/features/rbac/data/RbacTypes.ts +15 -0
- package/src/features/rbac/data/index.ts +6 -0
- package/src/features/rbac/hooks/useRbacState.test.ts +178 -0
- package/src/features/rbac/hooks/useRbacState.ts +319 -0
- package/src/features/rbac/index.ts +19 -0
- package/src/features/rbac/rbac.module.ts +19 -0
- package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +124 -0
- package/src/features/rbac/utils/RbacMigrationGenerator.ts +184 -0
- package/src/index.ts +4 -0
- package/dist/chunk-BTLJZIDS.mjs.map +0 -1
- package/dist/chunk-BUCV5VFT.mjs.map +0 -1
- package/dist/chunk-QIA5FOQB.js.map +0 -1
- package/dist/chunk-V63TFESU.js.map +0 -1
- package/dist/chunk-XNISXVQL.mjs.map +0 -1
- package/dist/chunk-YKPIFJOB.js.map +0 -1
- package/dist/useDataListRetriever-BqJSFBck.d.mts +0 -33
- package/dist/useDataListRetriever-BqJSFBck.d.ts +0 -33
- /package/dist/{BlockNoteEditor-WUVRCTQI.mjs.map → BlockNoteEditor-CNMSBGCL.mjs.map} +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FeatureInterface } from "../../feature";
|
|
4
|
+
import { ModuleInterface } from "../../module";
|
|
5
|
+
import { RoleInterface } from "../../role";
|
|
6
|
+
import { useCallback, useMemo, useReducer } from "react";
|
|
7
|
+
import { ModulePathsInterface } from "../data/ModulePathsInterface";
|
|
8
|
+
import { PermissionMappingInterface } from "../data/PermissionMappingInterface";
|
|
9
|
+
import { ActionType, ACTION_TYPES, PermissionsMap, PermissionValue } from "../data/RbacTypes";
|
|
10
|
+
|
|
11
|
+
// --- State shape ---
|
|
12
|
+
|
|
13
|
+
interface OriginalData {
|
|
14
|
+
features: FeatureInterface[];
|
|
15
|
+
roles: RoleInterface[];
|
|
16
|
+
permissionMappings: PermissionMappingInterface[];
|
|
17
|
+
moduleRelationshipPaths: Map<string, string[]>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RbacState {
|
|
21
|
+
original: OriginalData | null;
|
|
22
|
+
// Edited values: only store overrides from original
|
|
23
|
+
featureIsCore: Map<string, boolean>;
|
|
24
|
+
modulePermissions: Map<string, PermissionsMap>; // moduleId -> permissions overrides
|
|
25
|
+
rolePermissions: Map<string, PermissionsMap | null>; // "roleId:moduleId" -> permissions (null = cleared/inherit)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Actions ---
|
|
29
|
+
|
|
30
|
+
type RbacAction =
|
|
31
|
+
| { type: "INIT"; payload: OriginalData }
|
|
32
|
+
| { type: "SET_FEATURE_IS_CORE"; featureId: string; isCore: boolean }
|
|
33
|
+
| { type: "SET_MODULE_DEFAULT_PERMISSION"; moduleId: string; actionType: ActionType; value: PermissionValue }
|
|
34
|
+
| { type: "SET_ROLE_PERMISSION"; roleId: string; moduleId: string; actionType: ActionType; value: PermissionValue }
|
|
35
|
+
| { type: "CLEAR_ROLE_PERMISSION"; roleId: string; moduleId: string; actionType: ActionType }
|
|
36
|
+
| { type: "CLEAR_ALL_ROLE_PERMISSIONS"; roleId: string; moduleId: string }
|
|
37
|
+
| { type: "RESET" };
|
|
38
|
+
|
|
39
|
+
function createInitialState(): RbacState {
|
|
40
|
+
return {
|
|
41
|
+
original: null,
|
|
42
|
+
featureIsCore: new Map(),
|
|
43
|
+
modulePermissions: new Map(),
|
|
44
|
+
rolePermissions: new Map(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findModule(features: FeatureInterface[], moduleId: string): ModuleInterface | undefined {
|
|
49
|
+
for (const feature of features) {
|
|
50
|
+
for (const mod of feature.modules) {
|
|
51
|
+
if (mod.id === moduleId) return mod;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findPermissionMapping(
|
|
58
|
+
mappings: PermissionMappingInterface[],
|
|
59
|
+
roleId: string,
|
|
60
|
+
moduleId: string,
|
|
61
|
+
): PermissionMappingInterface | undefined {
|
|
62
|
+
return mappings.find((pm) => pm.roleId === roleId && pm.moduleId === moduleId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function rbacReducer(state: RbacState, action: RbacAction): RbacState {
|
|
66
|
+
switch (action.type) {
|
|
67
|
+
case "INIT": {
|
|
68
|
+
return {
|
|
69
|
+
...createInitialState(),
|
|
70
|
+
original: action.payload,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "SET_FEATURE_IS_CORE": {
|
|
75
|
+
const newMap = new Map(state.featureIsCore);
|
|
76
|
+
const originalFeature = state.original?.features.find((f) => f.id === action.featureId);
|
|
77
|
+
if (originalFeature && originalFeature.isCore === action.isCore) {
|
|
78
|
+
newMap.delete(action.featureId);
|
|
79
|
+
} else {
|
|
80
|
+
newMap.set(action.featureId, action.isCore);
|
|
81
|
+
}
|
|
82
|
+
return { ...state, featureIsCore: newMap };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "SET_MODULE_DEFAULT_PERMISSION": {
|
|
86
|
+
const newMap = new Map(state.modulePermissions);
|
|
87
|
+
const originalModule = state.original ? findModule(state.original.features, action.moduleId) : undefined;
|
|
88
|
+
const current = newMap.get(action.moduleId) ?? { ...originalModule?.permissions };
|
|
89
|
+
const updated = { ...current, [action.actionType]: action.value };
|
|
90
|
+
newMap.set(action.moduleId, updated);
|
|
91
|
+
return { ...state, modulePermissions: newMap };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "SET_ROLE_PERMISSION": {
|
|
95
|
+
const key = `${action.roleId}:${action.moduleId}`;
|
|
96
|
+
const newMap = new Map(state.rolePermissions);
|
|
97
|
+
const existing = newMap.get(key);
|
|
98
|
+
|
|
99
|
+
if (existing === null) {
|
|
100
|
+
// Was cleared, start fresh
|
|
101
|
+
newMap.set(key, { [action.actionType]: action.value });
|
|
102
|
+
} else {
|
|
103
|
+
const originalMapping = state.original
|
|
104
|
+
? findPermissionMapping(state.original.permissionMappings, action.roleId, action.moduleId)
|
|
105
|
+
: undefined;
|
|
106
|
+
const current = existing ?? (originalMapping ? { ...originalMapping.permissions } : {});
|
|
107
|
+
newMap.set(key, { ...current, [action.actionType]: action.value });
|
|
108
|
+
}
|
|
109
|
+
return { ...state, rolePermissions: newMap };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "CLEAR_ROLE_PERMISSION": {
|
|
113
|
+
const key = `${action.roleId}:${action.moduleId}`;
|
|
114
|
+
const newMap = new Map(state.rolePermissions);
|
|
115
|
+
const existing = newMap.get(key);
|
|
116
|
+
if (existing === null) return state;
|
|
117
|
+
|
|
118
|
+
const originalMapping = state.original
|
|
119
|
+
? findPermissionMapping(state.original.permissionMappings, action.roleId, action.moduleId)
|
|
120
|
+
: undefined;
|
|
121
|
+
const current = existing ?? (originalMapping ? { ...originalMapping.permissions } : {});
|
|
122
|
+
const updated = { ...current };
|
|
123
|
+
delete updated[action.actionType];
|
|
124
|
+
|
|
125
|
+
// If all actions cleared, set to null (inherit all)
|
|
126
|
+
const hasAnyPermission = ACTION_TYPES.some((at) => updated[at] !== undefined);
|
|
127
|
+
newMap.set(key, hasAnyPermission ? updated : null);
|
|
128
|
+
return { ...state, rolePermissions: newMap };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case "CLEAR_ALL_ROLE_PERMISSIONS": {
|
|
132
|
+
const key = `${action.roleId}:${action.moduleId}`;
|
|
133
|
+
const newMap = new Map(state.rolePermissions);
|
|
134
|
+
newMap.set(key, null);
|
|
135
|
+
return { ...state, rolePermissions: newMap };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "RESET":
|
|
139
|
+
return {
|
|
140
|
+
...createInitialState(),
|
|
141
|
+
original: state.original,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
default:
|
|
145
|
+
return state;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Hook ---
|
|
150
|
+
|
|
151
|
+
export function useRbacState() {
|
|
152
|
+
const [state, dispatch] = useReducer(rbacReducer, undefined, createInitialState);
|
|
153
|
+
|
|
154
|
+
const init = useCallback(
|
|
155
|
+
(
|
|
156
|
+
features: FeatureInterface[],
|
|
157
|
+
roles: RoleInterface[],
|
|
158
|
+
permissionMappings: PermissionMappingInterface[],
|
|
159
|
+
modulePaths: ModulePathsInterface[],
|
|
160
|
+
) => {
|
|
161
|
+
const moduleRelationshipPaths = new Map<string, string[]>();
|
|
162
|
+
for (const mp of modulePaths) {
|
|
163
|
+
moduleRelationshipPaths.set(mp.moduleId, mp.paths);
|
|
164
|
+
}
|
|
165
|
+
dispatch({ type: "INIT", payload: { features, roles, permissionMappings, moduleRelationshipPaths } });
|
|
166
|
+
},
|
|
167
|
+
[],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const setFeatureIsCore = useCallback((featureId: string, isCore: boolean) => {
|
|
171
|
+
dispatch({ type: "SET_FEATURE_IS_CORE", featureId, isCore });
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
const setModuleDefaultPermission = useCallback((moduleId: string, actionType: ActionType, value: PermissionValue) => {
|
|
175
|
+
dispatch({ type: "SET_MODULE_DEFAULT_PERMISSION", moduleId, actionType, value });
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const setRolePermission = useCallback(
|
|
179
|
+
(roleId: string, moduleId: string, actionType: ActionType, value: PermissionValue) => {
|
|
180
|
+
dispatch({ type: "SET_ROLE_PERMISSION", roleId, moduleId, actionType, value });
|
|
181
|
+
},
|
|
182
|
+
[],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const clearRolePermission = useCallback((roleId: string, moduleId: string, actionType: ActionType) => {
|
|
186
|
+
dispatch({ type: "CLEAR_ROLE_PERMISSION", roleId, moduleId, actionType });
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
const clearAllRolePermissions = useCallback((roleId: string, moduleId: string) => {
|
|
190
|
+
dispatch({ type: "CLEAR_ALL_ROLE_PERMISSIONS", roleId, moduleId });
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
const resetModulePermissions = useCallback((moduleId: string, roles: { id: string }[]) => {
|
|
194
|
+
// Set all default permissions to true
|
|
195
|
+
for (const actionType of ACTION_TYPES) {
|
|
196
|
+
dispatch({ type: "SET_MODULE_DEFAULT_PERMISSION", moduleId, actionType, value: true });
|
|
197
|
+
}
|
|
198
|
+
// Clear all role permissions for this module (set to inherit / "-")
|
|
199
|
+
for (const role of roles) {
|
|
200
|
+
dispatch({ type: "CLEAR_ALL_ROLE_PERMISSIONS", roleId: role.id, moduleId });
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
const reset = useCallback(() => {
|
|
205
|
+
dispatch({ type: "RESET" });
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
// --- Getters ---
|
|
209
|
+
|
|
210
|
+
const getFeatureIsCore = useCallback(
|
|
211
|
+
(featureId: string): boolean => {
|
|
212
|
+
if (state.featureIsCore.has(featureId)) return state.featureIsCore.get(featureId)!;
|
|
213
|
+
const feature = state.original?.features.find((f) => f.id === featureId);
|
|
214
|
+
return feature?.isCore ?? false;
|
|
215
|
+
},
|
|
216
|
+
[state.featureIsCore, state.original],
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const getModuleDefaultPermission = useCallback(
|
|
220
|
+
(moduleId: string, actionType: ActionType): PermissionValue | undefined => {
|
|
221
|
+
const edited = state.modulePermissions.get(moduleId);
|
|
222
|
+
if (edited && edited[actionType] !== undefined) return edited[actionType];
|
|
223
|
+
if (!state.original) return undefined;
|
|
224
|
+
const mod = findModule(state.original.features, moduleId);
|
|
225
|
+
return mod?.permissions[actionType];
|
|
226
|
+
},
|
|
227
|
+
[state.modulePermissions, state.original],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const getRolePermission = useCallback(
|
|
231
|
+
(roleId: string, moduleId: string, actionType: ActionType): PermissionValue | undefined | null => {
|
|
232
|
+
const key = `${roleId}:${moduleId}`;
|
|
233
|
+
if (state.rolePermissions.has(key)) {
|
|
234
|
+
const perms = state.rolePermissions.get(key);
|
|
235
|
+
if (perms === null || perms === undefined) return null; // explicitly cleared = inherit
|
|
236
|
+
return perms[actionType] ?? null; // has role mapping but no entry for this action = inherit
|
|
237
|
+
}
|
|
238
|
+
// Fall back to original
|
|
239
|
+
if (!state.original) return undefined;
|
|
240
|
+
const mapping = findPermissionMapping(state.original.permissionMappings, roleId, moduleId);
|
|
241
|
+
if (!mapping) return undefined; // no mapping exists
|
|
242
|
+
return mapping.permissions[actionType] ?? null;
|
|
243
|
+
},
|
|
244
|
+
[state.rolePermissions, state.original],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const isDirty = useMemo(() => {
|
|
248
|
+
return state.featureIsCore.size > 0 || state.modulePermissions.size > 0 || state.rolePermissions.size > 0;
|
|
249
|
+
}, [state.featureIsCore, state.modulePermissions, state.rolePermissions]);
|
|
250
|
+
|
|
251
|
+
// Build the full effective configuration for migration generation
|
|
252
|
+
const getEffectiveConfiguration = useCallback(() => {
|
|
253
|
+
if (!state.original) return null;
|
|
254
|
+
|
|
255
|
+
const features = state.original.features.map((f) => ({
|
|
256
|
+
id: f.id,
|
|
257
|
+
name: f.name,
|
|
258
|
+
isCore: state.featureIsCore.has(f.id) ? state.featureIsCore.get(f.id)! : f.isCore,
|
|
259
|
+
modules: f.modules.map((m) => {
|
|
260
|
+
const editedPerms = state.modulePermissions.get(m.id);
|
|
261
|
+
return {
|
|
262
|
+
id: m.id,
|
|
263
|
+
name: m.name,
|
|
264
|
+
permissions: editedPerms ?? m.permissions,
|
|
265
|
+
};
|
|
266
|
+
}),
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
// Build role permissions map
|
|
270
|
+
const rolePermissionsMap = new Map<string, PermissionsMap>();
|
|
271
|
+
|
|
272
|
+
// Start with originals
|
|
273
|
+
for (const pm of state.original.permissionMappings) {
|
|
274
|
+
rolePermissionsMap.set(`${pm.roleId}:${pm.moduleId}`, { ...pm.permissions });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Apply edits
|
|
278
|
+
for (const [key, perms] of state.rolePermissions) {
|
|
279
|
+
if (perms === null) {
|
|
280
|
+
rolePermissionsMap.delete(key);
|
|
281
|
+
} else {
|
|
282
|
+
rolePermissionsMap.set(key, perms);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
features,
|
|
288
|
+
roles: state.original.roles,
|
|
289
|
+
rolePermissionsMap,
|
|
290
|
+
};
|
|
291
|
+
}, [state]);
|
|
292
|
+
|
|
293
|
+
const getModuleRelationshipPaths = useCallback(
|
|
294
|
+
(moduleId: string): string[] => {
|
|
295
|
+
return state.original?.moduleRelationshipPaths.get(moduleId) ?? [];
|
|
296
|
+
},
|
|
297
|
+
[state.original],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
original: state.original,
|
|
302
|
+
isDirty,
|
|
303
|
+
init,
|
|
304
|
+
setFeatureIsCore,
|
|
305
|
+
setModuleDefaultPermission,
|
|
306
|
+
setRolePermission,
|
|
307
|
+
clearRolePermission,
|
|
308
|
+
clearAllRolePermissions,
|
|
309
|
+
resetModulePermissions,
|
|
310
|
+
reset,
|
|
311
|
+
getFeatureIsCore,
|
|
312
|
+
getModuleDefaultPermission,
|
|
313
|
+
getRolePermission,
|
|
314
|
+
getEffectiveConfiguration,
|
|
315
|
+
getModuleRelationshipPaths,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export type RbacStateApi = ReturnType<typeof useRbacState>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Data layer
|
|
2
|
+
export * from "./data";
|
|
3
|
+
|
|
4
|
+
// Hooks
|
|
5
|
+
export { useRbacState } from "./hooks/useRbacState";
|
|
6
|
+
|
|
7
|
+
// Utils
|
|
8
|
+
export { generateMigrationFile, downloadMigrationFile } from "./utils/RbacMigrationGenerator";
|
|
9
|
+
|
|
10
|
+
// Components
|
|
11
|
+
export { RbacContainer } from "./components/RbacContainer";
|
|
12
|
+
export { RbacToolbar } from "./components/RbacToolbar";
|
|
13
|
+
export { RbacFeatureSection } from "./components/RbacFeatureSection";
|
|
14
|
+
export { RbacModuleTable } from "./components/RbacModuleTable";
|
|
15
|
+
export { RbacPermissionCell } from "./components/RbacPermissionCell";
|
|
16
|
+
export { RbacPermissionPicker } from "./components/RbacPermissionPicker";
|
|
17
|
+
|
|
18
|
+
// Module registrations
|
|
19
|
+
export { PermissionMappingModule, ModulePathsModule } from "./rbac.module";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ModuleFactory } from "../../permissions";
|
|
2
|
+
import { PermissionMapping } from "./data/PermissionMapping";
|
|
3
|
+
import { ModulePaths } from "./data/ModulePaths";
|
|
4
|
+
|
|
5
|
+
export const PermissionMappingModule = (factory: ModuleFactory) =>
|
|
6
|
+
factory({
|
|
7
|
+
pageUrl: "/rbac/permission-mappings",
|
|
8
|
+
name: "rbac/permission-mappings",
|
|
9
|
+
model: PermissionMapping,
|
|
10
|
+
moduleId: "f3aef019-c75f-4ca8-a9b5-a7495d901fc8",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const ModulePathsModule = (factory: ModuleFactory) =>
|
|
14
|
+
factory({
|
|
15
|
+
pageUrl: "/rbac/module-paths",
|
|
16
|
+
name: "rbac/module-paths",
|
|
17
|
+
model: ModulePaths,
|
|
18
|
+
moduleId: "f4fb3f01-a947-4c2e-89c8-354a518cdb13",
|
|
19
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { generateMigrationFile, downloadMigrationFile } from "./RbacMigrationGenerator";
|
|
3
|
+
import { RoleInterface } from "../../role";
|
|
4
|
+
import { PermissionsMap } from "../data/RbacTypes";
|
|
5
|
+
|
|
6
|
+
const mockFeatures = [
|
|
7
|
+
{
|
|
8
|
+
id: "feat-1",
|
|
9
|
+
name: "CRM",
|
|
10
|
+
isCore: false,
|
|
11
|
+
modules: [
|
|
12
|
+
{
|
|
13
|
+
id: "mod-1",
|
|
14
|
+
name: "pipelines",
|
|
15
|
+
permissions: { create: true, read: true, update: true, delete: false } as PermissionsMap,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const mockRoles: RoleInterface[] = [
|
|
22
|
+
{
|
|
23
|
+
id: "role-1",
|
|
24
|
+
type: "roles",
|
|
25
|
+
included: [],
|
|
26
|
+
createdAt: new Date(),
|
|
27
|
+
updatedAt: new Date(),
|
|
28
|
+
name: "Manager",
|
|
29
|
+
description: "",
|
|
30
|
+
isSelectable: true,
|
|
31
|
+
requiredFeature: undefined,
|
|
32
|
+
} as unknown as RoleInterface,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const mockRolePermissionsMap = new Map<string, PermissionsMap>();
|
|
36
|
+
mockRolePermissionsMap.set("role-1:mod-1", { create: true, read: true, update: true, delete: true });
|
|
37
|
+
|
|
38
|
+
describe("RbacMigrationGenerator", () => {
|
|
39
|
+
describe("Scenario: Generate valid migration file content", () => {
|
|
40
|
+
it("should produce a valid TypeScript file with MigrationInterface structure", () => {
|
|
41
|
+
const content = generateMigrationFile({
|
|
42
|
+
features: mockFeatures,
|
|
43
|
+
roles: mockRoles,
|
|
44
|
+
rolePermissionsMap: mockRolePermissionsMap,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(content).toContain("MigrationInterface");
|
|
48
|
+
expect(content).toContain("moduleQuery");
|
|
49
|
+
expect(content).toContain("pipelines");
|
|
50
|
+
expect(content).toContain("permissionQuery");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should include default permissions for each module", () => {
|
|
54
|
+
const content = generateMigrationFile({
|
|
55
|
+
features: mockFeatures,
|
|
56
|
+
roles: mockRoles,
|
|
57
|
+
rolePermissionsMap: mockRolePermissionsMap,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(content).toContain("Action.Create");
|
|
61
|
+
expect(content).toContain("Action.Read");
|
|
62
|
+
expect(content).toContain("Action.Update");
|
|
63
|
+
expect(content).toContain("Action.Delete");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("Scenario: CompanyAdministrator always gets all-true permissions", () => {
|
|
68
|
+
it("should include CompanyAdministrator role with all true permissions", () => {
|
|
69
|
+
const content = generateMigrationFile({
|
|
70
|
+
features: mockFeatures,
|
|
71
|
+
roles: mockRoles,
|
|
72
|
+
rolePermissionsMap: mockRolePermissionsMap,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(content).toContain("2e1eee00-6cba-4506-9059-ccd24e4ea5b0");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Scenario: Migration file contains role-specific overrides", () => {
|
|
80
|
+
it("should include role permission overrides in generated queries", () => {
|
|
81
|
+
const content = generateMigrationFile({
|
|
82
|
+
features: mockFeatures,
|
|
83
|
+
roles: mockRoles,
|
|
84
|
+
rolePermissionsMap: mockRolePermissionsMap,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(content).toContain("Manager");
|
|
88
|
+
expect(content).toContain("permissionQuery");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Scenario: Download triggers browser download", () => {
|
|
93
|
+
it("should create a blob and trigger download", () => {
|
|
94
|
+
const mockAnchor = {
|
|
95
|
+
href: "",
|
|
96
|
+
download: "",
|
|
97
|
+
click: vi.fn(),
|
|
98
|
+
style: {},
|
|
99
|
+
};
|
|
100
|
+
const mockCreateElement = vi.fn().mockReturnValue(mockAnchor);
|
|
101
|
+
const mockCreateObjectURL = vi.fn().mockReturnValue("blob:url");
|
|
102
|
+
const mockRevokeObjectURL = vi.fn();
|
|
103
|
+
const mockAppendChild = vi.fn();
|
|
104
|
+
const mockRemoveChild = vi.fn();
|
|
105
|
+
|
|
106
|
+
vi.stubGlobal("document", {
|
|
107
|
+
createElement: mockCreateElement,
|
|
108
|
+
body: { appendChild: mockAppendChild, removeChild: mockRemoveChild },
|
|
109
|
+
});
|
|
110
|
+
vi.stubGlobal("URL", {
|
|
111
|
+
createObjectURL: mockCreateObjectURL,
|
|
112
|
+
revokeObjectURL: mockRevokeObjectURL,
|
|
113
|
+
});
|
|
114
|
+
vi.stubGlobal("Blob", vi.fn());
|
|
115
|
+
|
|
116
|
+
downloadMigrationFile("test content");
|
|
117
|
+
|
|
118
|
+
expect(mockCreateElement).toHaveBeenCalledWith("a");
|
|
119
|
+
expect(mockAnchor.click).toHaveBeenCalled();
|
|
120
|
+
|
|
121
|
+
vi.unstubAllGlobals();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { RoleInterface } from "../../role";
|
|
2
|
+
import { ActionType, ACTION_TYPES, COMPANY_ADMINISTRATOR_ROLE_ID, PermissionsMap } from "../data/RbacTypes";
|
|
3
|
+
|
|
4
|
+
const ACTION_ENUM_MAP: Record<ActionType, string> = {
|
|
5
|
+
read: "Action.Read",
|
|
6
|
+
create: "Action.Create",
|
|
7
|
+
update: "Action.Update",
|
|
8
|
+
delete: "Action.Delete",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function formatPermissionValue(value: boolean | string): string {
|
|
12
|
+
if (value === true) return "true";
|
|
13
|
+
if (value === false) return "false";
|
|
14
|
+
return `"${value}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatPermissionsFromMap(perms: PermissionsMap): string {
|
|
18
|
+
const entries: string[] = [];
|
|
19
|
+
for (const actionType of ACTION_TYPES) {
|
|
20
|
+
const value = perms[actionType];
|
|
21
|
+
if (value !== undefined) {
|
|
22
|
+
entries.push(`{ type: ${ACTION_ENUM_MAP[actionType]}, value: ${formatPermissionValue(value)} }`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (entries.length === 0) return "[]";
|
|
27
|
+
if (entries.length === 1) return `[${entries[0]}]`;
|
|
28
|
+
return `[\n ${entries.join(",\n ")},\n ]`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface EffectiveFeature {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
isCore: boolean;
|
|
35
|
+
modules: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
permissions: PermissionsMap;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function generateMigrationFile(params: {
|
|
43
|
+
features: EffectiveFeature[];
|
|
44
|
+
roles: RoleInterface[];
|
|
45
|
+
rolePermissionsMap: Map<string, PermissionsMap>;
|
|
46
|
+
}): string {
|
|
47
|
+
const { features, roles, rolePermissionsMap } = params;
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
|
|
50
|
+
// Header
|
|
51
|
+
lines.push(`/**`);
|
|
52
|
+
lines.push(` * RBAC Migration - Generated on ${new Date().toISOString().split("T")[0]}`);
|
|
53
|
+
lines.push(` * Contains features, modules, roles, and permission mappings.`);
|
|
54
|
+
lines.push(` */`);
|
|
55
|
+
lines.push(``);
|
|
56
|
+
lines.push(`import { Action } from "src/common/enums/action";`);
|
|
57
|
+
lines.push(`import { MigrationInterface } from "src/core/migrator/interfaces/migration.interface";`);
|
|
58
|
+
lines.push(
|
|
59
|
+
`import { featureQuery, moduleQuery, roleQuery, permissionQuery } from "src/neo4j.migrations/queries/migration.queries";`,
|
|
60
|
+
);
|
|
61
|
+
lines.push(``);
|
|
62
|
+
lines.push(`export const migration: MigrationInterface[] = [`);
|
|
63
|
+
|
|
64
|
+
// Features
|
|
65
|
+
lines.push(` /* ************************************ */`);
|
|
66
|
+
lines.push(` /* FEATURES */`);
|
|
67
|
+
lines.push(` /* ************************************ */`);
|
|
68
|
+
for (const feature of features) {
|
|
69
|
+
lines.push(` {`);
|
|
70
|
+
lines.push(` query: featureQuery,`);
|
|
71
|
+
lines.push(` queryParams: {`);
|
|
72
|
+
lines.push(` featureId: "${feature.id}",`);
|
|
73
|
+
lines.push(` featureName: "${feature.name}",`);
|
|
74
|
+
lines.push(` isCore: ${feature.isCore},`);
|
|
75
|
+
lines.push(` },`);
|
|
76
|
+
lines.push(` },`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Modules grouped by feature
|
|
80
|
+
for (const feature of features) {
|
|
81
|
+
if (feature.modules.length === 0) continue;
|
|
82
|
+
|
|
83
|
+
lines.push(` /* ************************************ */`);
|
|
84
|
+
lines.push(` /* ${feature.name.toUpperCase().padEnd(37)} */`);
|
|
85
|
+
lines.push(` /* ************************************ */`);
|
|
86
|
+
|
|
87
|
+
for (const mod of feature.modules) {
|
|
88
|
+
lines.push(` {`);
|
|
89
|
+
lines.push(` query: moduleQuery,`);
|
|
90
|
+
lines.push(` queryParams: {`);
|
|
91
|
+
lines.push(` moduleName: "${mod.name}",`);
|
|
92
|
+
lines.push(` moduleId: "${mod.id}",`);
|
|
93
|
+
lines.push(` featureId: "${feature.id}",`);
|
|
94
|
+
lines.push(` permissions: JSON.stringify(${formatPermissionsFromMap(mod.permissions)}),`);
|
|
95
|
+
lines.push(` },`);
|
|
96
|
+
lines.push(` },`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Roles
|
|
101
|
+
lines.push(` /* ************************************ */`);
|
|
102
|
+
lines.push(` /* ROLES */`);
|
|
103
|
+
lines.push(` /* ************************************ */`);
|
|
104
|
+
for (const role of roles) {
|
|
105
|
+
lines.push(` {`);
|
|
106
|
+
lines.push(` query: roleQuery,`);
|
|
107
|
+
lines.push(` queryParams: {`);
|
|
108
|
+
lines.push(` roleId: "${role.id}",`);
|
|
109
|
+
lines.push(` roleName: "${role.name}",`);
|
|
110
|
+
lines.push(` isSelectable: ${role.isSelectable},`);
|
|
111
|
+
lines.push(` },`);
|
|
112
|
+
lines.push(` },`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Permission mappings
|
|
116
|
+
lines.push(` /* ************************************ */`);
|
|
117
|
+
lines.push(` /* PERMISSIONS */`);
|
|
118
|
+
lines.push(` /* ************************************ */`);
|
|
119
|
+
|
|
120
|
+
// Collect all module IDs for CompanyAdministrator (always all-true)
|
|
121
|
+
const allModuleIds: string[] = [];
|
|
122
|
+
for (const feature of features) {
|
|
123
|
+
for (const mod of feature.modules) {
|
|
124
|
+
allModuleIds.push(mod.id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const allTruePermissions: PermissionsMap = { read: true, create: true, update: true, delete: true };
|
|
129
|
+
|
|
130
|
+
// Group by role for readability
|
|
131
|
+
const permsByRole = new Map<string, Array<{ moduleId: string; permissions: PermissionsMap }>>();
|
|
132
|
+
|
|
133
|
+
// CompanyAdministrator always gets all-true for every module
|
|
134
|
+
permsByRole.set(
|
|
135
|
+
COMPANY_ADMINISTRATOR_ROLE_ID,
|
|
136
|
+
allModuleIds.map((moduleId) => ({ moduleId, permissions: allTruePermissions })),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
for (const [key, perms] of rolePermissionsMap) {
|
|
140
|
+
const [roleId, moduleId] = key.split(":");
|
|
141
|
+
if (roleId === COMPANY_ADMINISTRATOR_ROLE_ID) continue; // already handled
|
|
142
|
+
if (!permsByRole.has(roleId)) permsByRole.set(roleId, []);
|
|
143
|
+
permsByRole.get(roleId)!.push({ moduleId, permissions: perms });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const [roleId, moduleMappings] of permsByRole) {
|
|
147
|
+
const role = roles.find((r) => r.id === roleId);
|
|
148
|
+
if (role) {
|
|
149
|
+
lines.push(` // ${role.name}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const mapping of moduleMappings) {
|
|
153
|
+
lines.push(` {`);
|
|
154
|
+
lines.push(` query: permissionQuery,`);
|
|
155
|
+
lines.push(` queryParams: {`);
|
|
156
|
+
lines.push(` roleId: "${roleId}",`);
|
|
157
|
+
lines.push(` moduleId: "${mapping.moduleId}",`);
|
|
158
|
+
lines.push(` permissions: JSON.stringify(${formatPermissionsFromMap(mapping.permissions)}),`);
|
|
159
|
+
lines.push(` },`);
|
|
160
|
+
lines.push(` },`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lines.push(`];`);
|
|
165
|
+
lines.push(``);
|
|
166
|
+
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function downloadMigrationFile(content: string): void {
|
|
171
|
+
const now = new Date();
|
|
172
|
+
const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
|
|
173
|
+
const filename = `${dateStr}_001.ts`;
|
|
174
|
+
|
|
175
|
+
const blob = new Blob([content], { type: "text/plain" });
|
|
176
|
+
const url = URL.createObjectURL(blob);
|
|
177
|
+
const a = document.createElement("a");
|
|
178
|
+
a.href = url;
|
|
179
|
+
a.download = filename;
|
|
180
|
+
document.body.appendChild(a);
|
|
181
|
+
a.click();
|
|
182
|
+
document.body.removeChild(a);
|
|
183
|
+
URL.revokeObjectURL(url);
|
|
184
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -42,3 +42,7 @@ export type { ReferralConfig } from "./features/referral/config";
|
|
|
42
42
|
|
|
43
43
|
// Toast utilities
|
|
44
44
|
export { showToast, showError, dismissToast, showCustomToast, type ToastOptions } from "./utils/toast";
|
|
45
|
+
|
|
46
|
+
// RBAC feature (data + modules only; components via /components, hooks via /client)
|
|
47
|
+
export * from "./features/rbac/data";
|
|
48
|
+
export * from "./features/rbac/rbac.module";
|