From 301229ef88576412665908ee761308e4b6799b3f Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 22 Jan 2021 10:18:02 +0800 Subject: [PATCH] feat: route permissions (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- packages/app/src/api/migrations/init.ts | 12 ++ .../components/views/SortableTable/index.tsx | 31 ++++- packages/app/src/components/views/Table.tsx | 2 +- packages/database/src/database.ts | 19 ++- .../src/hooks/fields-before-validate.ts | 2 +- .../plugin-pages/src/actions/getRoutes.ts | 30 +++++ packages/plugin-pages/src/actions/getView.ts | 33 +++-- .../plugin-pages/src/actions/roles.pages.ts | 125 +++++++++++++++++- .../plugin-pages/src/collections/pages.ts | 9 ++ .../plugin-pages/src/collections/roles.ts | 43 ++++++ .../src/collections/routes_permissions.ts | 30 +++++ .../src/AccessController.ts | 120 ++++++++++++++--- .../src/actions/roles.collections.ts | 73 +++++----- .../src/actions/roles.pages.ts | 27 ---- ..._premissions.ts => actions_permissions.ts} | 0 .../src/collections/collections.ts | 6 + .../src/collections/permissions.ts | 2 +- .../src/collections/roles.ts | 42 ++++-- packages/plugin-permissions/src/server.ts | 19 ++- 19 files changed, 501 insertions(+), 124 deletions(-) create mode 100644 packages/plugin-pages/src/collections/roles.ts create mode 100644 packages/plugin-pages/src/collections/routes_permissions.ts delete mode 100644 packages/plugin-permissions/src/actions/roles.pages.ts rename packages/plugin-permissions/src/collections/{actions_premissions.ts => actions_permissions.ts} (100%) diff --git a/packages/app/src/api/migrations/init.ts b/packages/app/src/api/migrations/init.ts index 5753f856a9..e4ada9195a 100644 --- a/packages/app/src/api/migrations/init.ts +++ b/packages/app/src/api/migrations/init.ts @@ -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 diff --git a/packages/app/src/components/views/SortableTable/index.tsx b/packages/app/src/components/views/SortableTable/index.tsx index 477efa26cd..4aa4dffddb 100644 --- a/packages/app/src/components/views/SortableTable/index.tsx +++ b/packages/app/src/components/views/SortableTable/index.tsx @@ -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 => ); export const SortableContainer = sortableContainer(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' ? : ; field.className = `${field.className||''} noco-field-${field.interface}`; + if (field.editable && field.interface === 'boolean') { + field.title = ( + + { + 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} + + ); + } return { ...field, ...(field.component||{}), diff --git a/packages/app/src/components/views/Table.tsx b/packages/app/src/components/views/Table.tsx index b6895f0db9..1a3cab4be7 100644 --- a/packages/app/src/components/views/Table.tsx +++ b/packages/app/src/components/views/Table.tsx @@ -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}); diff --git a/packages/database/src/database.ts b/packages/database/src/database.ts index ace0c59190..dcf166189b 100644 --- a/packages/database/src/database.ts +++ b/packages/database/src/database.ts @@ -68,6 +68,8 @@ export default class Database { protected hooks = {}; + protected extTableOptions = new Map(); + 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); } }); diff --git a/packages/plugin-collections/src/hooks/fields-before-validate.ts b/packages/plugin-collections/src/hooks/fields-before-validate.ts index d745aaff05..03742cca7b 100644 --- a/packages/plugin-collections/src/hooks/fields-before-validate.ts +++ b/packages/plugin-collections/src/hooks/fields-before-validate.ts @@ -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}; diff --git a/packages/plugin-pages/src/actions/getRoutes.ts b/packages/plugin-pages/src/actions/getRoutes.ts index 258a66224d..48544c0246 100644 --- a/packages/plugin-pages/src/actions/getRoutes.ts +++ b/packages/plugin-pages/src/actions/getRoutes.ts @@ -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', diff --git a/packages/plugin-pages/src/actions/getView.ts b/packages/plugin-pages/src/actions/getView.ts index 16a0c5c7a5..3612ce9d98 100644 --- a/packages/plugin-pages/src/actions/getView.ts +++ b/packages/plugin-pages/src/actions/getView.ts @@ -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 = { diff --git a/packages/plugin-pages/src/actions/roles.pages.ts b/packages/plugin-pages/src/actions/roles.pages.ts index 06b32e1d7e..84c3486bdb 100644 --- a/packages/plugin-pages/src/actions/roles.pages.ts +++ b/packages/plugin-pages/src/actions/roles.pages.ts @@ -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(); } diff --git a/packages/plugin-pages/src/collections/pages.ts b/packages/plugin-pages/src/collections/pages.ts index 0bced642ab..9f9fc75ddd 100644 --- a/packages/plugin-pages/src/collections/pages.ts +++ b/packages/plugin-pages/src/collections/pages.ts @@ -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', diff --git a/packages/plugin-pages/src/collections/roles.ts b/packages/plugin-pages/src/collections/roles.ts new file mode 100644 index 0000000000..f0f5255114 --- /dev/null +++ b/packages/plugin-pages/src/collections/roles.ts @@ -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; + }; + } + } +}); diff --git a/packages/plugin-pages/src/collections/routes_permissions.ts b/packages/plugin-pages/src/collections/routes_permissions.ts new file mode 100644 index 0000000000..68cb3114da --- /dev/null +++ b/packages/plugin-pages/src/collections/routes_permissions.ts @@ -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; diff --git a/packages/plugin-permissions/src/AccessController.ts b/packages/plugin-permissions/src/AccessController.ts index ada154802e..5e5b97c0fd 100644 --- a/packages/plugin-permissions/src/AccessController.ts +++ b/packages/plugin-permissions/src/AccessController.ts @@ -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 { + 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 { 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 { 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 { + 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 diff --git a/packages/plugin-permissions/src/actions/roles.collections.ts b/packages/plugin-permissions/src/actions/roles.collections.ts index 33ab8376c5..053535a6b6 100644 --- a/packages/plugin-permissions/src/actions/roles.collections.ts +++ b/packages/plugin-permissions/src/actions/roles.collections.ts @@ -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(); } diff --git a/packages/plugin-permissions/src/actions/roles.pages.ts b/packages/plugin-permissions/src/actions/roles.pages.ts deleted file mode 100644 index 6e37286eb1..0000000000 --- a/packages/plugin-permissions/src/actions/roles.pages.ts +++ /dev/null @@ -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(); -} diff --git a/packages/plugin-permissions/src/collections/actions_premissions.ts b/packages/plugin-permissions/src/collections/actions_permissions.ts similarity index 100% rename from packages/plugin-permissions/src/collections/actions_premissions.ts rename to packages/plugin-permissions/src/collections/actions_permissions.ts diff --git a/packages/plugin-permissions/src/collections/collections.ts b/packages/plugin-permissions/src/collections/collections.ts index 7c62563dfb..b9fbf79841 100644 --- a/packages/plugin-permissions/src/collections/collections.ts +++ b/packages/plugin-permissions/src/collections/collections.ts @@ -27,6 +27,12 @@ export default extend({ through: 'permissions', sourceKey: 'name' }, + // { + // type: 'hasMany', + // name: 'permissions', + // sourceKey: 'name', + // foreignKey: 'collection_name' + // } ], views: [ { diff --git a/packages/plugin-permissions/src/collections/permissions.ts b/packages/plugin-permissions/src/collections/permissions.ts index 6c5160e104..f56e1d14f1 100644 --- a/packages/plugin-permissions/src/collections/permissions.ts +++ b/packages/plugin-permissions/src/collections/permissions.ts @@ -21,7 +21,7 @@ export default { }, { type: 'string', - name: 'desctiption', + name: 'description', }, { comment: '关联的角色', diff --git a/packages/plugin-permissions/src/collections/roles.ts b/packages/plugin-permissions/src/collections/roles.ts index efa20ca2df..4af3e615a2 100644 --- a/packages/plugin-permissions/src/collections/roles.ts +++ b/packages/plugin-permissions/src/collections/roles.ts @@ -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', diff --git a/packages/plugin-permissions/src/server.ts b/packages/plugin-permissions/src/server.ts index 1c95ec3a20..84876d908f 100644 --- a/packages/plugin-permissions/src/server.ts +++ b/packages/plugin-permissions/src/server.ts @@ -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) {