@nocobase/portal-template-default 1.0.8 → 1.0.10

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 (262) hide show
  1. package/.env.example +5 -0
  2. package/.github/workflows/publish.yml +106 -0
  3. package/.gitignore +33 -0
  4. package/AGENTS.md +19 -0
  5. package/README.MD +61 -180
  6. package/package.json +3 -5
  7. package/scripts/build-html.mjs +12 -0
  8. package/src/App.css +2 -0
  9. package/src/App.tsx +35 -186
  10. package/src/app/development.tsx +154 -0
  11. package/src/app/extension.ts +5 -0
  12. package/src/app/extensions.tsx +18 -4
  13. package/src/app/routes.tsx +65 -0
  14. package/src/components/access-control/acl-bootstrap.tsx +11 -3
  15. package/src/components/access-control/acl-gate.tsx +7 -6
  16. package/src/components/access-control/can-access.tsx +17 -0
  17. package/src/components/access-control/navigate-to-accessible-resource.tsx +8 -3
  18. package/src/components/app-shell/sidebar.tsx +21 -5
  19. package/src/components/auth/auto-redirect-provider.tsx +2 -1
  20. package/src/extensions/nocobase-acl/README.md +74 -0
  21. package/{registry → src/extensions}/nocobase-acl/components/acl-boundary.tsx +22 -25
  22. package/{registry → src/extensions}/nocobase-acl/components/role-switcher.tsx +26 -23
  23. package/src/extensions/nocobase-acl/demo/acl-preview-provider.tsx +81 -0
  24. package/{registry → src/extensions}/nocobase-acl/demo/boundary-api.tsx +58 -0
  25. package/{registry → src/extensions}/nocobase-acl/demo/index.tsx +243 -105
  26. package/{registry → src/extensions}/nocobase-acl/demo/prompt-generator.tsx +3 -1
  27. package/src/extensions/nocobase-acl/extension.tsx +73 -0
  28. package/{registry → src/extensions}/nocobase-ai/README.md +21 -5
  29. package/{registry → src/extensions}/nocobase-ai/components/chat/chat-composer.tsx +3 -1
  30. package/{registry → src/extensions}/nocobase-ai/components/chat/chat-messages.tsx +38 -3
  31. package/{registry → src/extensions}/nocobase-ai/components/chat/tool-call-card.tsx +1 -9
  32. package/{registry → src/extensions}/nocobase-ai/components/index.ts +5 -0
  33. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +17 -0
  34. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +35 -0
  35. package/src/extensions/nocobase-ai/components/surfaces/chat-surface.tsx +108 -0
  36. package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +2 -2
  37. package/{registry → src/extensions}/nocobase-ai/demo/configuration-gate.tsx +4 -1
  38. package/{registry → src/extensions}/nocobase-ai/demo/container-showcase.tsx +2 -2
  39. package/{registry → src/extensions}/nocobase-ai/demo/floating.tsx +45 -15
  40. package/{registry → src/extensions}/nocobase-ai/demo/index.tsx +116 -74
  41. package/{registry → src/extensions}/nocobase-ai/demo/prompt-generator.tsx +3 -3
  42. package/{registry → src/extensions}/nocobase-ai/demo/tool-cards.tsx +2 -3
  43. package/src/extensions/nocobase-ai/extension.tsx +160 -0
  44. package/{registry → src/extensions}/nocobase-ai/global-ai-chat.tsx +12 -28
  45. package/{registry → src/extensions}/nocobase-ai/providers/ai-provider.tsx +14 -1
  46. package/{registry → src/extensions}/nocobase-ai/providers/chat-context.tsx +1 -0
  47. package/{registry → src/extensions}/nocobase-ai/providers/chat-provider.tsx +29 -8
  48. package/{registry → src/extensions}/nocobase-ai/providers/use-chat-attachments.ts +6 -1
  49. package/{registry → src/extensions}/nocobase-ai/providers/use-chat-work-context.ts +4 -1
  50. package/{registry → src/extensions}/nocobase-route-surfaces/README.md +2 -1
  51. package/{registry → src/extensions}/nocobase-route-surfaces/demo/index.tsx +33 -7
  52. package/{registry → src/extensions}/nocobase-route-surfaces/demo/scenarios.ts +20 -14
  53. package/{registry → src/extensions}/nocobase-route-surfaces/extension.tsx +55 -52
  54. package/src/{pages/users → extensions/nocobase-users-example}/create.tsx +4 -4
  55. package/src/{pages/users → extensions/nocobase-users-example}/edit.tsx +5 -9
  56. package/src/extensions/nocobase-users-example/extension.tsx +109 -0
  57. package/src/{pages/users → extensions/nocobase-users-example}/form-context.ts +11 -9
  58. package/src/extensions/nocobase-users-example/layout.tsx +16 -0
  59. package/src/{pages/users → extensions/nocobase-users-example}/list.tsx +1 -1
  60. package/src/extensions/nocobase-users-example/locales/en-US.ts +77 -0
  61. package/src/extensions/nocobase-users-example/locales/index.ts +8 -0
  62. package/src/extensions/nocobase-users-example/locales/zh-CN.ts +73 -0
  63. package/src/extensions/nocobase-users-example/optional-ai.ts +67 -0
  64. package/src/{pages/users → extensions/nocobase-users-example}/role-detail.tsx +4 -15
  65. package/src/{pages/users → extensions/nocobase-users-example}/routes.ts +1 -4
  66. package/src/extensions/nocobase-users-example/show.tsx +266 -0
  67. package/src/lib/nocobase/acl/context.ts +32 -0
  68. package/src/lib/nocobase/acl/evaluator.ts +186 -0
  69. package/src/lib/nocobase/acl/hooks.ts +40 -0
  70. package/src/lib/nocobase/acl/index.ts +4 -1
  71. package/src/lib/nocobase/acl/menu.ts +6 -6
  72. package/src/lib/nocobase/acl/record-permissions.ts +37 -7
  73. package/src/lib/nocobase/acl/store-provider.tsx +14 -0
  74. package/src/lib/nocobase/acl/store.ts +92 -86
  75. package/src/lib/nocobase/acl/types.ts +52 -32
  76. package/src/lib/nocobase/auth-session.ts +128 -0
  77. package/src/lib/nocobase/client.ts +48 -50
  78. package/src/locales/en-US.ts +0 -78
  79. package/src/locales/index.ts +2 -7
  80. package/src/locales/zh-CN.ts +0 -74
  81. package/src/providers/access-control.ts +43 -8
  82. package/src/providers/auth-callback.ts +1 -0
  83. package/src/providers/auth.ts +24 -26
  84. package/src/providers/constants.ts +0 -4
  85. package/src/providers/data.ts +14 -26
  86. package/src/providers/runtime-config.ts +40 -0
  87. package/src/vite-env.d.ts +9 -0
  88. package/tests/auth-session-regression.mjs +101 -0
  89. package/tsconfig.json +2 -1
  90. package/vite.config.ts +9 -1
  91. package/registry/nocobase-acl/README.md +0 -19
  92. package/registry/nocobase-acl/extension.tsx +0 -71
  93. package/registry/nocobase-acl/tests/acl-regression.mjs +0 -314
  94. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  95. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  96. package/registry/nocobase-ai/extension.tsx +0 -158
  97. package/registry/nocobase-ai/tests/business-report-regression.mjs +0 -70
  98. package/registry/nocobase-ai/tests/form-filler-regression.mjs +0 -129
  99. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +0 -128
  100. package/registry/nocobase-ai/tests/page-context-regression.mjs +0 -48
  101. package/registry/nocobase-ai/tests/stream-regression.mjs +0 -207
  102. package/registry/nocobase-auth-cas/README.md +0 -6
  103. package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +0 -41
  104. package/registry/nocobase-auth-cas/demo.tsx +0 -20
  105. package/registry/nocobase-auth-cas/extension.tsx +0 -46
  106. package/registry/nocobase-auth-cas/index.ts +0 -1
  107. package/registry/nocobase-auth-cas/use-cas-sign-in.ts +0 -29
  108. package/registry/nocobase-auth-dingtalk/README.md +0 -7
  109. package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +0 -154
  110. package/registry/nocobase-auth-dingtalk/demo.tsx +0 -23
  111. package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +0 -45
  112. package/registry/nocobase-auth-dingtalk/extension.tsx +0 -55
  113. package/registry/nocobase-auth-dingtalk/index.ts +0 -4
  114. package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +0 -64
  115. package/registry/nocobase-auth-ldap/README.md +0 -6
  116. package/registry/nocobase-auth-ldap/demo.tsx +0 -21
  117. package/registry/nocobase-auth-ldap/extension.tsx +0 -46
  118. package/registry/nocobase-auth-ldap/index.ts +0 -1
  119. package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +0 -69
  120. package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +0 -5
  121. package/registry/nocobase-auth-oidc/README.md +0 -11
  122. package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +0 -13
  123. package/registry/nocobase-auth-oidc/demo.tsx +0 -20
  124. package/registry/nocobase-auth-oidc/extension.tsx +0 -51
  125. package/registry/nocobase-auth-oidc/index.ts +0 -1
  126. package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +0 -44
  127. package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +0 -58
  128. package/registry/nocobase-auth-saml/README.md +0 -6
  129. package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +0 -13
  130. package/registry/nocobase-auth-saml/demo.tsx +0 -20
  131. package/registry/nocobase-auth-saml/extension.tsx +0 -51
  132. package/registry/nocobase-auth-saml/index.ts +0 -1
  133. package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +0 -43
  134. package/registry/nocobase-auth-saml/use-saml-sign-in.ts +0 -51
  135. package/registry/nocobase-auth-sms/README.md +0 -7
  136. package/registry/nocobase-auth-sms/demo.tsx +0 -25
  137. package/registry/nocobase-auth-sms/extension.tsx +0 -46
  138. package/registry/nocobase-auth-sms/index.ts +0 -1
  139. package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +0 -107
  140. package/registry/nocobase-auth-sms/use-sms-sign-in.ts +0 -88
  141. package/registry/nocobase-auth-wecom/README.md +0 -6
  142. package/registry/nocobase-auth-wecom/auto-login-provider.tsx +0 -21
  143. package/registry/nocobase-auth-wecom/demo.tsx +0 -23
  144. package/registry/nocobase-auth-wecom/extension.tsx +0 -49
  145. package/registry/nocobase-auth-wecom/index.ts +0 -1
  146. package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +0 -62
  147. package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +0 -70
  148. package/registry/nocobase-i18n/README.md +0 -44
  149. package/registry/nocobase-i18n/components/index.ts +0 -1
  150. package/registry/nocobase-i18n/components/language-switcher.tsx +0 -138
  151. package/registry/nocobase-i18n/demo/index.tsx +0 -209
  152. package/registry/nocobase-i18n/demo/prompt-generator.tsx +0 -132
  153. package/registry/nocobase-i18n/extension.tsx +0 -45
  154. package/registry/nocobase-i18n/index.ts +0 -7
  155. package/registry/nocobase-i18n/locales/en-US.ts +0 -39
  156. package/registry/nocobase-i18n/locales/index.ts +0 -8
  157. package/registry/nocobase-i18n/locales/zh-CN.ts +0 -35
  158. package/registry/nocobase-i18n/provider.tsx +0 -44
  159. package/registry/nocobase-i18n/server-resources.ts +0 -166
  160. package/registry/nocobase-i18n/tests/i18n-regression.mjs +0 -51
  161. package/registry.config.json +0 -302
  162. package/scripts/registry.mjs +0 -164
  163. package/src/components/access-control/resource-access-guard.tsx +0 -21
  164. package/src/components/access-control/use-acl-runtime.ts +0 -28
  165. package/src/lib/nocobase/acl/action.ts +0 -147
  166. package/src/pages/users/layout.tsx +0 -15
  167. package/src/pages/users/show.tsx +0 -278
  168. /package/{registry → src/extensions}/nocobase-acl/components/index.ts +0 -0
  169. /package/{registry → src/extensions}/nocobase-acl/components/role-options.ts +0 -0
  170. /package/{registry → src/extensions}/nocobase-acl/demo/components.tsx +0 -0
  171. /package/{registry → src/extensions}/nocobase-acl/demo/lazy-route.tsx +0 -0
  172. /package/{registry → src/extensions}/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -0
  173. /package/{registry → src/extensions}/nocobase-acl/demo/scenario-section.tsx +0 -0
  174. /package/{registry → src/extensions}/nocobase-acl/index.ts +0 -0
  175. /package/{registry → src/extensions}/nocobase-ai/adapters/react-hook-form.ts +0 -0
  176. /package/{registry → src/extensions}/nocobase-ai/assets/nocobase-ai-chat.svg +0 -0
  177. /package/{registry → src/extensions}/nocobase-ai/components/ai-root-provider.tsx +0 -0
  178. /package/{registry → src/extensions}/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -0
  179. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-attachment.tsx +0 -0
  180. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-compact.tsx +0 -0
  181. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-empty-state.tsx +0 -0
  182. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-header.tsx +0 -0
  183. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -0
  184. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-message.tsx +0 -0
  185. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-window.tsx +0 -0
  186. /package/{registry → src/extensions}/nocobase-ai/components/chat/conversation-list.tsx +0 -0
  187. /package/{registry → src/extensions}/nocobase-ai/components/chat/markdown-message.tsx +0 -0
  188. /package/{registry → src/extensions}/nocobase-ai/components/chat/model-select-options.tsx +0 -0
  189. /package/{registry → src/extensions}/nocobase-ai/components/chat/reasoning-panel.tsx +0 -0
  190. /package/{registry → src/extensions}/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -0
  191. /package/{registry → src/extensions}/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -0
  192. /package/{registry → src/extensions}/nocobase-ai/components/chat/work-context-chip.tsx +0 -0
  193. /package/{registry → src/extensions}/nocobase-ai/components/page-elements/ai-form.tsx +0 -0
  194. /package/{registry → src/extensions}/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -0
  195. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-inline.tsx +0 -0
  196. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-page.tsx +0 -0
  197. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -0
  198. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -0
  199. /package/{registry → src/extensions}/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -0
  200. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-dialog.tsx +0 -0
  201. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-renderer.tsx +0 -0
  202. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-utils.tsx +0 -0
  203. /package/{registry → src/extensions}/nocobase-ai/components/tools/chart-renderer.tsx +0 -0
  204. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-preview.tsx +0 -0
  205. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -0
  206. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -0
  207. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -0
  208. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -0
  209. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime.ts +0 -0
  210. /package/{registry → src/extensions}/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -0
  211. /package/{registry → src/extensions}/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -0
  212. /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -0
  213. /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -0
  214. /package/{registry → src/extensions}/nocobase-ai/components/tools/workflow-renderer.tsx +0 -0
  215. /package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -0
  216. /package/{registry → src/extensions}/nocobase-ai/demo/interaction-showcase.tsx +0 -0
  217. /package/{registry → src/extensions}/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -0
  218. /package/{registry → src/extensions}/nocobase-ai/demo/page-context.tsx +0 -0
  219. /package/{registry → src/extensions}/nocobase-ai/demo/page-element-showcase.tsx +0 -0
  220. /package/{registry → src/extensions}/nocobase-ai/demo/prompt-card.tsx +0 -0
  221. /package/{registry → src/extensions}/nocobase-ai/demo/shortcut.tsx +0 -0
  222. /package/{registry → src/extensions}/nocobase-ai/index.ts +0 -0
  223. /package/{registry → src/extensions}/nocobase-ai/providers/avatars.ts +0 -0
  224. /package/{registry → src/extensions}/nocobase-ai/providers/chat-controller.ts +0 -0
  225. /package/{registry → src/extensions}/nocobase-ai/providers/chat-message-utils.ts +0 -0
  226. /package/{registry → src/extensions}/nocobase-ai/providers/chat-reducer.ts +0 -0
  227. /package/{registry → src/extensions}/nocobase-ai/providers/chat-task-utils.ts +0 -0
  228. /package/{registry → src/extensions}/nocobase-ai/providers/chat-transport.ts +0 -0
  229. /package/{registry → src/extensions}/nocobase-ai/providers/form-registry.tsx +0 -0
  230. /package/{registry → src/extensions}/nocobase-ai/providers/frontend-tool-registry.tsx +0 -0
  231. /package/{registry → src/extensions}/nocobase-ai/providers/index.ts +0 -0
  232. /package/{registry → src/extensions}/nocobase-ai/providers/model.ts +0 -0
  233. /package/{registry → src/extensions}/nocobase-ai/providers/page-context.tsx +0 -0
  234. /package/{registry → src/extensions}/nocobase-ai/providers/stream-coalescer.ts +0 -0
  235. /package/{registry → src/extensions}/nocobase-ai/providers/stream-event-utils.ts +0 -0
  236. /package/{registry → src/extensions}/nocobase-ai/providers/stream-parser.ts +0 -0
  237. /package/{registry → src/extensions}/nocobase-ai/providers/sub-agent-stream.ts +0 -0
  238. /package/{registry → src/extensions}/nocobase-ai/providers/types.ts +0 -0
  239. /package/{registry → src/extensions}/nocobase-ai/providers/ui-message-stream.ts +0 -0
  240. /package/{registry → src/extensions}/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -0
  241. /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-message-actions.ts +0 -0
  242. /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-runtime.ts +0 -0
  243. /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-catalog.ts +0 -0
  244. /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-history.ts +0 -0
  245. /package/{registry → src/extensions}/nocobase-ai/services/index.ts +0 -0
  246. /package/{registry → src/extensions}/nocobase-ai/services/nocobase-ai-service.ts +0 -0
  247. /package/{registry → src/extensions}/nocobase-ai/services/types.ts +0 -0
  248. /package/{registry → src/extensions}/nocobase-route-surfaces/demo/lazy-route.tsx +0 -0
  249. /package/{registry → src/extensions}/nocobase-route-surfaces/demo/prompt-generator.tsx +0 -0
  250. /package/{registry → src/extensions}/nocobase-route-surfaces/index.ts +0 -0
  251. /package/{registry → src/extensions}/nocobase-route-surfaces/route-dialog.tsx +0 -0
  252. /package/{registry → src/extensions}/nocobase-route-surfaces/route-drawer.tsx +0 -0
  253. /package/{registry → src/extensions}/nocobase-route-surfaces/route-page.tsx +0 -0
  254. /package/{registry → src/extensions}/nocobase-route-surfaces/route-surface-context.ts +0 -0
  255. /package/{registry → src/extensions}/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +0 -0
  256. /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-close.ts +0 -0
  257. /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-state.ts +0 -0
  258. /package/src/{pages/users → extensions/nocobase-users-example}/form-fields.tsx +0 -0
  259. /package/src/{pages/users → extensions/nocobase-users-example}/index.ts +0 -0
  260. /package/src/{pages/users → extensions/nocobase-users-example}/role-badges.tsx +0 -0
  261. /package/src/{pages/users → extensions/nocobase-users-example}/role-utils.ts +0 -0
  262. /package/src/{pages/users → extensions/nocobase-users-example}/types.ts +0 -0
@@ -1,15 +1,42 @@
1
1
  import type { AccessControlProvider } from "@refinedev/core";
2
2
 
3
3
  import {
4
- canAccessWithSnapshot,
5
- getAclSnapshot,
4
+ aclStore,
5
+ evaluateAccess,
6
+ getAclState,
6
7
  loadAcl,
8
+ type AclAccessRequest,
9
+ type RoleConstraint,
7
10
  } from "@/lib/nocobase/acl";
8
11
 
12
+ const toAclAccessRequest = ({
13
+ resource,
14
+ action,
15
+ params,
16
+ }: Parameters<AccessControlProvider["can"]>[0]): AclAccessRequest => ({
17
+ resource,
18
+ action,
19
+ id: params?.id,
20
+ field: typeof params?.field === "string" ? params.field : undefined,
21
+ dataSourceKey:
22
+ typeof params?.dataSourceKey === "string"
23
+ ? params.dataSourceKey
24
+ : undefined,
25
+ roles: params?.roles as RoleConstraint | undefined,
26
+ meta: params?.meta as Record<string, unknown> | undefined,
27
+ resourceItem: params?.resource,
28
+ });
29
+
9
30
  export const accessControlProvider: AccessControlProvider = {
10
- can: async ({ resource, action, params }) => {
11
- const snapshot = await loadAcl();
12
- const can = canAccessWithSnapshot(snapshot, { resource, action, params });
31
+ can: async (request) => {
32
+ const state = await loadAcl();
33
+ const can =
34
+ state.status === "ready" &&
35
+ evaluateAccess(
36
+ state.permissions,
37
+ toAclAccessRequest(request),
38
+ aclStore.recordPermissions.getPermission
39
+ );
13
40
 
14
41
  return {
15
42
  can,
@@ -29,6 +56,14 @@ export const accessControlProvider: AccessControlProvider = {
29
56
  },
30
57
  };
31
58
 
32
- export const canAccessCurrentAcl = (
33
- params: Parameters<typeof canAccessWithSnapshot>[1]
34
- ) => canAccessWithSnapshot(getAclSnapshot(), params);
59
+ export const canAccessCurrentAcl = (request: AclAccessRequest) => {
60
+ const state = getAclState();
61
+ return (
62
+ state.status === "ready" &&
63
+ evaluateAccess(
64
+ state.permissions,
65
+ request,
66
+ aclStore.recordPermissions.getPermission
67
+ )
68
+ );
69
+ };
@@ -12,6 +12,7 @@ export function captureAuthenticationCallback() {
12
12
 
13
13
  nocobaseClient.setToken(token);
14
14
  nocobaseClient.setAuthenticator(authenticator);
15
+ nocobaseClient.setRole(null);
15
16
  clearAcl();
16
17
 
17
18
  url.searchParams.delete("token");
@@ -30,7 +30,7 @@ type NocoBaseSignOutResponse = {
30
30
  };
31
31
 
32
32
  type CurrentUserCache = {
33
- token: string;
33
+ sessionKey: string;
34
34
  user: NocoBaseUser;
35
35
  expiresAt: number;
36
36
  };
@@ -51,10 +51,14 @@ const clearCurrentUserCache = () => {
51
51
 
52
52
  const getCurrentUser = async (): Promise<NocoBaseUser> => {
53
53
  const token = nocobaseClient.getToken();
54
- if (!token) throw new Error("No NocoBase token");
54
+ const sessionKey = token ?? "cookie";
55
55
 
56
56
  const cached = currentUserCache;
57
- if (cached && cached.token === token && cached.expiresAt > Date.now()) {
57
+ if (
58
+ cached &&
59
+ cached.sessionKey === sessionKey &&
60
+ cached.expiresAt > Date.now()
61
+ ) {
58
62
  return cached.user;
59
63
  }
60
64
 
@@ -63,10 +67,12 @@ const getCurrentUser = async (): Promise<NocoBaseUser> => {
63
67
  currentUserRequest = (async () => {
64
68
  const user = await nocobaseClient.action<NocoBaseUser>("auth", "check", {
65
69
  token,
66
- includeAuthenticator: true,
70
+ // A default authenticator header would override the server's
71
+ // authenticator cookie during a Cookie-only SSO session.
72
+ authenticator: token ? undefined : null,
67
73
  });
68
74
  currentUserCache = {
69
- token: nocobaseClient.getToken() ?? token,
75
+ sessionKey: nocobaseClient.getToken() ?? "cookie",
70
76
  user,
71
77
  expiresAt: Date.now() + CURRENT_USER_CACHE_MS,
72
78
  };
@@ -216,22 +222,18 @@ export const authProvider: AuthProvider = {
216
222
  const token = nocobaseClient.getToken();
217
223
  let redirect: string | undefined;
218
224
  try {
219
- if (token) {
220
- const result = await nocobaseClient.action<NocoBaseSignOutResponse>(
221
- "auth",
222
- "signOut",
223
- {
224
- method: "POST",
225
- token,
226
- includeAuthenticator: true,
227
- }
228
- );
229
- redirect = result?.redirect;
230
- }
225
+ const result = await nocobaseClient.action<NocoBaseSignOutResponse>(
226
+ "auth",
227
+ "signOut",
228
+ {
229
+ method: "POST",
230
+ token,
231
+ authenticator: token ? undefined : null,
232
+ }
233
+ );
234
+ redirect = result?.redirect;
231
235
  } finally {
232
- nocobaseClient.setToken(null);
233
- nocobaseClient.setAuthenticator(null);
234
- nocobaseClient.setRole(null);
236
+ nocobaseClient.clearAuthentication();
235
237
  clearCurrentUserCache();
236
238
  clearAcl();
237
239
  }
@@ -249,9 +251,7 @@ export const authProvider: AuthProvider = {
249
251
  await getCurrentUser();
250
252
  return { authenticated: true };
251
253
  } catch {
252
- nocobaseClient.setToken(null);
253
- nocobaseClient.setAuthenticator(null);
254
- nocobaseClient.setRole(null);
254
+ nocobaseClient.clearAuthentication();
255
255
  clearCurrentUserCache();
256
256
  clearAcl();
257
257
  return { authenticated: false, redirectTo: "/login" };
@@ -291,9 +291,7 @@ export const authProvider: AuthProvider = {
291
291
  (error as { status?: number; statusCode?: number }).statusCode;
292
292
 
293
293
  if (status === 401) {
294
- nocobaseClient.setToken(null);
295
- nocobaseClient.setAuthenticator(null);
296
- nocobaseClient.setRole(null);
294
+ nocobaseClient.clearAuthentication();
297
295
  clearCurrentUserCache();
298
296
  clearAcl();
299
297
  return { logout: true, redirectTo: "/login" };
@@ -36,9 +36,5 @@ const toProxyRelativeUrl = (url: string, target?: string) => {
36
36
 
37
37
  export const API_URL = toProxyRelativeUrl(rawApiUrl, proxyTarget);
38
38
  export const API_ORIGIN = getDefaultProxyTarget(rawApiUrl);
39
- export const NOCOBASE_TOKEN_KEY = "nocobase-auth-token";
40
- export const NOCOBASE_AUTHENTICATOR_KEY = "nocobase-authenticator";
41
- export const NOCOBASE_ROLE_KEY = "nocobase-auth-role";
42
- export const NOCOBASE_LOCALE_KEY = "nocobase-locale";
43
39
  export const NOCOBASE_AUTHENTICATOR =
44
40
  import.meta.env.NOCOBASE_AUTHENTICATOR ?? "basic";
@@ -24,7 +24,6 @@ import type {
24
24
 
25
25
  import { nocobaseClient } from "@/lib/nocobase/client";
26
26
  import {
27
- notifyRecordPermissionsChanged,
28
27
  resolveAclDataSourceKey,
29
28
  type ResourceAcl,
30
29
  updateRecordPermissions,
@@ -122,9 +121,7 @@ const request = async <T>(
122
121
  },
123
122
  body: options.body,
124
123
  token: options.meta?.token,
125
- headers: dataSourceKey
126
- ? { "X-Data-Source": dataSourceKey }
127
- : undefined,
124
+ headers: dataSourceKey ? { "X-Data-Source": dataSourceKey } : undefined,
128
125
  unwrap: options.unwrapData === false ? "none" : "data",
129
126
  });
130
127
  };
@@ -200,9 +197,7 @@ export const dataProvider: DataProvider = {
200
197
  ? { rows: response.data, count: response.meta?.count }
201
198
  : response.data ?? response;
202
199
  const records = list.rows ?? [];
203
- if (cacheAllowedActions({ resource, records, response, meta })) {
204
- notifyRecordPermissionsChanged();
205
- }
200
+ cacheAllowedActions({ resource, records, response, meta });
206
201
 
207
202
  return {
208
203
  data: records,
@@ -226,16 +221,12 @@ export const dataProvider: DataProvider = {
226
221
  }
227
222
  );
228
223
  const data = getResponseData(response);
229
- if (
230
- cacheAllowedActions({
231
- resource,
232
- records: [data],
233
- response: response as NocoBaseListResponse<TData>,
234
- meta,
235
- })
236
- ) {
237
- notifyRecordPermissionsChanged();
238
- }
224
+ cacheAllowedActions({
225
+ resource,
226
+ records: [data],
227
+ response: response as NocoBaseListResponse<TData>,
228
+ meta,
229
+ });
239
230
  return {
240
231
  data,
241
232
  };
@@ -256,17 +247,14 @@ export const dataProvider: DataProvider = {
256
247
  )
257
248
  );
258
249
  const data = responses.map(getResponseData);
259
- let permissionsChanged = false;
260
250
  responses.forEach((response, index) => {
261
- permissionsChanged =
262
- cacheAllowedActions({
263
- resource,
264
- records: [data[index]],
265
- response: response as NocoBaseListResponse<TData>,
266
- meta,
267
- }) || permissionsChanged;
251
+ cacheAllowedActions({
252
+ resource,
253
+ records: [data[index]],
254
+ response: response as NocoBaseListResponse<TData>,
255
+ meta,
256
+ });
268
257
  });
269
- if (permissionsChanged) notifyRecordPermissionsChanged();
270
258
  return { data };
271
259
  },
272
260
 
@@ -17,3 +17,43 @@ export const getPortalBase = () =>
17
17
 
18
18
  export const getRuntimeApiUrl = () =>
19
19
  getWindowValue("NOCOBASE_API_URL") ?? import.meta.env.NOCOBASE_API_URL;
20
+
21
+ const getAppNameFromPortalBase = (base: string) =>
22
+ base.match(/\/x\/apps\/([^/]+)(?:\/|$)/)?.[1];
23
+
24
+ const getAppNameFromApiUrl = (apiUrl?: string) =>
25
+ apiUrl?.match(/\/api\/__app\/([^/?#]+)(?:[/?#]|$)/)?.[1];
26
+
27
+ export const resolveNocoBaseAppName = (portalBase: string, apiUrl?: string) =>
28
+ getAppNameFromPortalBase(portalBase) ??
29
+ getAppNameFromApiUrl(apiUrl) ??
30
+ "main";
31
+
32
+ export const getNocoBaseAppName = () =>
33
+ resolveNocoBaseAppName(getPortalBase(), getRuntimeApiUrl());
34
+
35
+ export const resolvePortalUrl = (path = "/") => {
36
+ if (typeof window === "undefined") return path;
37
+ if (/^[a-z][a-z\d+.-]*:/i.test(path)) return path;
38
+
39
+ const portalBase = getPortalBase();
40
+ const relativePath = path.replace(/^\/+/, "");
41
+ const portalUrl = new URL(
42
+ relativePath,
43
+ `${window.location.origin}${portalBase}`
44
+ );
45
+ const apiUrl = new URL(
46
+ getRuntimeApiUrl() || "/api",
47
+ window.location.origin
48
+ );
49
+
50
+ // Same-origin deployments should use a root-relative callback. Besides
51
+ // avoiding unnecessary origin coupling, this remains compatible with older
52
+ // SSO plugins that only accept NocoBase-local redirect paths. A standalone
53
+ // Portal dev server keeps the absolute URL so the callback can return to
54
+ // Vite instead of the deployed Portal route.
55
+ if (apiUrl.origin === portalUrl.origin) {
56
+ return `${portalUrl.pathname}${portalUrl.search}${portalUrl.hash}`;
57
+ }
58
+ return portalUrl.toString();
59
+ };
package/src/vite-env.d.ts CHANGED
@@ -3,4 +3,13 @@
3
3
  interface Window {
4
4
  NOCOBASE_PORTAL_BASE?: string;
5
5
  NOCOBASE_API_URL?: string;
6
+ __nocobase_api_client_storage_prefix__?: string;
7
+ __nocobase_api_client_storage_type__?: string;
8
+ __nocobase_api_client_share_token__?: boolean | string;
9
+ }
10
+
11
+ interface ImportMetaEnv {
12
+ readonly API_CLIENT_STORAGE_PREFIX?: string;
13
+ readonly API_CLIENT_STORAGE_TYPE?: string;
14
+ readonly API_CLIENT_SHARE_TOKEN?: string;
6
15
  }
@@ -0,0 +1,101 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "vite";
3
+
4
+ const server = await createServer({
5
+ appType: "custom",
6
+ logLevel: "silent",
7
+ server: { middlewareMode: true },
8
+ });
9
+
10
+ const createStorage = () => {
11
+ const values = new Map();
12
+ return {
13
+ getItem: (key) => values.get(key) ?? null,
14
+ setItem: (key, value) => values.set(key, value),
15
+ removeItem: (key) => values.delete(key),
16
+ };
17
+ };
18
+
19
+ try {
20
+ const { AuthSession } = await server.ssrLoadModule(
21
+ "/src/lib/nocobase/auth-session.ts"
22
+ );
23
+ const { resolveNocoBaseAppName, resolvePortalUrl } = await server.ssrLoadModule(
24
+ "/src/providers/runtime-config.ts"
25
+ );
26
+
27
+ assert.equal(
28
+ resolveNocoBaseAppName(
29
+ "/nocobase/x/apps/crm/customer/",
30
+ "/nocobase/api/__app/crm"
31
+ ),
32
+ "crm"
33
+ );
34
+ assert.equal(
35
+ resolveNocoBaseAppName("/x/customer/", "/api/__app/analytics"),
36
+ "analytics"
37
+ );
38
+ assert.equal(resolveNocoBaseAppName("/x/customer/", "/api"), "main");
39
+
40
+ const originalWindow = globalThis.window;
41
+ Object.defineProperty(globalThis, "window", {
42
+ configurable: true,
43
+ value: {
44
+ location: { origin: "http://localhost:14000" },
45
+ NOCOBASE_API_URL: "/api",
46
+ NOCOBASE_PORTAL_BASE: "/x/demo/",
47
+ },
48
+ });
49
+ assert.equal(
50
+ resolvePortalUrl("/users?tab=active#list"),
51
+ "/x/demo/users?tab=active#list"
52
+ );
53
+
54
+ window.NOCOBASE_API_URL = "/api/__app/crm";
55
+ window.NOCOBASE_PORTAL_BASE = "/x/apps/crm/customer/";
56
+ assert.equal(
57
+ resolvePortalUrl("/users?tab=active#list"),
58
+ "/x/apps/crm/customer/users?tab=active#list"
59
+ );
60
+
61
+ window.NOCOBASE_API_URL = "http://127.0.0.1:13000/api";
62
+ window.NOCOBASE_PORTAL_BASE = "/x/demo/";
63
+ assert.equal(
64
+ resolvePortalUrl("/users?tab=active#list"),
65
+ "http://localhost:14000/x/demo/users?tab=active#list"
66
+ );
67
+
68
+ if (originalWindow === undefined) {
69
+ delete globalThis.window;
70
+ } else {
71
+ Object.defineProperty(globalThis, "window", {
72
+ configurable: true,
73
+ value: originalWindow,
74
+ });
75
+ }
76
+
77
+ const main = new AuthSession({ appName: "main", storage: createStorage() });
78
+ assert.equal(main.getStorageKey("token"), "NOCOBASE_TOKEN");
79
+ assert.equal(main.getStorageKey("auth"), "NOCOBASE_AUTH");
80
+ assert.equal(main.getStorageKey("role"), "NOCOBASE_ROLE");
81
+
82
+ const subApp = new AuthSession({
83
+ appName: "crm",
84
+ storage: createStorage(),
85
+ });
86
+ assert.equal(subApp.getStorageKey("token"), "NOCOBASE_CRM_TOKEN");
87
+ assert.equal(subApp.getStorageKey("auth"), "NOCOBASE_CRM_AUTH");
88
+ assert.equal(subApp.getStorageKey("role"), "NOCOBASE_CRM_ROLE");
89
+
90
+ const sharedToken = new AuthSession({
91
+ appName: "crm",
92
+ shareToken: true,
93
+ storage: createStorage(),
94
+ });
95
+ assert.equal(sharedToken.getStorageKey("token"), "NOCOBASE_TOKEN");
96
+ assert.equal(sharedToken.getStorageKey("auth"), "NOCOBASE_CRM_AUTH");
97
+
98
+ console.log("NocoBase authentication session regression tests passed");
99
+ } finally {
100
+ await server.close();
101
+ }
package/tsconfig.json CHANGED
@@ -18,8 +18,9 @@
18
18
  "noUncheckedSideEffectImports": true,
19
19
  "baseUrl": ".",
20
20
  "paths": {
21
+ "@/extensions/*": ["./registry/*", "./src/extensions/*"],
21
22
  "@/*": ["./src/*"]
22
23
  }
23
24
  },
24
- "include": ["src"]
25
+ "include": ["src", "registry"]
25
26
  }
package/vite.config.ts CHANGED
@@ -37,6 +37,9 @@ const defaultRuntimeConfig = `<!-- nocobase-runtime-config:start -->
37
37
  <script>
38
38
  window.NOCOBASE_PORTAL_BASE = "/";
39
39
  window.NOCOBASE_API_URL = "/api";
40
+ window.__nocobase_api_client_storage_prefix__ = window.__nocobase_api_client_storage_prefix__ ?? "NOCOBASE_";
41
+ window.__nocobase_api_client_storage_type__ = window.__nocobase_api_client_storage_type__ ?? "localStorage";
42
+ window.__nocobase_api_client_share_token__ = window.__nocobase_api_client_share_token__ ?? false;
40
43
  </script>
41
44
  <!-- nocobase-runtime-config:end -->
42
45
  `;
@@ -96,13 +99,18 @@ export default defineConfig(({ mode }) => {
96
99
  : undefined;
97
100
 
98
101
  const portalBase = normalizeBase(env.NOCOBASE_PORTAL_BASE);
102
+ const registrySourceRoot = path.resolve(__dirname, "./registry");
103
+ const extensionsRoot = fs.existsSync(registrySourceRoot)
104
+ ? registrySourceRoot
105
+ : path.resolve(__dirname, "./src/extensions");
99
106
 
100
107
  return {
101
108
  base: portalBase,
102
- envPrefix: ["VITE_", "NOCOBASE_"],
109
+ envPrefix: ["VITE_", "NOCOBASE_", "API_CLIENT_"],
103
110
  plugins: [react(), tailwindcss(), copyRawIndexHtmlPlugin(portalBase)],
104
111
  resolve: {
105
112
  alias: {
113
+ "@/extensions": extensionsRoot,
106
114
  "@": path.resolve(__dirname, "./src"),
107
115
  },
108
116
  },
@@ -1,19 +0,0 @@
1
- # NocoBase ACL UI
2
-
3
- Reusable ACL composition components for the NocoBase Refine starter.
4
-
5
- After installation, import the components from `@/extensions/nocobase-acl`.
6
-
7
- - `AclPage` combines page-level permissions.
8
- - `AclRegion` hides or replaces individual data regions.
9
- - `AclField` applies NocoBase field whitelists.
10
- - `RoleSwitcher` can be placed in any application surface. It supports
11
- normal roles, Anonymous when enabled, and Full permissions through the
12
- `__union__` role when the system role mode allows it.
13
-
14
- Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
15
- the `collection`, `snippet`, and `route` ACL metadata variants when the resource
16
- should participate in NocoBase permission checks.
17
-
18
- The underlying `accessControlProvider`, `roles:check` cache, record permissions,
19
- and Refine integration are provided by the compatible Starter.
@@ -1,71 +0,0 @@
1
- import type { AppExtension } from "@/app/extension";
2
- import { Blocks, PanelsTopLeft, ShieldCheck } from "lucide-react";
3
- import { lazy } from "react";
4
- import { Outlet, Route } from "react-router";
5
- import { LazyAclRoute } from "./demo/lazy-route";
6
-
7
- const AclComponentsPage = lazy(() =>
8
- import("./demo/components").then((module) => ({
9
- default: module.AclComponentsPage,
10
- }))
11
- );
12
- const AclPatternsPage = lazy(() =>
13
- import("./demo").then((module) => ({ default: module.AclPatternsPage }))
14
- );
15
-
16
- const nocobaseAclExtension: AppExtension = {
17
- id: "nocobase-acl",
18
- resources: [
19
- {
20
- name: "acl-integration",
21
- meta: {
22
- label: "Access control",
23
- icon: <ShieldCheck />,
24
- description: "NocoBase ACL integration for admin applications.",
25
- acl: { type: "authenticated" },
26
- },
27
- },
28
- {
29
- name: "acl-components",
30
- list: "/acl",
31
- meta: {
32
- parent: "acl-integration",
33
- label: "Role switcher",
34
- icon: <Blocks />,
35
- acl: { type: "authenticated" },
36
- },
37
- },
38
- {
39
- name: "acl-patterns",
40
- list: "/acl/patterns",
41
- meta: {
42
- parent: "acl-integration",
43
- label: "Permission patterns",
44
- icon: <PanelsTopLeft />,
45
- acl: { type: "authenticated" },
46
- },
47
- },
48
- ],
49
- routes: (
50
- <Route key="nocobase-acl" path="/acl" element={<Outlet />}>
51
- <Route
52
- index
53
- element={
54
- <LazyAclRoute>
55
- <AclComponentsPage />
56
- </LazyAclRoute>
57
- }
58
- />
59
- <Route
60
- path="patterns"
61
- element={
62
- <LazyAclRoute>
63
- <AclPatternsPage />
64
- </LazyAclRoute>
65
- }
66
- />
67
- </Route>
68
- ),
69
- };
70
-
71
- export default nocobaseAclExtension;