feat: acl provider (#279)
* feat: acl provider * feat: menu server hook * feat: improve code * fix: fields filter
This commit is contained in:
parent
e109e2a37e
commit
f791d43716
@ -1,6 +1,6 @@
|
|||||||
import { ACLResource } from './acl-resource';
|
|
||||||
import { AvailableStrategyOptions } from './acl-available-strategy';
|
|
||||||
import { ACL, DefineOptions } from './acl';
|
import { ACL, DefineOptions } from './acl';
|
||||||
|
import { AvailableStrategyOptions } from './acl-available-strategy';
|
||||||
|
import { ACLResource } from './acl-resource';
|
||||||
|
|
||||||
export interface RoleActionParams {
|
export interface RoleActionParams {
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
|
@ -241,7 +241,7 @@ export class ACL extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!permission.can || typeof permission.can !== 'object') {
|
if (!permission.can || typeof permission.can !== 'object') {
|
||||||
ctx.throw(403, 'no permission');
|
ctx.throw(403, 'No permissions');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ACLProvider,
|
||||||
ACLShortcut,
|
ACLShortcut,
|
||||||
AdminLayout,
|
AdminLayout,
|
||||||
AntdConfigProvider,
|
AntdConfigProvider,
|
||||||
@ -99,6 +100,7 @@ const providers = [
|
|||||||
],
|
],
|
||||||
BlockSchemaComponentProvider,
|
BlockSchemaComponentProvider,
|
||||||
AntdSchemaComponentProvider,
|
AntdSchemaComponentProvider,
|
||||||
|
ACLProvider,
|
||||||
ChinaRegionProvider,
|
ChinaRegionProvider,
|
||||||
WorkflowRouteProvider,
|
WorkflowRouteProvider,
|
||||||
[DocumentTitleProvider, { addonAfter: 'NocoBase' }],
|
[DocumentTitleProvider, { addonAfter: 'NocoBase' }],
|
||||||
|
@ -1,13 +1,103 @@
|
|||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
import { useRequest } from '../api-client';
|
||||||
|
import { useCollection } from '../collection-manager';
|
||||||
|
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||||
|
|
||||||
export const ACLContext = createContext(null);
|
export const ACLContext = createContext(null);
|
||||||
|
|
||||||
export function ACLProvider() {
|
export const ACLProvider = (props) => {
|
||||||
return (
|
return (
|
||||||
<ACLContext.Provider value={{}}>
|
<SchemaComponentOptions
|
||||||
|
components={{ ACLCollectionFieldProvider, ACLActionProvider, ACLMenuItemProvider, ACLCollectionProvider }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
</ACLContext.Provider>
|
export const ACLRolesCheckProvider = (props) => {
|
||||||
)
|
const { setDesignable } = useDesignable();
|
||||||
|
const result = useRequest(
|
||||||
|
{
|
||||||
|
url: 'roles:check',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
if (!data?.data?.allowConfigure) {
|
||||||
|
setDesignable(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
return <Redirect to={'/signin'} />;
|
||||||
|
}
|
||||||
|
return <ACLContext.Provider value={result}>{props.children}</ACLContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useACLRoleContext = () => {
|
||||||
|
const ctx = useContext(ACLContext);
|
||||||
|
const data = ctx.data?.data;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
getActionParams(path) {
|
||||||
|
return data?.actions?.[path];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACLAllowConfigure = (props) => {
|
||||||
|
const { allowAll, allowConfigure } = useACLRoleContext();
|
||||||
|
if (allowAll || allowConfigure) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACLCollectionProvider = (props) => {
|
||||||
|
const { name } = useCollection();
|
||||||
|
|
||||||
|
return <>{props.children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACLActionProvider = (props) => {
|
||||||
|
const { name } = useCollection();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { allowAll, getActionParams } = useACLRoleContext();
|
||||||
|
if (!name || allowAll) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
const actionName = fieldSchema['x-action'];
|
||||||
|
const params = getActionParams([`${name}:${actionName}`]);
|
||||||
|
if (!params) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <>{props.children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACLCollectionFieldProvider = (props) => {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACLMenuItemProvider = (props) => {
|
||||||
|
const { allowAll, allowMenuItemIds = [] } = useACLRoleContext();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
if (allowAll) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
if (!fieldSchema['x-uid']) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
if (allowMenuItemIds.includes(fieldSchema['x-uid'])) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default ACLProvider;
|
export default ACLProvider;
|
||||||
|
@ -2,6 +2,7 @@ import { FormItem, FormLayout } from '@formily/antd';
|
|||||||
import { ArrayField } from '@formily/core';
|
import { ArrayField } from '@formily/core';
|
||||||
import { connect, useField, useForm } from '@formily/react';
|
import { connect, useField, useForm } from '@formily/react';
|
||||||
import { Checkbox, Table, Tag } from 'antd';
|
import { Checkbox, Table, Tag } from 'antd';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import React, { createContext } from 'react';
|
import React, { createContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCollectionManager, useCompile, useRecord } from '../..';
|
import { useCollectionManager, useCompile, useRecord } from '../..';
|
||||||
@ -13,6 +14,7 @@ const toActionMap = (arr: any[]) => {
|
|||||||
arr?.forEach?.((action) => {
|
arr?.forEach?.((action) => {
|
||||||
if (action.name) {
|
if (action.name) {
|
||||||
obj[action.name] = action;
|
obj[action.name] = action;
|
||||||
|
obj[action.name]['scope'] = isEmpty(action.scope) ? null : action.scope;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return obj;
|
return obj;
|
||||||
@ -21,7 +23,16 @@ const toActionMap = (arr: any[]) => {
|
|||||||
export const RoleResourceCollectionContext = createContext<any>({});
|
export const RoleResourceCollectionContext = createContext<any>({});
|
||||||
|
|
||||||
export const RolesResourcesActions = connect((props) => {
|
export const RolesResourcesActions = connect((props) => {
|
||||||
const { onChange } = props;
|
// const { onChange } = props;
|
||||||
|
const onChange = (values) => {
|
||||||
|
const items = values.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
scope: isEmpty(item.scope) ? null : item.scope,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
props.onChange(items);
|
||||||
|
};
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const roleCollection = useRecord();
|
const roleCollection = useRecord();
|
||||||
const availableActions = useAvailableActions();
|
const availableActions = useAvailableActions();
|
||||||
@ -39,7 +50,7 @@ export const RolesResourcesActions = connect((props) => {
|
|||||||
return action?.fields?.includes(fieldName);
|
return action?.fields?.includes(fieldName);
|
||||||
};
|
};
|
||||||
const availableActionsWithFields = availableActions.filter((action) => action.allowConfigureFields);
|
const availableActionsWithFields = availableActions.filter((action) => action.allowConfigureFields);
|
||||||
const fieldPermissions = collection?.fields?.map((field) => {
|
const fieldPermissions = collection?.fields?.filter(field => field.interface)?.map((field) => {
|
||||||
const permission = { ...field };
|
const permission = { ...field };
|
||||||
for (const action of availableActionsWithFields) {
|
for (const action of availableActionsWithFields) {
|
||||||
permission[action.name] = inAction(action.name, field.name);
|
permission[action.name] = inAction(action.name, field.name);
|
||||||
@ -52,7 +63,7 @@ export const RolesResourcesActions = connect((props) => {
|
|||||||
} else {
|
} else {
|
||||||
actionMap[actionName] = {
|
actionMap[actionName] = {
|
||||||
name: actionName,
|
name: actionName,
|
||||||
fields: collection?.fields?.map?.((item) => item.name),
|
fields: collection?.fields?.filter(field => field.interface)?.map?.((item) => item.name),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
onChange(Object.values(actionMap));
|
onChange(Object.values(actionMap));
|
||||||
@ -60,12 +71,15 @@ export const RolesResourcesActions = connect((props) => {
|
|||||||
const setScope = (actionName, scope) => {
|
const setScope = (actionName, scope) => {
|
||||||
if (!actionMap[actionName]) {
|
if (!actionMap[actionName]) {
|
||||||
toggleAction(actionName);
|
toggleAction(actionName);
|
||||||
}
|
|
||||||
actionMap[actionName]['scope'] = scope;
|
actionMap[actionName]['scope'] = scope;
|
||||||
|
} else {
|
||||||
|
actionMap[actionName]['scope'] = scope;
|
||||||
|
onChange(Object.values(actionMap));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const allChecked = {};
|
const allChecked = {};
|
||||||
for (const action of availableActionsWithFields) {
|
for (const action of availableActionsWithFields) {
|
||||||
allChecked[action.name] = collection?.fields?.length === actionMap?.[action.name]?.fields?.length;
|
allChecked[action.name] = collection?.fields?.filter(field => field.interface)?.length === actionMap?.[action.name]?.fields?.length;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -122,8 +136,10 @@ export const RolesResourcesActions = connect((props) => {
|
|||||||
let scope = null;
|
let scope = null;
|
||||||
if (actionMap[item.name]) {
|
if (actionMap[item.name]) {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
|
if (!item.onNewRecord) {
|
||||||
scope = actionMap[item.name]['scope'];
|
scope = actionMap[item.name]['scope'];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
enabled,
|
enabled,
|
||||||
|
@ -4,7 +4,7 @@ import { useRequest } from 'ahooks';
|
|||||||
import template from 'lodash/template';
|
import template from 'lodash/template';
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { TableFieldResource, useAPIClient, useRecord } from '../';
|
import { ACLCollectionProvider, TableFieldResource, useAPIClient, useRecord } from '../';
|
||||||
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
|
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
|
||||||
import { useRecordIndex } from '../record-provider';
|
import { useRecordIndex } from '../record-provider';
|
||||||
|
|
||||||
@ -89,7 +89,9 @@ export const useResourceAction = (props, opts = {}) => {
|
|||||||
const MaybeCollectionProvider = (props) => {
|
const MaybeCollectionProvider = (props) => {
|
||||||
const { collection } = props;
|
const { collection } = props;
|
||||||
return collection ? (
|
return collection ? (
|
||||||
<CollectionProvider collection={collection}>{props.children}</CollectionProvider>
|
<CollectionProvider collection={collection}>
|
||||||
|
<ACLCollectionProvider>{props.children}</ACLCollectionProvider>
|
||||||
|
</CollectionProvider>
|
||||||
) : (
|
) : (
|
||||||
<>{props.children}</>
|
<>{props.children}</>
|
||||||
);
|
);
|
||||||
@ -183,5 +185,9 @@ export const RecordLink = (props) => {
|
|||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
const { title, to, ...others } = props;
|
const { title, to, ...others } = props;
|
||||||
const compiled = template(to || '');
|
const compiled = template(to || '');
|
||||||
return <Link {...others} to={compiled({ record: record || {} })}>{field.title}</Link>;
|
return (
|
||||||
|
<Link {...others} to={compiled({ record: record || {} })}>
|
||||||
|
{field.title}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { Layout } from 'antd';
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
ACLAllowConfigure, ACLRolesCheckProvider,
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
CurrentUserProvider,
|
CurrentUserProvider,
|
||||||
findByUid,
|
findByUid,
|
||||||
@ -97,6 +98,7 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: 'absolute', top: 0, right: 0 }}>
|
<div style={{ position: 'absolute', top: 0, right: 0 }}>
|
||||||
|
<ACLAllowConfigure>
|
||||||
<PluginManager.Toolbar
|
<PluginManager.Toolbar
|
||||||
items={[
|
items={[
|
||||||
{ component: 'DesignableSwitch', pin: true },
|
{ component: 'DesignableSwitch', pin: true },
|
||||||
@ -107,6 +109,7 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
{ component: 'SystemSettingsShortcut' },
|
{ component: 'SystemSettingsShortcut' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</ACLAllowConfigure>
|
||||||
<CurrentUser />
|
<CurrentUser />
|
||||||
</div>
|
</div>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
@ -137,13 +140,15 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
|
|
||||||
export const AdminLayout = (props) => {
|
export const AdminLayout = (props) => {
|
||||||
return (
|
return (
|
||||||
|
<CurrentUserProvider>
|
||||||
<RemoteSchemaTemplateManagerProvider>
|
<RemoteSchemaTemplateManagerProvider>
|
||||||
<RemoteCollectionManagerProvider>
|
<RemoteCollectionManagerProvider>
|
||||||
<CurrentUserProvider>
|
<ACLRolesCheckProvider>
|
||||||
<InternalAdminLayout {...props} />
|
<InternalAdminLayout {...props} />
|
||||||
</CurrentUserProvider>
|
</ACLRolesCheckProvider>
|
||||||
</RemoteCollectionManagerProvider>
|
</RemoteCollectionManagerProvider>
|
||||||
</RemoteSchemaTemplateManagerProvider>
|
</RemoteSchemaTemplateManagerProvider>
|
||||||
|
</CurrentUserProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,9 +78,16 @@ const InsertMenuItems = (props) => {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
'x-component': 'Menu.SubMenu',
|
'x-component': 'Menu.SubMenu',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
icon,
|
icon,
|
||||||
},
|
},
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -112,9 +119,16 @@ const InsertMenuItems = (props) => {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
'x-component': 'Menu.Item',
|
'x-component': 'Menu.Item',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
icon,
|
icon,
|
||||||
},
|
},
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
],
|
||||||
properties: {
|
properties: {
|
||||||
page: {
|
page: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -165,10 +179,17 @@ const InsertMenuItems = (props) => {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
'x-component': 'Menu.URL',
|
'x-component': 'Menu.URL',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
icon,
|
icon,
|
||||||
href,
|
href,
|
||||||
},
|
},
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -79,9 +79,16 @@ export const GroupItem = itemWrap((props) => {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
'x-component': 'Menu.SubMenu',
|
'x-component': 'Menu.SubMenu',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
icon,
|
icon,
|
||||||
},
|
},
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -128,9 +135,16 @@ export const PageMenuItem = itemWrap((props) => {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
'x-component': 'Menu.Item',
|
'x-component': 'Menu.Item',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
icon,
|
icon,
|
||||||
},
|
},
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
],
|
||||||
properties: {
|
properties: {
|
||||||
page: {
|
page: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -197,10 +211,17 @@ export const LinkMenuItem = itemWrap((props) => {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
'x-component': 'Menu.URL',
|
'x-component': 'Menu.URL',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
icon,
|
icon,
|
||||||
href,
|
href,
|
||||||
},
|
},
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -4,6 +4,7 @@ import React, { createContext, useContext, useState } from 'react';
|
|||||||
import { useTableSelectorProps as useTsp } from '../../../block-provider/TableSelectorProvider';
|
import { useTableSelectorProps as useTsp } from '../../../block-provider/TableSelectorProvider';
|
||||||
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
||||||
import { FormProvider, SchemaComponentOptions } from '../../core';
|
import { FormProvider, SchemaComponentOptions } from '../../core';
|
||||||
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionContext, useActionContext } from '../action';
|
import { ActionContext, useActionContext } from '../action';
|
||||||
import { useFieldNames } from './useFieldNames';
|
import { useFieldNames } from './useFieldNames';
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ const useAssociation = (props) => {
|
|||||||
return association;
|
return association;
|
||||||
}
|
}
|
||||||
return getField(fieldSchema.name);
|
return getField(fieldSchema.name);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const InputRecordPicker: React.FC<any> = (props) => {
|
export const InputRecordPicker: React.FC<any> = (props) => {
|
||||||
const { value, multiple, onChange } = props;
|
const { value, multiple, onChange } = props;
|
||||||
@ -57,7 +58,14 @@ export const InputRecordPicker: React.FC<any> = (props) => {
|
|||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const collectionField = useAssociation(props);
|
const collectionField = useAssociation(props);
|
||||||
const options = Array.isArray(value) ? value : value ? [value] : [];
|
const compile = useCompile();
|
||||||
|
const options = (Array.isArray(value) ? value : value ? [value] : []).map(option => {
|
||||||
|
const label = option[fieldNames.label];
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
[fieldNames.label]: compile(label),
|
||||||
|
};
|
||||||
|
});
|
||||||
const [selectedRows, setSelectedRows] = useState(options);
|
const [selectedRows, setSelectedRows] = useState(options);
|
||||||
const values = options?.map((option) => option[fieldNames.value]);
|
const values = options?.map((option) => option[fieldNames.value]);
|
||||||
return (
|
return (
|
||||||
|
@ -11,6 +11,7 @@ export interface RemoteSchemaComponentProps {
|
|||||||
scope?: any;
|
scope?: any;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
onSuccess?: any;
|
onSuccess?: any;
|
||||||
|
components?: any;
|
||||||
schemaTransform?: (schema: Schema) => Schema;
|
schemaTransform?: (schema: Schema) => Schema;
|
||||||
render?: any;
|
render?: any;
|
||||||
hidden?: any;
|
hidden?: any;
|
||||||
@ -20,7 +21,7 @@ export interface RemoteSchemaComponentProps {
|
|||||||
const defaultTransform = (s: Schema) => s;
|
const defaultTransform = (s: Schema) => s;
|
||||||
|
|
||||||
const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) => {
|
const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) => {
|
||||||
const { onlyRenderProperties, hidden, scope, uid, onSuccess, schemaTransform = defaultTransform } = props;
|
const { onlyRenderProperties, hidden, scope, uid, components, onSuccess, schemaTransform = defaultTransform } = props;
|
||||||
const { reset } = useSchemaComponentContext();
|
const { reset } = useSchemaComponentContext();
|
||||||
const conf = {
|
const conf = {
|
||||||
url: `/uiSchemas:${onlyRenderProperties ? 'getProperties' : 'getJsonSchema'}/${uid}`,
|
url: `/uiSchemas:${onlyRenderProperties ? 'getProperties' : 'getJsonSchema'}/${uid}`,
|
||||||
@ -41,7 +42,7 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FormProvider form={form}>
|
<FormProvider form={form}>
|
||||||
<SchemaComponent memoized scope={scope} schema={schemaTransform(data?.data || {})} />
|
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(data?.data || {})} />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -67,6 +67,7 @@ export const CalendarActionInitializers = {
|
|||||||
component: 'CreateActionInitializer',
|
component: 'CreateActionInitializer',
|
||||||
schema: {
|
schema: {
|
||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -44,6 +44,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
schema: {
|
schema: {
|
||||||
'x-component': 'Action.Link',
|
'x-component': 'Action.Link',
|
||||||
'x-action': 'view',
|
'x-action': 'view',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -53,6 +54,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
schema: {
|
schema: {
|
||||||
'x-component': 'Action.Link',
|
'x-component': 'Action.Link',
|
||||||
'x-action': 'update',
|
'x-action': 'update',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -62,6 +64,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
schema: {
|
schema: {
|
||||||
'x-component': 'Action.Link',
|
'x-component': 'Action.Link',
|
||||||
'x-action': 'destroy',
|
'x-action': 'destroy',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -24,6 +24,7 @@ export const TableActionInitializers = {
|
|||||||
component: 'CreateActionInitializer',
|
component: 'CreateActionInitializer',
|
||||||
schema: {
|
schema: {
|
||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -32,6 +33,7 @@ export const TableActionInitializers = {
|
|||||||
component: 'BulkDestroyActionInitializer',
|
component: 'BulkDestroyActionInitializer',
|
||||||
schema: {
|
schema: {
|
||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -2,14 +2,22 @@ import { useCookieState } from 'ahooks';
|
|||||||
import { Menu, Select } from 'antd';
|
import { Menu, Select } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useAPIClient } from '../api-client';
|
||||||
import { useCurrentUserContext } from './CurrentUserProvider';
|
import { useCurrentUserContext } from './CurrentUserProvider';
|
||||||
|
|
||||||
const useCurrentRoles = () => {
|
const useCurrentRoles = () => {
|
||||||
const { data } = useCurrentUserContext();
|
const { data } = useCurrentUserContext();
|
||||||
return data?.data?.roles || [];
|
return [
|
||||||
|
...(data?.data?.roles || []),
|
||||||
|
{
|
||||||
|
title: 'Anonymous',
|
||||||
|
name: 'anonymous',
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SwitchRole = () => {
|
export const SwitchRole = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
const roles = useCurrentRoles();
|
const roles = useCurrentRoles();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [roleName, setRoleName] = useCookieState('currentRoleName', {
|
const [roleName, setRoleName] = useCookieState('currentRoleName', {
|
||||||
@ -30,8 +38,9 @@ export const SwitchRole = () => {
|
|||||||
}}
|
}}
|
||||||
options={roles}
|
options={roles}
|
||||||
value={roleName}
|
value={roleName}
|
||||||
onChange={(roleName) => {
|
onChange={async (roleName) => {
|
||||||
setRoleName(roleName);
|
setRoleName(roleName);
|
||||||
|
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -9,9 +9,10 @@ export async function checkAction(ctx, next) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
role: ctx.app.acl.getRole(currentRole).toJSON(),
|
...ctx.app.acl.getRole(currentRole).toJSON(),
|
||||||
|
allowAll: currentRole === 'root',
|
||||||
allowConfigure: roleInstance.get('allowConfigure'),
|
allowConfigure: roleInstance.get('allowConfigure'),
|
||||||
roleMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')),
|
allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,11 @@ export default {
|
|||||||
name: 'default',
|
name: 'default',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'hidden',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'allowConfigure',
|
name: 'allowConfigure',
|
||||||
|
@ -245,9 +245,14 @@ export class PluginACL extends Plugin {
|
|||||||
if (plugin.constructor.name !== 'UsersPlugin') {
|
if (plugin.constructor.name !== 'UsersPlugin') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const repository = this.app.db.getRepository('roles');
|
const roles = this.app.db.getRepository('roles');
|
||||||
await repository.createMany({
|
await roles.createMany({
|
||||||
records: [
|
records: [
|
||||||
|
{
|
||||||
|
name: 'root',
|
||||||
|
title: 'Root',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
title: 'Admin',
|
title: 'Admin',
|
||||||
@ -263,8 +268,42 @@ export class PluginACL extends Plugin {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
const rolesResourcesScopes = this.app.db.getRepository('rolesResourcesScopes');
|
||||||
|
await rolesResourcesScopes.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: '{{t("All records")}}',
|
||||||
|
scope: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '{{t("Own records")}}',
|
||||||
|
scope: {
|
||||||
|
createdById: '{{ ctx.state.currentUser.id }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
this.app.acl.skip('roles.menuUiSchemas', 'set', 'logged-in');
|
||||||
|
this.app.acl.skip('roles.menuUiSchemas', 'toggle', 'logged-in');
|
||||||
|
this.app.acl.skip('roles.menuUiSchemas', 'list', 'logged-in');
|
||||||
this.app.acl.skip('roles', 'check', 'logged-in');
|
this.app.acl.skip('roles', 'check', 'logged-in');
|
||||||
|
this.app.acl.skip('*', '*', (ctx) => {
|
||||||
|
return ctx.state.currentRole === 'root';
|
||||||
|
});
|
||||||
|
|
||||||
|
// root role
|
||||||
|
this.app.resourcer.use(async (ctx, next) => {
|
||||||
|
const { actionName, resourceName } = ctx.action.params;
|
||||||
|
if (actionName === 'list' && resourceName === 'roles') {
|
||||||
|
ctx.action.mergeParams({
|
||||||
|
filter: {
|
||||||
|
'name.$ne': 'root',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
async install() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { bindMenuToRole } from './bind-menu-to-role';
|
||||||
import { hookFactory } from './factory';
|
import { hookFactory } from './factory';
|
||||||
import { removeSchema } from './remove-schema';
|
|
||||||
import { bindMenuToRole } from './bind-menu-to-row';
|
|
||||||
import { removeParentsIfNoChildren } from './remove-parents-if-no-children';
|
import { removeParentsIfNoChildren } from './remove-parents-if-no-children';
|
||||||
|
import { removeSchema } from './remove-schema';
|
||||||
|
|
||||||
const hooks = [
|
const hooks = [
|
||||||
hookFactory('onCollectionDestroy', 'removeSchema', removeSchema),
|
hookFactory('onCollectionDestroy', 'removeSchema', removeSchema),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Database, { BelongsToManyRepository } from '@nocobase/database';
|
import Database, { BelongsToManyRepository } from '@nocobase/database';
|
||||||
import PluginACL from '@nocobase/plugin-acl';
|
import PluginACL from '@nocobase/plugin-acl';
|
||||||
|
import UsersPlugin from '@nocobase/plugin-users';
|
||||||
import { MockServer, mockServer } from '@nocobase/test';
|
import { MockServer, mockServer } from '@nocobase/test';
|
||||||
import { userPluginConfig } from './utils';
|
import { userPluginConfig } from './utils';
|
||||||
import UsersPlugin from '@nocobase/plugin-users';
|
|
||||||
|
|
||||||
describe('role', () => {
|
describe('role', () => {
|
||||||
let api: MockServer;
|
let api: MockServer;
|
||||||
@ -108,7 +108,7 @@ describe('role', () => {
|
|||||||
.agent()
|
.agent()
|
||||||
.post('/users:setDefaultRole')
|
.post('/users:setDefaultRole')
|
||||||
.send({
|
.send({
|
||||||
defaultRole: 'test2',
|
roleName: 'test2',
|
||||||
})
|
})
|
||||||
.set({
|
.set({
|
||||||
Authorization: `Bearer ${userToken}`,
|
Authorization: `Bearer ${userToken}`,
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { Context, Next } from '@nocobase/actions';
|
import { Context, Next } from '@nocobase/actions';
|
||||||
import { PasswordField } from '@nocobase/database';
|
import { PasswordField } from '@nocobase/database';
|
||||||
import UsersPlugin from '../server';
|
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
export async function check(ctx: Context, next: Next) {
|
export async function check(ctx: Context, next: Next) {
|
||||||
@ -154,9 +152,14 @@ export async function changePassword(ctx: Context, next: Next) {
|
|||||||
|
|
||||||
export async function setDefaultRole(ctx: Context, next: Next) {
|
export async function setDefaultRole(ctx: Context, next: Next) {
|
||||||
const {
|
const {
|
||||||
values: { defaultRole },
|
values: { roleName },
|
||||||
} = ctx.action.params;
|
} = ctx.action.params;
|
||||||
|
|
||||||
|
if (roleName == 'anonymous') {
|
||||||
|
ctx.body = 'ok';
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const currentUserId = ctx.state.currentUser.id;
|
const currentUserId = ctx.state.currentUser.id;
|
||||||
|
|
||||||
await ctx.db.getRepository('rolesUsers').update({
|
await ctx.db.getRepository('rolesUsers').update({
|
||||||
@ -171,7 +174,7 @@ export async function setDefaultRole(ctx: Context, next: Next) {
|
|||||||
await ctx.db.getRepository('rolesUsers').update({
|
await ctx.db.getRepository('rolesUsers').update({
|
||||||
filter: {
|
filter: {
|
||||||
userId: currentUserId,
|
userId: currentUserId,
|
||||||
roleName: defaultRole,
|
roleName,
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -13,6 +13,13 @@ export function parseToken(options?: { plugin: UsersPlugin }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentRole(ctx, user) {
|
function setCurrentRole(ctx, user) {
|
||||||
|
const roleName = ctx.get('X-Role');
|
||||||
|
|
||||||
|
if (roleName === 'anonymous') {
|
||||||
|
ctx.state.currentRole = roleName;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userRoles = user.get('roles');
|
const userRoles = user.get('roles');
|
||||||
let userRole;
|
let userRole;
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
|||||||
nickname: adminNickname,
|
nickname: adminNickname,
|
||||||
email: adminEmail,
|
email: adminEmail,
|
||||||
password: adminPassword,
|
password: adminPassword,
|
||||||
roles: ['admin'],
|
roles: ['root', 'admin'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user