feat: 支持审计日志,操作日志

This commit is contained in:
xiaojunnuo
2026-07-12 02:29:54 +08:00
parent 4250d0e266
commit f2855d6dac
83 changed files with 1252 additions and 1341 deletions
@@ -0,0 +1,35 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { AuditLogContext } from "./audit.js";
// AuditLog decorator and getAuditLogOptions are removed since auditLog()
// now signals audit intent directly via ctx.auditLog.enabled
describe("AuditLogContext type", () => {
it("supports enabled flag", () => {
const ctx: AuditLogContext = {
type: "pipeline",
action: "删除流水线",
append: ["ID:5"],
content: "删除了流水线(ID:5)",
projectId: 3,
projectName: "默认项目",
enabled: true,
};
assert.equal(ctx.enabled, true);
assert.equal(ctx.type, "pipeline");
assert.equal(ctx.content, "删除了流水线(ID:5)");
assert.equal(ctx.projectId, 3);
});
it("works with minimal fields", () => {
const ctx: AuditLogContext = {
enabled: true,
append: ["提交2条"],
};
assert.equal(ctx.enabled, true);
});
});
@@ -0,0 +1,21 @@
export type AuditLogOptions = {
type?: string;
action?: string;
content?: string;
template?: string;
disabled?: boolean;
};
export type AuditLogContext = {
type?: string;
action?: string;
append?: string | string[];
content?: string;
projectId?: number;
projectName?: string;
enabled?: boolean;
allowAnonymous?: boolean;
scope?: string;
userId?: number;
username?: string;
};
@@ -3,6 +3,7 @@ import type { IMidwayContainer } from "@midwayjs/core";
import * as koa from "@midwayjs/koa";
import { Constants } from "./constants.js";
import { isEnterprise } from "./mode.js";
import type { AuditLogContext } from "./audit.js";
export abstract class BaseController {
@Inject()
@@ -128,17 +129,42 @@ export abstract class BaseController {
return { projectId, userId };
}
async auditLog(bean: { type: string; action: string; content: string; projectId?: number; projectName?: string }) {
const auditService: any = await this.applicationContext.getAsync("auditService");
await auditService.log({
userId: this.getUserId(),
type: bean.type,
action: bean.action,
content: bean.content,
username: this.ctx.user?.username,
projectId: bean.projectId,
projectName: bean.projectName,
ipAddress: this.ctx.ip,
});
getAuditType(): string {
return "unknown";
}
auditLog(bean: { type?: string; action?: string; content?: string; append?: string | string[]; projectId?: number; userId?: number; username?: string } = {}) {
const auditLog = this.ensureAuditLogContext();
auditLog.enabled = true;
if (bean.userId != null) {
auditLog.userId = bean.userId;
}
if (bean.username != null) {
auditLog.username = bean.username;
}
if (bean.type != null) {
auditLog.type = bean.type;
}
if (bean.action != null) {
auditLog.action = bean.action;
}
if (bean.projectId != null) {
auditLog.projectId = bean.projectId;
}
if (bean.content) {
auditLog.content = bean.content;
}
if (bean.append) {
const items = Array.isArray(bean.append) ? bean.append : [bean.append];
const old = Array.isArray(auditLog.append) ? auditLog.append : auditLog.append ? [auditLog.append] : [];
auditLog.append = [...old, ...items].filter(item => item && String(item).trim());
}
}
private ensureAuditLogContext(): AuditLogContext {
if (!this.ctx.auditLog) {
this.ctx.auditLog = {};
}
return this.ctx.auditLog;
}
}
@@ -279,7 +279,7 @@ export abstract class BaseService<T> {
return item != null && item != "";
});
}
async batchDelete(ids: number[], userId: number, projectId?: number) {
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<number> {
ids = this.filterIds(ids);
if (userId != null) {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -295,6 +295,7 @@ export abstract class BaseService<T> {
}
await this.delete(ids);
return ids.length;
}
async findOne(options: FindOneOptions<T>) {
+35 -35
View File
@@ -1,21 +1,21 @@
export const Constants = {
dataDir: './data',
dataDir: "./data",
role: {
defaultUser: 3,
},
per: {
//无需登录
guest: '_guest_',
guest: "_guest_",
//无需登录
anonymous: '_guest_',
anonymous: "_guest_",
//无需登录,有 token 时解析当前用户
guestOptionalAuth: '_guestOptionalAuth_',
guestOptionalAuth: "_guestOptionalAuth_",
//仅需要登录
authOnly: '_authOnly_',
authOnly: "_authOnly_",
//仅需要登录
loginOnly: '_authOnly_',
loginOnly: "_authOnly_",
open: '_open_',
open: "_open_",
},
res: {
serverError(message: string) {
@@ -26,102 +26,102 @@ export const Constants = {
},
error: {
code: 1,
message: 'Internal server error',
message: "Internal server error",
},
success: {
code: 0,
message: 'success',
message: "success",
},
validation: {
code: 10,
message: '参数错误',
message: "参数错误",
},
needvip: {
code: 88,
message: '需要VIP',
message: "需要VIP",
},
needsuite: {
code: 89,
message: '需要购买或升级套餐',
message: "需要购买或升级套餐",
},
loginError: {
code: 2,
message: '登录失败',
message: "登录失败",
},
codeError: {
code: 3,
message: '验证码错误',
message: "验证码错误",
},
auth: {
code: 401,
message: '您还未登录或token已过期',
message: "您还未登录或token已过期",
},
permission: {
code: 402,
message: '您没有权限',
message: "您没有权限",
},
param: {
code: 400,
message: '参数错误',
message: "参数错误",
},
notFound: {
code: 404,
message: '页面/文件/资源不存在',
message: "页面/文件/资源不存在",
},
preview: {
code: 10001,
message: '对不起,预览环境不允许修改此数据',
message: "对不起,预览环境不允许修改此数据",
},
siteOff:{
siteOff: {
code: 10010,
message: '站点已关闭',
message: "站点已关闭",
},
need2fa:{
need2fa: {
code: 10020,
message: '需要2FA认证',
message: "需要2FA认证",
},
openKeyError: {
code: 20000,
message: 'ApiToken错误',
message: "ApiToken错误",
},
openKeySignError: {
code: 20001,
message: 'ApiToken签名错误',
message: "ApiToken签名错误",
},
openKeyExpiresError: {
code: 20002,
message: 'ApiToken时间戳错误',
message: "ApiToken时间戳错误",
},
openKeySignTypeError: {
code: 20003,
message: 'ApiToken签名类型不支持',
message: "ApiToken签名类型不支持",
},
openParamError: {
code: 20010,
message: '请求参数错误',
message: "请求参数错误",
},
openCertNotFound: {
code: 20011,
message: '证书不存在',
message: "证书不存在",
},
openCertNotReady: {
code: 20012,
message: '证书还未生成',
message: "证书还未生成",
},
openCertApplying: {
code: 20013,
message: '证书正在申请中,请稍后重新获取',
message: "证书正在申请中,请稍后重新获取",
},
openDomainNoVerifier:{
openDomainNoVerifier: {
code: 20014,
message: '域名校验方式未配置',
message: "域名校验方式未配置",
},
openEmailNotFound: {
code: 20021,
message: '用户邮箱还未配置',
message: "用户邮箱还未配置",
},
},
systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1 // 企业模式用户id固定为-1
enterpriseUserId: -1, // 企业模式用户id固定为-1
};
@@ -1,12 +1,14 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 通用异常
*/
export class LoginErrorException extends BaseException {
leftCount: number;
constructor(message, leftCount: number) {
super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
userId?: number;
constructor(message, leftCount: number, userId?: number) {
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
this.leftCount = leftCount;
this.userId = userId;
}
}
+9 -8
View File
@@ -1,8 +1,9 @@
export * from './base-controller.js';
export * from './constants.js';
export * from './crud-controller.js';
export * from './enum-item.js';
export * from './exception/index.js';
export * from './result.js';
export * from './base-service.js';
export * from "./mode.js"
export * from "./base-controller.js";
export * from "./constants.js";
export * from "./crud-controller.js";
export * from "./enum-item.js";
export * from "./exception/index.js";
export * from "./result.js";
export * from "./base-service.js";
export * from "./audit.js";
export * from "./mode.js";
+8 -8
View File
@@ -1,12 +1,12 @@
let adminMode = "saas"
let adminMode = "saas";
export function setAdminMode(mode:string = "saas"){
adminMode = mode
export function setAdminMode(mode: string = "saas") {
adminMode = mode;
}
export function getAdminMode(){
return adminMode
export function getAdminMode() {
return adminMode;
}
export function isEnterprise(){
return adminMode === "enterprise"
}
export function isEnterprise() {
return adminMode === "enterprise";
}