Assist_Design/apps/bff/src/infra/audit/audit.service.ts

198 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-08-22 17:02:49 +09:00
import { Injectable, Inject } from "@nestjs/common";
2025-08-27 10:54:05 +09:00
import { Prisma } from "@prisma/client";
import { PrismaService } from "../database/prisma.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
2025-08-22 17:02:49 +09:00
import { Logger } from "nestjs-pino";
export enum AuditAction {
2025-08-21 15:24:40 +09:00
LOGIN_SUCCESS = "LOGIN_SUCCESS",
LOGIN_FAILED = "LOGIN_FAILED",
LOGOUT = "LOGOUT",
SIGNUP = "SIGNUP",
PASSWORD_RESET = "PASSWORD_RESET",
PASSWORD_CHANGE = "PASSWORD_CHANGE",
ACCOUNT_LOCKED = "ACCOUNT_LOCKED",
ACCOUNT_UNLOCKED = "ACCOUNT_UNLOCKED",
PROFILE_UPDATE = "PROFILE_UPDATE",
MFA_ENABLED = "MFA_ENABLED",
MFA_DISABLED = "MFA_DISABLED",
API_ACCESS = "API_ACCESS",
SYSTEM_MAINTENANCE = "SYSTEM_MAINTENANCE",
}
export interface AuditLogData {
userId?: string;
action: AuditAction;
resource?: string;
2025-08-27 10:54:05 +09:00
details?: Record<string, unknown> | string | number | boolean | null;
ipAddress?: string;
userAgent?: string;
success?: boolean;
error?: string;
}
@Injectable()
export class AuditService {
2025-08-22 17:02:49 +09:00
constructor(
private readonly prisma: PrismaService,
@Inject(Logger) private readonly logger: Logger
) {}
async log(data: AuditLogData): Promise<void> {
try {
await this.prisma.auditLog.create({
data: {
userId: data.userId,
action: data.action,
resource: data.resource,
2025-08-27 10:54:05 +09:00
details:
data.details === undefined
? undefined
: data.details === null
? Prisma.JsonNull
: (JSON.parse(JSON.stringify(data.details)) as Prisma.InputJsonValue),
ipAddress: data.ipAddress,
userAgent: data.userAgent,
success: data.success ?? true,
error: data.error,
},
});
} catch (error) {
2025-08-22 17:02:49 +09:00
this.logger.error("Audit logging failed", {
errorType: error instanceof Error ? error.constructor.name : "Unknown",
message: getErrorMessage(error),
});
}
}
async logAuthEvent(
action: AuditAction,
userId?: string,
2025-08-27 10:54:05 +09:00
details?: Record<string, unknown> | string | number | boolean | null,
request?: {
headers?: Record<string, string | string[] | undefined>;
ip?: string;
connection?: { remoteAddress?: string };
socket?: { remoteAddress?: string };
},
success: boolean = true,
2025-08-22 17:02:49 +09:00
error?: string
): Promise<void> {
const ipAddress = this.extractIpAddress(request);
2025-08-27 10:54:05 +09:00
const uaHeader = request?.headers?.["user-agent"];
const userAgent = Array.isArray(uaHeader) ? uaHeader[0] : uaHeader;
await this.log({
userId,
action,
2025-08-21 15:24:40 +09:00
resource: "auth",
details,
ipAddress,
userAgent,
success,
error,
});
}
2025-08-27 10:54:05 +09:00
private extractIpAddress(request?: {
headers?: Record<string, string | string[] | undefined>;
connection?: { remoteAddress?: string };
socket?: { remoteAddress?: string };
ip?: string;
}): string | undefined {
if (!request) return undefined;
2025-08-21 15:24:40 +09:00
return (
2025-08-27 10:54:05 +09:00
(typeof request.headers?.["x-forwarded-for"] === "string"
? request.headers["x-forwarded-for"].split(",")[0]?.trim()
: Array.isArray(request.headers?.["x-forwarded-for"]) &&
request.headers?.["x-forwarded-for"][0]
? request.headers["x-forwarded-for"][0]
: undefined) ||
(typeof request.headers?.["x-real-ip"] === "string"
? request.headers["x-real-ip"]
: undefined) ||
request.connection?.remoteAddress ||
request.socket?.remoteAddress ||
request.ip
);
}
async getAuditLogs({
page,
limit,
action,
userId,
}: {
page: number;
limit: number;
action?: AuditAction;
userId?: string;
}) {
const skip = (page - 1) * limit;
const where: Prisma.AuditLogWhereInput = {};
if (action) where.action = action;
if (userId) where.userId = userId;
const [logs, total] = await Promise.all([
this.prisma.auditLog.findMany({
where,
include: {
user: {
select: {
id: true,
email: true,
},
},
},
orderBy: { createdAt: "desc" },
skip,
take: limit,
}),
this.prisma.auditLog.count({ where }),
]);
return { logs, total };
}
async getSecurityStats() {
const today = new Date(new Date().setHours(0, 0, 0, 0));
const [totalUsers, lockedAccounts, failedLoginsToday, successfulLoginsToday] =
await Promise.all([
this.prisma.user.count(),
this.prisma.user.count({
where: {
lockedUntil: {
gt: new Date(),
},
},
}),
this.prisma.auditLog.count({
where: {
action: AuditAction.LOGIN_FAILED,
createdAt: {
gte: today,
},
},
}),
this.prisma.auditLog.count({
where: {
action: AuditAction.LOGIN_SUCCESS,
createdAt: {
gte: today,
},
},
}),
]);
return {
totalUsers,
lockedAccounts,
failedLoginsToday,
successfulLoginsToday,
securityEventsToday: failedLoginsToday + successfulLoginsToday,
};
}
}