diff --git a/docker/nocobase-full/nocobase.conf b/docker/nocobase-full/nocobase.conf index fa37c8ea85..61ddc70335 100644 --- a/docker/nocobase-full/nocobase.conf +++ b/docker/nocobase-full/nocobase.conf @@ -34,6 +34,7 @@ server { location ^~ /api/ { proxy_pass http://127.0.0.1:13000/api/; proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; diff --git a/docker/nocobase/nocobase.conf b/docker/nocobase/nocobase.conf index d462e5be66..7c0c4d2c41 100644 --- a/docker/nocobase/nocobase.conf +++ b/docker/nocobase/nocobase.conf @@ -59,6 +59,7 @@ server { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_cache_bypass $http_upgrade; proxy_connect_timeout 600; proxy_send_timeout 600; diff --git a/lerna.json b/lerna.json index 424deb9885..14642ab0a6 100644 --- a/lerna.json +++ b/lerna.json @@ -2,9 +2,7 @@ "version": "1.5.0-alpha.5", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/cli/nocobase.conf.tpl b/packages/core/cli/nocobase.conf.tpl index edfce983e9..1046688c4b 100644 --- a/packages/core/cli/nocobase.conf.tpl +++ b/packages/core/cli/nocobase.conf.tpl @@ -70,6 +70,7 @@ server { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; add_header Cache-Control 'no-cache, no-store'; proxy_cache_bypass $http_upgrade; diff --git a/packages/core/server/src/__tests__/audit-manager.test.ts b/packages/core/server/src/__tests__/audit-manager.test.ts new file mode 100644 index 0000000000..a5c3d3b8d0 --- /dev/null +++ b/packages/core/server/src/__tests__/audit-manager.test.ts @@ -0,0 +1,347 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ +import { MockServer, mockServer } from '@nocobase/test'; +import { SourceAndTarget, UserInfo } from '../audit-manager'; + +async function createApp(options: any = {}) { + const app = mockServer({ + ...options, + // 还会有些其他参数配置 + }); + // 这里可以补充一些需要特殊处理的逻辑,比如导入测试需要的数据表 + return app; +} + +async function startApp() { + const app = createApp(); + return app; +} + +describe('audit manager', () => { + let app: MockServer; + + beforeEach(async () => { + app = await startApp(); + }); + + afterEach(async () => { + // 运行测试后,清空数据库 + await app.destroy(); + // 只停止不清空数据库 + await app.stop(); + }); + + it('register action', () => { + app.auditManager.registerAction('create'); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "__default__" => Map { + "create" => { + "name": "create", + }, + }, + } + `); + }); + + it('register actions with getMetaData and getUserInfo and getresourceUk', () => { + const getMetaData = () => { + return new Promise((resolve, reject) => { + resolve({ + request: { + params: { + testData: 'testParamData', + }, + body: { + testBody: 'testBodyData', + }, + }, + response: { + body: { + testBody: 'testBody', + }, + }, + }); + }); + }; + const getUserInfo = () => { + return new Promise((resolve, reject) => { + resolve({ + userId: '1', + }); + }); + }; + const getSourceAndTarget = () => { + return new Promise((resolve, reject) => { + resolve({ + sourceCollection: 'users', + sourceRecordUK: '1', + targetCollection: 'roles', + targetRecordUK: '2', + }); + }); + }; + app.auditManager.registerAction({ name: 'create', getMetaData, getUserInfo, getSourceAndTarget }); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "__default__" => Map { + "create" => { + "getMetaData": [Function], + "getSourceAndTarget": [Function], + "getUserInfo": [Function], + "name": "create", + }, + }, + } + `); + }); + + it('register actions', () => { + app.auditManager.registerActions(['create', 'update', 'user:create', 'user:*', 'user:destory', 'role:*']); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "__default__" => Map { + "create" => { + "name": "create", + }, + "update" => { + "name": "update", + }, + }, + "user" => Map { + "create" => { + "name": "user:create", + }, + "__default__" => { + "name": "user:*", + }, + "destory" => { + "name": "user:destory", + }, + }, + "role" => Map { + "__default__" => { + "name": "role:*", + }, + }, + } + `); + }); + + it('register actions with getMetaData', async () => { + const getMetaData = () => { + return new Promise((resolve, reject) => { + resolve({ + request: { + params: { + testData: 'testParamData', + }, + body: { + testBody: 'testBodyData', + }, + }, + response: { + body: { + testBody: 'testBody', + }, + }, + }); + }); + }; + app.auditManager.registerActions(['pm:enable', { name: 'pm:enable', getMetaData }, 'pm:add']); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "pm" => Map { + "enable" => { + "getMetaData": [Function], + "name": "pm:enable", + }, + "add" => { + "name": "pm:add", + }, + }, + } + `); + }); + + it('register actions with getUserInfo', async () => { + const getUserInfo = () => { + return new Promise((resolve, reject) => { + resolve({ + userId: '1', + roleName: 'test', + }); + }); + }; + app.auditManager.registerActions(['pm:enable', { name: 'pm:enable', getUserInfo }, 'pm:add']); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "pm" => Map { + "enable" => { + "getUserInfo": [Function], + "name": "pm:enable", + }, + "add" => { + "name": "pm:add", + }, + }, + } + `); + }); + + it('register actions with getSourceAndTarget', async () => { + const getSourceAndTarget = () => { + return new Promise((resolve, reject) => { + resolve({ + sourceCollection: 'users', + sourceRecordUK: '1', + targetCollection: 'roles', + targetRecordUK: '2', + }); + }); + }; + app.auditManager.registerActions(['pm:enable', { name: 'pm:enable', getSourceAndTarget }, 'pm:add']); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "pm" => Map { + "enable" => { + "getSourceAndTarget": [Function], + "name": "pm:enable", + }, + "add" => { + "name": "pm:add", + }, + }, + } + `); + }); + + it('getAction', () => { + app.auditManager.registerActions(['create', 'update', 'user:create', 'user:*', 'user:destory', 'role:*']); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "__default__" => Map { + "create" => { + "name": "create", + }, + "update" => { + "name": "update", + }, + }, + "user" => Map { + "create" => { + "name": "user:create", + }, + "__default__" => { + "name": "user:*", + }, + "destory" => { + "name": "user:destory", + }, + }, + "role" => Map { + "__default__" => { + "name": "role:*", + }, + }, + } + `); + + const action = app.auditManager.getAction('update'); + expect(action).toMatchInlineSnapshot(` + { + "name": "update", + } + `); + + const action2 = app.auditManager.getAction('user:update'); + expect(action2).toEqual(null); + + const action3 = app.auditManager.getAction('update', 'app'); + expect(action3).toMatchInlineSnapshot(` + { + "name": "update", + } + `); + + const action4 = app.auditManager.getAction('update', 'role'); + expect(action4).toMatchInlineSnapshot(` + { + "name": "role:*", + } + `); + + const action5 = app.auditManager.getAction('update', 'user'); + expect(action5).toMatchInlineSnapshot(` + { + "name": "user:*", + } + `); + + const action6 = app.auditManager.getAction('update', 'department'); + expect(action6).toMatchInlineSnapshot(` + { + "name": "update", + } + `); + + const action7 = app.auditManager.getAction('destory'); + expect(action7).toEqual(null); + }); + + it('getAction priority', () => { + app.auditManager.registerActions(['list']); + const action8 = app.auditManager.getAction('list', 'department'); + expect(action8).toMatchInlineSnapshot(` + { + "name": "list", + } + `); + + app.auditManager.registerActions(['department:*']); + const action9 = app.auditManager.getAction('list', 'department'); + expect(action9).toMatchInlineSnapshot(` + { + "name": "department:*", + } + `); + + app.auditManager.registerActions(['department:list']); + const action10 = app.auditManager.getAction('list', 'department'); + expect(action10).toMatchInlineSnapshot(` + { + "name": "department:list", + } + `); + }); + + it('getAction default', () => { + app.auditManager.registerActions(['users:updateProfile', 'update']); + expect(app.auditManager.resources).toMatchInlineSnapshot(` + Map { + "users" => Map { + "updateProfile" => { + "name": "users:updateProfile", + }, + }, + "__default__" => Map { + "update" => { + "name": "update", + }, + }, + } + `); + const action = app.auditManager.getAction('update', 'users'); + expect(action).toMatchInlineSnapshot(` + { + "name": "update", + } + `); + }); +}); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 5df5511266..4b443089a4 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -73,6 +73,7 @@ import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub- import { SyncMessageManager } from './sync-message-manager'; import packageJson from '../package.json'; +import { AuditManager } from './audit-manager'; export type PluginType = string | typeof Plugin; export type PluginConfiguration = PluginType | [PluginType, any]; @@ -124,7 +125,9 @@ export interface ApplicationOptions { pmSock?: string; name?: string; authManager?: AuthManagerOptions; + auditManager?: AuditManager; lockManager?: LockManagerOptions; + /** * @internal */ @@ -382,6 +385,11 @@ export class Application exten return this._authManager; } + protected _auditManager: AuditManager; + get auditManager() { + return this._auditManager; + } + protected _locales: Locale; /** @@ -1207,6 +1215,8 @@ export class Application exten ...(this.options.authManager || {}), }); + this._auditManager = new AuditManager(); + this.resourceManager.define({ name: 'auth', actions: authActions, diff --git a/packages/core/server/src/audit-manager/index.ts b/packages/core/server/src/audit-manager/index.ts new file mode 100644 index 0000000000..07c5ff8234 --- /dev/null +++ b/packages/core/server/src/audit-manager/index.ts @@ -0,0 +1,346 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Context } from '@nocobase/actions'; +import { head } from 'lodash'; +import Stream from 'stream'; + +function isStream(obj) { + return ( + obj instanceof Stream.Readable || + obj instanceof Stream.Writable || + obj instanceof Stream.Duplex || + obj instanceof Stream.Transform + ); +} + +export interface AuditLog { + uuid: string; + dataSource: string; + resource: string; + action: string; + sourceCollection?: string; + sourceRecordUK?: string; + targetCollection?: string; + targetRecordUK?: string; + userId: string; + roleName: string; + ip: string; + ua: string; + status: number; + metadata?: Record; +} + +export interface UserInfo { + userId?: string; + roleName?: string; +} + +export interface SourceAndTarget { + sourceCollection?: string; + sourceRecordUK?: string; + targetCollection?: string; + targetRecordUK?: string; +} + +export interface AuditLogger { + log(auditLog: AuditLog): Promise; +} + +type Action = + | string + | { + name: string; + getMetaData?: (ctx: Context) => Promise>; + getUserInfo?: (ctx: Context) => Promise; + getSourceAndTarget?: (ctx: Context) => Promise; + }; + +export class AuditManager { + logger: AuditLogger; + resources: Map>; + + constructor() { + this.resources = new Map(); + } + + public setLogger(logger: AuditLogger) { + this.logger = logger; + } + /** + * 注册需要参与审计的资源和操作,支持几种写法 + * + * 对所有资源生效; + * registerActions(['create']) + * + * 对某个资源的所有操作生效 resource:* + * registerActions(['app:*']) + * + * 对某个资源的某个操作生效 resouce:action + * registerAction(['pm:update']) + * + * 支持传getMetaData方法 + * + * registerActions([ + * 'create', + * { name: 'auth:signIn', getMetaData} + * ]) + * + * 支持传getUserInfo方法 + * + * registerActions([ + * 'create', + * { name: 'auth:signIn', getUserInfo } + * ]) + * + * 当注册的接口有重叠时,颗粒度细的注册方法优先级更高 + * + * Action1: registerActions(['create']); + * + * Action2: registerAction([{ name: 'user:*', getMetaData }]); + * + * Action3: registerAction([{ name: 'user:create', getMetaData }]); + * + * 对于user:create接口,以上优先级顺序是 Action3 > Action2 > Action1 + * + * @param actions 操作列表 + */ + registerActions(actions: Action[]) { + actions.forEach((action) => { + this.registerAction(action); + }); + } + + /** + * 注册单个操作,支持的用法同registerActions + * @param action 操作 + */ + registerAction(action: Action) { + let originAction = ''; + let getMetaData = null; + let getUserInfo = null; + let getSourceAndTarget = null; + if (typeof action === 'string') { + originAction = action; + } else { + originAction = action.name; + getMetaData = action.getMetaData; + getUserInfo = action.getUserInfo; + getSourceAndTarget = action.getSourceAndTarget; + } + // 解析originAction, 获取actionName, resourceName + const nameRegex = /^[a-zA-Z0-9_-]+$/; + const resourceWildcardRegex = /^([a-zA-Z0-9_-]+):\*$/; + const resourceAndActionRegex = /^([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/; + let resourceName = ''; + let actionName = ''; + if (nameRegex.test(originAction)) { + actionName = originAction; + resourceName = '__default__'; + } + if (resourceWildcardRegex.test(originAction)) { + const match = originAction.match(resourceWildcardRegex); + resourceName = match[1]; + actionName = '__default__'; + } + if (resourceAndActionRegex.test(originAction)) { + const match = originAction.match(resourceAndActionRegex); + resourceName = match[1]; + actionName = match[2]; + } + if (!resourceName && !actionName) { + return; + } + let resource = this.resources.get(resourceName); + if (!resource) { + resource = new Map(); + this.resources.set(resourceName, resource); + } + const saveAction: Action = { + name: originAction, + }; + if (getMetaData) { + saveAction.getMetaData = getMetaData; + } + if (getUserInfo) { + saveAction.getUserInfo = getUserInfo; + } + if (getSourceAndTarget) { + saveAction.getSourceAndTarget = getSourceAndTarget; + } + resource.set(actionName, saveAction); + } + + getAction(action: string, resource?: string) { + let resourceName = resource; + if (!resource) { + resourceName = '__default__'; + } + if (resourceName === '__default__') { + const resourceActions = this.resources.get(resourceName); + if (resourceActions) { + const resourceAction = resourceActions.get(action); + if (resourceAction) { + return resourceAction; + } + } + } else { + const resourceActions = this.resources.get(resourceName); + if (resourceActions) { + let resourceAction = resourceActions.get(action); + if (resourceAction) { + return resourceAction; + } else { + resourceAction = resourceActions.get('__default__'); + if (resourceAction) { + return resourceAction; + } else { + const defaultResourceActions = this.resources.get('__default__'); + if (defaultResourceActions) { + const defaultResourceAction = defaultResourceActions.get(action); + if (defaultResourceAction) { + return defaultResourceAction; + } + } + } + } + } else { + const resourceActions = this.resources.get('__default__'); + if (resourceActions) { + const resourceAction = resourceActions.get(action); + if (resourceAction) { + return resourceAction; + } + } + } + } + return null; + } + + async getDefaultMetaData(ctx: any) { + let body: any = null; + if (ctx.body) { + if (!Buffer.isBuffer(ctx.body) && !isStream(ctx.body)) { + body = ctx.body; + } + } + return { + request: { + params: ctx.request.params, + query: ctx.request.query, + body: ctx.request.body, + path: ctx.request.path, + headers: { + 'x-authenticator': ctx.request?.headers['x-authenticator'], + 'x-locale': ctx.request?.headers['x-locale'], + 'x-timezone': ctx.request?.headers['x-timezone'], + }, + }, + response: { + body, + }, + }; + } + + formatAuditData(ctx: Context) { + const { resourceName } = ctx.action; + // 获取ip,优先使用X-Forwarded-For,如果没有则使用ctx.request.ip + const ipvalues = ctx.request.header['x-forwarded-for']; + let ipvalue = ''; + if (ipvalues instanceof Array) { + ipvalue = ipvalues[0]; + } else { + ipvalue = ipvalues; + } + const ips = ipvalue ? ipvalue?.split(/\s*,\s*/) : []; + + const auditLog: AuditLog = { + uuid: ctx.reqId, + dataSource: (ctx.request.header['x-data-source'] || 'main') as string, + resource: resourceName, + action: ctx.action.actionName, + userId: ctx.state?.currentUser?.id, + roleName: ctx.state?.currentRole, + ip: ips.length > 0 ? ips[0] : ctx.request.ip, + ua: ctx.request.header['user-agent'], + status: ctx.response.status, + }; + return auditLog; + } + + async output(ctx: any, reqId: any, metadata?: Record) { + try { + const { resourceName, actionName } = ctx.action; + const action: Action = this.getAction(actionName, resourceName); + if (!action) { + return; + } + const auditLog: AuditLog = this.formatAuditData(ctx); + auditLog.uuid = reqId; + auditLog.status = ctx.status; + if (typeof action !== 'string') { + if (action.getUserInfo) { + const userInfo = await action.getUserInfo(ctx); + if (userInfo) { + if (userInfo.userId) { + auditLog.userId = userInfo.userId; + } + if (userInfo.roleName) { + auditLog.roleName = userInfo.roleName; + } + } + } + if (action.getMetaData) { + const extra = await action.getMetaData(ctx); + auditLog.metadata = { ...metadata, ...extra }; + } else { + const defaultMetaData = await this.getDefaultMetaData(ctx); + auditLog.metadata = { ...metadata, ...defaultMetaData }; + } + if (action.getSourceAndTarget) { + const sourceAndTarget = await action.getSourceAndTarget(ctx); + if (sourceAndTarget) { + auditLog.sourceCollection = sourceAndTarget.sourceCollection; + auditLog.sourceRecordUK = sourceAndTarget.sourceRecordUK; + auditLog.targetCollection = sourceAndTarget.targetCollection; + auditLog.targetRecordUK = sourceAndTarget.targetRecordUK; + } + } + } else { + const defaultMetaData = await this.getDefaultMetaData(ctx); + auditLog.metadata = { ...metadata, ...defaultMetaData }; + } + this.logger.log(auditLog); + } catch (err) { + ctx.log?.error(err); + } + } + // 中间件 + middleware() { + return async (ctx: any, next: any) => { + const reqId = ctx.reqId; + let metadata = {}; + try { + await next(); + } catch (err) { + // 操作失败的时候 + // HTTP相应状态码和error message 放到 metadata + metadata = { + status: ctx.status, + errMsg: err.message, + }; + throw err; + } finally { + if (this.logger) { + this.output(ctx, reqId, metadata); + } + } + }; + } +} diff --git a/packages/core/server/src/helper.ts b/packages/core/server/src/helper.ts index bd8c7885ea..6dcf96a30a 100644 --- a/packages/core/server/src/helper.ts +++ b/packages/core/server/src/helper.ts @@ -47,6 +47,8 @@ export function registerMiddlewares(app: Application, options: ApplicationOption { tag: 'generateReqId' }, ); + app.use(app.auditManager.middleware(), { tag: 'audit', after: 'generateReqId' }); + app.use(requestLogger(app.name, app.requestLogger, options.logger?.request), { tag: 'logger' }); app.use( diff --git a/packages/core/server/src/index.ts b/packages/core/server/src/index.ts index 8e801c1e76..806e3e26ad 100644 --- a/packages/core/server/src/index.ts +++ b/packages/core/server/src/index.ts @@ -15,6 +15,7 @@ export * as middlewares from './middlewares'; export * from './migration'; export * from './plugin'; export * from './plugin-manager'; +export * from './audit-manager'; export * from './pub-sub-manager'; export const OFFICIAL_PLUGIN_PREFIX = '@nocobase/plugin-'; diff --git a/packages/core/server/src/plugin-manager/plugin-manager.ts b/packages/core/server/src/plugin-manager/plugin-manager.ts index 21097505e2..0c074b837a 100644 --- a/packages/core/server/src/plugin-manager/plugin-manager.ts +++ b/packages/core/server/src/plugin-manager/plugin-manager.ts @@ -455,6 +455,41 @@ export class PluginManager { await this.app.emitAsync('afterLoadPlugin', plugin, options); } + const getSourceAndTargetForAddAction = async (ctx: any) => { + const { packageName } = ctx.action.params; + return { + targetCollection: 'applicationPlugins', + targetRecordUK: packageName, + }; + }; + + const getSourceAndTargetForUpdateAction = async (ctx: any) => { + let { packageName } = ctx.action.params; + if (ctx.file) { + packageName = ctx.request.body.packageName; + } + return { + targetCollection: 'applicationPlugins', + targetRecordUK: packageName, + }; + }; + + const getSourceAndTargetForOtherActions = async (ctx: any) => { + const { filterByTk } = ctx.action.params; + return { + targetCollection: 'applicationPlugins', + targetRecordUK: filterByTk, + }; + }; + + this.app.auditManager.registerActions([ + { name: 'pm:add', getSourceAndTarget: getSourceAndTargetForAddAction }, + { name: 'pm:update', getSourceAndTarget: getSourceAndTargetForUpdateAction }, + { name: 'pm:enable', getSourceAndTarget: getSourceAndTargetForOtherActions }, + { name: 'pm:disable', getSourceAndTarget: getSourceAndTargetForOtherActions }, + { name: 'pm:remove', getSourceAndTarget: getSourceAndTargetForOtherActions }, + ]); + this.app.log.debug('plugins loaded'); this.app.setMaintainingMessage('plugins loaded'); } diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts index 6cc42d42ea..9feaecdabc 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts @@ -10,7 +10,6 @@ import { Cache } from '@nocobase/cache'; import { Model } from '@nocobase/database'; import { InstallOptions, Plugin } from '@nocobase/server'; -import { resolve } from 'path'; import { namespace, presetAuthType, presetAuthenticator } from '../preset'; import authActions from './actions/auth'; import authenticatorsActions from './actions/authenticators'; @@ -112,6 +111,126 @@ export class PluginAuthServer extends Plugin { this.app.on('cache:del:auth', async ({ userId }) => { await this.cache.del(`auth:${userId}`); }); + + this.app.auditManager.registerActions([ + { + name: 'auth:signIn', + getMetaData: async (ctx: any) => { + let body = {}; + if (ctx.status === 200) { + body = { + data: { + ...ctx.body.data, + token: undefined, + }, + }; + } else { + body = ctx.body; + } + return { + request: { + params: ctx.request?.params, + body: { + ...ctx.request?.body, + password: undefined, + }, + path: ctx.request?.path, + headers: { + 'x-authenticator': ctx.request?.headers['x-authenticator'], + 'x-locale': ctx.request?.headers['x-locale'], + 'x-timezone': ctx.request?.headers['x-timezone'], + }, + }, + response: { + body, + }, + }; + }, + getUserInfo: async (ctx: any) => { + if (!ctx.body?.data?.user) { + return null; + } + // 查询用户角色 + const userId = ctx.body.data.user.id; + const user = await ctx.db.getRepository('users').findOne({ + filterByTk: userId, + }); + const roles = await user?.getRoles(); + if (!roles) { + return { + userId, + }; + } else { + if (roles.length === 1) { + return { + userId, + roleName: roles[0].name, + }; + } else { + // 多角色的情况下暂时不返回角色名 + return { + userId, + }; + } + } + }, + }, + { + name: 'auth:signUp', + getMetaData: async (ctx: any) => { + return { + request: { + params: ctx.request?.params, + body: { + ...ctx.request?.body, + password: undefined, + confirm_password: undefined, + }, + path: ctx.request?.path, + headers: { + 'x-authenticator': ctx.request?.headers['x-authenticator'], + 'x-locale': ctx.request?.headers['x-locale'], + 'x-timezone': ctx.request?.headers['x-timezone'], + }, + }, + response: { + body: { + ...ctx.response?.body, + token: undefined, + }, + }, + }; + }, + }, + { + name: 'auth:changePassword', + getMetaData: async (ctx: any) => { + return { + request: { + params: ctx.request.params, + query: ctx.request.query, + body: {}, + path: ctx.request.path, + headers: { + 'x-authenticator': ctx.request?.headers['x-authenticator'], + 'x-locale': ctx.request?.headers['x-locale'], + 'x-timezone': ctx.request?.headers['x-timezone'], + }, + }, + response: { + body: {}, + }, + }; + }, + getSourceAndTarget: async (ctx: any) => { + return { + targetCollection: 'users', + targetRecordUK: ctx.auth.user.id, + }; + }, + }, + 'auth:signOut', + ]); } async install(options?: InstallOptions) { diff --git a/packages/plugins/@nocobase/plugin-client/src/server/server.ts b/packages/plugins/@nocobase/plugin-client/src/server/server.ts index e0772d7bb7..369a083608 100644 --- a/packages/plugins/@nocobase/plugin-client/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-client/src/server/server.ts @@ -118,6 +118,8 @@ export class PluginClientServer extends Plugin { }, }, }); + + this.app.auditManager.registerActions(['app:restart', 'app:clearCache']); } } diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts index ce84e72ab2..e1b7157b85 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts @@ -97,6 +97,33 @@ export class PluginUISchemaStorageServer extends Plugin { }, }); + const getSourceAndTargetForRemoveAction = async (ctx: any) => { + const { filterByTk } = ctx.action.params; + return { + targetCollection: 'uiSchemas', + targetRecordUK: filterByTk, + }; + }; + + const getSourceAndTargetForInsertAdjacentAction = async (ctx: any) => { + return { + targetCollection: 'uiSchemas', + targetRecordUK: ctx.request.body?.schema?.['x-uid'], + }; + }; + + const getSourceAndTargetForPatchAction = async (ctx: any) => { + return { + targetCollection: 'uiSchemas', + targetRecordUK: ctx.request.body?.['x-uid'], + }; + }; + this.app.auditManager.registerActions([ + { name: 'uiSchemas:remove', getSourceAndTarget: getSourceAndTargetForRemoveAction }, + { name: 'uiSchemas:insertAdjacent', getSourceAndTarget: getSourceAndTargetForInsertAdjacentAction }, + { name: 'uiSchemas:patch', getSourceAndTarget: getSourceAndTargetForPatchAction }, + ]); + await this.importCollections(resolve(__dirname, 'collections')); } } diff --git a/packages/plugins/@nocobase/plugin-users/src/server/server.ts b/packages/plugins/@nocobase/plugin-users/src/server/server.ts index df87f5832f..3e8f6ad064 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/server.ts @@ -150,6 +150,45 @@ export default class PluginUsersServer extends Plugin { name: `pm.${this.name}`, actions: ['users:*'], }); + + const getMetaDataForUpdateProfileAction = async (ctx: any) => { + return { + request: { + params: ctx.request.params, + query: ctx.request.query, + body: { + ...ctx.request.body, + password: '******', + }, + path: ctx.request.path, + }, + response: { + body: ctx.body, + }, + }; + }; + + const getSourceAndTargetForUpdateProfileAction = async (ctx: any) => { + const { id } = ctx.state.currentUser; + let idStr = ''; + if (typeof id === 'number') { + idStr = id.toString(); + } else if (typeof id === 'string') { + idStr = id; + } + return { + targetCollection: 'users', + targetRecordUK: idStr, + }; + }; + + this.app.auditManager.registerActions([ + { + name: 'users:updateProfile', + getMetaData: getMetaDataForUpdateProfileAction, + getSourceAndTarget: getSourceAndTargetForUpdateProfileAction, + }, + ]); } async load() {