nocobase/packages/core/client/src/acl/ACLProvider.tsx
jack zhang 2cb1203aa4
refactor(client)!: application, router and plugin (#2068)
BREAKING CHANGE:

* refactor: update umi version 3.x to version 4.x

* refactor: update react-router-dom version to 6.x

* refactor(react-router-dom): change Layout Component `props.children` to `<Outlet />`

* refactor(react-router-dom): change <Route /> props and <RouteSwitch /> correct

* refactor(react-router-dom): replace `<Redirect />` to `<Navigate replace />`

* refactor(react-router-dom): replace `useHistory` to `useNavigate`

* refactor(react-router-dom): replace `useRouteMatch` to `useParams`

* refactor(react-router-dom & dumi): fix <RouteSwitch /> & umi document bug

* refactor(react-router-dom): `useRoutes` Optimize `<RouteSwitch />` code

* refactor(react-router-dom): update `Route` types and docs

* refactor(react-router-dom): optimize RouteSwitch code

* refactor(react-router-dom): `useLocation` no generics type

* refactor(react-router-dom): add `less v3.9.0` to `resolutions` to solve the error of `gulp-less`

* refactor(react-router-dom): fix `<RouteSwitch />`  `props.routes` as an array is not handled

* chore: upgrade `dumi` and refactor docs

* fix: completed code review, add `targets` to solve browser compatibility & removed `chainWebpack`

* refactor(dumi): upgraded dumi under `packages/core/client`

* refactor(dumi): delete `packages/core/dumi-theme-nocobase`

* refactor(dumi): degrade `react`  & replace `dumi-theme-antd` to `dumi-theme-nocobase`

* refactor(dumi): solve conflicts between multiple dumi applications

* fix: login page error in react 17

* refactor(dumi): remove less resolutions

* refactor(dumi): umi add `msfu: true` config

* fix: merge bug

* fix: self code review

* fix: code reivew and test bug

* refactor: upgrade react to 18

* refactor: degrade react types to 17

* chore: fix ci error

* fix: support routerBase & fix workflow page params

* fix(doc): menu externel link

* fix: build error

* fix: delete

* fix: vitest error

* fix: react-router new code replace

* fix: vitest markdown error

* fix: title is none when refresh

* fix: merge error

* fix: sidebar width is wrong

* fix: useProps error

* fix: side-menu-width

* fix: menu selectId is wrong & useProps is string

* fix: menu selected first default & side menu hide when change

* fix: test error & v0.10 change log

* fix: new compnent doc modify

* fix: set umi `fastRefresh=false`

* refactor: application v2

* fix: improve code

* fix: bug

* fix: page = 0 error

* fix: workflow navigate error

* feat: plugin manager

* fix: afterAdd

* feat: complete basic functional refactor

* fix: performance Application

* feat: support client and server build

* refactor: nocobase build-in plugin and providers

* fix: server can't start

* refactor: all plugins package `Prodiver` change to `Plugin`

* feat: nested router and change mobile client

* feat: delete application-v1 and router-switch

* feat: improve routes

* fix: change mobile not nested

* feat: delete RouteSwitchContext and change buildin Provider to Plugin

* feat: delete RouteSwitchContext plugins

* fix: refactor SchemaComponentOptions

* feat: improve SchemaComponentOptions

* fix: add useAdminSchemaUid

* fix: merge master error

* fix: vitest error

* fix: bug

* feat: bugs

* fix: improve code

* fix: restore code

* feat: vitest

* fix: bugs

* fix: bugs

* docs: update doc

* feat: improve code

* feat: add docs and imporve code

* fix: bugs

* feat: add tests

* fix: remove deps

* fix: muti app router error

* fix: router error

* fix: workflow error

* fix: cli error

* feat: change NoCobase -> Nocobase

* fix: code review

* fix: type error

* fix: cli error and plugin demo

* feat: update doc theme

* fix: build error

* fix: mobile router

* fix: code rewview

* fix: bug

* fix: test bug

* fix: bug

* refactor: add the "client" directory to all plugins

* refactor: modify samples client and plugin template

* fix: merge error

* fix: add files in package.json

* refactor: add README to files in package.json

* fix: adjust plugins depencies

* refactor: completing plugins' devDependencies and dependencies

* fix: bug

* refactor: remove @emotion/css

* refactor: jsonwebtoken deps

* refactor: remove sequelize

* refactor: dayjs and moment deps

* fix: bugs

* fix: bug

* fix: cycle detect

* fix: merge bug

* feat: new plugin bug

* fix: lang bug

* fix: dynamic import bug

* refactor: plugins and example add father config

* feat: improve code

* fix: add AppSpin and AppError components

* Revert "refactor: plugins and example add father config"

This reverts commit 483315bca5524e4b8cbbb20cbad77986f081089d.

# Conflicts:
#	packages/plugins/auth/package.json
#	packages/plugins/multi-app-manager/package.json
#	packages/samples/command/package.json
#	packages/samples/custom-collection-template/package.json
#	packages/samples/ratelimit/package.json
#	packages/samples/shop-actions/package.json
#	packages/samples/shop-events/package.json
#	packages/samples/shop-modeling/package.json

* feat: update doc

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-07-07 14:35:22 +08:00

278 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Field } from '@formily/core';
import { Schema, useField, useFieldSchema } from '@formily/react';
import { Spin } from 'antd';
import React, { createContext, useContext, useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useAPIClient, useRequest } from '../api-client';
import { useBlockRequestContext } from '../block-provider/BlockProvider';
import { useCollection } from '../collection-manager';
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
import { useRecord } from '../record-provider';
import { SchemaComponentOptions, useDesignable } from '../schema-component';
export const ACLContext = createContext<any>({});
// TODO: delete thisreplace by `ACLPlugin`
export const ACLProvider = (props) => {
return (
<SchemaComponentOptions
components={{ ACLCollectionFieldProvider, ACLActionProvider, ACLMenuItemProvider, ACLCollectionProvider }}
>
{props.children}
</SchemaComponentOptions>
);
};
const getRouteUrl = (props) => {
if (props?.match) {
return props.match;
}
return props && getRouteUrl(props?.children?.props);
};
export const ACLRolesCheckProvider = (props) => {
const route = getRouteUrl(props.children.props);
const { setDesignable } = useDesignable();
const api = useAPIClient();
const result = useRequest(
{
url: 'roles:check',
},
{
onSuccess(data) {
if (!data?.data?.snippets.includes('ui.*')) {
setDesignable(false);
}
if (data?.data?.role !== api.auth.role) {
api.auth.setRole(data?.data?.role);
}
},
},
);
if (result.loading) {
return <Spin />;
}
if (result.error) {
return <Navigate replace to={'/signin'} />;
}
return <ACLContext.Provider value={result}>{props.children}</ACLContext.Provider>;
};
export const useRoleRecheck = () => {
const ctx = useContext(ACLContext);
const { allowAll } = useACLRoleContext();
return () => {
if (allowAll) {
return;
}
ctx.refresh();
};
};
export const useACLContext = () => {
return useContext(ACLContext);
};
export const ACLActionParamsContext = createContext<any>({});
export const useACLRolesCheck = () => {
const ctx = useContext(ACLContext);
const data = ctx?.data?.data;
const getActionAlias = (actionPath: string) => {
const actionName = actionPath.split(':').pop();
return data?.actionAlias?.[actionName] || actionName;
};
return {
data,
getActionAlias,
inResources: (resourceName: string) => {
return data?.resources?.includes?.(resourceName);
},
getResourceActionParams: (actionPath: string) => {
const [resourceName] = actionPath.split(':');
const actionAlias = getActionAlias(actionPath);
return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath];
},
getStrategyActionParams: (actionPath: string) => {
const actionAlias = getActionAlias(actionPath);
const strategyAction = data?.strategy?.actions?.find((action) => {
const [value] = action.split(':');
return value === actionAlias;
});
return strategyAction ? {} : null;
},
};
};
const getIgnoreScope = (options: any = {}) => {
const { schema, recordPkValue } = options;
let ignoreScope = false;
if (options.ignoreScope) {
ignoreScope = true;
}
if (schema?.['x-acl-ignore-scope']) {
ignoreScope = true;
}
if (schema?.['x-acl-action-props']?.['skipScopeCheck']) {
ignoreScope = true;
}
if (!recordPkValue) {
ignoreScope = true;
}
return ignoreScope;
};
const useAllowedActions = () => {
const result = useBlockRequestContext() || { service: useResourceActionContext() };
return result?.allowedActions ?? result?.service?.data?.meta?.allowedActions;
};
const useResourceName = () => {
const result = useBlockRequestContext() || { service: useResourceActionContext() };
return result?.props?.resource || result?.service?.defaultRequest?.resource;
};
export function useACLRoleContext() {
const { data, getActionAlias, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck();
const allowedActions = useAllowedActions();
const verifyScope = (actionName: string, recordPkValue: any) => {
const actionAlias = getActionAlias(actionName);
if (!Array.isArray(allowedActions?.[actionAlias])) {
return null;
}
return allowedActions[actionAlias].includes(recordPkValue);
};
return {
...data,
parseAction: (actionPath: string, options: any = {}) => {
const [resourceName, actionName] = actionPath.split(':');
if (!getIgnoreScope(options)) {
const r = verifyScope(actionName, options.recordPkValue);
if (r !== null) {
return r ? {} : null;
}
}
if (data?.allowAll) {
return {};
}
if (inResources(resourceName)) {
return getResourceActionParams(actionPath);
}
return getStrategyActionParams(actionPath);
},
};
}
export const ACLCollectionProvider = (props) => {
const { allowAll, parseAction } = useACLRoleContext();
const schema = useFieldSchema();
if (allowAll) {
return props.children;
}
const actionPath = schema?.['x-acl-action'];
if (!actionPath) {
return props.children;
}
const params = parseAction(actionPath, { schema });
if (!params) {
return null;
}
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
};
export const useACLActionParamsContext = () => {
return useContext(ACLActionParamsContext);
};
export const useRecordPkValue = () => {
const { getPrimaryKey } = useCollection();
const record = useRecord();
const primaryKey = getPrimaryKey();
return record?.[primaryKey];
};
export const ACLActionProvider = (props) => {
const recordPkValue = useRecordPkValue();
const resource = useResourceName();
const { parseAction } = useACLRoleContext();
const schema = useFieldSchema();
let actionPath = schema['x-acl-action'];
if (!actionPath && resource && schema['x-action']) {
actionPath = `${resource}:${schema['x-action']}`;
}
if (!actionPath?.includes(':')) {
actionPath = `${resource}:${actionPath}`;
}
if (!actionPath) {
return <>{props.children}</>;
}
const params = parseAction(actionPath, { schema, recordPkValue });
if (!params) {
return null;
}
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
};
export const useACLFieldWhitelist = () => {
const params = useContext(ACLActionParamsContext);
const whitelist = []
.concat(params?.whitelist || [])
.concat(params?.fields || [])
.concat(params?.appends || []);
return {
whitelist,
schemaInWhitelist(fieldSchema: Schema) {
if (whitelist.length === 0) {
return true;
}
if (!fieldSchema) {
return true;
}
if (!fieldSchema['x-collection-field']) {
return true;
}
const [key1, key2] = fieldSchema['x-collection-field'].split('.');
return whitelist?.includes(key2 || key1);
},
};
};
export const ACLCollectionFieldProvider = (props) => {
const fieldSchema = useFieldSchema();
const field = useField<Field>();
const { allowAll } = useACLRoleContext();
if (allowAll) {
return <>{props.children}</>;
}
if (!fieldSchema['x-collection-field']) {
return <>{props.children}</>;
}
const { whitelist } = useACLFieldWhitelist();
const allowed = whitelist.length > 0 ? whitelist.includes(fieldSchema.name) : true;
useEffect(() => {
if (!allowed) {
field.required = false;
field.display = 'hidden';
}
}, [allowed]);
if (!allowed) {
return null;
}
return <>{props.children}</>;
};
export const ACLMenuItemProvider = (props) => {
const { allowAll, allowMenuItemIds = [], snippets } = useACLRoleContext();
const fieldSchema = useFieldSchema();
if (allowAll || snippets.includes('ui.*')) {
return <>{props.children}</>;
}
if (!fieldSchema['x-uid']) {
return <>{props.children}</>;
}
if (allowMenuItemIds.includes(fieldSchema['x-uid'])) {
return <>{props.children}</>;
}
return null;
};