feat: Add token-based authorization to category deletion and enhance user registration with error handling and audit logging.

This commit is contained in:
JakkrapartXD 2026-02-12 17:55:45 +07:00
parent 45941fbe6c
commit af14610442
16 changed files with 1003 additions and 236 deletions

View file

@ -634,6 +634,8 @@ enum AuditAction {
VERIFY_EMAIL VERIFY_EMAIL
DEACTIVATE_USER DEACTIVATE_USER
ACTIVATE_USER ACTIVATE_USER
ERROR
WARNING
} }
model AuditLog { model AuditLog {

View file

@ -25,10 +25,8 @@ export class AdminCourseApprovalController {
@Response('403', 'Forbidden - Admin only') @Response('403', 'Forbidden - Admin only')
public async listPendingCourses(@Request() request: any): Promise<ListPendingCoursesResponse> { public async listPendingCourses(@Request() request: any): Promise<ListPendingCoursesResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided'); return await AdminCourseApprovalService.listPendingCourses(token);
}
return await AdminCourseApprovalService.listPendingCourses();
} }
/** /**
@ -44,10 +42,8 @@ export class AdminCourseApprovalController {
@Response('404', 'Course not found') @Response('404', 'Course not found')
public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise<GetCourseDetailForAdminResponse> { public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise<GetCourseDetailForAdminResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided'); return await AdminCourseApprovalService.getCourseDetail(token, courseId);
}
return await AdminCourseApprovalService.getCourseDetail(courseId);
} }
/** /**
@ -68,9 +64,7 @@ export class AdminCourseApprovalController {
@Body() body?: ApproveCourseBody @Body() body?: ApproveCourseBody
): Promise<ApproveCourseResponse> { ): Promise<ApproveCourseResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided');
}
return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment); return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment);
} }
@ -92,9 +86,7 @@ export class AdminCourseApprovalController {
@Body() body: RejectCourseBody @Body() body: RejectCourseBody
): Promise<RejectCourseResponse> { ): Promise<RejectCourseResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided');
}
return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment); return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment);
} }
} }

View file

@ -45,6 +45,6 @@ export class CategoriesAdminController {
@Response('401', 'Invalid or expired token') @Response('401', 'Invalid or expired token')
public async deleteCategory(@Request() request: any, @Path() id: number): Promise<deleteCategoryResponse> { public async deleteCategory(@Request() request: any, @Path() id: number): Promise<deleteCategoryResponse> {
const token = request.headers.authorization?.replace('Bearer ', '') || ''; const token = request.headers.authorization?.replace('Bearer ', '') || '';
return await this.categoryService.deleteCategory(id); return await this.categoryService.deleteCategory(token,id);
} }
} }

View file

@ -22,10 +22,8 @@ export class RecommendedCoursesController {
@Response('403', 'Forbidden - Admin only') @Response('403', 'Forbidden - Admin only')
public async listApprovedCourses(@Request() request: any): Promise<ListApprovedCoursesResponse> { public async listApprovedCourses(@Request() request: any): Promise<ListApprovedCoursesResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided'); return await RecommendedCoursesService.listApprovedCourses(token);
}
return await RecommendedCoursesService.listApprovedCourses();
} }
/** /**
@ -42,10 +40,8 @@ export class RecommendedCoursesController {
@Response('404', 'Course not found') @Response('404', 'Course not found')
public async getCourseById(@Request() request: any, @Path() courseId: number): Promise<GetCourseByIdResponse> { public async getCourseById(@Request() request: any, @Path() courseId: number): Promise<GetCourseByIdResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided'); return await RecommendedCoursesService.getCourseById(token, courseId);
}
return await RecommendedCoursesService.getCourseById(courseId);
} }
/** /**
@ -66,9 +62,7 @@ export class RecommendedCoursesController {
@Query() is_recommended: boolean @Query() is_recommended: boolean
): Promise<ToggleRecommendedResponse> { ): Promise<ToggleRecommendedResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) throw new ValidationError('No token provided');
throw new ValidationError('No token provided');
}
return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended); return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended);
} }
} }

View file

@ -18,7 +18,7 @@ export class AdminCourseApprovalService {
/** /**
* Get all pending courses for admin review * Get all pending courses for admin review
*/ */
static async listPendingCourses(): Promise<ListPendingCoursesResponse> { static async listPendingCourses(token: string): Promise<ListPendingCoursesResponse> {
try { try {
const courses = await prisma.course.findMany({ const courses = await prisma.course.findMany({
where: { status: 'PENDING' }, where: { status: 'PENDING' },
@ -96,6 +96,16 @@ export class AdminCourseApprovalService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to list pending courses', { error }); logger.error('Failed to list pending courses', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -103,7 +113,7 @@ export class AdminCourseApprovalService {
/** /**
* Get course details for admin review * Get course details for admin review
*/ */
static async getCourseDetail(courseId: number): Promise<GetCourseDetailForAdminResponse> { static async getCourseDetail(token: string,courseId: number): Promise<GetCourseDetailForAdminResponse> {
try { try {
const course = await prisma.course.findUnique({ const course = await prisma.course.findUnique({
where: { id: courseId }, where: { id: courseId },
@ -214,6 +224,16 @@ export class AdminCourseApprovalService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to get course detail', { error }); logger.error('Failed to get course detail', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -275,6 +295,17 @@ export class AdminCourseApprovalService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to approve course', { error }); logger.error('Failed to approve course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'approve_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -341,6 +372,17 @@ export class AdminCourseApprovalService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to reject course', { error }); logger.error('Failed to reject course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'reject_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -142,6 +142,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData }; return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData };
} catch (error) { } catch (error) {
logger.error(`Error creating chapter: ${error}`); logger.error(`Error creating chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: 0,
metadata: {
operation: 'create_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -163,6 +174,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData }; return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData };
} catch (error) { } catch (error) {
logger.error(`Error updating chapter: ${error}`); logger.error(`Error updating chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: request.chapter_id,
metadata: {
operation: 'update_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -197,6 +219,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter deleted successfully' }; return { code: 200, message: 'Chapter deleted successfully' };
} catch (error) { } catch (error) {
logger.error(`Error deleting chapter: ${error}`); logger.error(`Error deleting chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: request.chapter_id,
metadata: {
operation: 'delete_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -280,6 +313,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] }; return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] };
} catch (error) { } catch (error) {
logger.error(`Error reordering chapter: ${error}`); logger.error(`Error reordering chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: request.chapter_id,
metadata: {
operation: 'reorder_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -354,6 +398,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData }; return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData };
} catch (error) { } catch (error) {
logger.error(`Error creating lesson: ${error}`); logger.error(`Error creating lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: 0,
metadata: {
operation: 'create_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -494,6 +549,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData }; return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData };
} catch (error) { } catch (error) {
logger.error(`Error fetching lesson: ${error}`); logger.error(`Error fetching lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'get_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -515,6 +581,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData }; return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData };
} catch (error) { } catch (error) {
logger.error(`Error updating lesson: ${error}`); logger.error(`Error updating lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'update_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -605,6 +682,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] }; return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] };
} catch (error) { } catch (error) {
logger.error(`Error reordering lessons: ${error}`); logger.error(`Error reordering lessons: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'reorder_lessons',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -676,6 +764,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson deleted successfully' }; return { code: 200, message: 'Lesson deleted successfully' };
} catch (error) { } catch (error) {
logger.error(`Error deleting lesson: ${error}`); logger.error(`Error deleting lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'delete_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -754,6 +853,17 @@ export class ChaptersLessonService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error uploading video: ${error}`); logger.error(`Error uploading video: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'upload_video',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -836,6 +946,17 @@ export class ChaptersLessonService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error updating video: ${error}`); logger.error(`Error updating video: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'update_video',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -917,6 +1038,17 @@ export class ChaptersLessonService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error setting YouTube video: ${error}`); logger.error(`Error setting YouTube video: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'set_youtube_video',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -993,6 +1125,17 @@ export class ChaptersLessonService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error uploading attachment: ${error}`); logger.error(`Error uploading attachment: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'LessonAttachment',
entityId: request.lesson_id,
metadata: {
operation: 'upload_attachment',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1051,6 +1194,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Attachment deleted successfully' }; return { code: 200, message: 'Attachment deleted successfully' };
} catch (error) { } catch (error) {
logger.error(`Error deleting attachment: ${error}`); logger.error(`Error deleting attachment: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'LessonAttachment',
entityId: request.attachment_id,
metadata: {
operation: 'delete_attachment',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1127,6 +1281,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData }; return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData };
} catch (error) { } catch (error) {
logger.error(`Error adding question: ${error}`); logger.error(`Error adding question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: 0,
metadata: {
operation: 'add_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1202,6 +1367,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData }; return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData };
} catch (error) { } catch (error) {
logger.error(`Error updating question: ${error}`); logger.error(`Error updating question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: request.question_id,
metadata: {
operation: 'update_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1295,6 +1471,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] }; return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] };
} catch (error) { } catch (error) {
logger.error(`Error reordering question: ${error}`); logger.error(`Error reordering question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: request.question_id,
metadata: {
operation: 'reorder_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1343,6 +1530,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question deleted successfully' }; return { code: 200, message: 'Question deleted successfully' };
} catch (error) { } catch (error) {
logger.error(`Error deleting question: ${error}`); logger.error(`Error deleting question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: request.question_id,
metadata: {
operation: 'delete_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -347,6 +347,15 @@ export class CoursesInstructorService {
id: courseId id: courseId
} }
}); });
await auditService.logSync({
userId: courseInstructorId.user_id,
action: AuditAction.DELETE,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'delete_course'
}
});
return { return {
code: 200, code: 200,
message: 'Course deleted successfully', message: 'Course deleted successfully',
@ -386,6 +395,15 @@ export class CoursesInstructorService {
status: 'PENDING' status: 'PENDING'
} }
}); });
await auditService.logSync({
userId: decoded.id,
action: AuditAction.UPDATE,
entityType: 'Course',
entityId: sendCourseForReview.course_id,
metadata: {
operation: 'send_course_for_review'
}
});
return { return {
code: 200, code: 200,
message: 'Course sent for review successfully', message: 'Course sent for review successfully',
@ -447,8 +465,6 @@ export class CoursesInstructorService {
total: number; total: number;
}> { }> {
try { try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
// Validate instructor access // Validate instructor access
await this.validateCourseInstructor(token, courseId); await this.validateCourseInstructor(token, courseId);
@ -601,6 +617,18 @@ export class CoursesInstructorService {
} }
}); });
const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.CREATE,
entityType: 'Course',
entityId: addinstructorCourse.course_id,
metadata: {
operation: 'add_instructor_to_course',
instructor_id: user.id,
}
});
return { return {
code: 200, code: 200,
message: 'Instructor added to course successfully', message: 'Instructor added to course successfully',
@ -633,6 +661,19 @@ export class CoursesInstructorService {
}, },
} }
}); });
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.DELETE,
entityType: 'Course',
entityId: removeinstructorCourse.course_id,
metadata: {
operation: 'remove_instructor_from_course',
instructor_id: removeinstructorCourse.user_id,
course_id: removeinstructorCourse.course_id,
}
});
return { return {
code: 200, code: 200,
message: 'Instructor removed from course successfully', message: 'Instructor removed from course successfully',
@ -729,6 +770,19 @@ export class CoursesInstructorService {
is_primary: true, is_primary: true,
} }
}); });
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.UPDATE,
entityType: 'Course',
entityId: setprimaryCourseInstructor.course_id,
metadata: {
operation: 'set_primary_instructor',
instructor_id: setprimaryCourseInstructor.user_id,
course_id: setprimaryCourseInstructor.course_id,
}
});
return { return {
code: 200, code: 200,
message: 'Primary instructor set successfully', message: 'Primary instructor set successfully',
@ -784,7 +838,6 @@ export class CoursesInstructorService {
static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise<GetEnrolledStudentsResponse> { static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise<GetEnrolledStudentsResponse> {
try { try {
const { token, course_id, page = 1, limit = 20, search, status } = input; const { token, course_id, page = 1, limit = 20, search, status } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Validate instructor // Validate instructor
await this.validateCourseInstructor(token, course_id); await this.validateCourseInstructor(token, course_id);
@ -1062,7 +1115,6 @@ export class CoursesInstructorService {
static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise<GetQuizAttemptDetailResponse> { static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise<GetQuizAttemptDetailResponse> {
try { try {
const { token, course_id, lesson_id, student_id } = input; const { token, course_id, lesson_id, student_id } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Validate instructor // Validate instructor
await this.validateCourseInstructor(token, course_id); await this.validateCourseInstructor(token, course_id);

View file

@ -186,7 +186,20 @@ export class CoursesStudentService {
}, },
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(`Error enrolling in course: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'enroll_course',
course_id: input.course_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -261,6 +274,17 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_enrolled_courses',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -416,6 +440,17 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -678,6 +713,17 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -866,6 +912,17 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -940,6 +997,17 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1037,6 +1105,17 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1168,7 +1247,19 @@ export class CoursesStudentService {
}, },
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(`Error completing lesson: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'LessonProgress',
entityId: input.lesson_id,
metadata: {
operation: 'complete_lesson',
lesson_id: input.lesson_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1213,22 +1304,14 @@ export class CoursesStudentService {
}, },
}); });
if (!lesson) { if (!lesson) throw new NotFoundError('Lesson not found');
throw new NotFoundError('Lesson not found');
}
if (lesson.type !== 'QUIZ') { if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz');
throw new ValidationError('This lesson is not a quiz');
}
if (!lesson.quiz) { if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
throw new NotFoundError('Quiz not found for this lesson');
}
// Verify lesson belongs to the course // Verify lesson belongs to the course
if (lesson.chapter.course_id !== course_id) { if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course');
throw new NotFoundError('Lesson not found in this course');
}
const quiz = lesson.quiz; const quiz = lesson.quiz;
@ -1332,7 +1415,20 @@ export class CoursesStudentService {
}, },
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(`Error submitting quiz: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'QuizAttempt',
entityId: 0,
metadata: {
operation: 'submit_quiz',
course_id: input.course_id,
lesson_id: input.lesson_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -1373,22 +1469,14 @@ export class CoursesStudentService {
}, },
}); });
if (!lesson) { if (!lesson) throw new NotFoundError('Lesson not found');
throw new NotFoundError('Lesson not found');
}
if (lesson.type !== 'QUIZ') { if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz');
throw new ValidationError('This lesson is not a quiz');
}
if (!lesson.quiz) { if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
throw new NotFoundError('Quiz not found for this lesson');
}
// Verify lesson belongs to the course // Verify lesson belongs to the course
if (lesson.chapter.course_id !== course_id) { if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course');
throw new NotFoundError('Lesson not found in this course');
}
// Get all quiz attempts for this user // Get all quiz attempts for this user
const attempts = await prisma.quizAttempt.findMany({ const attempts = await prisma.quizAttempt.findMany({
@ -1438,6 +1526,21 @@ export class CoursesStudentService {
}; };
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
if (decoded?.id) {
await auditService.logSync({
userId: decoded.id,
action: AuditAction.ERROR,
entityType: 'QuizAttempt',
entityId: 0,
metadata: {
operation: 'get_quiz_attempts',
course_id: input.course_id,
lesson_id: input.lesson_id,
error: error instanceof Error ? error.message : String(error)
}
});
}
throw error; throw error;
} }
} }

View file

@ -18,7 +18,7 @@ export class RecommendedCoursesService {
/** /**
* List all approved courses (for admin to manage recommendations) * List all approved courses (for admin to manage recommendations)
*/ */
static async listApprovedCourses(): Promise<ListApprovedCoursesResponse> { static async listApprovedCourses(token: string): Promise<ListApprovedCoursesResponse> {
try { try {
const courses = await prisma.course.findMany({ const courses = await prisma.course.findMany({
where: { status: 'APPROVED' }, where: { status: 'APPROVED' },
@ -94,6 +94,19 @@ export class RecommendedCoursesService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to list approved courses', { error }); logger.error('Failed to list approved courses', { error });
const decoded = jwt.decode(token) as { id: number } | null;
if (decoded?.id) {
await auditService.logSync({
userId: decoded.id,
action: AuditAction.ERROR,
entityType: 'RecommendedCourses',
entityId: 0,
metadata: {
operation: 'list_approved_courses',
error: error instanceof Error ? error.message : String(error)
}
});
}
throw error; throw error;
} }
} }
@ -101,7 +114,7 @@ export class RecommendedCoursesService {
/** /**
* Get course by ID (for admin to view details) * Get course by ID (for admin to view details)
*/ */
static async getCourseById(courseId: number): Promise<GetCourseByIdResponse> { static async getCourseById(token: string, courseId: number): Promise<GetCourseByIdResponse> {
try { try {
const course = await prisma.course.findUnique({ const course = await prisma.course.findUnique({
where: { id: courseId }, where: { id: courseId },
@ -179,6 +192,19 @@ export class RecommendedCoursesService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to get course by ID', { error }); logger.error('Failed to get course by ID', { error });
const decoded = jwt.decode(token) as { id: number } | null;
if (decoded?.id) {
await auditService.logSync({
userId: decoded.id,
action: AuditAction.ERROR,
entityType: 'RecommendedCourses',
entityId: 0,
metadata: {
operation: 'get_course_by_id',
error: error instanceof Error ? error.message : String(error)
}
});
}
throw error; throw error;
} }
} }
@ -229,6 +255,17 @@ export class RecommendedCoursesService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to toggle recommended status', { error }); logger.error('Failed to toggle recommended status', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'RecommendedCourses',
entityId: courseId,
metadata: {
operation: 'toggle_recommended',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -20,6 +20,8 @@ import {
} from '../types/announcements.types'; } from '../types/announcements.types';
import { CoursesInstructorService } from './CoursesInstructor.service'; import { CoursesInstructorService } from './CoursesInstructor.service';
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class AnnouncementsService { export class AnnouncementsService {
@ -37,9 +39,7 @@ export class AnnouncementsService {
where: { id: decoded.id }, where: { id: decoded.id },
include: { role: true }, include: { role: true },
}); });
if (!user) { if (!user) throw new UnauthorizedError('Invalid token');
throw new UnauthorizedError('Invalid token');
}
// Admin can access all courses // Admin can access all courses
const isAdmin = user.role.code === 'ADMIN'; const isAdmin = user.role.code === 'ADMIN';
@ -130,6 +130,16 @@ export class AnnouncementsService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error listing announcements: ${error}`); logger.error(`Error listing announcements: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -226,6 +236,16 @@ export class AnnouncementsService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error creating announcement: ${error}`); logger.error(`Error creating announcement: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -300,6 +320,16 @@ export class AnnouncementsService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error updating announcement: ${error}`); logger.error(`Error updating announcement: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -346,6 +376,16 @@ export class AnnouncementsService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error deleting announcement: ${error}`); logger.error(`Error deleting announcement: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -411,6 +451,16 @@ export class AnnouncementsService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error uploading attachment: ${error}`); logger.error(`Error uploading attachment: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -458,6 +508,16 @@ export class AnnouncementsService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error deleting attachment: ${error}`); logger.error(`Error deleting attachment: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -83,6 +83,7 @@ export class AuthService {
* User registration * User registration
*/ */
async register(data: RegisterRequest): Promise<RegisterResponse> { async register(data: RegisterRequest): Promise<RegisterResponse> {
try {
const { username, email, password, first_name, last_name, prefix, phone } = data; const { username, email, password, first_name, last_name, prefix, phone } = data;
// Check if username already exists // Check if username already exists
@ -162,9 +163,26 @@ export class AuthService {
user: this.formatUserResponseSync(user), user: this.formatUserResponseSync(user),
message: 'Registration successful' message: 'Registration successful'
}; };
} catch (error) {
logger.error('Failed to register user', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'User',
entityId: 0,
metadata: {
operation: 'register_user',
email: data.email,
username: data.username,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
} }
async registerInstructor(data: RegisterRequest): Promise<RegisterResponse> { async registerInstructor(data: RegisterRequest): Promise<RegisterResponse> {
try {
const { username, email, password, first_name, last_name, prefix, phone } = data; const { username, email, password, first_name, last_name, prefix, phone } = data;
// Check if username already exists // Check if username already exists
@ -244,6 +262,22 @@ export class AuthService {
user: this.formatUserResponseSync(user), user: this.formatUserResponseSync(user),
message: 'Registration successful' message: 'Registration successful'
}; };
} catch (error) {
logger.error('Failed to register instructor', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'User',
entityId: 0,
metadata: {
operation: 'register_instructor',
email: data.email,
username: data.username,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
} }
/** /**

View file

@ -5,6 +5,8 @@ import { logger } from '../config/logger';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type'; import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class CategoryService { export class CategoryService {
async listCategories(): Promise<ListCategoriesResponse> { async listCategories(): Promise<ListCategoriesResponse> {
@ -30,6 +32,13 @@ export class CategoryService {
const newCategory = await prisma.category.create({ const newCategory = await prisma.category.create({
data: category data: category
}); });
auditService.log({
userId: decoded.id,
action: AuditAction.CREATE,
entityType: 'Category',
entityId: newCategory.id,
newValue: { name: newCategory.name as { th: string; en: string }, slug: newCategory.slug, description: newCategory.description as { th: string; en: string } },
});
return { return {
code: 200, code: 200,
message: 'Category created successfully', message: 'Category created successfully',
@ -43,6 +52,16 @@ export class CategoryService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to create category', { error }); logger.error('Failed to create category', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Category',
entityId: 0,
metadata: {
operation: 'create_category',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -54,6 +73,13 @@ export class CategoryService {
where: { id }, where: { id },
data: category data: category
}); });
auditService.log({
userId: decoded.id,
action: AuditAction.UPDATE,
entityType: 'Category',
entityId: id,
newValue: { name: updatedCategory.name as { th: string; en: string }, slug: updatedCategory.slug, description: updatedCategory.description as { th: string; en: string } },
});
return { return {
code: 200, code: 200,
message: 'Category updated successfully', message: 'Category updated successfully',
@ -67,21 +93,49 @@ export class CategoryService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to update category', { error }); logger.error('Failed to update category', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Category',
entityId: 0,
metadata: {
operation: 'update_category',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
async deleteCategory(id: number): Promise<deleteCategoryResponse> { async deleteCategory(token: string, id: number): Promise<deleteCategoryResponse> {
try { try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string };
const deletedCategory = await prisma.category.delete({ const deletedCategory = await prisma.category.delete({
where: { id } where: { id }
}); });
auditService.log({
userId: decoded.id,
action: AuditAction.DELETE,
entityType: 'Category',
entityId: id,
newValue: { name: deletedCategory.name as { th: string; en: string }, slug: deletedCategory.slug, description: deletedCategory.description as { th: string; en: string } },
});
return { return {
code: 200, code: 200,
message: 'Category deleted successfully', message: 'Category deleted successfully',
}; };
} catch (error) { } catch (error) {
logger.error('Failed to delete category', { error }); logger.error('Failed to delete category', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Category',
entityId: 0,
metadata: {
operation: 'delete_category',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -16,6 +16,8 @@ import {
ListMyCertificatesInput, ListMyCertificatesInput,
ListMyCertificatesResponse, ListMyCertificatesResponse,
} from '../types/certificate.types'; } from '../types/certificate.types';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class CertificateService { export class CertificateService {
private static TEMPLATE_PATH = path.join(__dirname, '../../assets/templates/Certificate.pdf'); private static TEMPLATE_PATH = path.join(__dirname, '../../assets/templates/Certificate.pdf');
@ -54,17 +56,11 @@ export class CertificateService {
}, },
}); });
if (!enrollment) { if (!enrollment) throw new NotFoundError('Enrollment not found');
throw new NotFoundError('Enrollment not found');
}
if (enrollment.status !== 'COMPLETED') { if (enrollment.status !== 'COMPLETED') throw new ForbiddenError('Course not completed yet');
throw new ForbiddenError('Course not completed yet');
}
if (!enrollment.course.have_certificate) { if (!enrollment.course.have_certificate) throw new ValidationError('This course does not offer certificates');
throw new ValidationError('This course does not offer certificates');
}
// Check if certificate already exists // Check if certificate already exists
const existingCertificate = await prisma.certificate.findFirst({ const existingCertificate = await prisma.certificate.findFirst({
@ -121,6 +117,14 @@ export class CertificateService {
}, },
}); });
auditService.log({
userId: decoded.id,
action: AuditAction.CREATE,
entityType: 'Certificate',
entityId: certificate.id,
newValue: { file_path: certificate.file_path, issued_at: certificate.issued_at },
});
const downloadUrl = await getPresignedUrl(filePath, 3600); const downloadUrl = await getPresignedUrl(filePath, 3600);
return { return {
@ -135,6 +139,18 @@ export class CertificateService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to generate certificate', { error }); logger.error('Failed to generate certificate', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id,
action: AuditAction.ERROR,
entityType: 'Certificate',
entityId: 0,
metadata: {
operation: 'generate_certificate',
course_id: input.course_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -186,6 +202,18 @@ export class CertificateService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to get certificate', { error }); logger.error('Failed to get certificate', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id,
action: AuditAction.ERROR,
entityType: 'Certificate',
entityId: 0,
metadata: {
operation: 'get_certificate',
course_id: input.course_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -239,6 +267,17 @@ export class CertificateService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to list certificates', { error }); logger.error('Failed to list certificates', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id,
action: AuditAction.ERROR,
entityType: 'Certificate',
entityId: 0,
metadata: {
operation: 'list_my_certificates',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -5,6 +5,8 @@ import { logger } from '../config/logger';
import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.types'; import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { getPresignedUrl } from '../config/minio'; import { getPresignedUrl } from '../config/minio';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class CoursesService { export class CoursesService {
async ListCourses(input: ListCoursesInput): Promise<listCourseResponse> { async ListCourses(input: ListCoursesInput): Promise<listCourseResponse> {
@ -82,6 +84,16 @@ export class CoursesService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to fetch courses', { error }); logger.error('Failed to fetch courses', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: 0,
metadata: {
operation: 'list_courses',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -122,6 +134,16 @@ export class CoursesService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to fetch course', { error }); logger.error('Failed to fetch course', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: id,
metadata: {
operation: 'get_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -135,6 +135,17 @@ export class UserService {
throw new UnauthorizedError('Token expired'); throw new UnauthorizedError('Token expired');
} }
logger.error('Failed to change password', { error }); logger.error('Failed to change password', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'User',
entityId: decoded?.id || 0,
metadata: {
operation: 'change_password',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -186,6 +197,17 @@ export class UserService {
throw new UnauthorizedError('Token expired'); throw new UnauthorizedError('Token expired');
} }
logger.error('Failed to update profile', { error }); logger.error('Failed to update profile', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.UPDATE,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'update_profile',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -252,6 +274,18 @@ export class UserService {
}); });
} }
// Audit log - UPLOAD_AVATAR
await auditService.logSync({
userId: decoded.id,
action: AuditAction.UPLOAD_FILE,
entityType: 'User',
entityId: decoded.id,
metadata: {
operation: 'upload_avatar',
filePath
}
});
// Generate presigned URL for response // Generate presigned URL for response
const presignedUrl = await this.getAvatarPresignedUrl(filePath); const presignedUrl = await this.getAvatarPresignedUrl(filePath);
@ -273,6 +307,18 @@ export class UserService {
throw new UnauthorizedError('Token expired'); throw new UnauthorizedError('Token expired');
} }
logger.error('Failed to upload avatar', { error }); logger.error('Failed to upload avatar', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.UPLOAD_FILE,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'upload_avatar',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -385,6 +431,17 @@ export class UserService {
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token'); if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token');
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired'); if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired');
logger.error('Failed to send verification email', { error }); logger.error('Failed to send verification email', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'send_verification_email',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -415,6 +472,15 @@ export class UserService {
}); });
logger.info('Email verified successfully', { userId: user.id, email: user.email }); logger.info('Email verified successfully', { userId: user.id, email: user.email });
await auditService.logSync({
userId: user.id,
action: AuditAction.VERIFY_EMAIL,
entityType: 'UserProfile',
entityId: user.id,
metadata: {
operation: 'verify_email'
}
});
return { return {
code: 200, code: 200,
message: 'Email verified successfully' message: 'Email verified successfully'
@ -423,6 +489,17 @@ export class UserService {
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid verification token'); if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid verification token');
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Verification link has expired'); if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Verification link has expired');
logger.error('Failed to verify email', { error }); logger.error('Failed to verify email', { error });
const decoded = jwt.decode(verifyToken) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'verify_email',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -39,6 +39,16 @@ export class UserManagementService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to fetch users', { error }); logger.error('Failed to fetch users', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'User',
entityId: 0,
metadata: {
operation: 'list_users',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -61,6 +71,16 @@ export class UserManagementService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to fetch user by ID', { error }); logger.error('Failed to fetch user by ID', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'get_user_by_id',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -95,6 +115,17 @@ export class UserManagementService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to update user role', { error }); logger.error('Failed to update user role', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'update_user_role',
target_role_id: role_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -114,6 +145,16 @@ export class UserManagementService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to deactivate user', { error }); logger.error('Failed to deactivate user', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'delete_user',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -160,6 +201,16 @@ export class UserManagementService {
throw new UnauthorizedError('Token expired'); throw new UnauthorizedError('Token expired');
} }
logger.error('Failed to deactivate account', { error }); logger.error('Failed to deactivate account', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'deactivate_account',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -207,6 +258,16 @@ export class UserManagementService {
throw new UnauthorizedError('Token expired'); throw new UnauthorizedError('Token expired');
} }
logger.error('Failed to activate account', { error }); logger.error('Failed to activate account', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'activate_account',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }