@carlonicora/nextjs-jsonapi 1.52.0 → 1.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/scripts/generate-web-module/templates/components/list-container.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/list-container.template.js +7 -1
- package/dist/scripts/generate-web-module/templates/components/list-container.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +7 -4
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/pages/list-page.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +1 -4
- package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -1
- 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 +2 -2
- package/scripts/generate-web-module/templates/components/list-container.template.ts +7 -1
- package/scripts/generate-web-module/templates/components/multi-selector.template.ts +7 -4
- package/scripts/generate-web-module/templates/pages/list-page.template.ts +1 -4
- 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
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunkQOLVON35js = require('../chunk-QOLVON35.js');
|
|
19
19
|
require('../chunk-LXKSUWAV.js');
|
|
20
20
|
require('../chunk-IBS6NI7D.js');
|
|
21
21
|
|
|
@@ -86,7 +86,7 @@ var ServerSession = class {
|
|
|
86
86
|
if (!rawModules) return false;
|
|
87
87
|
const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
|
|
88
88
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
89
|
-
return
|
|
89
|
+
return _chunkQOLVON35js.checkPermissionsFromServer.call(void 0, {
|
|
90
90
|
module: params.module,
|
|
91
91
|
action: params.action,
|
|
92
92
|
data: params.data,
|
|
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
exports.ServerAuthService =
|
|
299
|
+
exports.ServerAuthService = _chunkQOLVON35js.AuthService; exports.ServerCompanyService = _chunkQOLVON35js.CompanyService; exports.ServerContentService = _chunkQOLVON35js.ContentService; exports.ServerFeatureService = _chunkQOLVON35js.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkQOLVON35js.NotificationService; exports.ServerPushService = _chunkQOLVON35js.PushService; exports.ServerRoleService = _chunkQOLVON35js.RoleService; exports.ServerS3Service = _chunkQOLVON35js.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkQOLVON35js.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
|
|
300
300
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { F as FeatureInterface } from './feature.interface-BxFFOPNq.mjs';
|
|
2
|
+
import { R as RoleInterface } from './notification.interface-DG7cq9oG.mjs';
|
|
3
|
+
import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-49EWvbWy.mjs';
|
|
4
|
+
|
|
5
|
+
type PageInfo = {
|
|
6
|
+
startItem: number;
|
|
7
|
+
endItem: number;
|
|
8
|
+
pageSize: number;
|
|
9
|
+
};
|
|
10
|
+
type DataListRetriever<T> = {
|
|
11
|
+
ready?: boolean;
|
|
12
|
+
setReady: (state: boolean) => void;
|
|
13
|
+
isLoaded: boolean;
|
|
14
|
+
data: T[] | undefined;
|
|
15
|
+
total?: number;
|
|
16
|
+
next?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
17
|
+
previous?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
18
|
+
search: (search: string) => Promise<void>;
|
|
19
|
+
refresh: () => Promise<void>;
|
|
20
|
+
addAdditionalParameter: (key: string, value: any | null) => void;
|
|
21
|
+
removeAdditionalParameter: (key: string) => void;
|
|
22
|
+
setRefreshedElement: (element: T) => void;
|
|
23
|
+
removeElement: (element: T) => void;
|
|
24
|
+
isSearch: boolean;
|
|
25
|
+
pageInfo?: PageInfo;
|
|
26
|
+
};
|
|
27
|
+
declare function useDataListRetriever<T>(params: {
|
|
28
|
+
ready?: boolean;
|
|
29
|
+
retriever: (params: any) => Promise<T[]>;
|
|
30
|
+
retrieverParams?: any;
|
|
31
|
+
search?: string;
|
|
32
|
+
addAdditionalParameter?: (key: string, value: any | null) => void;
|
|
33
|
+
requiresSearch?: boolean;
|
|
34
|
+
module: any;
|
|
35
|
+
}): DataListRetriever<T>;
|
|
36
|
+
|
|
37
|
+
interface OriginalData {
|
|
38
|
+
features: FeatureInterface[];
|
|
39
|
+
roles: RoleInterface[];
|
|
40
|
+
permissionMappings: PermissionMappingInterface[];
|
|
41
|
+
moduleRelationshipPaths: Map<string, string[]>;
|
|
42
|
+
}
|
|
43
|
+
declare function useRbacState(): {
|
|
44
|
+
original: OriginalData | null;
|
|
45
|
+
isDirty: boolean;
|
|
46
|
+
init: (features: FeatureInterface[], roles: RoleInterface[], permissionMappings: PermissionMappingInterface[], modulePaths: ModulePathsInterface[]) => void;
|
|
47
|
+
setFeatureIsCore: (featureId: string, isCore: boolean) => void;
|
|
48
|
+
setModuleDefaultPermission: (moduleId: string, actionType: ActionType, value: PermissionValue) => void;
|
|
49
|
+
setRolePermission: (roleId: string, moduleId: string, actionType: ActionType, value: PermissionValue) => void;
|
|
50
|
+
clearRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => void;
|
|
51
|
+
clearAllRolePermissions: (roleId: string, moduleId: string) => void;
|
|
52
|
+
resetModulePermissions: (moduleId: string, roles: {
|
|
53
|
+
id: string;
|
|
54
|
+
}[]) => void;
|
|
55
|
+
reset: () => void;
|
|
56
|
+
getFeatureIsCore: (featureId: string) => boolean;
|
|
57
|
+
getModuleDefaultPermission: (moduleId: string, actionType: ActionType) => PermissionValue | undefined;
|
|
58
|
+
getRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => PermissionValue | undefined | null;
|
|
59
|
+
getEffectiveConfiguration: () => {
|
|
60
|
+
features: {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
isCore: boolean;
|
|
64
|
+
modules: {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
permissions: PermissionsMap;
|
|
68
|
+
}[];
|
|
69
|
+
}[];
|
|
70
|
+
roles: RoleInterface[];
|
|
71
|
+
rolePermissionsMap: Map<string, PermissionsMap>;
|
|
72
|
+
} | null;
|
|
73
|
+
getModuleRelationshipPaths: (moduleId: string) => string[];
|
|
74
|
+
};
|
|
75
|
+
type RbacStateApi = ReturnType<typeof useRbacState>;
|
|
76
|
+
|
|
77
|
+
export { type DataListRetriever as D, type RbacStateApi as R, useDataListRetriever as a, useRbacState as u };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { F as FeatureInterface } from './feature.interface-CIWxo8NP.js';
|
|
2
|
+
import { R as RoleInterface } from './notification.interface-COKHDQeE.js';
|
|
3
|
+
import { P as PermissionMappingInterface, M as ModulePathsInterface, A as ActionType, a as PermissionValue, b as PermissionsMap } from './ModulePathsInterface-wVS5Raa4.js';
|
|
4
|
+
|
|
5
|
+
type PageInfo = {
|
|
6
|
+
startItem: number;
|
|
7
|
+
endItem: number;
|
|
8
|
+
pageSize: number;
|
|
9
|
+
};
|
|
10
|
+
type DataListRetriever<T> = {
|
|
11
|
+
ready?: boolean;
|
|
12
|
+
setReady: (state: boolean) => void;
|
|
13
|
+
isLoaded: boolean;
|
|
14
|
+
data: T[] | undefined;
|
|
15
|
+
total?: number;
|
|
16
|
+
next?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
17
|
+
previous?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
18
|
+
search: (search: string) => Promise<void>;
|
|
19
|
+
refresh: () => Promise<void>;
|
|
20
|
+
addAdditionalParameter: (key: string, value: any | null) => void;
|
|
21
|
+
removeAdditionalParameter: (key: string) => void;
|
|
22
|
+
setRefreshedElement: (element: T) => void;
|
|
23
|
+
removeElement: (element: T) => void;
|
|
24
|
+
isSearch: boolean;
|
|
25
|
+
pageInfo?: PageInfo;
|
|
26
|
+
};
|
|
27
|
+
declare function useDataListRetriever<T>(params: {
|
|
28
|
+
ready?: boolean;
|
|
29
|
+
retriever: (params: any) => Promise<T[]>;
|
|
30
|
+
retrieverParams?: any;
|
|
31
|
+
search?: string;
|
|
32
|
+
addAdditionalParameter?: (key: string, value: any | null) => void;
|
|
33
|
+
requiresSearch?: boolean;
|
|
34
|
+
module: any;
|
|
35
|
+
}): DataListRetriever<T>;
|
|
36
|
+
|
|
37
|
+
interface OriginalData {
|
|
38
|
+
features: FeatureInterface[];
|
|
39
|
+
roles: RoleInterface[];
|
|
40
|
+
permissionMappings: PermissionMappingInterface[];
|
|
41
|
+
moduleRelationshipPaths: Map<string, string[]>;
|
|
42
|
+
}
|
|
43
|
+
declare function useRbacState(): {
|
|
44
|
+
original: OriginalData | null;
|
|
45
|
+
isDirty: boolean;
|
|
46
|
+
init: (features: FeatureInterface[], roles: RoleInterface[], permissionMappings: PermissionMappingInterface[], modulePaths: ModulePathsInterface[]) => void;
|
|
47
|
+
setFeatureIsCore: (featureId: string, isCore: boolean) => void;
|
|
48
|
+
setModuleDefaultPermission: (moduleId: string, actionType: ActionType, value: PermissionValue) => void;
|
|
49
|
+
setRolePermission: (roleId: string, moduleId: string, actionType: ActionType, value: PermissionValue) => void;
|
|
50
|
+
clearRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => void;
|
|
51
|
+
clearAllRolePermissions: (roleId: string, moduleId: string) => void;
|
|
52
|
+
resetModulePermissions: (moduleId: string, roles: {
|
|
53
|
+
id: string;
|
|
54
|
+
}[]) => void;
|
|
55
|
+
reset: () => void;
|
|
56
|
+
getFeatureIsCore: (featureId: string) => boolean;
|
|
57
|
+
getModuleDefaultPermission: (moduleId: string, actionType: ActionType) => PermissionValue | undefined;
|
|
58
|
+
getRolePermission: (roleId: string, moduleId: string, actionType: ActionType) => PermissionValue | undefined | null;
|
|
59
|
+
getEffectiveConfiguration: () => {
|
|
60
|
+
features: {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
isCore: boolean;
|
|
64
|
+
modules: {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
permissions: PermissionsMap;
|
|
68
|
+
}[];
|
|
69
|
+
}[];
|
|
70
|
+
roles: RoleInterface[];
|
|
71
|
+
rolePermissionsMap: Map<string, PermissionsMap>;
|
|
72
|
+
} | null;
|
|
73
|
+
getModuleRelationshipPaths: (moduleId: string) => string[];
|
|
74
|
+
};
|
|
75
|
+
type RbacStateApi = ReturnType<typeof useRbacState>;
|
|
76
|
+
|
|
77
|
+
export { type DataListRetriever as D, type RbacStateApi as R, useDataListRetriever as a, useRbacState as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlonicora/nextjs-jsonapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.1",
|
|
4
4
|
"description": "Next.js JSON:API client with server/client support and caching",
|
|
5
5
|
"author": "Carlo Nicora",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "NODE_OPTIONS='--max-old-space-size=8192' tsup && tsc -p scripts/generate-web-module/tsconfig.json",
|
|
59
|
-
"dev": "NODE_OPTIONS='--max-old-space-size=8192' tsup --watch
|
|
59
|
+
"dev": "NODE_OPTIONS='--max-old-space-size=8192' tsup --watch",
|
|
60
60
|
"clean": "rm -rf dist",
|
|
61
61
|
"lint": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" --fix",
|
|
62
62
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
@@ -18,9 +18,15 @@ export function generateListContainerTemplate(data: FrontendTemplateData): strin
|
|
|
18
18
|
return `"use client";
|
|
19
19
|
|
|
20
20
|
import ${names.pascalCase}List from "@/features/${data.importTargetDir}/${names.kebabCase}/components/lists/${names.pascalCase}List";
|
|
21
|
+
import { RoundPageContainer } from "@carlonicora/nextjs-jsonapi/components";
|
|
22
|
+
import { Modules } from "@carlonicora/nextjs-jsonapi/core";
|
|
21
23
|
|
|
22
24
|
function ${names.pascalCase}ListContainerInternal() {
|
|
23
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
<RoundPageContainer module={Modules.${names.pascalCase}}>
|
|
27
|
+
<${names.pascalCase}List />
|
|
28
|
+
</RoundPageContainer>
|
|
29
|
+
);
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export default function ${names.pascalCase}ListContainer() {
|
|
@@ -15,7 +15,8 @@ import { FrontendTemplateData } from "../../types/template-data.interface";
|
|
|
15
15
|
export function generateMultiSelectorTemplate(data: FrontendTemplateData): string {
|
|
16
16
|
const { names, fields, extendsContent } = data;
|
|
17
17
|
const hasNameField = extendsContent || fields.some((f) => f.name === "name");
|
|
18
|
-
const
|
|
18
|
+
const firstStringField = fields.find((f) => f.tsType === "string" || f.tsType === "string | null");
|
|
19
|
+
const displayProp = hasNameField ? "name" : (firstStringField?.name ?? "id");
|
|
19
20
|
|
|
20
21
|
return `"use client";
|
|
21
22
|
|
|
@@ -26,11 +27,12 @@ import { FormFieldWrapper, MultipleSelector } from "@carlonicora/nextjs-jsonapi/
|
|
|
26
27
|
import { Option } from "@carlonicora/nextjs-jsonapi/components";
|
|
27
28
|
import { Modules } from "@carlonicora/nextjs-jsonapi/core";
|
|
28
29
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
30
|
+
import { useTranslations } from "next-intl";
|
|
29
31
|
import { useWatch } from "react-hook-form";
|
|
30
32
|
|
|
31
33
|
type ${names.pascalCase}MultiSelectType = {
|
|
32
34
|
id: string;
|
|
33
|
-
|
|
35
|
+
${displayProp}: string;
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
type ${names.pascalCase}MultiSelectorProps = {
|
|
@@ -58,6 +60,7 @@ export default function ${names.pascalCase}MultiSelector({
|
|
|
58
60
|
maxCount = 3,
|
|
59
61
|
isRequired = false,
|
|
60
62
|
}: ${names.pascalCase}MultiSelectorProps) {
|
|
63
|
+
const t = useTranslations();
|
|
61
64
|
const [${names.camelCase}Options, set${names.pascalCase}Options] = useState<${names.pascalCase}Option[]>([]);
|
|
62
65
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
63
66
|
|
|
@@ -124,7 +127,7 @@ export default function ${names.pascalCase}MultiSelector({
|
|
|
124
127
|
// Convert to form format
|
|
125
128
|
const formValues = options.map((option) => ({
|
|
126
129
|
id: option.value,
|
|
127
|
-
|
|
130
|
+
${displayProp}: option.label,
|
|
128
131
|
}));
|
|
129
132
|
|
|
130
133
|
form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
|
|
@@ -160,7 +163,7 @@ export default function ${names.pascalCase}MultiSelector({
|
|
|
160
163
|
hideClearAllButton
|
|
161
164
|
onSearchSync={handleSearchSync}
|
|
162
165
|
delay={0}
|
|
163
|
-
emptyIndicator={<span className="text-muted-foreground">
|
|
166
|
+
emptyIndicator={<span className="text-muted-foreground">{t("ui.search.no_results_generic")}</span>}
|
|
164
167
|
/>
|
|
165
168
|
)}
|
|
166
169
|
</FormFieldWrapper>
|
|
@@ -17,7 +17,6 @@ export function generateListPageTemplate(data: FrontendTemplateData): string {
|
|
|
17
17
|
|
|
18
18
|
return `import ${names.pascalCase}ListContainer from "@/features/${data.importTargetDir}/${names.kebabCase}/components/containers/${names.pascalCase}ListContainer";
|
|
19
19
|
import { ${names.pascalCase}Provider } from "@/features/${data.importTargetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
|
|
20
|
-
import { RoundPageContainer } from "@carlonicora/nextjs-jsonapi/components";
|
|
21
20
|
import { Modules } from "@carlonicora/nextjs-jsonapi/core";
|
|
22
21
|
import { Action } from "@carlonicora/nextjs-jsonapi/core";
|
|
23
22
|
import { ServerSession } from "@carlonicora/nextjs-jsonapi/server";
|
|
@@ -27,9 +26,7 @@ export default async function ${names.pluralPascal}ListPage() {
|
|
|
27
26
|
|
|
28
27
|
return (
|
|
29
28
|
<${names.pascalCase}Provider>
|
|
30
|
-
|
|
31
|
-
<${names.pascalCase}ListContainer />
|
|
32
|
-
</RoundPageContainer>
|
|
29
|
+
<${names.pascalCase}ListContainer />
|
|
33
30
|
</${names.pascalCase}Provider>
|
|
34
31
|
);
|
|
35
32
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -27,6 +27,10 @@ export * from "../features/user/hooks";
|
|
|
27
27
|
export * from "../features/oauth/hooks";
|
|
28
28
|
export * from "../features/company/hooks/useSubscriptionStatus";
|
|
29
29
|
|
|
30
|
+
// RBAC hooks and utils
|
|
31
|
+
export { useRbacState } from "../features/rbac/hooks/useRbacState";
|
|
32
|
+
export { generateMigrationFile, downloadMigrationFile } from "../features/rbac/utils/RbacMigrationGenerator";
|
|
33
|
+
|
|
30
34
|
registerTableGenerator("roles", useRoleTableStructure);
|
|
31
35
|
registerTableGenerator("users", useUserTableStructure);
|
|
32
36
|
registerTableGenerator("companies", useCompanyTableStructure);
|
|
@@ -10,7 +10,7 @@ import { ModuleWithPermissions } from "@/permissions";
|
|
|
10
10
|
import { ReactNode, useState } from "react";
|
|
11
11
|
|
|
12
12
|
type RoundPageContainerProps = {
|
|
13
|
-
module
|
|
13
|
+
module?: ModuleWithPermissions;
|
|
14
14
|
details?: ReactNode;
|
|
15
15
|
tabs?: Tab[];
|
|
16
16
|
children?: ReactNode;
|
|
@@ -8,7 +8,7 @@ import { PanelRightCloseIcon, PanelRightOpenIcon } from "lucide-react";
|
|
|
8
8
|
import { ReactNode } from "react";
|
|
9
9
|
|
|
10
10
|
type RoundPageContainerTitleProps = {
|
|
11
|
-
module
|
|
11
|
+
module?: ModuleWithPermissions;
|
|
12
12
|
details?: ReactNode;
|
|
13
13
|
showDetails: boolean;
|
|
14
14
|
setShowDetails: (show: boolean) => void;
|
package/src/components/index.ts
CHANGED
|
@@ -21,6 +21,12 @@ export * from "../features/role/components";
|
|
|
21
21
|
export * from "../features/user/components";
|
|
22
22
|
export * from "../features/oauth/components";
|
|
23
23
|
export * from "../features/waitlist/components";
|
|
24
|
+
export { RbacContainer } from "../features/rbac/components/RbacContainer";
|
|
25
|
+
export { RbacToolbar } from "../features/rbac/components/RbacToolbar";
|
|
26
|
+
export { RbacFeatureSection } from "../features/rbac/components/RbacFeatureSection";
|
|
27
|
+
export { RbacModuleTable } from "../features/rbac/components/RbacModuleTable";
|
|
28
|
+
export { RbacPermissionCell } from "../features/rbac/components/RbacPermissionCell";
|
|
29
|
+
export { RbacPermissionPicker } from "../features/rbac/components/RbacPermissionPicker";
|
|
24
30
|
|
|
25
31
|
// shadcn/ui components (merged from /shadcnui entry point)
|
|
26
32
|
export * from "../shadcnui";
|
package/src/core/index.ts
CHANGED
|
@@ -79,6 +79,7 @@ export * from "../features/oauth/interfaces";
|
|
|
79
79
|
export * from "../features/waitlist/data";
|
|
80
80
|
export * from "../features/waitlist/waitlist.module";
|
|
81
81
|
export * from "../features/waitlist/waitlist-stats.module";
|
|
82
|
+
export * from "../features/rbac/rbac.module";
|
|
82
83
|
export * from "../features/referral/data";
|
|
83
84
|
export * from "../features/referral/referral.module";
|
|
84
85
|
export * from "../features/referral/referral-stats.module";
|
|
@@ -48,6 +48,9 @@ export interface FoundationModuleDefinitions {
|
|
|
48
48
|
TwoFactorChallenge: ModuleWithPermissions;
|
|
49
49
|
TwoFactorStatus: ModuleWithPermissions;
|
|
50
50
|
BackupCodeVerify: ModuleWithPermissions;
|
|
51
|
+
// RBAC modules
|
|
52
|
+
PermissionMapping: ModuleWithPermissions;
|
|
53
|
+
ModulePaths: ModuleWithPermissions;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// App-specific modules - apps will augment this interface ONLY
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { RoundPageContainer } from "@/components";
|
|
4
|
+
import { Loader2Icon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useCallback, useEffect, useState } from "react";
|
|
7
|
+
import { RbacService } from "../data/RbacService";
|
|
8
|
+
import { useRbacState } from "../hooks/useRbacState";
|
|
9
|
+
import { downloadMigrationFile, generateMigrationFile } from "../utils/RbacMigrationGenerator";
|
|
10
|
+
import RbacFeatureSection from "./RbacFeatureSection";
|
|
11
|
+
import RbacToolbar from "./RbacToolbar";
|
|
12
|
+
|
|
13
|
+
export default function RbacContainer() {
|
|
14
|
+
const t = useTranslations();
|
|
15
|
+
const stateApi = useRbacState();
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
async function fetchData() {
|
|
21
|
+
try {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
const [features, roles, permissionMappings, modulePaths] = await Promise.all([
|
|
24
|
+
RbacService.getFeatures(),
|
|
25
|
+
RbacService.getRoles(),
|
|
26
|
+
RbacService.getPermissionMappings(),
|
|
27
|
+
RbacService.getModuleRelationshipPaths(),
|
|
28
|
+
]);
|
|
29
|
+
stateApi.init(features, roles, permissionMappings, modulePaths);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error("Failed to load RBAC configuration:", err);
|
|
32
|
+
setError(t("rbac.loading_error"));
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
fetchData();
|
|
38
|
+
}, [t]);
|
|
39
|
+
|
|
40
|
+
const handleGenerate = useCallback(() => {
|
|
41
|
+
const effective = stateApi.getEffectiveConfiguration();
|
|
42
|
+
if (!effective) return;
|
|
43
|
+
|
|
44
|
+
const content = generateMigrationFile({
|
|
45
|
+
features: effective.features,
|
|
46
|
+
roles: effective.roles,
|
|
47
|
+
rolePermissionsMap: effective.rolePermissionsMap,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
downloadMigrationFile(content);
|
|
51
|
+
}, [stateApi]);
|
|
52
|
+
|
|
53
|
+
if (loading) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex h-full items-center justify-center">
|
|
56
|
+
<Loader2Icon className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (error) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex h-full items-center justify-center">
|
|
64
|
+
<p className="text-destructive">{error}</p>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!stateApi.original) return null;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<RoundPageContainer>
|
|
73
|
+
<RbacToolbar isDirty={stateApi.isDirty} onGenerate={handleGenerate} onReset={stateApi.reset} />
|
|
74
|
+
|
|
75
|
+
{stateApi.original.features.map((feature) => (
|
|
76
|
+
<RbacFeatureSection key={feature.id} feature={feature} roles={stateApi.original!.roles} stateApi={stateApi} />
|
|
77
|
+
))}
|
|
78
|
+
</RoundPageContainer>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { RbacContainer };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../../lib/utils";
|
|
4
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../shadcnui";
|
|
8
|
+
import { Switch } from "../../../shadcnui";
|
|
9
|
+
import { FeatureInterface } from "../../feature";
|
|
10
|
+
import { RoleInterface } from "../../role";
|
|
11
|
+
import { RbacStateApi } from "../hooks/useRbacState";
|
|
12
|
+
import RbacModuleTable from "./RbacModuleTable";
|
|
13
|
+
|
|
14
|
+
interface RbacFeatureSectionProps {
|
|
15
|
+
feature: FeatureInterface;
|
|
16
|
+
roles: RoleInterface[];
|
|
17
|
+
stateApi: RbacStateApi;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function RbacFeatureSection({ feature, roles, stateApi }: RbacFeatureSectionProps) {
|
|
21
|
+
const t = useTranslations();
|
|
22
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
23
|
+
const featureIsCore = stateApi.getFeatureIsCore(feature.id);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
27
|
+
<div className="rounded-lg border bg-card">
|
|
28
|
+
{/* Feature header */}
|
|
29
|
+
<CollapsibleTrigger className="w-full">
|
|
30
|
+
<div className="flex cursor-pointer items-center justify-between px-4 py-3 hover:bg-muted/50 transition-colors">
|
|
31
|
+
<div className="flex items-center gap-3">
|
|
32
|
+
<ChevronDownIcon
|
|
33
|
+
className={cn("h-4 w-4 text-muted-foreground transition-transform", !isOpen && "-rotate-90")}
|
|
34
|
+
/>
|
|
35
|
+
<h3 className="text-base font-semibold">{feature.name}</h3>
|
|
36
|
+
<span className="text-xs text-muted-foreground">
|
|
37
|
+
{t("rbac.module_count", { count: feature.modules.length })}
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
41
|
+
<span className="text-xs text-muted-foreground">{t("rbac.core")}</span>
|
|
42
|
+
<Switch
|
|
43
|
+
checked={featureIsCore}
|
|
44
|
+
onCheckedChange={(checked) => stateApi.setFeatureIsCore(feature.id, checked)}
|
|
45
|
+
className="data-checked:bg-accent data-unchecked:bg-gray-300"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</CollapsibleTrigger>
|
|
50
|
+
|
|
51
|
+
<CollapsibleContent>
|
|
52
|
+
<div className="space-y-3 p-4 pt-0">
|
|
53
|
+
{feature.modules.map((mod) => (
|
|
54
|
+
<RbacModuleTable key={mod.id} module={mod} roles={roles} stateApi={stateApi} />
|
|
55
|
+
))}
|
|
56
|
+
{feature.modules.length === 0 && (
|
|
57
|
+
<p className="text-sm text-muted-foreground italic py-4 text-center">{t("rbac.no_modules")}</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</CollapsibleContent>
|
|
61
|
+
</div>
|
|
62
|
+
</Collapsible>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { RbacFeatureSection };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { RotateCcwIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { Button } from "../../../shadcnui";
|
|
6
|
+
import { ModuleInterface } from "../../module";
|
|
7
|
+
import { RoleInterface } from "../../role";
|
|
8
|
+
import { ACTION_TYPES, ActionType, COMPANY_ADMINISTRATOR_ROLE_ID } from "../data/RbacTypes";
|
|
9
|
+
import { RbacStateApi } from "../hooks/useRbacState";
|
|
10
|
+
import RbacPermissionPicker from "./RbacPermissionPicker";
|
|
11
|
+
|
|
12
|
+
interface RbacModuleTableProps {
|
|
13
|
+
module: ModuleInterface;
|
|
14
|
+
roles: RoleInterface[];
|
|
15
|
+
stateApi: RbacStateApi;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ACTION_LABELS: Record<ActionType, string> = {
|
|
19
|
+
read: "Read",
|
|
20
|
+
create: "Create",
|
|
21
|
+
update: "Update",
|
|
22
|
+
delete: "Delete",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function RbacModuleTable({ module, roles, stateApi }: RbacModuleTableProps) {
|
|
26
|
+
const t = useTranslations();
|
|
27
|
+
const handleReset = () => {
|
|
28
|
+
stateApi.resetModulePermissions(module.id, roles);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="rounded-lg border border-accent bg-card">
|
|
33
|
+
{/* Module header */}
|
|
34
|
+
<div className="flex items-center justify-between border-b px-4 py-2">
|
|
35
|
+
<h4 className="text-sm font-medium">{module.name}</h4>
|
|
36
|
+
<Button
|
|
37
|
+
variant="ghost"
|
|
38
|
+
size="sm"
|
|
39
|
+
onClick={handleReset}
|
|
40
|
+
className="h-7 px-2 text-muted-foreground hover:text-foreground"
|
|
41
|
+
>
|
|
42
|
+
<RotateCcwIcon className="h-3.5 w-3.5" />
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Permission grid - Rows = Default + Roles, Columns = Actions */}
|
|
47
|
+
<div className="overflow-x-auto">
|
|
48
|
+
<table className="w-full text-sm">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr className="border-b bg-muted/50">
|
|
51
|
+
<th className="px-4 py-2 text-left text-xs font-medium text-muted-foreground w-40">{t("rbac.role")}</th>
|
|
52
|
+
{ACTION_TYPES.map((actionType) => (
|
|
53
|
+
<th
|
|
54
|
+
key={actionType}
|
|
55
|
+
className="px-2 py-2 text-center text-xs font-medium text-muted-foreground min-w-28"
|
|
56
|
+
>
|
|
57
|
+
{ACTION_LABELS[actionType]}
|
|
58
|
+
</th>
|
|
59
|
+
))}
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
{/* Default permissions row */}
|
|
64
|
+
<tr className="border-b">
|
|
65
|
+
<td className="px-4 py-1 text-xs font-medium text-muted-foreground">{t("rbac.defaults")}</td>
|
|
66
|
+
{ACTION_TYPES.map((actionType) => {
|
|
67
|
+
const defaultValue = stateApi.getModuleDefaultPermission(module.id, actionType) ?? false;
|
|
68
|
+
const originalDefaultValue = module.permissions[actionType] ?? false;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<td key={actionType} className="px-2 py-1">
|
|
72
|
+
<RbacPermissionPicker
|
|
73
|
+
value={defaultValue}
|
|
74
|
+
originalValue={originalDefaultValue}
|
|
75
|
+
isRoleColumn={false}
|
|
76
|
+
knownSegments={stateApi.getModuleRelationshipPaths(module.id)}
|
|
77
|
+
onSetValue={(value) => stateApi.setModuleDefaultPermission(module.id, actionType, value)}
|
|
78
|
+
/>
|
|
79
|
+
</td>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</tr>
|
|
83
|
+
|
|
84
|
+
{/* Role rows (CompanyAdministrator hidden — always all-true in migration) */}
|
|
85
|
+
{roles
|
|
86
|
+
.filter((role) => role.id !== COMPANY_ADMINISTRATOR_ROLE_ID)
|
|
87
|
+
.map((role) => (
|
|
88
|
+
<tr key={role.id} className="border-b last:border-b-0">
|
|
89
|
+
<td className="px-4 py-1 text-xs font-medium text-muted-foreground">{role.name}</td>
|
|
90
|
+
{ACTION_TYPES.map((actionType) => {
|
|
91
|
+
const roleValue = stateApi.getRolePermission(role.id, module.id, actionType);
|
|
92
|
+
const originalMapping = stateApi.original?.permissionMappings.find(
|
|
93
|
+
(pm) => pm.roleId === role.id && pm.moduleId === module.id,
|
|
94
|
+
);
|
|
95
|
+
const originalRoleValue = originalMapping
|
|
96
|
+
? (originalMapping.permissions[actionType] ?? null)
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<td key={actionType} className="px-2 py-1">
|
|
101
|
+
<RbacPermissionPicker
|
|
102
|
+
value={roleValue}
|
|
103
|
+
originalValue={originalRoleValue}
|
|
104
|
+
isRoleColumn={true}
|
|
105
|
+
knownSegments={stateApi.getModuleRelationshipPaths(module.id)}
|
|
106
|
+
onSetValue={(value) => stateApi.setRolePermission(role.id, module.id, actionType, value)}
|
|
107
|
+
onClear={() => stateApi.clearRolePermission(role.id, module.id, actionType)}
|
|
108
|
+
/>
|
|
109
|
+
</td>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</tr>
|
|
113
|
+
))}
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { RbacModuleTable };
|