@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.
Files changed (111) hide show
  1. package/dist/{AuthComponent-BkK4Sf3q.d.mts → AuthComponent-CK9aRRW2.d.mts} +1 -1
  2. package/dist/{AuthComponent-DCfP4o32.d.ts → AuthComponent-IqFWLNIU.d.ts} +1 -1
  3. package/dist/{BlockNoteEditor-KQPSJCYG.js → BlockNoteEditor-AROKR3J6.js} +14 -14
  4. package/dist/{BlockNoteEditor-KQPSJCYG.js.map → BlockNoteEditor-AROKR3J6.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-WUVRCTQI.mjs → BlockNoteEditor-CNMSBGCL.mjs} +4 -4
  6. package/dist/ModulePathsInterface-49EWvbWy.d.mts +31 -0
  7. package/dist/ModulePathsInterface-wVS5Raa4.d.ts +31 -0
  8. package/dist/{auth.interface-C4kEZscm.d.ts → auth.interface-C1WjZ0fM.d.ts} +1 -1
  9. package/dist/{auth.interface-24ID4yhT.d.mts → auth.interface-fBFqIrw4.d.mts} +1 -1
  10. package/dist/billing/index.js +346 -346
  11. package/dist/billing/index.mjs +3 -3
  12. package/dist/{chunk-BUCV5VFT.mjs → chunk-FE26PIZK.mjs} +53 -2
  13. package/dist/chunk-FE26PIZK.mjs.map +1 -0
  14. package/dist/{chunk-BTLJZIDS.mjs → chunk-G5473JP3.mjs} +869 -40
  15. package/dist/chunk-G5473JP3.mjs.map +1 -0
  16. package/dist/{chunk-XNISXVQL.mjs → chunk-J2PYGXVD.mjs} +70 -1
  17. package/dist/chunk-J2PYGXVD.mjs.map +1 -0
  18. package/dist/{chunk-YKPIFJOB.js → chunk-PQIXFKHT.js} +1457 -628
  19. package/dist/chunk-PQIXFKHT.js.map +1 -0
  20. package/dist/{chunk-QIA5FOQB.js → chunk-QOLVON35.js} +71 -2
  21. package/dist/chunk-QOLVON35.js.map +1 -0
  22. package/dist/{chunk-V63TFESU.js → chunk-UJBUJALX.js} +53 -2
  23. package/dist/chunk-UJBUJALX.js.map +1 -0
  24. package/dist/client/index.d.mts +25 -7
  25. package/dist/client/index.d.ts +25 -7
  26. package/dist/client/index.js +10 -4
  27. package/dist/client/index.js.map +1 -1
  28. package/dist/client/index.mjs +9 -3
  29. package/dist/components/index.d.mts +52 -10
  30. package/dist/components/index.d.ts +52 -10
  31. package/dist/components/index.js +16 -4
  32. package/dist/components/index.js.map +1 -1
  33. package/dist/components/index.mjs +15 -3
  34. package/dist/{config-CPN6QZfo.d.ts → config-DZWAFB7H.d.ts} +1 -1
  35. package/dist/{config-DaxjKdIo.d.mts → config-ndRJIQsP.d.mts} +1 -1
  36. package/dist/{content.interface-DvPs_JbX.d.mts → content.interface-B5ySfiOE.d.mts} +1 -1
  37. package/dist/{content.interface-Czin-YRh.d.ts → content.interface-mmz0uMwm.d.ts} +1 -1
  38. package/dist/contexts/index.d.mts +2 -2
  39. package/dist/contexts/index.d.ts +2 -2
  40. package/dist/contexts/index.js +4 -4
  41. package/dist/contexts/index.mjs +3 -3
  42. package/dist/core/index.d.mts +15 -10
  43. package/dist/core/index.d.ts +15 -10
  44. package/dist/core/index.js +6 -2
  45. package/dist/core/index.js.map +1 -1
  46. package/dist/core/index.mjs +5 -1
  47. package/dist/index.d.mts +47 -10
  48. package/dist/index.d.ts +47 -10
  49. package/dist/index.js +17 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +16 -2
  52. package/dist/{notification.interface-DEW8hR8g.d.ts → notification.interface-COKHDQeE.d.ts} +1 -1
  53. package/dist/{notification.interface-DKR5WGKH.d.mts → notification.interface-DG7cq9oG.d.mts} +1 -1
  54. package/dist/{s3.service-BHjcTA0t.d.mts → s3.service-BoRPFx82.d.mts} +4 -4
  55. package/dist/{s3.service-C_K1VHyx.d.ts → s3.service-ppn9iGJU.d.ts} +4 -4
  56. package/dist/scripts/generate-web-module/templates/components/list-container.template.d.ts.map +1 -1
  57. package/dist/scripts/generate-web-module/templates/components/list-container.template.js +7 -1
  58. package/dist/scripts/generate-web-module/templates/components/list-container.template.js.map +1 -1
  59. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
  60. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +7 -4
  61. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
  62. package/dist/scripts/generate-web-module/templates/pages/list-page.template.d.ts.map +1 -1
  63. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +1 -4
  64. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -1
  65. package/dist/server/index.d.mts +4 -4
  66. package/dist/server/index.d.ts +4 -4
  67. package/dist/server/index.js +3 -3
  68. package/dist/server/index.mjs +1 -1
  69. package/dist/useRbacState-DhuYYr0S.d.mts +77 -0
  70. package/dist/useRbacState-NnzNL2ED.d.ts +77 -0
  71. package/dist/{useSocket-BW6haECW.d.mts → useSocket-CtfuR5wD.d.mts} +1 -1
  72. package/dist/{useSocket-C9FmYuRM.d.ts → useSocket-bsV-K4qR.d.ts} +1 -1
  73. package/package.json +2 -2
  74. package/scripts/generate-web-module/templates/components/list-container.template.ts +7 -1
  75. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +7 -4
  76. package/scripts/generate-web-module/templates/pages/list-page.template.ts +1 -4
  77. package/src/client/index.ts +4 -0
  78. package/src/components/containers/RoundPageContainer.tsx +1 -1
  79. package/src/components/containers/RoundPageContainerTitle.tsx +1 -1
  80. package/src/components/index.ts +6 -0
  81. package/src/core/index.ts +1 -0
  82. package/src/core/registry/ModuleRegistry.ts +3 -0
  83. package/src/features/rbac/components/RbacContainer.tsx +82 -0
  84. package/src/features/rbac/components/RbacFeatureSection.tsx +66 -0
  85. package/src/features/rbac/components/RbacModuleTable.tsx +121 -0
  86. package/src/features/rbac/components/RbacPermissionCell.tsx +97 -0
  87. package/src/features/rbac/components/RbacPermissionPicker.tsx +179 -0
  88. package/src/features/rbac/components/RbacToolbar.tsx +40 -0
  89. package/src/features/rbac/data/ModulePaths.ts +25 -0
  90. package/src/features/rbac/data/ModulePathsInterface.ts +6 -0
  91. package/src/features/rbac/data/PermissionMapping.ts +43 -0
  92. package/src/features/rbac/data/PermissionMappingInterface.ts +12 -0
  93. package/src/features/rbac/data/RbacService.ts +47 -0
  94. package/src/features/rbac/data/RbacTypes.ts +15 -0
  95. package/src/features/rbac/data/index.ts +6 -0
  96. package/src/features/rbac/hooks/useRbacState.test.ts +178 -0
  97. package/src/features/rbac/hooks/useRbacState.ts +319 -0
  98. package/src/features/rbac/index.ts +19 -0
  99. package/src/features/rbac/rbac.module.ts +19 -0
  100. package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +124 -0
  101. package/src/features/rbac/utils/RbacMigrationGenerator.ts +184 -0
  102. package/src/index.ts +4 -0
  103. package/dist/chunk-BTLJZIDS.mjs.map +0 -1
  104. package/dist/chunk-BUCV5VFT.mjs.map +0 -1
  105. package/dist/chunk-QIA5FOQB.js.map +0 -1
  106. package/dist/chunk-V63TFESU.js.map +0 -1
  107. package/dist/chunk-XNISXVQL.mjs.map +0 -1
  108. package/dist/chunk-YKPIFJOB.js.map +0 -1
  109. package/dist/useDataListRetriever-BqJSFBck.d.mts +0 -33
  110. package/dist/useDataListRetriever-BqJSFBck.d.ts +0 -33
  111. /package/dist/{BlockNoteEditor-WUVRCTQI.mjs.map → BlockNoteEditor-CNMSBGCL.mjs.map} +0 -0
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunkQIA5FOQBjs = require('../chunk-QIA5FOQB.js');
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 _chunkQIA5FOQBjs.checkPermissionsFromServer.call(void 0, {
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 = _chunkQIA5FOQBjs.AuthService; exports.ServerCompanyService = _chunkQIA5FOQBjs.CompanyService; exports.ServerContentService = _chunkQIA5FOQBjs.ContentService; exports.ServerFeatureService = _chunkQIA5FOQBjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkQIA5FOQBjs.NotificationService; exports.ServerPushService = _chunkQIA5FOQBjs.PushService; exports.ServerRoleService = _chunkQIA5FOQBjs.RoleService; exports.ServerS3Service = _chunkQIA5FOQBjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkQIA5FOQBjs.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;
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
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-XNISXVQL.mjs";
18
+ } from "../chunk-J2PYGXVD.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-DKR5WGKH.mjs';
1
+ import { N as NotificationInterface } from './notification.interface-DG7cq9oG.mjs';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-DEW8hR8g.js';
1
+ import { N as NotificationInterface } from './notification.interface-COKHDQeE.js';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.52.0",
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 --no-dts",
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 <${names.pascalCase}List />;
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 displayProp = hasNameField ? "name" : "id";
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
- name: string;
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
- name: option.label,
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">No results found</span>}
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
- <RoundPageContainer module={Modules.${names.pascalCase}}>
31
- <${names.pascalCase}ListContainer />
32
- </RoundPageContainer>
29
+ <${names.pascalCase}ListContainer />
33
30
  </${names.pascalCase}Provider>
34
31
  );
35
32
  }
@@ -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: ModuleWithPermissions;
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: ModuleWithPermissions;
11
+ module?: ModuleWithPermissions;
12
12
  details?: ReactNode;
13
13
  showDetails: boolean;
14
14
  setShowDetails: (show: boolean) => void;
@@ -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 };