@carlonicora/nextjs-jsonapi 1.78.0 → 1.80.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/{AssistantMessageInterface-DS_tyJTV.d.ts → AssistantMessageInterface-BpEhx2pC.d.ts} +19 -2
- package/dist/{AssistantMessageInterface-D0Kwf8CR.d.mts → AssistantMessageInterface-DJ3Me16Y.d.mts} +19 -2
- package/dist/{AuthComponent-Blbs06ud.d.ts → AuthComponent-B6DIk8Vf.d.ts} +1 -1
- package/dist/{AuthComponent-huIaK5rm.d.mts → AuthComponent-BKI0ZbtD.d.mts} +1 -1
- package/dist/{BlockNoteEditor-JXK3JGKJ.mjs → BlockNoteEditor-3M5PD3BZ.mjs} +4 -4
- package/dist/{BlockNoteEditor-2G5UYALC.js → BlockNoteEditor-YLTPJPTV.js} +14 -14
- package/dist/{BlockNoteEditor-2G5UYALC.js.map → BlockNoteEditor-YLTPJPTV.js.map} +1 -1
- package/dist/RbacTypes-BTbr27Ew.d.mts +43 -0
- package/dist/RbacTypes-BTbr27Ew.d.ts +43 -0
- package/dist/{auth.interface-CQJ6A2Cj.d.ts → auth.interface-BBUgMZzs.d.ts} +1 -1
- package/dist/{auth.interface-Bdq7-8iV.d.mts → auth.interface-XYEREOD6.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-ZEDB6JVB.js → chunk-4NOQNTFI.js} +1585 -1405
- package/dist/chunk-4NOQNTFI.js.map +1 -0
- package/dist/{chunk-I65SSQ5Z.mjs → chunk-6UMB5LTQ.mjs} +157 -7
- package/dist/chunk-6UMB5LTQ.mjs.map +1 -0
- package/dist/{chunk-FDJQRIMY.js → chunk-N4YZ45SK.js} +174 -24
- package/dist/chunk-N4YZ45SK.js.map +1 -0
- package/dist/{chunk-NB6TIKHK.mjs → chunk-NQV5RDCK.mjs} +2524 -2344
- package/dist/chunk-NQV5RDCK.mjs.map +1 -0
- package/dist/{chunk-NZOUEN67.mjs → chunk-PV5V6CVW.mjs} +38 -29
- package/dist/{chunk-NZOUEN67.mjs.map → chunk-PV5V6CVW.mjs.map} +1 -1
- package/dist/{chunk-X4YDETTD.js → chunk-ZEJSPTHS.js} +39 -30
- package/dist/chunk-ZEJSPTHS.js.map +1 -0
- package/dist/client/index.d.mts +6 -24
- package/dist/client/index.d.ts +6 -24
- package/dist/client/index.js +4 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -9
- package/dist/components/index.d.mts +55 -39
- package/dist/components/index.d.ts +55 -39
- package/dist/components/index.js +4 -8
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +5 -9
- package/dist/{config-B3jKt9P7.d.ts → config-B5oBQVEA.d.ts} +1 -1
- package/dist/{config-DkHF61xA.d.mts → config-Bx_uh22h.d.mts} +1 -1
- package/dist/contexts/index.d.mts +41 -4
- package/dist/contexts/index.d.ts +41 -4
- package/dist/contexts/index.js +8 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +7 -3
- package/dist/core/index.d.mts +51 -11
- package/dist/core/index.d.ts +51 -11
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +7 -1
- package/dist/index.d.mts +117 -20
- package/dist/index.d.ts +117 -20
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/{notification.interface-DG6obXUH.d.mts → notification.interface-DLZGtV7Z.d.mts} +1 -1
- package/dist/{notification.interface-DcSuc9CL.d.ts → notification.interface-aLEJbA_g.d.ts} +1 -1
- package/dist/{s3.service-DGilbikH.d.mts → s3.service-CVgLWaDc.d.mts} +2 -2
- package/dist/{s3.service-DjwEQJPe.d.ts → s3.service-SLlX0Zbz.d.ts} +2 -2
- package/dist/server/index.d.mts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/useDataListRetriever-BqJSFBck.d.mts +33 -0
- package/dist/useDataListRetriever-BqJSFBck.d.ts +33 -0
- package/dist/{useSocket-CmzVtg32.d.mts → useSocket-BkxHHujj.d.mts} +1 -1
- package/dist/{useSocket-8eUtnL7J.d.ts → useSocket-CMDjWFYm.d.ts} +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +0 -4
- package/src/components/index.ts +0 -3
- package/src/contexts/index.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +2 -0
- package/src/features/assistant/components/parts/AssistantThread.tsx +1 -1
- package/src/features/assistant-message/AssistantMessageModule.ts +4 -0
- package/src/features/assistant-message/components/MessageItem.tsx +7 -7
- package/src/features/assistant-message/components/MessageList.tsx +1 -1
- package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +11 -7
- package/src/features/assistant-message/components/index.ts +1 -0
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +135 -0
- package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +151 -0
- package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +29 -0
- package/src/features/assistant-message/components/parts/__tests__/MessageSourcesPanel.spec.tsx +70 -0
- package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +105 -0
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +88 -0
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +51 -0
- package/src/features/assistant-message/components/parts/tabs/SuggestedQuestionsTab.tsx +24 -0
- package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +142 -0
- package/src/features/assistant-message/data/AssistantMessage.ts +20 -0
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +2 -0
- package/src/features/assistant-message/data/AssistantMessageService.ts +13 -4
- package/src/features/assistant-message/data/__tests__/AssistantMessage.citations.spec.ts +65 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +8 -0
- package/src/features/chunk/ChunkModule.ts +18 -0
- package/src/features/chunk/data/Chunk.ts +49 -0
- package/src/features/chunk/data/ChunkInput.ts +3 -0
- package/src/features/chunk/data/ChunkInterface.ts +18 -0
- package/src/features/chunk/data/__tests__/Chunk.spec.ts +83 -0
- package/src/features/chunk/data/index.ts +3 -0
- package/src/features/chunk/index.ts +2 -0
- package/src/features/rbac/components/RbacContainer.tsx +318 -49
- package/src/features/rbac/components/RbacPermissionPicker.tsx +144 -121
- package/src/features/rbac/contexts/RbacContext.tsx +209 -0
- package/src/features/rbac/contexts/index.ts +1 -0
- package/src/features/rbac/data/RbacMatrixModel.ts +84 -0
- package/src/features/rbac/data/RbacService.ts +61 -33
- package/src/features/rbac/data/RbacTypes.ts +28 -0
- package/src/features/rbac/data/index.ts +1 -0
- package/src/features/rbac/index.ts +1 -10
- package/src/features/rbac/rbac.module.ts +13 -0
- package/dist/ModulePathsInterface-BrdqgteS.d.mts +0 -31
- package/dist/ModulePathsInterface-DJKs7s_s.d.ts +0 -31
- package/dist/chunk-FDJQRIMY.js.map +0 -1
- package/dist/chunk-I65SSQ5Z.mjs.map +0 -1
- package/dist/chunk-NB6TIKHK.mjs.map +0 -1
- package/dist/chunk-X4YDETTD.js.map +0 -1
- package/dist/chunk-ZEDB6JVB.js.map +0 -1
- package/dist/useRbacState-C88O-5L8.d.ts +0 -77
- package/dist/useRbacState-mqYiRp3J.d.mts +0 -77
- package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +0 -46
- package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +0 -52
- package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +0 -59
- package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +0 -29
- package/src/features/rbac/components/RbacFeatureSection.tsx +0 -66
- package/src/features/rbac/components/RbacModuleTable.tsx +0 -121
- package/src/features/rbac/components/RbacToolbar.tsx +0 -40
- package/src/features/rbac/hooks/useRbacState.test.ts +0 -180
- package/src/features/rbac/hooks/useRbacState.ts +0 -319
- package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +0 -124
- package/src/features/rbac/utils/RbacMigrationGenerator.ts +0 -184
- /package/dist/{BlockNoteEditor-JXK3JGKJ.mjs.map → BlockNoteEditor-3M5PD3BZ.mjs.map} +0 -0
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { renderHook, act } from "@testing-library/react";
|
|
3
|
-
import { useRbacState } from "./useRbacState";
|
|
4
|
-
import { FeatureInterface } from "../../feature";
|
|
5
|
-
import { RoleInterface } from "../../role";
|
|
6
|
-
import { PermissionMappingInterface } from "../data/PermissionMappingInterface";
|
|
7
|
-
import { ModulePathsInterface } from "../data/ModulePathsInterface";
|
|
8
|
-
import { ModuleInterface } from "../../module";
|
|
9
|
-
|
|
10
|
-
const mockModule: ModuleInterface = {
|
|
11
|
-
id: "mod-1",
|
|
12
|
-
type: "modules",
|
|
13
|
-
included: [],
|
|
14
|
-
createdAt: new Date(),
|
|
15
|
-
updatedAt: new Date(),
|
|
16
|
-
identifier: "pipelines",
|
|
17
|
-
name: "pipelines",
|
|
18
|
-
permissions: { create: true, read: true, update: true, delete: false },
|
|
19
|
-
} as ModuleInterface;
|
|
20
|
-
|
|
21
|
-
const mockFeatures: FeatureInterface[] = [
|
|
22
|
-
{
|
|
23
|
-
id: "feat-1",
|
|
24
|
-
type: "features",
|
|
25
|
-
included: [],
|
|
26
|
-
createdAt: new Date(),
|
|
27
|
-
updatedAt: new Date(),
|
|
28
|
-
identifier: "CRM",
|
|
29
|
-
name: "CRM",
|
|
30
|
-
isCore: false,
|
|
31
|
-
modules: [mockModule],
|
|
32
|
-
} as FeatureInterface,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const mockRoles: RoleInterface[] = [
|
|
36
|
-
{
|
|
37
|
-
id: "role-1",
|
|
38
|
-
type: "roles",
|
|
39
|
-
included: [],
|
|
40
|
-
createdAt: new Date(),
|
|
41
|
-
updatedAt: new Date(),
|
|
42
|
-
name: "Manager",
|
|
43
|
-
description: "",
|
|
44
|
-
isSelectable: true,
|
|
45
|
-
requiredFeature: undefined,
|
|
46
|
-
} as unknown as RoleInterface,
|
|
47
|
-
{
|
|
48
|
-
id: "role-2",
|
|
49
|
-
type: "roles",
|
|
50
|
-
included: [],
|
|
51
|
-
createdAt: new Date(),
|
|
52
|
-
updatedAt: new Date(),
|
|
53
|
-
name: "Viewer",
|
|
54
|
-
description: "",
|
|
55
|
-
isSelectable: true,
|
|
56
|
-
requiredFeature: undefined,
|
|
57
|
-
} as unknown as RoleInterface,
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const mockPermissionMappings: PermissionMappingInterface[] = [];
|
|
61
|
-
|
|
62
|
-
const mockModulePaths: ModulePathsInterface[] = [
|
|
63
|
-
{
|
|
64
|
-
id: "mod-1",
|
|
65
|
-
type: "module-paths",
|
|
66
|
-
included: [],
|
|
67
|
-
createdAt: new Date(),
|
|
68
|
-
updatedAt: new Date(),
|
|
69
|
-
moduleId: "mod-1",
|
|
70
|
-
paths: ["owner", "company.user"],
|
|
71
|
-
} as unknown as ModulePathsInterface,
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
function initHook() {
|
|
75
|
-
const { result } = renderHook(() => useRbacState());
|
|
76
|
-
act(() => {
|
|
77
|
-
result.current.init(mockFeatures, mockRoles, mockPermissionMappings, mockModulePaths);
|
|
78
|
-
});
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe("useRbacState", () => {
|
|
83
|
-
describe("Scenario: Initialize state from fetched data", () => {
|
|
84
|
-
it("should initialize with features and roles", () => {
|
|
85
|
-
const result = initHook();
|
|
86
|
-
|
|
87
|
-
expect(result.current.original).not.toBeNull();
|
|
88
|
-
expect(result.current.original!.features).toHaveLength(1);
|
|
89
|
-
expect(result.current.original!.features[0].name).toBe("CRM");
|
|
90
|
-
expect(result.current.isDirty).toBe(false);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("Scenario: Set module default permission", () => {
|
|
95
|
-
it("should update default permission and mark as dirty", () => {
|
|
96
|
-
const result = initHook();
|
|
97
|
-
|
|
98
|
-
act(() => {
|
|
99
|
-
result.current.setModuleDefaultPermission("mod-1", "delete", true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(result.current.getModuleDefaultPermission("mod-1", "delete")).toBe(true);
|
|
103
|
-
expect(result.current.isDirty).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("Scenario: Set role-specific permission override", () => {
|
|
108
|
-
it("should set role permission for a specific module and action", () => {
|
|
109
|
-
const result = initHook();
|
|
110
|
-
|
|
111
|
-
act(() => {
|
|
112
|
-
result.current.setRolePermission("role-1", "mod-1", "create", false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(result.current.getRolePermission("role-1", "mod-1", "create")).toBe(false);
|
|
116
|
-
expect(result.current.isDirty).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("should support string path values for role permissions", () => {
|
|
120
|
-
const result = initHook();
|
|
121
|
-
|
|
122
|
-
act(() => {
|
|
123
|
-
result.current.setRolePermission("role-2", "mod-1", "update", "owner");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(result.current.getRolePermission("role-2", "mod-1", "update")).toBe("owner");
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("Scenario: Clear role permission (inherit from default)", () => {
|
|
131
|
-
it("should clear a role permission so it inherits from default", () => {
|
|
132
|
-
const result = initHook();
|
|
133
|
-
|
|
134
|
-
act(() => {
|
|
135
|
-
result.current.setRolePermission("role-1", "mod-1", "create", false);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
act(() => {
|
|
139
|
-
result.current.clearRolePermission("role-1", "mod-1", "create");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(result.current.getRolePermission("role-1", "mod-1", "create")).toBeNull();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("Scenario: Reset to initial state", () => {
|
|
147
|
-
it("should reset all changes and clear dirty flag", () => {
|
|
148
|
-
const result = initHook();
|
|
149
|
-
|
|
150
|
-
act(() => {
|
|
151
|
-
result.current.setModuleDefaultPermission("mod-1", "delete", true);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
expect(result.current.isDirty).toBe(true);
|
|
155
|
-
|
|
156
|
-
act(() => {
|
|
157
|
-
result.current.reset();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
expect(result.current.isDirty).toBe(false);
|
|
161
|
-
expect(result.current.getModuleDefaultPermission("mod-1", "delete")).toBe(false);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("Scenario: Get effective configuration for migration", () => {
|
|
166
|
-
it("should return complete configuration with all changes applied", () => {
|
|
167
|
-
const result = initHook();
|
|
168
|
-
|
|
169
|
-
act(() => {
|
|
170
|
-
result.current.setRolePermission("role-1", "mod-1", "delete", true);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const config = result.current.getEffectiveConfiguration();
|
|
174
|
-
expect(config).not.toBeNull();
|
|
175
|
-
expect(config!.features).toHaveLength(1);
|
|
176
|
-
expect(config!.roles).toHaveLength(2);
|
|
177
|
-
expect(config!.rolePermissionsMap).toBeInstanceOf(Map);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
});
|
|
@@ -1,319 +0,0 @@
|
|
|
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>;
|
|
@@ -1,124 +0,0 @@
|
|
|
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
|
-
});
|