feat: integrate audit logging across authentication, course management, and user operations
Add comprehensive audit trail tracking by integrating auditService throughout the application. Track user authentication (LOGIN, REGISTER), course lifecycle (CREATE, APPROVE_COURSE, REJECT_COURSE, ENROLL), content management (CREATE/DELETE Chapter/Lesson), file operations (UPLOAD_FILE, DELETE_FILE for videos and attachments), password management (CHANGE_PASSWORD, RESET_PASSWORD), user role updates (UPDATE
This commit is contained in:
parent
923c8b727a
commit
108f1b73f2
10 changed files with 701 additions and 0 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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<createCourseResponse> {
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
245
Backend/src/services/audit.service.ts
Normal file
245
Backend/src/services/audit.service.ts
Normal file
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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<ListAuditLogsResponse> {
|
||||
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<AuditLogResponse | null> {
|
||||
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<AuditLogResponse[]> {
|
||||
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<AuditLogResponse[]> {
|
||||
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<AuditLogStats> {
|
||||
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<number> {
|
||||
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();
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<UserResponse> {
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<ListUsersResponse> {
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue