134 lines
3.4 KiB
TypeScript
134 lines
3.4 KiB
TypeScript
|
|
import { Injectable } from '@nestjs/common';
|
||
|
|
import { PrismaService } from '../prisma/prisma.service';
|
||
|
|
|
||
|
|
// Define audit actions to match Prisma schema
|
||
|
|
export enum AuditAction {
|
||
|
|
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,
|
||
|
|
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)
|
||
|
|
console.error('Failed to create audit log:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async logAuthEvent(
|
||
|
|
action: AuditAction,
|
||
|
|
userId?: string,
|
||
|
|
details?: any,
|
||
|
|
request?: any,
|
||
|
|
success: boolean = true,
|
||
|
|
error?: string
|
||
|
|
): Promise<void> {
|
||
|
|
const ipAddress = this.extractIpAddress(request);
|
||
|
|
const userAgent = request?.headers?.['user-agent'];
|
||
|
|
|
||
|
|
await this.log({
|
||
|
|
userId,
|
||
|
|
action,
|
||
|
|
resource: 'auth',
|
||
|
|
details,
|
||
|
|
ipAddress,
|
||
|
|
userAgent,
|
||
|
|
success,
|
||
|
|
error,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private extractIpAddress(request?: any): string | undefined {
|
||
|
|
if (!request) return undefined;
|
||
|
|
|
||
|
|
return (
|
||
|
|
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
|
||
|
|
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,
|
||
|
|
resource: 'auth',
|
||
|
|
},
|
||
|
|
orderBy: {
|
||
|
|
createdAt: 'desc',
|
||
|
|
},
|
||
|
|
take: limit,
|
||
|
|
select: {
|
||
|
|
action: true,
|
||
|
|
success: true,
|
||
|
|
ipAddress: true,
|
||
|
|
createdAt: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|