mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 05:25:52 +00:00
feat: route permissions (#58)
* feat: routes permissions * fix: try to fix roles.pages action * 多态关联 * bugfix * fix: auto generate when option value is undefined or null * feat: add pages' permissions saving/listing * feat: add permission filter for getRoutes * roles description * feat: get root permissions all true and create user with default role * feat: roles.collections list output with permission * add permissions description * fix: add context to parseApiJson * fix: typo * 小细节补充 Co-authored-by: mytharcher <mytharcher@gmail.com>
This commit is contained in:
parent
bd756a6a5c
commit
301229ef88
@ -1,3 +1,11 @@
|
||||
// @ts-ignore
|
||||
global.sync = {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
};
|
||||
|
||||
import api from '../app';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
@ -135,6 +143,9 @@ const data = [
|
||||
const tables = database.getTables([]);
|
||||
for (let table of tables) {
|
||||
console.log(table.getName());
|
||||
if (table.getName() === 'roles') {
|
||||
console.log('roles', table.getOptions())
|
||||
}
|
||||
await Collection.import(table.getOptions(), { update: true, migrate: false });
|
||||
}
|
||||
await Page.import(data);
|
||||
@ -182,6 +193,7 @@ const data = [
|
||||
const roles = await Role.bulkCreate([
|
||||
{ title: '系统开发组', type: -1 },
|
||||
{ title: '匿名用户组', type: 0 },
|
||||
{ title: '普通用户组', default: true },
|
||||
]);
|
||||
await roles[0].updateAssociations({
|
||||
users: user
|
||||
|
@ -8,6 +8,8 @@ import get from 'lodash/get';
|
||||
import './style.less';
|
||||
import Field from '../Field';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { Checkbox, message } from 'antd';
|
||||
import api from '@/api-client';
|
||||
|
||||
export const SortableItem = sortableElement(props => <tr {...props} />);
|
||||
export const SortableContainer = sortableContainer(props => <tbody {...props} />);
|
||||
@ -58,11 +60,38 @@ export const components = ({data = {}, rowKey, mutate, onMoved, isFieldComponent
|
||||
};
|
||||
};
|
||||
|
||||
export function fields2columns(fields) {
|
||||
export function fields2columns(fields, ctx: any = {}) {
|
||||
const columns: any[] = fields.map(item => {
|
||||
const field = cloneDeep(item);
|
||||
field.render = (value, record) => field.interface === 'sort' ? <DragHandle/> : <Field data={record} viewType={'table'} schema={field} value={value}/>;
|
||||
field.className = `${field.className||''} noco-field-${field.interface}`;
|
||||
if (field.editable && field.interface === 'boolean') {
|
||||
field.title = (
|
||||
<span>
|
||||
<Checkbox onChange={async (e) => {
|
||||
try {
|
||||
await api.resource(field.resource).update({
|
||||
associatedKey: ctx.associatedKey,
|
||||
// resourceKey: data.id,
|
||||
// tableName: data.tableName||'pages',
|
||||
values: {
|
||||
accessible: e.target.checked,
|
||||
},
|
||||
});
|
||||
message.success('保存成功');
|
||||
if (ctx.refresh) {
|
||||
ctx.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
}
|
||||
|
||||
}}/>
|
||||
{' '}
|
||||
{field.title}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return {
|
||||
...field,
|
||||
...(field.component||{}),
|
||||
|
@ -154,7 +154,7 @@ export function Table(props: TableProps) {
|
||||
indicator: icon,
|
||||
// className: 'spinning--absolute m32',
|
||||
}}
|
||||
columns={fields2columns(fields)}
|
||||
columns={fields2columns(fields, {associatedKey, refresh})}
|
||||
dataSource={data?.list||(data as any)}
|
||||
onChange={(pagination, filters, sorter, extra) => {
|
||||
run({...params[0], sorter});
|
||||
|
@ -68,6 +68,8 @@ export default class Database {
|
||||
|
||||
protected hooks = {};
|
||||
|
||||
protected extTableOptions = new Map<string, any>();
|
||||
|
||||
constructor(options: DatabaseOptions) {
|
||||
this.options = options;
|
||||
this.sequelize = new Sequelize(options);
|
||||
@ -94,10 +96,21 @@ export default class Database {
|
||||
files.forEach((file: string) => {
|
||||
const result = requireModule(file);
|
||||
if (result instanceof Extend) {
|
||||
const table = this.extend(result.tableOptions, result.mergeOptions);
|
||||
tables.set(table.getName(), table);
|
||||
// 如果还没初始化,extend 的先暂存起来,后续处理
|
||||
if (!this.tables.has(result.tableOptions.name)) {
|
||||
this.extTableOptions.set(result.tableOptions.name, result);
|
||||
} else {
|
||||
const table = this.extend(result.tableOptions, result.mergeOptions);
|
||||
tables.set(table.getName(), table);
|
||||
}
|
||||
} else {
|
||||
const table = this.extend(typeof result === 'function' ? result(this) : result);
|
||||
let table = this.extend(typeof result === 'function' ? result(this) : result);
|
||||
// 如果有未处理的 extend 取回来合并
|
||||
if (this.extTableOptions.has(table.getName())) {
|
||||
const result = this.extTableOptions.get(table.getName());
|
||||
table = this.extend(result.tableOptions, result.mergeOptions);
|
||||
this.extTableOptions.delete(table.getName());
|
||||
}
|
||||
tables.set(table.getName(), table);
|
||||
}
|
||||
});
|
||||
|
@ -18,7 +18,7 @@ export default async function (model: FieldModel, options) {
|
||||
const dataSource = model.get('dataSource');
|
||||
if (Array.isArray(dataSource)) {
|
||||
model.set('dataSource', dataSource.map(item => {
|
||||
if (!item.value) {
|
||||
if (item.value === null || typeof item.value === 'undefined') {
|
||||
item.value = generateValueName();
|
||||
}
|
||||
return {...item};
|
||||
|
@ -40,6 +40,21 @@ export default async function getRoutes(ctx, next) {
|
||||
const database: Database = ctx.database;
|
||||
const Page = database.getModel('pages');
|
||||
const Collection = database.getModel('collections');
|
||||
const RoutePermission = database.getModel('routes_permissions');
|
||||
const roles = await ctx.ac.getRoles();
|
||||
// TODO(optimize): isRoot 的判断需要在内部完成,尽量不要交给调用者
|
||||
const isRoot = ctx.ac.constructor.isRoot(roles);
|
||||
const routesPermissionsMap = new Map();
|
||||
if (!isRoot) {
|
||||
const routesPermissions = await RoutePermission.findAll({
|
||||
where: {
|
||||
role_id: roles.map(({ id }) => id)
|
||||
}
|
||||
});
|
||||
routesPermissions.forEach(permission => {
|
||||
routesPermissionsMap.set(`${permission.routable_type}:${permission.routable_id}`, permission);
|
||||
});
|
||||
}
|
||||
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
|
||||
filter: {
|
||||
},
|
||||
@ -52,6 +67,15 @@ export default async function getRoutes(ctx, next) {
|
||||
}));
|
||||
const items = [];
|
||||
for (const page of pages) {
|
||||
if (!isRoot
|
||||
&& !routesPermissionsMap.has(`pages:${page.id}`)
|
||||
// 以下路径先临时处理
|
||||
&& page.get('path') !== '/'
|
||||
&& page.get('path') !== '/register'
|
||||
&& page.get('path') !== '/login'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
items.push(page.toJSON());
|
||||
if (page.get('path') === '/collections') {
|
||||
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
|
||||
@ -67,6 +91,9 @@ export default async function getRoutes(ctx, next) {
|
||||
sort: ['sort'],
|
||||
}));
|
||||
for (const collection of collections) {
|
||||
if (!isRoot && !routesPermissionsMap.has(`collections:${collection.id}`)) {
|
||||
continue;
|
||||
}
|
||||
const pageId = `collection-${collection.id}`;
|
||||
items.push({
|
||||
id: pageId,
|
||||
@ -90,6 +117,9 @@ export default async function getRoutes(ctx, next) {
|
||||
});
|
||||
if (views.length > 1) {
|
||||
for (const view of views) {
|
||||
if (!isRoot && !routesPermissionsMap.has(`views:${view.id}`)) {
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
id: `view-${view.get('id')}`,
|
||||
type: 'collection',
|
||||
|
@ -201,7 +201,7 @@ export default async (ctx, next) => {
|
||||
}));
|
||||
let throughName;
|
||||
const { resourceKey: resourceKey2, associatedName, resourceFieldName, associatedKey } = values;
|
||||
const permissions = await ctx.can(resourceName).permissions();
|
||||
const permissions = await ctx.ac.can(resourceName).permissions();
|
||||
ctx.listFields = [];
|
||||
ctx.createFields = [];
|
||||
ctx.updateFields = [];
|
||||
@ -209,7 +209,7 @@ export default async (ctx, next) => {
|
||||
for (const action of permissions.actions) {
|
||||
ctx.allowedActions.push(action.name);
|
||||
}
|
||||
console.log(ctx.allowedActions);
|
||||
// console.log(ctx.allowedActions);
|
||||
for (const permissionField of permissions.fields) {
|
||||
const pfc = permissionField.actions;
|
||||
if (pfc.includes(`${resourceName}:list`)) {
|
||||
@ -222,11 +222,11 @@ export default async (ctx, next) => {
|
||||
ctx.updateFields.push(permissionField.field_id);
|
||||
}
|
||||
}
|
||||
console.log({
|
||||
listFields: ctx.listFields,
|
||||
createFields: ctx.createFields,
|
||||
updateFields: ctx.updateFields,
|
||||
})
|
||||
// console.log({
|
||||
// listFields: ctx.listFields,
|
||||
// createFields: ctx.createFields,
|
||||
// updateFields: ctx.updateFields,
|
||||
// })
|
||||
if (associatedName) {
|
||||
const table = ctx.db.getTable(associatedName);
|
||||
const resourceField = table.getField(resourceFieldName);
|
||||
@ -420,6 +420,23 @@ export default async (ctx, next) => {
|
||||
"showInDetail": true
|
||||
},
|
||||
"dataIndex": ["title"]
|
||||
},
|
||||
{
|
||||
"title": "描述",
|
||||
"name": "permissions[0].description",
|
||||
"interface": "string",
|
||||
"type": "string",
|
||||
"parent_id": null,
|
||||
"required": true,
|
||||
"developerMode": false,
|
||||
"component": {
|
||||
"type": "string",
|
||||
"className": "drag-visible",
|
||||
"showInForm": true,
|
||||
"showInTable": true,
|
||||
"showInDetail": true
|
||||
},
|
||||
"dataIndex": ["permissions", 0, 'description']
|
||||
}
|
||||
],
|
||||
};
|
||||
@ -496,7 +513,7 @@ export default async (ctx, next) => {
|
||||
};
|
||||
} else {
|
||||
let allowedUpdate = false;
|
||||
if (view.type === 'details' && await ctx.can(resourceName).act('update').one(resourceKey2)) {
|
||||
if (view.type === 'details' && await ctx.ac.can(resourceName).act('update').one(resourceKey2)) {
|
||||
allowedUpdate = true;
|
||||
}
|
||||
ctx.body = {
|
||||
|
@ -3,9 +3,8 @@ import Database from '@nocobase/database';
|
||||
import { flatToTree } from '../utils';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
async function getRoutes(ctx) {
|
||||
const database: Database = ctx.db;
|
||||
const { associatedKey } = ctx.action.params;
|
||||
const Page = database.getModel('pages');
|
||||
const Collection = database.getModel('collections');
|
||||
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
|
||||
@ -21,6 +20,80 @@ export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
sort: ['sort'],
|
||||
}));
|
||||
const items = [];
|
||||
for (const page of pages) {
|
||||
items.push({
|
||||
routable_type: 'pages',
|
||||
routable_id: page.id,
|
||||
});
|
||||
if (page.get('path') === '/collections') {
|
||||
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
|
||||
filter: {
|
||||
showInDataMenu: true,
|
||||
},
|
||||
sort: ['sort'],
|
||||
}: {
|
||||
filter: {
|
||||
developerMode: {'$isFalsy': true},
|
||||
showInDataMenu: true,
|
||||
},
|
||||
sort: ['sort'],
|
||||
}));
|
||||
for (const collection of collections) {
|
||||
items.push({
|
||||
routable_type: 'collections',
|
||||
routable_id: collection.id,
|
||||
});
|
||||
const views = await collection.getViews({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ showInDataMenu: true },
|
||||
{ default: true }
|
||||
]
|
||||
},
|
||||
order: [['sort', 'asc']]
|
||||
});
|
||||
if (views.length > 1) {
|
||||
for (const view of views) {
|
||||
items.push({
|
||||
routable_id: view.id,
|
||||
routable_type: 'views',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
const database: Database = ctx.db;
|
||||
const { associatedKey, associated } = ctx.action.params;
|
||||
const Page = database.getModel('pages');
|
||||
const Collection = database.getModel('collections');
|
||||
// TODO(optimize): isRoot 的判断需要在内部完成,尽量不要交给调用者
|
||||
const isRoot = ctx.ac.constructor.isRoot(associated);
|
||||
const routesPermissionsMap = new Map();
|
||||
if (!isRoot) {
|
||||
const routesPermissions = await associated.getRoutes();
|
||||
|
||||
routesPermissions.forEach(permission => {
|
||||
routesPermissionsMap.set(`${permission.routable_type}:${permission.routable_id}`, permission);
|
||||
});
|
||||
}
|
||||
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
|
||||
filter: {
|
||||
'parent_id.$notNull': true,
|
||||
},
|
||||
sort: ['sort'],
|
||||
} : {
|
||||
filter: {
|
||||
'parent_id.$notNull': true,
|
||||
developerMode: {'$isFalsy': true},
|
||||
},
|
||||
sort: ['sort'],
|
||||
}));
|
||||
const items = [];
|
||||
for (const page of pages) {
|
||||
items.push({
|
||||
id: page.id,
|
||||
@ -29,7 +102,7 @@ export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
tableName: 'pages',
|
||||
parent_id: `page-${page.parent_id}`,
|
||||
associatedKey,
|
||||
accessible: false, // TODO 对接权限
|
||||
accessible: isRoot || routesPermissionsMap.has(`pages:${page.id}`), // TODO 对接权限
|
||||
});
|
||||
if (page.get('path') === '/collections') {
|
||||
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
|
||||
@ -52,7 +125,7 @@ export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
tableName: 'collections',
|
||||
title: collection.get('title'),
|
||||
parent_id: `page-${page.id}`,
|
||||
accessible: false, // TODO 对接权限
|
||||
accessible: isRoot || routesPermissionsMap.has(`collections:${collection.id}`), // TODO 对接权限
|
||||
});
|
||||
const views = await collection.getViews({
|
||||
where: {
|
||||
@ -72,7 +145,7 @@ export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
title: view.title,
|
||||
key: `view-${view.id}`,
|
||||
parent_id: `collection-${collection.id}`,
|
||||
accessible: false, // TODO 对接权限
|
||||
accessible: isRoot || routesPermissionsMap.has(`views:${view.id}`), // TODO 对接权限
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -106,6 +179,46 @@ export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
}
|
||||
|
||||
export async function update(ctx: actions.Context, next: actions.Next) {
|
||||
ctx.body = {};
|
||||
const {
|
||||
associated,
|
||||
resourceKey,
|
||||
values: {
|
||||
tableName,
|
||||
accessible
|
||||
}
|
||||
} = ctx.action.params;
|
||||
|
||||
if (!resourceKey) {
|
||||
if (accessible === false) {
|
||||
await associated.updateAssociations({
|
||||
routes: [],
|
||||
});
|
||||
} else if (accessible === true) {
|
||||
const routes = await getRoutes(ctx);
|
||||
// console.log(routes);
|
||||
await associated.updateAssociations({
|
||||
routes,
|
||||
});
|
||||
}
|
||||
ctx.body = {};
|
||||
return next();
|
||||
}
|
||||
|
||||
console.log(ctx.action.params, { routable_type: tableName, routable_id: resourceKey });
|
||||
let [route] = await associated.getRoutes({
|
||||
where: { routable_type: tableName, routable_id: resourceKey },
|
||||
limit: 1
|
||||
});
|
||||
if (accessible) {
|
||||
if (!route) {
|
||||
route = await associated.createRoute({ routable_type: tableName, routable_id: resourceKey });
|
||||
}
|
||||
ctx.body = route;
|
||||
} else {
|
||||
if (route) {
|
||||
await route.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
@ -192,6 +192,15 @@ export default {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// type: 'belongsToMany',
|
||||
// name: 'roles',
|
||||
// through: 'routes_permissions',
|
||||
// foreignKey: 'routable_id',
|
||||
// otherKey: 'role_id',
|
||||
// morphType: 'routable',
|
||||
// constraints: false,
|
||||
// },
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
|
43
packages/plugin-pages/src/collections/roles.ts
Normal file
43
packages/plugin-pages/src/collections/roles.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { extend } from '@nocobase/database';
|
||||
|
||||
export default extend({
|
||||
name: 'roles',
|
||||
fields: [
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'routes',
|
||||
target: 'routes_permissions',
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'belongsToMany',
|
||||
name: 'pages',
|
||||
title: '可访问的页面',
|
||||
through: 'routes_permissions',
|
||||
foreignKey: 'role_id',
|
||||
otherKey: 'routable_id',
|
||||
morphType: 'routable', // 现在没有多态关联的设置,暂时先这么写了
|
||||
constraints: false, // 多态关联建立外键约束会有问题
|
||||
}
|
||||
],
|
||||
tabs: [
|
||||
{
|
||||
type: 'association',
|
||||
name: 'pages',
|
||||
title: '系统菜单权限',
|
||||
association: 'pages',
|
||||
viewName: 'permissionTable',
|
||||
},
|
||||
]
|
||||
}, {
|
||||
customMerge(key) {
|
||||
if (['tabs'].includes(key)) {
|
||||
return (x = [], y = []) => {
|
||||
const last = x.pop();
|
||||
const tabs = x.concat(y);
|
||||
tabs.push(last);
|
||||
return tabs;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
30
packages/plugin-pages/src/collections/routes_permissions.ts
Normal file
30
packages/plugin-pages/src/collections/routes_permissions.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'routes_permissions',
|
||||
title: '页面权限',
|
||||
developerMode: true,
|
||||
internal: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'id',
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'routable_type',
|
||||
title: '关联的表', // 仅 pages 和 collections
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'routable_id',
|
||||
title: '关联的对象'
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'role'
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
@ -5,7 +5,7 @@ import { ROLE_TYPE_ANONYMOUS, ROLE_TYPE_ROOT, ROLE_TYPE_USER } from './constants
|
||||
|
||||
|
||||
function getPermissions(roles) {
|
||||
return roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []);
|
||||
return roles.reduce((permissions, role) => permissions.concat(role.permissions), []);
|
||||
}
|
||||
|
||||
function getActionPermissions(permissions) {
|
||||
@ -97,10 +97,16 @@ export type PermissionParams = true | null | {
|
||||
// ctx.can('collection').act('update').any()
|
||||
// ctx.can('collection').act('update').one(resourceKey)
|
||||
// ctx.can('collection').act('get').one(resourceKey)
|
||||
// ctx.as(roles).can('collection').permissions()
|
||||
|
||||
// TODO(optimize): 需要优化链式调用结构和上下文,以避免顺序或重置等问题
|
||||
export default class AccessController<T extends typeof AccessController = typeof AccessController> {
|
||||
static isRoot(roles): boolean {
|
||||
return (Array.isArray(roles) ? roles : [roles]).some(role => role.type === ROLE_TYPE_ROOT);
|
||||
}
|
||||
|
||||
export default class AccessController {
|
||||
context;
|
||||
|
||||
roles;
|
||||
resourceName: string | null = null;
|
||||
actionName: string | null = null;
|
||||
|
||||
@ -108,11 +114,21 @@ export default class AccessController {
|
||||
this.context = ctx;
|
||||
}
|
||||
|
||||
can = (resourceName: string | null) => {
|
||||
/**
|
||||
* 用于临时创建新的身份进行相关操作
|
||||
* @param roles Array
|
||||
*/
|
||||
as(roles) {
|
||||
const instance = new (this.constructor as T)(this.context);
|
||||
instance.roles = Array.isArray(roles) ? roles : [roles];
|
||||
return instance;
|
||||
}
|
||||
|
||||
can(resourceName: string | null) {
|
||||
this.resourceName = resourceName;
|
||||
this.actionName = null;
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
act(name: string | null) {
|
||||
this.actionName = name;
|
||||
@ -121,7 +137,7 @@ export default class AccessController {
|
||||
|
||||
async permissions(): Promise<CollectionPermissions> {
|
||||
const roles = await this.getRolesWithPermissions();
|
||||
if (roles.some(role => role.type === ROLE_TYPE_ROOT)) {
|
||||
if ((this.constructor as T).isRoot(roles)) {
|
||||
return this.getRootPermissions();
|
||||
}
|
||||
|
||||
@ -136,7 +152,7 @@ export default class AccessController {
|
||||
|
||||
async any(): Promise<PermissionParams> {
|
||||
const roles = await this.getRolesWithPermissions();
|
||||
if (roles.some(role => role.type === ROLE_TYPE_ROOT)) {
|
||||
if ((this.constructor as T).isRoot(roles)) {
|
||||
return true;
|
||||
}
|
||||
// 只处理 actions 表里的权限,其余跳过
|
||||
@ -180,7 +196,7 @@ export default class AccessController {
|
||||
const Collection = this.context.db.getModel(this.resourceName);
|
||||
const existed = await Collection.count({
|
||||
where: {
|
||||
...Collection.parseApiJson({ filter }).where,
|
||||
...Collection.parseApiJson({ filter, context: this.context }).where,
|
||||
[Collection.primaryKeyAttribute]: resourceKey
|
||||
}
|
||||
});
|
||||
@ -188,25 +204,78 @@ export default class AccessController {
|
||||
return existed ? any : null;
|
||||
}
|
||||
|
||||
async isRoot(): Promise<boolean> {
|
||||
const { context } = this;
|
||||
const { currentUser } = context.state;
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootRoles = await currentUser.countRoles({
|
||||
where: {
|
||||
type: ROLE_TYPE_ROOT
|
||||
}
|
||||
});
|
||||
if (!rootRoles.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getRoles() {
|
||||
if (this.roles) {
|
||||
return this.roles;
|
||||
}
|
||||
const { context } = this;
|
||||
let userRoles = [];
|
||||
const { currentUser } = context.state;
|
||||
if (currentUser) {
|
||||
const rootRoles = await currentUser.getRoles({
|
||||
where: {
|
||||
type: ROLE_TYPE_ROOT
|
||||
}
|
||||
});
|
||||
if (rootRoles.length) {
|
||||
return rootRoles;
|
||||
}
|
||||
|
||||
userRoles = await currentUser.getRoles({
|
||||
where: {
|
||||
type: ROLE_TYPE_USER
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const Role = context.db.getModel('roles');
|
||||
const anonymousRoles = await Role.findAll({
|
||||
where: {
|
||||
type: ROLE_TYPE_ANONYMOUS
|
||||
}
|
||||
});
|
||||
|
||||
return [...userRoles, ...anonymousRoles];
|
||||
}
|
||||
|
||||
async getRolesWithPermissions() {
|
||||
const { context, resourceName, actionName = null } = this;
|
||||
if (!resourceName) {
|
||||
throw new Error('resource name must be set first by `can(resourceName)`');
|
||||
}
|
||||
const Role = context.db.getModel('roles');
|
||||
const permissionInclusion = {
|
||||
association: 'permissions',
|
||||
|
||||
const permissionOptions = {
|
||||
where: {
|
||||
collection_name: resourceName
|
||||
},
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
association: 'actions',
|
||||
where: actionName ? {
|
||||
name: `${resourceName}:${actionName}`
|
||||
} : {},
|
||||
required: true,
|
||||
...(actionName ? {
|
||||
where: {
|
||||
name: `${resourceName}:${actionName}`
|
||||
},
|
||||
required: true,
|
||||
} : {}),
|
||||
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
|
||||
separate: true,
|
||||
include: [
|
||||
@ -228,8 +297,24 @@ export default class AccessController {
|
||||
association: 'tabs_permissions',
|
||||
separate: true,
|
||||
}
|
||||
],
|
||||
]
|
||||
};
|
||||
const permissionInclusion = {
|
||||
...permissionOptions,
|
||||
association: 'permissions',
|
||||
required: true
|
||||
};
|
||||
|
||||
if (this.roles) {
|
||||
if ((this.constructor as T).isRoot(this.roles)) {
|
||||
return this.roles;
|
||||
}
|
||||
for (const role of this.roles) {
|
||||
role.permissions = await role.getPermissions(permissionOptions);
|
||||
role.set('permissions', role.permissions);
|
||||
}
|
||||
return this.roles;
|
||||
}
|
||||
|
||||
let userRoles = [];
|
||||
// 获取登入用户的角色及权限
|
||||
@ -255,6 +340,7 @@ export default class AccessController {
|
||||
}
|
||||
|
||||
// 获取匿名用户的角色及权限
|
||||
const Role = context.db.getModel('roles');
|
||||
const anonymousRoles = await Role.findAll({
|
||||
where: {
|
||||
type: ROLE_TYPE_ANONYMOUS
|
||||
|
@ -1,10 +1,24 @@
|
||||
import { Op } from 'sequelize';
|
||||
import { actions } from '@nocobase/actions';
|
||||
import _ from 'lodash';
|
||||
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
const { associated } = ctx.action.params;
|
||||
// TODO: 暂时 action 中间件就这么写了
|
||||
ctx.action.mergeParams({associated: null});
|
||||
return actions.common.list(ctx, next);
|
||||
ctx.action.mergeParams({
|
||||
associated: null
|
||||
});
|
||||
await actions.common.list(ctx, async () => {
|
||||
const permissions = await associated.getPermissions();
|
||||
ctx.body.rows.forEach(item => {
|
||||
const permission = permissions.find(p => p.collection_name === item.get('name'));
|
||||
if (permission) {
|
||||
// item.permissions = [permission]; // 不输出
|
||||
item.set('permissions', [permission]); // 输出
|
||||
}
|
||||
});
|
||||
});
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function get(ctx: actions.Context, next: actions.Next) {
|
||||
@ -13,47 +27,22 @@ export async function get(ctx: actions.Context, next: actions.Next) {
|
||||
associated
|
||||
} = ctx.action.params;
|
||||
|
||||
const [permission] = await associated.getPermissions({
|
||||
where: {
|
||||
collection_name: resourceKey
|
||||
},
|
||||
include: [
|
||||
{
|
||||
association: 'actions',
|
||||
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
|
||||
separate: true,
|
||||
include: [
|
||||
{
|
||||
association: 'scope'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
association: 'fields_permissions',
|
||||
separate: true,
|
||||
},
|
||||
{
|
||||
association: 'tabs_permissions',
|
||||
separate: true,
|
||||
}
|
||||
],
|
||||
distinct: true,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
const result = permission
|
||||
? {
|
||||
actions: permission.actions || [],
|
||||
fields: permission.fields_permissions || [],
|
||||
tabs: (permission.tabs_permissions || []).map(item => item.tab_id),
|
||||
}
|
||||
: {
|
||||
actions: [],
|
||||
fields: [],
|
||||
tabs: []
|
||||
};
|
||||
const permissions = await ctx.ac.as(associated).can(resourceKey).permissions();
|
||||
|
||||
ctx.body = result;
|
||||
const permission = await associated.getPermissions({
|
||||
where: {
|
||||
collection_name: resourceKey,
|
||||
},
|
||||
plain: true,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
console.log(permission);
|
||||
|
||||
ctx.body = {
|
||||
...permissions,
|
||||
description: _.get(permission, 'description'),
|
||||
};
|
||||
|
||||
await next();
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
// TODO: 暂时 action 中间件就这么写了
|
||||
ctx.action.mergeParams({associated: null});
|
||||
const { associatedKey } = ctx.action.params;
|
||||
ctx.action.mergeParams({
|
||||
filter: {
|
||||
'parent_id.$notNull': true,
|
||||
}
|
||||
})
|
||||
const done = async () => {
|
||||
ctx.body.rows = ctx.body.rows.map(row => {
|
||||
row.setDataValue('tableName', 'pages');
|
||||
row.setDataValue('associatedKey', parseInt(associatedKey));
|
||||
return row.get();
|
||||
});
|
||||
console.log(ctx.body.rows);
|
||||
await next();
|
||||
}
|
||||
return actions.common.list(ctx, done);
|
||||
}
|
||||
|
||||
export async function update(ctx: actions.Context, next: actions.Next) {
|
||||
ctx.body = {};
|
||||
await next();
|
||||
}
|
@ -27,6 +27,12 @@ export default extend({
|
||||
through: 'permissions',
|
||||
sourceKey: 'name'
|
||||
},
|
||||
// {
|
||||
// type: 'hasMany',
|
||||
// name: 'permissions',
|
||||
// sourceKey: 'name',
|
||||
// foreignKey: 'collection_name'
|
||||
// }
|
||||
],
|
||||
views: [
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ export default {
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'desctiption',
|
||||
name: 'description',
|
||||
},
|
||||
{
|
||||
comment: '关联的角色',
|
||||
|
@ -24,12 +24,39 @@ export default {
|
||||
title: '角色类型',
|
||||
type: 'integer',
|
||||
name: 'type',
|
||||
developerMode: true,
|
||||
dataSource: [
|
||||
{ value: ROLE_TYPE_ROOT, label: '系统角色' },
|
||||
{ value: ROLE_TYPE_ANONYMOUS, label: '匿名角色' },
|
||||
{ value: ROLE_TYPE_USER, label: '自定义角色' },
|
||||
],
|
||||
defaultValue: ROLE_TYPE_USER
|
||||
defaultValue: ROLE_TYPE_USER,
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
title: '默认角色',
|
||||
type: 'radio',
|
||||
name: 'default',
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInForm: true,
|
||||
showInDetail: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
interface: 'textarea',
|
||||
title: '描述',
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInForm: true,
|
||||
showInDetail: true,
|
||||
},
|
||||
},
|
||||
// TODO(feature): 用户组后续考虑
|
||||
// TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况
|
||||
@ -51,12 +78,6 @@ export default {
|
||||
through: 'permissions',
|
||||
targetKey: 'name'
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
title: '页面',
|
||||
type: 'belongsToMany',
|
||||
name: 'pages',
|
||||
},
|
||||
{
|
||||
comment: '权限集(方便访问)',
|
||||
type: 'hasMany',
|
||||
@ -134,13 +155,6 @@ export default {
|
||||
association: 'collections',
|
||||
viewName: 'permissionTable',
|
||||
},
|
||||
{
|
||||
type: 'association',
|
||||
name: 'pages',
|
||||
title: '系统菜单权限',
|
||||
association: 'pages',
|
||||
viewName: 'permissionTable',
|
||||
},
|
||||
{
|
||||
type: 'association',
|
||||
name: 'users',
|
||||
|
@ -63,6 +63,19 @@ export class Permissions {
|
||||
}
|
||||
});
|
||||
|
||||
database.getModel('users').addHook('afterCreate', async(model, options) => {
|
||||
const { transaction = await database.sequelize.transaction() } = options;
|
||||
const Role = database.getModel('roles');
|
||||
const defaultRole = await Role.findOne({ where: { default: true }, transaction });
|
||||
if (defaultRole) {
|
||||
// @ts-ignore
|
||||
await model.addRole(defaultRole, { transaction });
|
||||
}
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
});
|
||||
|
||||
// 针对“自己创建的” scope 添加特殊的操作符以生成查询条件
|
||||
if (!Operator.has('$currentUser')) {
|
||||
Operator.register('$currentUser', (value, { ctx }) => {
|
||||
@ -76,7 +89,7 @@ export class Permissions {
|
||||
}
|
||||
|
||||
injection = async (ctx, next) => {
|
||||
ctx.can = new AccessController(ctx).can;
|
||||
ctx.ac = new AccessController(ctx);
|
||||
|
||||
return next();
|
||||
};
|
||||
@ -93,9 +106,9 @@ export class Permissions {
|
||||
|
||||
// 关系数据的权限
|
||||
if (associatedName && resourceField) {
|
||||
result = await ctx.can(resourceField.options.target).act(actionName).any();
|
||||
result = await ctx.ac.can(resourceField.options.target).act(actionName).any();
|
||||
} else {
|
||||
result = await ctx.can(resourceName).act(actionName).any();
|
||||
result = await ctx.ac.can(resourceName).act(actionName).any();
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
|
Loading…
Reference in New Issue
Block a user