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