2025-08-22 17:02:49 +09:00
|
|
|
import { Injectable, Inject } from "@nestjs/common";
|
2025-08-21 15:24:40 +09:00
|
|
|
import { PrismaService } from "../prisma/prisma.service";
|
2025-08-22 17:02:49 +09:00
|
|
|
import { getErrorMessage } from "../utils/error.util";
|
|
|
|
|
import { Logger } from "nestjs-pino";
|
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 {
|
2025-08-22 17:02:49 +09:00
|
|
|
constructor(
|
|
|
|
|
private readonly prisma: PrismaService,
|
|
|
|
|
@Inject(Logger) private readonly logger: Logger
|
|
|
|
|
) {}
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
// 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-22 17:02:49 +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
|
2025-08-22 17:02:49 +09:00
|
|
|
// Log error without exposing sensitive information
|
|
|
|
|
this.logger.error("Audit logging failed", {
|
|
|
|
|
errorType: error instanceof Error ? error.constructor.name : "Unknown",
|
|
|
|
|
message: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async logAuthEvent(
|
|
|
|
|
action: AuditAction,
|
|
|
|
|
userId?: string,
|
|
|
|
|
details?: any,
|
|
|
|
|
request?: any,
|
|
|
|
|
success: boolean = true,
|
2025-08-22 17:02:49 +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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|