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

138 lines
3.4 KiB
TypeScript
Raw Normal View History

2025-08-21 15:24:40 +09:00
import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
// Define audit actions to match Prisma schema
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",
}
export interface AuditLogData {
userId?: string;
action: AuditAction;
resource?: string;
details?: any;
ipAddress?: string;
userAgent?: string;
success?: boolean;
error?: string;
}
@Injectable()
export class AuditService {
constructor(private readonly prisma: PrismaService) {}
// Expose prisma for admin operations
get prismaClient() {
return this.prisma;
}
async log(data: AuditLogData): Promise<void> {
try {
await this.prisma.auditLog.create({
data: {
userId: data.userId,
action: data.action,
resource: data.resource,
2025-08-21 15:24:40 +09:00
details: data.details
? JSON.parse(JSON.stringify(data.details))
: null,
ipAddress: data.ipAddress,
userAgent: data.userAgent,
success: data.success ?? true,
error: data.error,
},
});
} catch (error) {
// Don't fail the original operation if audit logging fails
// Use a simple console.error here since we can't use this.logger (circular dependency risk)
2025-08-21 15:24:40 +09:00
console.error("Failed to create audit log:", error);
}
}
async logAuthEvent(
action: AuditAction,
userId?: string,
details?: any,
request?: any,
success: boolean = true,
2025-08-21 15:24:40 +09:00
error?: string,
): Promise<void> {
const ipAddress = this.extractIpAddress(request);
2025-08-21 15:24:40 +09:00
const userAgent = request?.headers?.["user-agent"];
await this.log({
userId,
action,
2025-08-21 15:24:40 +09:00
resource: "auth",
details,
ipAddress,
userAgent,
success,
error,
});
}
private extractIpAddress(request?: any): string | undefined {
if (!request) return undefined;
2025-08-21 15:24:40 +09:00
return (
2025-08-21 15:24:40 +09:00
request.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
request.headers["x-real-ip"] ||
request.connection?.remoteAddress ||
request.socket?.remoteAddress ||
request.ip
);
}
// Cleanup old audit logs (run as a scheduled job)
async cleanupOldLogs(daysToKeep: number = 90): Promise<number> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const result = await this.prisma.auditLog.deleteMany({
where: {
createdAt: {
lt: cutoffDate,
},
},
});
// Log cleanup result - use console.log for maintenance operations
2025-08-21 15:24:40 +09:00
console.log(
`Cleaned up ${result.count} audit logs older than ${daysToKeep} days`,
);
return result.count;
}
// Get user's recent auth events
async getUserAuthHistory(userId: string, limit: number = 10) {
return this.prisma.auditLog.findMany({
where: {
userId,
2025-08-21 15:24:40 +09:00
resource: "auth",
},
orderBy: {
2025-08-21 15:24:40 +09:00
createdAt: "desc",
},
take: limit,
select: {
action: true,
success: true,
ipAddress: true,
createdAt: true,
},
});
}
}