2025-08-21 15:24:40 +09:00
|
|
|
import { Injectable } from "@nestjs/common";
|
|
|
|
|
import { PrismaService } from "../prisma/prisma.service";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
// 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",
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2025-08-20 18:02:50 +09:00
|
|
|
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);
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async logAuthEvent(
|
|
|
|
|
action: AuditAction,
|
|
|
|
|
userId?: string,
|
|
|
|
|
details?: any,
|
|
|
|
|
request?: any,
|
|
|
|
|
success: boolean = true,
|
2025-08-21 15:24:40 +09:00
|
|
|
error?: string,
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<void> {
|
|
|
|
|
const ipAddress = this.extractIpAddress(request);
|
2025-08-21 15:24:40 +09:00
|
|
|
const userAgent = request?.headers?.["user-agent"];
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
await this.log({
|
|
|
|
|
userId,
|
|
|
|
|
action,
|
2025-08-21 15:24:40 +09:00
|
|
|
resource: "auth",
|
2025-08-20 18:02:50 +09:00
|
|
|
details,
|
|
|
|
|
ipAddress,
|
|
|
|
|
userAgent,
|
|
|
|
|
success,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extractIpAddress(request?: any): string | undefined {
|
|
|
|
|
if (!request) return undefined;
|
2025-08-21 15:24:40 +09:00
|
|
|
|
2025-08-20 18:02:50 +09:00
|
|
|
return (
|
2025-08-21 15:24:40 +09:00
|
|
|
request.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
|
|
|
request.headers["x-real-ip"] ||
|
2025-08-20 18:02:50 +09:00
|
|
|
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`,
|
|
|
|
|
);
|
2025-08-20 18:02:50 +09:00
|
|
|
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",
|
2025-08-20 18:02:50 +09:00
|
|
|
},
|
|
|
|
|
orderBy: {
|
2025-08-21 15:24:40 +09:00
|
|
|
createdAt: "desc",
|
2025-08-20 18:02:50 +09:00
|
|
|
},
|
|
|
|
|
take: limit,
|
|
|
|
|
select: {
|
|
|
|
|
action: true,
|
|
|
|
|
success: true,
|
|
|
|
|
ipAddress: true,
|
|
|
|
|
createdAt: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|