@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.
- package/.env.example +5 -0
- package/.github/workflows/publish.yml +106 -0
- package/.gitignore +33 -0
- package/AGENTS.md +19 -0
- package/README.MD +61 -180
- package/package.json +3 -5
- package/scripts/build-html.mjs +12 -0
- package/src/App.css +2 -0
- package/src/App.tsx +35 -186
- package/src/app/development.tsx +154 -0
- package/src/app/extension.ts +5 -0
- package/src/app/extensions.tsx +18 -4
- package/src/app/routes.tsx +65 -0
- package/src/components/access-control/acl-bootstrap.tsx +11 -3
- package/src/components/access-control/acl-gate.tsx +7 -6
- package/src/components/access-control/can-access.tsx +17 -0
- package/src/components/access-control/navigate-to-accessible-resource.tsx +8 -3
- package/src/components/app-shell/sidebar.tsx +21 -5
- package/src/components/auth/auto-redirect-provider.tsx +2 -1
- package/src/extensions/nocobase-acl/README.md +74 -0
- package/{registry → src/extensions}/nocobase-acl/components/acl-boundary.tsx +22 -25
- package/{registry → src/extensions}/nocobase-acl/components/role-switcher.tsx +26 -23
- package/src/extensions/nocobase-acl/demo/acl-preview-provider.tsx +81 -0
- package/{registry → src/extensions}/nocobase-acl/demo/boundary-api.tsx +58 -0
- package/{registry → src/extensions}/nocobase-acl/demo/index.tsx +243 -105
- package/{registry → src/extensions}/nocobase-acl/demo/prompt-generator.tsx +3 -1
- package/src/extensions/nocobase-acl/extension.tsx +73 -0
- package/{registry → src/extensions}/nocobase-ai/README.md +21 -5
- package/{registry → src/extensions}/nocobase-ai/components/chat/chat-composer.tsx +3 -1
- package/{registry → src/extensions}/nocobase-ai/components/chat/chat-messages.tsx +38 -3
- package/{registry → src/extensions}/nocobase-ai/components/chat/tool-call-card.tsx +1 -9
- package/{registry → src/extensions}/nocobase-ai/components/index.ts +5 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +17 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +35 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface.tsx +108 -0
- package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +2 -2
- package/{registry → src/extensions}/nocobase-ai/demo/configuration-gate.tsx +4 -1
- package/{registry → src/extensions}/nocobase-ai/demo/container-showcase.tsx +2 -2
- package/{registry → src/extensions}/nocobase-ai/demo/floating.tsx +45 -15
- package/{registry → src/extensions}/nocobase-ai/demo/index.tsx +116 -74
- package/{registry → src/extensions}/nocobase-ai/demo/prompt-generator.tsx +3 -3
- package/{registry → src/extensions}/nocobase-ai/demo/tool-cards.tsx +2 -3
- package/src/extensions/nocobase-ai/extension.tsx +160 -0
- package/{registry → src/extensions}/nocobase-ai/global-ai-chat.tsx +12 -28
- package/{registry → src/extensions}/nocobase-ai/providers/ai-provider.tsx +14 -1
- package/{registry → src/extensions}/nocobase-ai/providers/chat-context.tsx +1 -0
- package/{registry → src/extensions}/nocobase-ai/providers/chat-provider.tsx +29 -8
- package/{registry → src/extensions}/nocobase-ai/providers/use-chat-attachments.ts +6 -1
- package/{registry → src/extensions}/nocobase-ai/providers/use-chat-work-context.ts +4 -1
- package/{registry → src/extensions}/nocobase-route-surfaces/README.md +2 -1
- package/{registry → src/extensions}/nocobase-route-surfaces/demo/index.tsx +33 -7
- package/{registry → src/extensions}/nocobase-route-surfaces/demo/scenarios.ts +20 -14
- package/{registry → src/extensions}/nocobase-route-surfaces/extension.tsx +55 -52
- package/src/{pages/users → extensions/nocobase-users-example}/create.tsx +4 -4
- package/src/{pages/users → extensions/nocobase-users-example}/edit.tsx +5 -9
- package/src/extensions/nocobase-users-example/extension.tsx +109 -0
- package/src/{pages/users → extensions/nocobase-users-example}/form-context.ts +11 -9
- package/src/extensions/nocobase-users-example/layout.tsx +16 -0
- package/src/{pages/users → extensions/nocobase-users-example}/list.tsx +1 -1
- package/src/extensions/nocobase-users-example/locales/en-US.ts +77 -0
- package/src/extensions/nocobase-users-example/locales/index.ts +8 -0
- package/src/extensions/nocobase-users-example/locales/zh-CN.ts +73 -0
- package/src/extensions/nocobase-users-example/optional-ai.ts +67 -0
- package/src/{pages/users → extensions/nocobase-users-example}/role-detail.tsx +4 -15
- package/src/{pages/users → extensions/nocobase-users-example}/routes.ts +1 -4
- package/src/extensions/nocobase-users-example/show.tsx +266 -0
- package/src/lib/nocobase/acl/context.ts +32 -0
- package/src/lib/nocobase/acl/evaluator.ts +186 -0
- package/src/lib/nocobase/acl/hooks.ts +40 -0
- package/src/lib/nocobase/acl/index.ts +4 -1
- package/src/lib/nocobase/acl/menu.ts +6 -6
- package/src/lib/nocobase/acl/record-permissions.ts +37 -7
- package/src/lib/nocobase/acl/store-provider.tsx +14 -0
- package/src/lib/nocobase/acl/store.ts +92 -86
- package/src/lib/nocobase/acl/types.ts +52 -32
- package/src/lib/nocobase/auth-session.ts +128 -0
- package/src/lib/nocobase/client.ts +48 -50
- package/src/locales/en-US.ts +0 -78
- package/src/locales/index.ts +2 -7
- package/src/locales/zh-CN.ts +0 -74
- package/src/providers/access-control.ts +43 -8
- package/src/providers/auth-callback.ts +1 -0
- package/src/providers/auth.ts +24 -26
- package/src/providers/constants.ts +0 -4
- package/src/providers/data.ts +14 -26
- package/src/providers/runtime-config.ts +40 -0
- package/src/vite-env.d.ts +9 -0
- package/tests/auth-session-regression.mjs +101 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +9 -1
- package/registry/nocobase-acl/README.md +0 -19
- package/registry/nocobase-acl/extension.tsx +0 -71
- package/registry/nocobase-acl/tests/acl-regression.mjs +0 -314
- package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
- package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
- package/registry/nocobase-ai/extension.tsx +0 -158
- package/registry/nocobase-ai/tests/business-report-regression.mjs +0 -70
- package/registry/nocobase-ai/tests/form-filler-regression.mjs +0 -129
- package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +0 -128
- package/registry/nocobase-ai/tests/page-context-regression.mjs +0 -48
- package/registry/nocobase-ai/tests/stream-regression.mjs +0 -207
- package/registry/nocobase-auth-cas/README.md +0 -6
- package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +0 -41
- package/registry/nocobase-auth-cas/demo.tsx +0 -20
- package/registry/nocobase-auth-cas/extension.tsx +0 -46
- package/registry/nocobase-auth-cas/index.ts +0 -1
- package/registry/nocobase-auth-cas/use-cas-sign-in.ts +0 -29
- package/registry/nocobase-auth-dingtalk/README.md +0 -7
- package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +0 -154
- package/registry/nocobase-auth-dingtalk/demo.tsx +0 -23
- package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +0 -45
- package/registry/nocobase-auth-dingtalk/extension.tsx +0 -55
- package/registry/nocobase-auth-dingtalk/index.ts +0 -4
- package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +0 -64
- package/registry/nocobase-auth-ldap/README.md +0 -6
- package/registry/nocobase-auth-ldap/demo.tsx +0 -21
- package/registry/nocobase-auth-ldap/extension.tsx +0 -46
- package/registry/nocobase-auth-ldap/index.ts +0 -1
- package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +0 -69
- package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +0 -5
- package/registry/nocobase-auth-oidc/README.md +0 -11
- package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +0 -13
- package/registry/nocobase-auth-oidc/demo.tsx +0 -20
- package/registry/nocobase-auth-oidc/extension.tsx +0 -51
- package/registry/nocobase-auth-oidc/index.ts +0 -1
- package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +0 -44
- package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +0 -58
- package/registry/nocobase-auth-saml/README.md +0 -6
- package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +0 -13
- package/registry/nocobase-auth-saml/demo.tsx +0 -20
- package/registry/nocobase-auth-saml/extension.tsx +0 -51
- package/registry/nocobase-auth-saml/index.ts +0 -1
- package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +0 -43
- package/registry/nocobase-auth-saml/use-saml-sign-in.ts +0 -51
- package/registry/nocobase-auth-sms/README.md +0 -7
- package/registry/nocobase-auth-sms/demo.tsx +0 -25
- package/registry/nocobase-auth-sms/extension.tsx +0 -46
- package/registry/nocobase-auth-sms/index.ts +0 -1
- package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +0 -107
- package/registry/nocobase-auth-sms/use-sms-sign-in.ts +0 -88
- package/registry/nocobase-auth-wecom/README.md +0 -6
- package/registry/nocobase-auth-wecom/auto-login-provider.tsx +0 -21
- package/registry/nocobase-auth-wecom/demo.tsx +0 -23
- package/registry/nocobase-auth-wecom/extension.tsx +0 -49
- package/registry/nocobase-auth-wecom/index.ts +0 -1
- package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +0 -62
- package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +0 -70
- package/registry/nocobase-i18n/README.md +0 -44
- package/registry/nocobase-i18n/components/index.ts +0 -1
- package/registry/nocobase-i18n/components/language-switcher.tsx +0 -138
- package/registry/nocobase-i18n/demo/index.tsx +0 -209
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +0 -132
- package/registry/nocobase-i18n/extension.tsx +0 -45
- package/registry/nocobase-i18n/index.ts +0 -7
- package/registry/nocobase-i18n/locales/en-US.ts +0 -39
- package/registry/nocobase-i18n/locales/index.ts +0 -8
- package/registry/nocobase-i18n/locales/zh-CN.ts +0 -35
- package/registry/nocobase-i18n/provider.tsx +0 -44
- package/registry/nocobase-i18n/server-resources.ts +0 -166
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +0 -51
- package/registry.config.json +0 -302
- package/scripts/registry.mjs +0 -164
- package/src/components/access-control/resource-access-guard.tsx +0 -21
- package/src/components/access-control/use-acl-runtime.ts +0 -28
- package/src/lib/nocobase/acl/action.ts +0 -147
- package/src/pages/users/layout.tsx +0 -15
- package/src/pages/users/show.tsx +0 -278
- /package/{registry → src/extensions}/nocobase-acl/components/index.ts +0 -0
- /package/{registry → src/extensions}/nocobase-acl/components/role-options.ts +0 -0
- /package/{registry → src/extensions}/nocobase-acl/demo/components.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-acl/demo/lazy-route.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-acl/demo/scenario-section.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-acl/index.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/adapters/react-hook-form.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/assets/nocobase-ai-chat.svg +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/ai-root-provider.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-attachment.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-compact.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-empty-state.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-header.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-message.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-window.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/conversation-list.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/markdown-message.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/model-select-options.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/reasoning-panel.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/chat/work-context-chip.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/page-elements/ai-form.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-inline.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-page.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-dialog.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-renderer.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-utils.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/chart-renderer.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-preview.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/tools/workflow-renderer.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/demo/interaction-showcase.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/demo/page-context.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/demo/page-element-showcase.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/demo/prompt-card.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/demo/shortcut.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/index.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/avatars.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/chat-controller.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/chat-message-utils.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/chat-reducer.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/chat-task-utils.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/chat-transport.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/form-registry.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/frontend-tool-registry.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/index.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/model.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/page-context.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/stream-coalescer.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/stream-event-utils.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/stream-parser.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/sub-agent-stream.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/types.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/ui-message-stream.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-message-actions.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-runtime.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-catalog.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-history.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/services/index.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/services/nocobase-ai-service.ts +0 -0
- /package/{registry → src/extensions}/nocobase-ai/services/types.ts +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/demo/lazy-route.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/demo/prompt-generator.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/index.ts +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/route-dialog.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/route-drawer.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/route-page.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/route-surface-context.ts +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-close.ts +0 -0
- /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-state.ts +0 -0
- /package/src/{pages/users → extensions/nocobase-users-example}/form-fields.tsx +0 -0
- /package/src/{pages/users → extensions/nocobase-users-example}/index.ts +0 -0
- /package/src/{pages/users → extensions/nocobase-users-example}/role-badges.tsx +0 -0
- /package/src/{pages/users → extensions/nocobase-users-example}/role-utils.ts +0 -0
- /package/src/{pages/users → extensions/nocobase-users-example}/types.ts +0 -0
|
@@ -3,6 +3,7 @@ import { useLocation } from "react-router";
|
|
|
3
3
|
|
|
4
4
|
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
5
5
|
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
6
|
+
import { resolvePortalUrl } from "@/providers/runtime-config";
|
|
6
7
|
|
|
7
8
|
type AutoRedirectResponse = {
|
|
8
9
|
success?: boolean;
|
|
@@ -32,7 +33,7 @@ export function AuthAutoRedirectProvider({
|
|
|
32
33
|
}: AuthAutoRedirectProviderProps) {
|
|
33
34
|
const { pathname, search } = useLocation();
|
|
34
35
|
const attemptedRef = useRef(false);
|
|
35
|
-
const redirect = `${pathname}${search}
|
|
36
|
+
const redirect = resolvePortalUrl(`${pathname}${search}`);
|
|
36
37
|
const shouldCheck =
|
|
37
38
|
enabled &&
|
|
38
39
|
!nocobaseClient.getToken() &&
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
- The Starter's `CanAccess` supports role, resource, record, and field checks.
|
|
8
|
+
- `AclPage` combines page-level permissions.
|
|
9
|
+
- `AclRegion` hides or replaces individual data regions.
|
|
10
|
+
- `AclField` applies NocoBase field whitelists.
|
|
11
|
+
- `RoleSwitcher` can be placed in any application surface. It supports
|
|
12
|
+
normal roles, Anonymous when enabled, and Full permissions through the
|
|
13
|
+
`__union__` role when the system role mode allows it.
|
|
14
|
+
|
|
15
|
+
Route resource metadata and `AclPage` accept a `roles` constraint with `anyOf`,
|
|
16
|
+
`allOf`, and `noneOf`. Role constraints use the roles in the current effective
|
|
17
|
+
ACL context. In union mode this is the set of roles participating in the union;
|
|
18
|
+
in single-role mode it is only the selected role. Regions, fields, and actions
|
|
19
|
+
continue to use resource permissions.
|
|
20
|
+
|
|
21
|
+
Use the Starter's `useGetRoles()` hook when application UI needs the same
|
|
22
|
+
effective role names. Its result follows the familiar Refine query shape with
|
|
23
|
+
`data`, `isLoading`, `isPending`, `isError`, and `error`. `data` is the active
|
|
24
|
+
ACL role set, not every role assigned to the signed-in user.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useGetRoles } from "@/lib/nocobase/acl";
|
|
28
|
+
|
|
29
|
+
function CurrentAccessContext() {
|
|
30
|
+
const { data: roles, isLoading } = useGetRoles();
|
|
31
|
+
|
|
32
|
+
if (isLoading) return null;
|
|
33
|
+
return <span>Effective roles: {roles?.join(", ")}</span>;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
const adminRoute = {
|
|
39
|
+
name: "administration",
|
|
40
|
+
meta: {
|
|
41
|
+
acl: {
|
|
42
|
+
type: "authenticated",
|
|
43
|
+
roles: { anyOf: ["admin"] },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function AuditLogsRoute() {
|
|
49
|
+
return (
|
|
50
|
+
<AclPage roles={{ anyOf: ["admin", "auditor"] }}>
|
|
51
|
+
<AclRegion resource="auditLogs" action="list">
|
|
52
|
+
<AuditLogTable />
|
|
53
|
+
</AclRegion>
|
|
54
|
+
</AclPage>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
|
|
60
|
+
the `collection`, `snippet`, and `route` ACL metadata variants when the resource
|
|
61
|
+
should participate in NocoBase permission checks.
|
|
62
|
+
|
|
63
|
+
The underlying `accessControlProvider`, `roles:check` cache, record permissions,
|
|
64
|
+
and Refine integration are provided by the compatible Starter.
|
|
65
|
+
|
|
66
|
+
The Demo uses the real `CanAccess`, `AclPage`, `AclRegion`, and `AclField`
|
|
67
|
+
components. A local `AclPreviewProvider` supplies a static in-memory ACL store
|
|
68
|
+
so both allowed and restricted outcomes remain visible without changing the
|
|
69
|
+
current user's real roles. It does not replace Refine's access-control context.
|
|
70
|
+
Applications use the Starter's root `AclStoreProvider` and production `aclStore`.
|
|
71
|
+
|
|
72
|
+
Global ACL permissions remain fixed for one page session. Switching roles saves
|
|
73
|
+
the selected role and reloads the page so navigation, routes, data access, and
|
|
74
|
+
Refine permission queries all start from the same `roles:check` result.
|
|
@@ -2,10 +2,8 @@ import type { BaseKey } from "@refinedev/core";
|
|
|
2
2
|
import type { PropsWithChildren, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { AccessDenied } from "@/components/access-control/access-denied";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
useAclSnapshot,
|
|
8
|
-
} from "@/lib/nocobase/acl";
|
|
5
|
+
import { useAclEvaluator } from "@/lib/nocobase/acl";
|
|
6
|
+
import type { RoleConstraint } from "@/lib/nocobase/acl";
|
|
9
7
|
|
|
10
8
|
export type AclPermission = {
|
|
11
9
|
resource: string;
|
|
@@ -18,6 +16,7 @@ export type AclPermission = {
|
|
|
18
16
|
export type AclPageProps = PropsWithChildren<{
|
|
19
17
|
anyOf?: AclPermission[];
|
|
20
18
|
allOf?: AclPermission[];
|
|
19
|
+
roles?: RoleConstraint;
|
|
21
20
|
fallback?: ReactNode;
|
|
22
21
|
}>;
|
|
23
22
|
|
|
@@ -35,31 +34,29 @@ export type AclFieldProps = PropsWithChildren<{
|
|
|
35
34
|
fallback?: ReactNode;
|
|
36
35
|
}>;
|
|
37
36
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
params: {
|
|
46
|
-
id: permission.id,
|
|
47
|
-
field: permission.field,
|
|
48
|
-
dataSourceKey: permission.dataSourceKey,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
37
|
+
const toAccessRequest = (permission: AclPermission) => ({
|
|
38
|
+
resource: permission.resource,
|
|
39
|
+
action: permission.action,
|
|
40
|
+
id: permission.id,
|
|
41
|
+
field: permission.field,
|
|
42
|
+
dataSourceKey: permission.dataSourceKey,
|
|
43
|
+
});
|
|
51
44
|
|
|
52
45
|
export function AclPage({
|
|
53
46
|
children,
|
|
54
47
|
anyOf,
|
|
55
48
|
allOf,
|
|
49
|
+
roles,
|
|
56
50
|
fallback = <AccessDenied />,
|
|
57
51
|
}: AclPageProps) {
|
|
58
|
-
const
|
|
59
|
-
const anyAllowed =
|
|
60
|
-
|
|
52
|
+
const canAccess = useAclEvaluator();
|
|
53
|
+
const anyAllowed =
|
|
54
|
+
!anyOf?.length || anyOf.some((item) => canAccess(toAccessRequest(item)));
|
|
55
|
+
const allAllowed =
|
|
56
|
+
!allOf?.length || allOf.every((item) => canAccess(toAccessRequest(item)));
|
|
57
|
+
const roleAllowed = canAccess({ roles });
|
|
61
58
|
|
|
62
|
-
return anyAllowed && allAllowed ? children : fallback;
|
|
59
|
+
return roleAllowed && anyAllowed && allAllowed ? children : fallback;
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
export function AclRegion({
|
|
@@ -70,8 +67,8 @@ export function AclRegion({
|
|
|
70
67
|
dataSourceKey,
|
|
71
68
|
fallback = "hidden",
|
|
72
69
|
}: AclRegionProps) {
|
|
73
|
-
const
|
|
74
|
-
const allowed = canAccess(
|
|
70
|
+
const canAccess = useAclEvaluator();
|
|
71
|
+
const allowed = canAccess({
|
|
75
72
|
resource,
|
|
76
73
|
action,
|
|
77
74
|
id,
|
|
@@ -94,8 +91,8 @@ export function AclField({
|
|
|
94
91
|
dataSourceKey,
|
|
95
92
|
fallback = null,
|
|
96
93
|
}: AclFieldProps) {
|
|
97
|
-
const
|
|
98
|
-
return canAccess(
|
|
94
|
+
const canAccess = useAclEvaluator();
|
|
95
|
+
return canAccess({
|
|
99
96
|
resource,
|
|
100
97
|
action,
|
|
101
98
|
field,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "@/components/ui/select";
|
|
14
14
|
import {
|
|
15
15
|
switchRole,
|
|
16
|
-
|
|
16
|
+
useAclState,
|
|
17
17
|
type AclIdentity,
|
|
18
18
|
type Role,
|
|
19
19
|
} from "@/lib/nocobase/acl";
|
|
@@ -34,24 +34,41 @@ export function RoleSwitcher({
|
|
|
34
34
|
showWhenUnavailable = false,
|
|
35
35
|
}: RoleSwitcherProps) {
|
|
36
36
|
const { data: identity, isLoading } = useGetIdentity<AclIdentity>();
|
|
37
|
-
const acl =
|
|
37
|
+
const acl = useAclState();
|
|
38
38
|
const [switching, setSwitching] = useState(false);
|
|
39
39
|
const [error, setError] = useState<string>();
|
|
40
|
+
const permissions = acl.status === "ready" ? acl.permissions : undefined;
|
|
40
41
|
|
|
41
42
|
const roles = useMemo(
|
|
42
43
|
() =>
|
|
43
44
|
getRoleOptions({
|
|
44
45
|
roles: identity?.roles ?? [],
|
|
45
|
-
roleMode:
|
|
46
|
-
allowAnonymous:
|
|
46
|
+
roleMode: permissions?.roleMode,
|
|
47
|
+
allowAnonymous: permissions?.allowAnonymous,
|
|
47
48
|
}),
|
|
48
|
-
[
|
|
49
|
+
[identity?.roles, permissions?.allowAnonymous, permissions?.roleMode]
|
|
49
50
|
);
|
|
50
51
|
|
|
51
|
-
const currentRole =
|
|
52
|
-
const canSwitch =
|
|
52
|
+
const currentRole = permissions?.currentRole ?? roles[0]?.name;
|
|
53
|
+
const canSwitch =
|
|
54
|
+
roles.length > 1 && permissions?.roleMode !== "only-use-union";
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
const handleRoleChange = async (value: string | null) => {
|
|
57
|
+
if (!value || value === currentRole) return;
|
|
58
|
+
setSwitching(true);
|
|
59
|
+
setError(undefined);
|
|
60
|
+
try {
|
|
61
|
+
await switchRole(value);
|
|
62
|
+
window.location.reload();
|
|
63
|
+
} catch (reason) {
|
|
64
|
+
setError(
|
|
65
|
+
reason instanceof Error ? reason.message : "Unable to switch role"
|
|
66
|
+
);
|
|
67
|
+
setSwitching(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (isLoading || acl.status === "idle" || acl.status === "loading") {
|
|
55
72
|
return <Loader2 className="size-4 animate-spin text-muted-foreground" />;
|
|
56
73
|
}
|
|
57
74
|
if (!canSwitch && !showWhenUnavailable) return null;
|
|
@@ -74,21 +91,7 @@ export function RoleSwitcher({
|
|
|
74
91
|
<Select
|
|
75
92
|
value={currentRole}
|
|
76
93
|
disabled={switching}
|
|
77
|
-
onValueChange={
|
|
78
|
-
if (!value || value === currentRole) return;
|
|
79
|
-
setSwitching(true);
|
|
80
|
-
setError(undefined);
|
|
81
|
-
void switchRole(value, { reloadAcl: false })
|
|
82
|
-
.then(() => window.location.reload())
|
|
83
|
-
.catch((reason) => {
|
|
84
|
-
setError(
|
|
85
|
-
reason instanceof Error
|
|
86
|
-
? reason.message
|
|
87
|
-
: "Unable to switch role"
|
|
88
|
-
);
|
|
89
|
-
setSwitching(false);
|
|
90
|
-
});
|
|
91
|
-
}}
|
|
94
|
+
onValueChange={handleRoleChange}
|
|
92
95
|
>
|
|
93
96
|
<SelectTrigger
|
|
94
97
|
className={cn("w-full min-w-52", triggerClassName)}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useMemo, type PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AclStoreProvider,
|
|
5
|
+
type AclActionParams,
|
|
6
|
+
type AclState,
|
|
7
|
+
type AclStore,
|
|
8
|
+
} from "@/lib/nocobase/acl";
|
|
9
|
+
|
|
10
|
+
type PreviewRecordPermission = {
|
|
11
|
+
dataSourceKey?: string;
|
|
12
|
+
resource: string;
|
|
13
|
+
action: string;
|
|
14
|
+
id: string | number;
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type AclPreviewProviderProps = PropsWithChildren<{
|
|
19
|
+
roles: string[];
|
|
20
|
+
permissions?: Record<string, AclActionParams>;
|
|
21
|
+
recordPermissions?: PreviewRecordPermission[];
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export function AclPreviewProvider({
|
|
25
|
+
children,
|
|
26
|
+
roles,
|
|
27
|
+
permissions = {},
|
|
28
|
+
recordPermissions = [],
|
|
29
|
+
}: AclPreviewProviderProps) {
|
|
30
|
+
const store = useMemo<AclStore>(() => {
|
|
31
|
+
const resources = Array.from(
|
|
32
|
+
new Set(
|
|
33
|
+
Object.keys(permissions).map((permission) =>
|
|
34
|
+
permission.slice(0, permission.lastIndexOf(":"))
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
const state: AclState = {
|
|
39
|
+
status: "ready",
|
|
40
|
+
permissions: {
|
|
41
|
+
currentRole: roles.length > 1 ? "__union__" : roles[0],
|
|
42
|
+
roles,
|
|
43
|
+
roleMode: roles.length > 1 ? "allow-use-union" : "default",
|
|
44
|
+
resources,
|
|
45
|
+
actions: permissions,
|
|
46
|
+
actionAlias: {
|
|
47
|
+
list: "list",
|
|
48
|
+
get: "get",
|
|
49
|
+
create: "create",
|
|
50
|
+
update: "update",
|
|
51
|
+
destroy: "destroy",
|
|
52
|
+
},
|
|
53
|
+
snippets: [],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const recordState = new Map(
|
|
57
|
+
recordPermissions.map((permission) => [
|
|
58
|
+
`${permission.dataSourceKey ?? "main"}:${permission.resource}:${
|
|
59
|
+
permission.action
|
|
60
|
+
}:${permission.id}`,
|
|
61
|
+
permission.allowed,
|
|
62
|
+
])
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
getState: () => state,
|
|
67
|
+
subscribe: () => () => undefined,
|
|
68
|
+
load: async () => state,
|
|
69
|
+
retry: async () => state,
|
|
70
|
+
clear: () => undefined,
|
|
71
|
+
recordPermissions: {
|
|
72
|
+
getState: () => recordState,
|
|
73
|
+
subscribe: () => () => undefined,
|
|
74
|
+
getPermission: ({ dataSourceKey = "main", resource, action, id }) =>
|
|
75
|
+
recordState.get(`${dataSourceKey}:${resource}:${action}:${id}`),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}, [permissions, recordPermissions, roles]);
|
|
79
|
+
|
|
80
|
+
return <AclStoreProvider store={store}>{children}</AclStoreProvider>;
|
|
81
|
+
}
|
|
@@ -15,6 +15,33 @@ type PropRow = [
|
|
|
15
15
|
description: string
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
const canAccessProps: PropRow[] = [
|
|
19
|
+
[
|
|
20
|
+
"roles",
|
|
21
|
+
"RoleConstraint",
|
|
22
|
+
"undefined",
|
|
23
|
+
"Checks the current effective roles with anyOf, allOf, and noneOf.",
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
"resource / action",
|
|
27
|
+
"string",
|
|
28
|
+
"undefined",
|
|
29
|
+
"Checks a NocoBase resource action. When roles are also present, both checks must pass.",
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"id / field",
|
|
33
|
+
"BaseKey / string",
|
|
34
|
+
"undefined",
|
|
35
|
+
"Optionally narrows the decision to one record or field.",
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"fallback",
|
|
39
|
+
"ReactNode",
|
|
40
|
+
"null",
|
|
41
|
+
"Renders when the access request is denied.",
|
|
42
|
+
],
|
|
43
|
+
];
|
|
44
|
+
|
|
18
45
|
const pageProps: PropRow[] = [
|
|
19
46
|
[
|
|
20
47
|
"anyOf",
|
|
@@ -28,6 +55,12 @@ const pageProps: PropRow[] = [
|
|
|
28
55
|
"undefined",
|
|
29
56
|
"Requires every listed permission before rendering the page.",
|
|
30
57
|
],
|
|
58
|
+
[
|
|
59
|
+
"roles",
|
|
60
|
+
"RoleConstraint",
|
|
61
|
+
"undefined",
|
|
62
|
+
"Requires the current effective roles to match anyOf, allOf, and noneOf constraints.",
|
|
63
|
+
],
|
|
31
64
|
[
|
|
32
65
|
"fallback",
|
|
33
66
|
"ReactNode",
|
|
@@ -100,6 +133,21 @@ const fieldProps: PropRow[] = [
|
|
|
100
133
|
["children", "ReactNode", "required", "The protected field or field group."],
|
|
101
134
|
];
|
|
102
135
|
|
|
136
|
+
const runtimeApis: PropRow[] = [
|
|
137
|
+
[
|
|
138
|
+
"AclStoreProvider",
|
|
139
|
+
"{ store, children }",
|
|
140
|
+
"required",
|
|
141
|
+
"Connects React ACL hooks and components to the application aclStore. The Starter mounts it at the application root.",
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"useGetRoles",
|
|
145
|
+
"() => QueryResult<string[]>",
|
|
146
|
+
"-",
|
|
147
|
+
"Returns the current effective ACL role names, including all participating roles in union mode.",
|
|
148
|
+
],
|
|
149
|
+
];
|
|
150
|
+
|
|
103
151
|
export function AclBoundaryApi() {
|
|
104
152
|
return (
|
|
105
153
|
<section className="space-y-5 border-t pt-8">
|
|
@@ -116,6 +164,11 @@ export function AclBoundaryApi() {
|
|
|
116
164
|
</p>
|
|
117
165
|
</div>
|
|
118
166
|
<div className="space-y-7">
|
|
167
|
+
<ComponentApi
|
|
168
|
+
title="CanAccess"
|
|
169
|
+
description="The Starter access boundary for role-only, resource-only, or combined permission checks."
|
|
170
|
+
rows={canAccessProps}
|
|
171
|
+
/>
|
|
119
172
|
<ComponentApi
|
|
120
173
|
title="AclPage"
|
|
121
174
|
description="Protects a complete route or business page. This corresponds to the Page permission pattern above."
|
|
@@ -131,6 +184,11 @@ export function AclBoundaryApi() {
|
|
|
131
184
|
description="Protects one field or field group. This corresponds to the Field permission pattern."
|
|
132
185
|
rows={fieldProps}
|
|
133
186
|
/>
|
|
187
|
+
<ComponentApi
|
|
188
|
+
title="ACL context APIs"
|
|
189
|
+
description="Read the active role context from the application ACL store. Demo previews use an isolated in-memory store with the same evaluator."
|
|
190
|
+
rows={runtimeApis}
|
|
191
|
+
/>
|
|
134
192
|
</div>
|
|
135
193
|
</section>
|
|
136
194
|
);
|