diff --git a/Backend/src/controllers/AuditController.ts b/Backend/src/controllers/AuditController.ts new file mode 100644 index 00000000..5de912fc --- /dev/null +++ b/Backend/src/controllers/AuditController.ts @@ -0,0 +1,182 @@ +import { Get, Path, Query, Request, Response, Route, Security, SuccessResponse, Tags, Delete } from 'tsoa'; +import { ValidationError } from '../middleware/errorHandler'; +import { auditService } from '../services/audit.service'; +import { AuditAction } from '@prisma/client'; +import { + ListAuditLogsResponse, + AuditLogResponse, + AuditLogStats, +} from '../types/audit.types'; + +@Route('api/admin/audit-logs') +@Tags('Admin/AuditLogs') +export class AuditController { + + /** + * ดึงรายการ audit logs ทั้งหมด + * Get all audit logs with filters and pagination + * @param userId - กรองตาม user ID + * @param action - กรองตาม action type + * @param entityType - กรองตาม entity type (Course, User, Lesson, etc.) + * @param entityId - กรองตาม entity ID + * @param startDate - วันที่เริ่มต้น (ISO format) + * @param endDate - วันที่สิ้นสุด (ISO format) + * @param page - หน้าที่ต้องการ + * @param limit - จำนวนรายการต่อหน้า + */ + @Get('') + @Security('jwt', ['admin']) + @SuccessResponse('200', 'Audit logs retrieved successfully') + @Response('401', 'Unauthorized') + @Response('403', 'Forbidden - Admin only') + public async getAuditLogs( + @Request() request: any, + @Query() userId?: number, + @Query() action?: AuditAction, + @Query() entityType?: string, + @Query() entityId?: number, + @Query() startDate?: string, + @Query() endDate?: string, + @Query() page?: number, + @Query() limit?: number + ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + return await auditService.getLogs({ + userId, + action, + entityType, + entityId, + startDate: startDate ? new Date(startDate) : undefined, + endDate: endDate ? new Date(endDate) : undefined, + page: page || 1, + limit: limit || 50, + }); + } + + /** + * ดึง audit log by ID + * Get audit log detail by ID + * @param logId - รหัส audit log + */ + @Get('{logId}') + @Security('jwt', ['admin']) + @SuccessResponse('200', 'Audit log retrieved successfully') + @Response('401', 'Unauthorized') + @Response('403', 'Forbidden - Admin only') + @Response('404', 'Audit log not found') + public async getAuditLogById( + @Request() request: any, + @Path() logId: number + ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + const log = await auditService.getLogById(logId); + if (!log) { + throw new ValidationError('Audit log not found'); + } + return log; + } + + /** + * ดึงสถิติ audit logs สำหรับ dashboard + * Get audit log statistics for admin dashboard + */ + @Get('stats/summary') + @Security('jwt', ['admin']) + @SuccessResponse('200', 'Audit stats retrieved successfully') + @Response('401', 'Unauthorized') + @Response('403', 'Forbidden - Admin only') + public async getAuditStats(@Request() request: any): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + return await auditService.getStats(); + } + + /** + * ดึง history ของ entity เฉพาะ + * Get audit history for a specific entity + * @param entityType - ประเภท entity (Course, User, Lesson, etc.) + * @param entityId - รหัส entity + */ + @Get('entity/{entityType}/{entityId}') + @Security('jwt', ['admin']) + @SuccessResponse('200', 'Entity history retrieved successfully') + @Response('401', 'Unauthorized') + @Response('403', 'Forbidden - Admin only') + public async getEntityHistory( + @Request() request: any, + @Path() entityType: string, + @Path() entityId: number + ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + return await auditService.getEntityHistory(entityType, entityId); + } + + /** + * ดึง activity ของ user เฉพาะ + * Get activity logs for a specific user + * @param userId - รหัส user + * @param limit - จำนวนรายการ + */ + @Get('user/{userId}/activity') + @Security('jwt', ['admin']) + @SuccessResponse('200', 'User activity retrieved successfully') + @Response('401', 'Unauthorized') + @Response('403', 'Forbidden - Admin only') + public async getUserActivity( + @Request() request: any, + @Path() userId: number, + @Query() limit?: number + ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + return await auditService.getUserActivity(userId, limit || 50); + } + + /** + * ลบ audit logs เก่า (maintenance) + * Delete old audit logs for maintenance + * @param days - ลบ logs ที่เก่ากว่ากี่วัน + */ + @Delete('cleanup') + @Security('jwt', ['admin']) + @SuccessResponse('200', 'Old logs deleted successfully') + @Response('401', 'Unauthorized') + @Response('403', 'Forbidden - Admin only') + public async deleteOldLogs( + @Request() request: any, + @Query() days: number = 90 + ): Promise<{ deleted: number; message: string }> { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + if (days < 30) { + throw new ValidationError('Cannot delete logs newer than 30 days'); + } + + const deleted = await auditService.deleteOldLogs(days); + return { + deleted, + message: `Deleted ${deleted} audit logs older than ${days} days`, + }; + } +} diff --git a/Backend/src/services/AdminCourseApproval.service.ts b/Backend/src/services/AdminCourseApproval.service.ts index 63ceb348..90f79793 100644 --- a/Backend/src/services/AdminCourseApproval.service.ts +++ b/Backend/src/services/AdminCourseApproval.service.ts @@ -9,6 +9,8 @@ import { ApproveCourseResponse, RejectCourseResponse, } from '../types/AdminCourseApproval.types'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class AdminCourseApprovalService { @@ -235,6 +237,17 @@ export class AdminCourseApprovalService { }) ]); + // Audit log - APPROVE_COURSE + await auditService.logSync({ + userId: decoded.id, + action: AuditAction.APPROVE_COURSE, + entityType: 'Course', + entityId: courseId, + oldValue: { status: 'PENDING' }, + newValue: { status: 'APPROVED' }, + metadata: { comment: comment || null } + }); + return { code: 200, message: 'Course approved successfully' @@ -290,6 +303,17 @@ export class AdminCourseApprovalService { }) ]); + // Audit log - REJECT_COURSE + await auditService.logSync({ + userId: decoded.id, + action: AuditAction.REJECT_COURSE, + entityType: 'Course', + entityId: courseId, + oldValue: { status: 'PENDING' }, + newValue: { status: 'DRAFT' }, + metadata: { comment: comment } + }); + return { code: 200, message: 'Course rejected successfully' diff --git a/Backend/src/services/ChaptersLesson.service.ts b/Backend/src/services/ChaptersLesson.service.ts index 99246af1..d24ff9f3 100644 --- a/Backend/src/services/ChaptersLesson.service.ts +++ b/Backend/src/services/ChaptersLesson.service.ts @@ -52,6 +52,8 @@ import { QuizData, } from "../types/ChaptersLesson.typs"; import { CoursesInstructorService } from './CoursesInstructor.service'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; /** * ตรวจสอบสิทธิ์เข้าถึง Course (สำหรับทั้ง Instructor และ Student) @@ -127,6 +129,16 @@ export class ChaptersLessonService { throw new ForbiddenError('You are not permitted to create chapter'); } const chapter = await prisma.chapter.create({ data: { course_id, title, description, sort_order } }); + + // Audit log - CREATE Chapter + auditService.log({ + userId: decodedToken.id, + action: AuditAction.CREATE, + entityType: 'Chapter', + entityId: chapter.id, + newValue: { course_id, title, sort_order } + }); + return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error creating chapter: ${error}`); @@ -170,6 +182,15 @@ export class ChaptersLessonService { } await prisma.chapter.delete({ where: { id: chapter_id } }); + // Audit log - DELETE Chapter + auditService.log({ + userId: decodedToken.id, + action: AuditAction.DELETE, + entityType: 'Chapter', + entityId: chapter_id, + oldValue: { course_id, chapter_id } + }); + // Normalize sort_order for remaining chapters (fill gaps) await this.normalizeChapterSortOrder(course_id); @@ -308,9 +329,28 @@ export class ChaptersLessonService { where: { id: lesson.id }, include: { quiz: true } }); + + // Audit log - CREATE Lesson (QUIZ) + auditService.log({ + userId: decodedToken.id, + action: AuditAction.CREATE, + entityType: 'Lesson', + entityId: lesson.id, + newValue: { chapter_id, title, type: 'QUIZ', sort_order } + }); + return { code: 200, message: 'Lesson created successfully', data: completeLesson as LessonData }; } + // Audit log - CREATE Lesson + auditService.log({ + userId: decodedToken.id, + action: AuditAction.CREATE, + entityType: 'Lesson', + entityId: lesson.id, + newValue: { chapter_id, title, type, sort_order } + }); + return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData }; } catch (error) { logger.error(`Error creating lesson: ${error}`); @@ -621,6 +661,15 @@ export class ChaptersLessonService { // Based on Prisma schema: onDelete: Cascade await prisma.lesson.delete({ where: { id: lesson_id } }); + // Audit log - DELETE Lesson + auditService.log({ + userId: decodedToken.id, + action: AuditAction.DELETE, + entityType: 'Lesson', + entityId: lesson_id, + oldValue: { chapter_id: chapterId, title: lesson.title, type: lesson.type } + }); + // Normalize sort_order for remaining lessons (fill gaps) await this.normalizeLessonSortOrder(chapterId); @@ -683,6 +732,15 @@ export class ChaptersLessonService { // Get presigned URL const video_url = await getPresignedUrl(videoPath, 3600); + // Audit log - UPLOAD_FILE (Video) + auditService.log({ + userId: decodedToken.id, + action: AuditAction.UPLOAD_FILE, + entityType: 'Lesson', + entityId: lesson_id, + newValue: { file_name: video.originalname, file_size: video.size, mime_type: video.mimetype } + }); + return { code: 200, message: 'Video uploaded successfully', @@ -909,6 +967,15 @@ export class ChaptersLessonService { // Get presigned URL const presigned_url = await getPresignedUrl(attachmentPath, 3600); + // Audit log - UPLOAD_FILE (Attachment) + auditService.log({ + userId: decodedToken.id, + action: AuditAction.UPLOAD_FILE, + entityType: 'LessonAttachment', + entityId: newAttachment.id, + newValue: { lesson_id, file_name: attachment.originalname, file_size: attachment.size } + }); + return { code: 200, message: 'Attachment uploaded successfully', @@ -972,6 +1039,15 @@ export class ChaptersLessonService { // Delete attachment record from database await prisma.lessonAttachment.delete({ where: { id: attachment_id } }); + // Audit log - DELETE_FILE (Attachment) + auditService.log({ + userId: decodedToken.id, + action: AuditAction.DELETE_FILE, + entityType: 'LessonAttachment', + entityId: attachment_id, + oldValue: { lesson_id, file_name: attachment.file_name, file_path: attachment.file_path } + }); + return { code: 200, message: 'Attachment deleted successfully' }; } catch (error) { logger.error(`Error deleting attachment: ${error}`); diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index 2caacada..e63eb912 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -33,6 +33,8 @@ import { GetEnrolledStudentDetailInput, GetEnrolledStudentDetailResponse, } from "../types/CoursesInstructor.types"; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class CoursesInstructorService { static async createCourse(courseData: CreateCourseInput, userId: number, thumbnailFile?: Express.Multer.File): Promise { @@ -81,6 +83,15 @@ export class CoursesInstructorService { return courseCreated; }); + // Audit log - CREATE Course + auditService.log({ + userId: userId, + action: AuditAction.CREATE, + entityType: 'Course', + entityId: result.id, + newValue: { title: courseData.title, slug: courseData.slug, status: 'DRAFT' } + }); + return { code: 201, message: 'Course created successfully', diff --git a/Backend/src/services/CoursesStudent.service.ts b/Backend/src/services/CoursesStudent.service.ts index d61ac47a..667ad012 100644 --- a/Backend/src/services/CoursesStudent.service.ts +++ b/Backend/src/services/CoursesStudent.service.ts @@ -27,6 +27,8 @@ import { GetQuizAttemptsResponse, } from "../types/CoursesStudent.types"; import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class CoursesStudentService { @@ -162,6 +164,16 @@ export class CoursesStudentService { enrolled_at: new Date(), }, }); + + // Audit log - ENROLL + auditService.log({ + userId: decoded.id, + action: AuditAction.ENROLL, + entityType: 'Enrollment', + entityId: enrollment.id, + newValue: { course_id, user_id: decoded.id, status: 'ENROLLED' } + }); + return { code: 200, message: 'Enrollment successful', diff --git a/Backend/src/services/audit.service.ts b/Backend/src/services/audit.service.ts new file mode 100644 index 00000000..e913be7b --- /dev/null +++ b/Backend/src/services/audit.service.ts @@ -0,0 +1,245 @@ +import { prisma } from '../config/database'; +import { AuditAction } from '@prisma/client'; +import { logger } from '../config/logger'; +import { + CreateAuditLogParams, + AuditLogFilters, + ListAuditLogsResponse, + AuditLogResponse, + AuditLogStats, +} from '../types/audit.types'; + +export class AuditService { + /** + * สร้าง audit log entry + */ + async log(params: CreateAuditLogParams): Promise { + try { + await prisma.auditLog.create({ + data: { + user_id: params.userId, + action: params.action, + entity_type: params.entityType, + entity_id: params.entityId, + old_value: params.oldValue, + new_value: params.newValue, + ip_address: params.ipAddress, + user_agent: params.userAgent, + metadata: params.metadata, + }, + }); + } catch (error) { + logger.error('Failed to create audit log', { error, params }); + } + } + + /** + * Log พร้อม await (สำหรับ critical actions) + */ + async logSync(params: CreateAuditLogParams): Promise { + await prisma.auditLog.create({ + data: { + user_id: params.userId, + action: params.action, + entity_type: params.entityType, + entity_id: params.entityId, + old_value: params.oldValue, + new_value: params.newValue, + ip_address: params.ipAddress, + user_agent: params.userAgent, + metadata: params.metadata, + }, + }); + } + + /** + * ดึง audit logs พร้อม filters และ pagination + */ + async getLogs(filters: AuditLogFilters): Promise { + const { page = 1, limit = 50 } = filters; + const skip = (page - 1) * limit; + + const where: any = {}; + + if (filters.userId) { + where.user_id = filters.userId; + } + if (filters.action) { + where.action = filters.action; + } + if (filters.entityType) { + where.entity_type = filters.entityType; + } + if (filters.entityId) { + where.entity_id = filters.entityId; + } + if (filters.startDate || filters.endDate) { + where.created_at = {}; + if (filters.startDate) { + where.created_at.gte = filters.startDate; + } + if (filters.endDate) { + where.created_at.lte = filters.endDate; + } + } + + const [logs, total] = await Promise.all([ + prisma.auditLog.findMany({ + where, + include: { + user: { + select: { + id: true, + username: true, + email: true, + }, + }, + }, + orderBy: { created_at: 'desc' }, + skip, + take: limit, + }), + prisma.auditLog.count({ where }), + ]); + + return { + data: logs as AuditLogResponse[], + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }; + } + + /** + * ดึง audit log by ID + */ + async getLogById(id: number): Promise { + const log = await prisma.auditLog.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + username: true, + email: true, + }, + }, + }, + }); + + return log as AuditLogResponse | null; + } + + /** + * ดึง logs ของ entity เฉพาะ (เช่น ดู history ของ course) + */ + async getEntityHistory(entityType: string, entityId: number): Promise { + const logs = await prisma.auditLog.findMany({ + where: { + entity_type: entityType, + entity_id: entityId, + }, + include: { + user: { + select: { + id: true, + username: true, + email: true, + }, + }, + }, + orderBy: { created_at: 'desc' }, + }); + + return logs as AuditLogResponse[]; + } + + /** + * ดึง logs ของ user เฉพาะ + */ + async getUserActivity(userId: number, limit: number = 50): Promise { + const logs = await prisma.auditLog.findMany({ + where: { user_id: userId }, + include: { + user: { + select: { + id: true, + username: true, + email: true, + }, + }, + }, + orderBy: { created_at: 'desc' }, + take: limit, + }); + + return logs as AuditLogResponse[]; + } + + /** + * ดึงสถิติ audit logs สำหรับ dashboard + */ + async getStats(): Promise { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const [totalLogs, todayLogs, actionCounts, recentActivity] = await Promise.all([ + prisma.auditLog.count(), + prisma.auditLog.count({ + where: { + created_at: { gte: today }, + }, + }), + prisma.auditLog.groupBy({ + by: ['action'], + _count: { action: true }, + orderBy: { _count: { action: 'desc' } }, + }), + prisma.auditLog.findMany({ + include: { + user: { + select: { + id: true, + username: true, + email: true, + }, + }, + }, + orderBy: { created_at: 'desc' }, + take: 10, + }), + ]); + + return { + totalLogs, + todayLogs, + actionSummary: actionCounts.map((item) => ({ + action: item.action, + count: item._count.action, + })), + recentActivity: recentActivity as AuditLogResponse[], + }; + } + + /** + * ลบ logs เก่า (สำหรับ maintenance) + */ + async deleteOldLogs(olderThanDays: number): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - olderThanDays); + + const result = await prisma.auditLog.deleteMany({ + where: { + created_at: { lt: cutoffDate }, + }, + }); + + logger.info(`Deleted ${result.count} old audit logs older than ${olderThanDays} days`); + return result.count; + } +} + +export const auditService = new AuditService(); diff --git a/Backend/src/services/auth.service.ts b/Backend/src/services/auth.service.ts index 8973497d..45f6f2f0 100644 --- a/Backend/src/services/auth.service.ts +++ b/Backend/src/services/auth.service.ts @@ -17,6 +17,8 @@ import { UserResponse } from '../types/user.types'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import nodemailer from 'nodemailer'; import { getPresignedUrl } from '../config/minio'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class AuthService { /** @@ -57,6 +59,15 @@ export class AuthService { logger.info('User logged in successfully', { userId: user.id, email: user.email }); + // Audit log - LOGIN + auditService.log({ + userId: user.id, + action: AuditAction.LOGIN, + entityType: 'User', + entityId: user.id, + metadata: { email: user.email, role: user.role.code } + }); + return { code: 200, message: 'Login successful', @@ -138,6 +149,15 @@ export class AuthService { logger.info('New user registered', { userId: user.id, username: user.username }); + // Audit log - REGISTER (Student) + auditService.log({ + userId: user.id, + action: AuditAction.CREATE, + entityType: 'User', + entityId: user.id, + newValue: { username: user.username, email: user.email, role: 'STUDENT' } + }); + return { user: this.formatUserResponseSync(user), message: 'Registration successful' @@ -211,6 +231,15 @@ export class AuthService { logger.info('New user registered', { userId: user.id, username: user.username }); + // Audit log - REGISTER (Instructor) + auditService.log({ + userId: user.id, + action: AuditAction.CREATE, + entityType: 'User', + entityId: user.id, + newValue: { username: user.username, email: user.email, role: 'INSTRUCTOR' } + }); + return { user: this.formatUserResponseSync(user), message: 'Registration successful' @@ -341,6 +370,16 @@ export class AuthService { }); logger.info('Password reset successfully', { userId: user.id }); + + // Audit log - RESET_PASSWORD + auditService.log({ + userId: user.id, + action: AuditAction.RESET_PASSWORD, + entityType: 'User', + entityId: user.id, + metadata: { email: user.email } + }); + return { code: 200, message: 'Password reset successfully' diff --git a/Backend/src/services/user.service.ts b/Backend/src/services/user.service.ts index d4294e29..226faf9c 100644 --- a/Backend/src/services/user.service.ts +++ b/Backend/src/services/user.service.ts @@ -19,6 +19,8 @@ import { import nodemailer from 'nodemailer'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class UserService { async getUserProfile(token: string): Promise { @@ -109,6 +111,16 @@ export class UserService { }); logger.info('Password changed successfully', { userId: user.id }); + + // Audit log - CHANGE_PASSWORD + auditService.log({ + userId: user.id, + action: AuditAction.CHANGE_PASSWORD, + entityType: 'User', + entityId: user.id, + metadata: { email: user.email } + }); + return { code: 200, message: 'Password changed successfully' diff --git a/Backend/src/services/usermanagement.service.ts b/Backend/src/services/usermanagement.service.ts index 1136be55..658382c2 100644 --- a/Backend/src/services/usermanagement.service.ts +++ b/Backend/src/services/usermanagement.service.ts @@ -14,6 +14,8 @@ import { import { UserResponse } from '../types/user.types'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import { getPresignedUrl } from '../config/minio'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class UserManagementService { async listUsers(): Promise { @@ -71,11 +73,22 @@ export class UserManagementService { const role = await prisma.role.findUnique({ where: { id: role_id } }); if (!role) throw new UnauthorizedError('Role not found'); + const oldRoleId = user.role_id; await prisma.user.update({ where: { id }, data: { role_id } }); + // Audit log - UPDATE (role change) + auditService.log({ + userId: id, + action: AuditAction.UPDATE, + entityType: 'User', + entityId: id, + oldValue: { role_id: oldRoleId }, + newValue: { role_id: role_id } + }); + return { code: 200, message: 'User role updated successfully' @@ -122,6 +135,17 @@ export class UserManagementService { }); logger.info('Account deactivated successfully', { userId: user.id }); + + // Audit log - DEACTIVATE_USER + auditService.log({ + userId: user.id, + action: AuditAction.DEACTIVATE_USER, + entityType: 'User', + entityId: user.id, + oldValue: { is_deactivated: false }, + newValue: { is_deactivated: true } + }); + return { code: 200, message: 'Account deactivated successfully' @@ -158,6 +182,17 @@ export class UserManagementService { }); logger.info('Account activated successfully', { userId: user.id }); + + // Audit log - ACTIVATE_USER + auditService.log({ + userId: user.id, + action: AuditAction.ACTIVATE_USER, + entityType: 'User', + entityId: user.id, + oldValue: { is_deactivated: true }, + newValue: { is_deactivated: false } + }); + return { code: 200, message: 'Account activated successfully' diff --git a/Backend/src/types/audit.types.ts b/Backend/src/types/audit.types.ts new file mode 100644 index 00000000..10da4ec1 --- /dev/null +++ b/Backend/src/types/audit.types.ts @@ -0,0 +1,65 @@ +import { AuditAction } from '@prisma/client'; + +export interface CreateAuditLogParams { + userId?: number; + action: AuditAction; + entityType: string; + entityId?: number; + oldValue?: any; + newValue?: any; + ipAddress?: string; + userAgent?: string; + metadata?: any; +} + +export interface AuditLogFilters { + userId?: number; + action?: AuditAction; + entityType?: string; + entityId?: number; + startDate?: Date; + endDate?: Date; + page?: number; + limit?: number; +} + +export interface AuditLogResponse { + id: number; + user_id: number | null; + action: AuditAction; + entity_type: string; + entity_id: number | null; + old_value: any; + new_value: any; + ip_address: string | null; + user_agent: string | null; + metadata: any; + created_at: Date; + user?: { + id: number; + username: string; + email: string; + } | null; +} + +export interface ListAuditLogsResponse { + data: AuditLogResponse[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export interface AuditLogSummary { + action: AuditAction; + count: number; +} + +export interface AuditLogStats { + totalLogs: number; + todayLogs: number; + actionSummary: AuditLogSummary[]; + recentActivity: AuditLogResponse[]; +}