mirror of
https://github.com/certd/certd.git
synced 2026-07-13 00:37:36 +08:00
chore: 登录失败也记录日志
This commit is contained in:
@@ -18,4 +18,5 @@ export type AuditLogContext = {
|
||||
scope?: string;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
success?: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createRequestParamDecorator } from "@midwayjs/core";
|
||||
|
||||
export const AuditLog = (opts: { enabled?: boolean }) => {
|
||||
return createRequestParamDecorator(ctx => {
|
||||
if (!ctx.auditLog) {
|
||||
ctx.auditLog = {};
|
||||
}
|
||||
if (opts.enabled !== undefined) {
|
||||
ctx.auditLog.enabled = opts.enabled || true;
|
||||
}
|
||||
return ctx.auditLog;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./decoractor.js";
|
||||
@@ -7,3 +7,4 @@ export * from "./result.js";
|
||||
export * from "./base-service.js";
|
||||
export * from "./audit.js";
|
||||
export * from "./mode.js";
|
||||
export * from "./core/index.js";
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
ALTER TABLE `cd_audit_log` ADD COLUMN `scope` varchar(32) NOT NULL DEFAULT 'user';
|
||||
CREATE INDEX `index_audit_log_scope` ON `cd_audit_log` (`scope`);
|
||||
|
||||
ALTER TABLE `cd_audit_log` ADD COLUMN `success` tinyint(1) NOT NULL DEFAULT 1;
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('user');
|
||||
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
|
||||
|
||||
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT true;
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('user');
|
||||
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
|
||||
|
||||
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT (1);
|
||||
@@ -134,6 +134,5 @@ export class MainConfiguration {
|
||||
logger.info(text);
|
||||
});
|
||||
logger.info("当前环境:", this.app.getEnv()); // prod
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class ForgotPasswordController extends BaseController {
|
||||
} else {
|
||||
throw new CommonException("暂不支持的找回类型,请联系管理员找回");
|
||||
}
|
||||
const {id,username} = await this.userService.forgotPassword(body);
|
||||
const { id, username } = await this.userService.forgotPassword(body);
|
||||
username && this.loginService.clearCacheOnSuccess(username);
|
||||
this.auditLog({ userId: id, content: "用户请求找回密码" });
|
||||
return this.ok();
|
||||
|
||||
@@ -7,8 +7,6 @@ import { CaptchaService } from "../../../modules/basic/service/captcha-service.j
|
||||
import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
|
||||
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provide()
|
||||
@Controller("/api/")
|
||||
export class LoginController extends BaseController {
|
||||
@@ -46,10 +44,10 @@ export class LoginController extends BaseController {
|
||||
try {
|
||||
const token = await this.loginService.loginByPassword(body);
|
||||
this.writeTokenCookie(token);
|
||||
this.auditLog({ userId: token.userId, username: body.username, content: `用户「${body.username}」登录成功` });
|
||||
this.auditLog({ userId: token.userId, username: token.username, content: `用户「${body.username}」登录成功` });
|
||||
return this.ok(token);
|
||||
} catch (err:any) {
|
||||
this.auditLog({userId:err.userId, username: body.username, content: `用户「${body.username}」登录失败` });
|
||||
} catch (err: any) {
|
||||
this.auditLog({userId:err.userId, username: body.username, content: `用户「${body.username}」登录失败:${err.message}` });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -79,10 +77,10 @@ export class LoginController extends BaseController {
|
||||
});
|
||||
|
||||
this.writeTokenCookie(token);
|
||||
this.auditLog({ userId: token.userId, username: body.mobile, content: `用户「${body.mobile}」短信登录成功` });
|
||||
this.auditLog({ userId: token.userId, username: token.username, content: `用户「${body.mobile}」短信登录成功` });
|
||||
return this.ok(token);
|
||||
} catch (err) {
|
||||
this.auditLog({ userId: err.userId, username: body.mobile, content: `用户「${body.mobile}」短信登录失败` });
|
||||
} catch (err: any) {
|
||||
this.auditLog({userId: err.userId, username: body.mobile, content: `用户「${body.mobile}」短信登录失败:${err.message}` });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -99,10 +97,10 @@ export class LoginController extends BaseController {
|
||||
});
|
||||
|
||||
this.writeTokenCookie(token);
|
||||
this.auditLog({ userId: token.userId, username: body.loginId, content: `用户「${body.loginId}」两步验证登录成功` });
|
||||
this.auditLog({ userId: token.userId, username: token.username, content: `用户「${body.loginId}」两步验证登录成功` });
|
||||
return this.ok(token);
|
||||
} catch (err) {
|
||||
this.auditLog({ userId: err.userId, username: body.loginId, content: `用户「${body.loginId}」两步验证登录失败` });
|
||||
} catch (err: any) {
|
||||
this.auditLog({userId: err.userId, username: body.loginId, content: `用户「${body.loginId}」两步验证登录失败:${err.message}` });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -132,10 +130,10 @@ export class LoginController extends BaseController {
|
||||
);
|
||||
|
||||
this.writeTokenCookie(token);
|
||||
this.auditLog({ userId: token.userId, content: "用户Passkey登录成功" });
|
||||
this.auditLog({ userId: token.userId, username: token.username, content: "用户Passkey登录成功" });
|
||||
return this.ok(token);
|
||||
} catch (err) {
|
||||
this.auditLog({ userId: err.userId, content: "用户Passkey登录失败" });
|
||||
} catch (err: any) {
|
||||
this.auditLog({userId: err.userId, username: body.credential, content: `用户Passkey登录失败:${err.message}` });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,18 @@ export class AuditLogController extends BaseController {
|
||||
@Post("/page", { description: Constants.per.authOnly, summary: "分页查询当前用户操作日志" })
|
||||
async page(@Body(ALL) body: any) {
|
||||
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||
const query: any = { }
|
||||
const query: any = {};
|
||||
if (projectId) {
|
||||
//如果是项目级别,排除userId参数,因为日志这里userId不是-1 ,而是实际的用户
|
||||
query.projectId = projectId;
|
||||
}else{
|
||||
} else {
|
||||
query.userId = userId;
|
||||
}
|
||||
body.query = {
|
||||
...body.query || {},
|
||||
...(body.query || {}),
|
||||
...query,
|
||||
scope: "user"
|
||||
}
|
||||
scope: "user",
|
||||
};
|
||||
|
||||
const pageRet = await this.auditService.page(body);
|
||||
return this.ok(pageRet);
|
||||
|
||||
@@ -33,7 +33,7 @@ export class DnsPersistRecordController extends CrudController<DnsPersistRecordS
|
||||
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||
bean.projectId = projectId;
|
||||
bean.userId = userId;
|
||||
const res = await this.getService().add(bean);
|
||||
const res = await this.getService().add(bean);
|
||||
this.auditLog({ content: `新增了DNS持久验证记录(ID:${res.id})` });
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@@ -94,14 +94,15 @@ describe("AuditLogMiddleware", () => {
|
||||
assert.equal(records.length, 0);
|
||||
});
|
||||
|
||||
it("skips failed response", async () => {
|
||||
it("writes audit log with success=false on failed response", async () => {
|
||||
const { middleware, records } = createMiddleware();
|
||||
const ctx = createCtx();
|
||||
ctx.body = Constants.res.error;
|
||||
|
||||
await middleware.resolve()(ctx, async () => {});
|
||||
|
||||
assert.equal(records.length, 0);
|
||||
assert.equal(records.length, 1);
|
||||
assert.equal(records[0].success, false);
|
||||
});
|
||||
|
||||
it("skips anonymous request", async () => {
|
||||
|
||||
@@ -2,53 +2,47 @@ import { Inject, Provide } from "@midwayjs/core";
|
||||
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from "@midwayjs/koa";
|
||||
import { Constants } from "@certd/lib-server";
|
||||
import { AuditService } from "../modules/sys/enterprise/service/audit-service.js";
|
||||
import { isPlus } from "@certd/plus-core";
|
||||
|
||||
@Provide()
|
||||
export class AuditLogMiddleware implements IWebMiddleware {
|
||||
@Inject()
|
||||
auditService: AuditService;
|
||||
|
||||
private isPlusFn: (() => Promise<boolean>) | null = null;
|
||||
|
||||
private async getIsPlus(): Promise<boolean> {
|
||||
if (!this.isPlusFn) {
|
||||
try {
|
||||
const mod = await import("@certd/plus-core");
|
||||
this.isPlusFn = async () => mod.isPlus();
|
||||
} catch {
|
||||
this.isPlusFn = async () => false;
|
||||
}
|
||||
}
|
||||
return this.isPlusFn();
|
||||
}
|
||||
|
||||
resolve() {
|
||||
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
|
||||
await next();
|
||||
await this.writeAuditLog(ctx);
|
||||
try {
|
||||
await next();
|
||||
await this.writeAuditLog(ctx);
|
||||
} catch (err) {
|
||||
await this.writeAuditLog(ctx, err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async writeAuditLog(ctx: IMidwayKoaContext) {
|
||||
private async writeAuditLog(ctx: IMidwayKoaContext, err?: any) {
|
||||
const routeInfo = ctx.auditRouteInfo;
|
||||
if (!routeInfo) {
|
||||
return;
|
||||
}
|
||||
if (!ctx.auditLog?.enabled) {
|
||||
return;
|
||||
}
|
||||
const isPlus = await this.getIsPlus();
|
||||
if (!isPlus) {
|
||||
return;
|
||||
}
|
||||
if (!this.isSuccessResponse(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auditLog = ctx.auditLog;
|
||||
if (!auditLog.enabled) {
|
||||
if (!auditLog?.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!isPlus()) {
|
||||
return;
|
||||
}
|
||||
let isSuccess = this.isSuccessResponse(ctx);
|
||||
|
||||
if (err) {
|
||||
isSuccess = false;
|
||||
if (err?.userId != null && auditLog?.userId == null) {
|
||||
auditLog.userId = err.userId;
|
||||
}
|
||||
}
|
||||
|
||||
const type = auditLog.type || (await this.resolveControllerType(routeInfo.controllerClz, ctx as any));
|
||||
const action = auditLog.action || routeInfo.summary || "";
|
||||
const append = auditLog.append;
|
||||
@@ -61,7 +55,7 @@ export class AuditLogMiddleware implements IWebMiddleware {
|
||||
|
||||
const projectId = auditLog.projectId ?? ctx.projectId ?? 0;
|
||||
const scope = auditLog.scope || (ctx.path.startsWith("/api/sys/") ? "system" : "user");
|
||||
const ipAddress = ctx.ip || "";
|
||||
const ipAddress = ctx.ip || "";
|
||||
|
||||
await this.auditService.log({
|
||||
userId: auditLog.userId ?? ctx.user?.id ?? 0,
|
||||
@@ -70,8 +64,10 @@ export class AuditLogMiddleware implements IWebMiddleware {
|
||||
content,
|
||||
username: auditLog.username || ctx.user?.username,
|
||||
projectId,
|
||||
projectName: auditLog.projectName,
|
||||
ipAddress,
|
||||
scope,
|
||||
success: isSuccess,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -252,6 +252,7 @@ export class LoginService {
|
||||
token,
|
||||
expire,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -974,7 +974,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
await this.checkUserId(id, projectId, "projectId");
|
||||
}
|
||||
await this.delete(id);
|
||||
ids.push(id);
|
||||
ids.push(id);
|
||||
}
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ export class AuditLogEntity {
|
||||
@Column({ name: "ip_address", comment: "IP地址" })
|
||||
ipAddress: string;
|
||||
|
||||
@Column({ name: "success", comment: "是否成功", default: true })
|
||||
success: boolean;
|
||||
|
||||
@Column({
|
||||
name: "create_time",
|
||||
comment: "创建时间",
|
||||
|
||||
@@ -25,13 +25,13 @@ export class AuditService extends BaseService<AuditLogEntity> {
|
||||
const end = pageReq.query.createTime[1];
|
||||
delete pageReq.query.createTime;
|
||||
pageReq.buildQuery = (qb: any) => {
|
||||
qb.andWhere("main.createTime BETWEEN :start AND :end", { start, end });
|
||||
qb.andWhere("main.createTime BETWEEN :start AND :end", { start, end });
|
||||
};
|
||||
}
|
||||
return await super.page(pageReq);
|
||||
}
|
||||
|
||||
async log(params: { userId: number; type: string; action: string; content: string; username?: string; projectId?: number; ipAddress?: string; scope?: string }): Promise<void> {
|
||||
async log(params: { userId: number; type: string; action: string; content: string; username?: string; projectId?: number; projectName?: string; ipAddress?: string; scope?: string; success?: boolean }): Promise<void> {
|
||||
try {
|
||||
let { username } = params;
|
||||
if (!username && params.userId != null) {
|
||||
@@ -47,9 +47,10 @@ export class AuditService extends BaseService<AuditLogEntity> {
|
||||
entity.action = params.action;
|
||||
entity.content = params.content;
|
||||
entity.projectId = params.projectId || 0;
|
||||
entity.projectName = "";
|
||||
entity.projectName = params.projectName || "";
|
||||
entity.ipAddress = params.ipAddress || "";
|
||||
entity.scope = params.scope || "user";
|
||||
entity.success = params.success ?? true;
|
||||
await this.repository.save(entity);
|
||||
} catch (e) {
|
||||
logger.error("写入审计日志失败:", e);
|
||||
|
||||
Reference in New Issue
Block a user