From bb38c0f3c97509516c629d7c986154ed8a854b75 Mon Sep 17 00:00:00 2001 From: JakkrapartXD Date: Thu, 12 Feb 2026 17:55:45 +0700 Subject: [PATCH] feat: Add token-based authorization to category deletion and enhance user registration with error handling and audit logging. --- Backend/prisma/schema.prisma | 2 + .../AdminCourseApprovalController.ts | 20 +- .../src/controllers/CategoriesController.ts | 2 +- .../RecommendedCoursesController.ts | 18 +- .../services/AdminCourseApproval.service.ts | 76 +++- .../src/services/ChaptersLesson.service.ts | 198 +++++++++++ .../src/services/CoursesInstructor.service.ts | 260 +++++++++++++- .../src/services/CoursesStudent.service.ts | 157 +++++++-- .../services/RecommendedCourses.service.ts | 41 ++- Backend/src/services/announcements.service.ts | 66 +++- Backend/src/services/auth.service.ts | 326 ++++++++++-------- Backend/src/services/categories.service.ts | 56 ++- Backend/src/services/certificate.service.ts | 57 ++- Backend/src/services/courses.service.ts | 22 ++ Backend/src/services/user.service.ts | 77 +++++ .../src/services/usermanagement.service.ts | 61 ++++ 16 files changed, 1202 insertions(+), 237 deletions(-) diff --git a/Backend/prisma/schema.prisma b/Backend/prisma/schema.prisma index 212b3bf3..bc4b2267 100644 --- a/Backend/prisma/schema.prisma +++ b/Backend/prisma/schema.prisma @@ -634,6 +634,8 @@ enum AuditAction { VERIFY_EMAIL DEACTIVATE_USER ACTIVATE_USER + ERROR + WARNING } model AuditLog { diff --git a/Backend/src/controllers/AdminCourseApprovalController.ts b/Backend/src/controllers/AdminCourseApprovalController.ts index 0ad0171a..dac45be4 100644 --- a/Backend/src/controllers/AdminCourseApprovalController.ts +++ b/Backend/src/controllers/AdminCourseApprovalController.ts @@ -25,10 +25,8 @@ export class AdminCourseApprovalController { @Response('403', 'Forbidden - Admin only') public async listPendingCourses(@Request() request: any): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await AdminCourseApprovalService.listPendingCourses(); + if (!token) throw new ValidationError('No token provided'); + return await AdminCourseApprovalService.listPendingCourses(token); } /** @@ -44,10 +42,8 @@ export class AdminCourseApprovalController { @Response('404', 'Course not found') public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await AdminCourseApprovalService.getCourseDetail(courseId); + if (!token) throw new ValidationError('No token provided'); + return await AdminCourseApprovalService.getCourseDetail(token, courseId); } /** @@ -68,9 +64,7 @@ export class AdminCourseApprovalController { @Body() body?: ApproveCourseBody ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } + if (!token) throw new ValidationError('No token provided'); return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment); } @@ -92,9 +86,7 @@ export class AdminCourseApprovalController { @Body() body: RejectCourseBody ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } + if (!token) throw new ValidationError('No token provided'); return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment); } } diff --git a/Backend/src/controllers/CategoriesController.ts b/Backend/src/controllers/CategoriesController.ts index 09a5a621..3c99a3b5 100644 --- a/Backend/src/controllers/CategoriesController.ts +++ b/Backend/src/controllers/CategoriesController.ts @@ -45,6 +45,6 @@ export class CategoriesAdminController { @Response('401', 'Invalid or expired token') public async deleteCategory(@Request() request: any, @Path() id: number): Promise { const token = request.headers.authorization?.replace('Bearer ', '') || ''; - return await this.categoryService.deleteCategory(id); + return await this.categoryService.deleteCategory(token,id); } } \ No newline at end of file diff --git a/Backend/src/controllers/RecommendedCoursesController.ts b/Backend/src/controllers/RecommendedCoursesController.ts index 7e770c12..06bff36d 100644 --- a/Backend/src/controllers/RecommendedCoursesController.ts +++ b/Backend/src/controllers/RecommendedCoursesController.ts @@ -22,10 +22,8 @@ export class RecommendedCoursesController { @Response('403', 'Forbidden - Admin only') public async listApprovedCourses(@Request() request: any): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await RecommendedCoursesService.listApprovedCourses(); + if (!token) throw new ValidationError('No token provided'); + return await RecommendedCoursesService.listApprovedCourses(token); } /** @@ -42,10 +40,8 @@ export class RecommendedCoursesController { @Response('404', 'Course not found') public async getCourseById(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await RecommendedCoursesService.getCourseById(courseId); + if (!token) throw new ValidationError('No token provided'); + return await RecommendedCoursesService.getCourseById(token, courseId); } /** @@ -62,13 +58,11 @@ export class RecommendedCoursesController { @Response('404', 'Course not found') public async toggleRecommended( @Request() request: any, - @Path() courseId: number, + @Path() courseId: number, @Query() is_recommended: boolean ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } + if (!token) throw new ValidationError('No token provided'); return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended); } } diff --git a/Backend/src/services/AdminCourseApproval.service.ts b/Backend/src/services/AdminCourseApproval.service.ts index 5e9da6ea..f9446457 100644 --- a/Backend/src/services/AdminCourseApproval.service.ts +++ b/Backend/src/services/AdminCourseApproval.service.ts @@ -18,7 +18,7 @@ export class AdminCourseApprovalService { /** * Get all pending courses for admin review */ - static async listPendingCourses(): Promise { + static async listPendingCourses(token: string): Promise { try { const courses = await prisma.course.findMany({ where: { status: 'PENDING' }, @@ -68,18 +68,18 @@ export class AdminCourseApprovalService { description: course.description as { th: string; en: string }, thumbnail_url: thumbnail_presigned_url, status: course.status, - created_at: course.created_at, - updated_at: course.updated_at, - created_by: course.created_by, - creator: course.creator, - instructors: course.instructors.map(i => ({ - user_id: i.user_id, - is_primary: i.is_primary, - user: i.user - })), - chapters_count: course.chapters.length, - lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0), - latest_submission: course.courseApprovals[0] ? { + created_at: course.created_at, + updated_at: course.updated_at, + created_by: course.created_by, + creator: course.creator, + instructors: course.instructors.map(i => ({ + user_id: i.user_id, + is_primary: i.is_primary, + user: i.user + })), + chapters_count: course.chapters.length, + lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0), + latest_submission: course.courseApprovals[0] ? { id: course.courseApprovals[0].id, submitted_by: course.courseApprovals[0].submitted_by, created_at: course.courseApprovals[0].created_at, @@ -96,6 +96,16 @@ export class AdminCourseApprovalService { }; } catch (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; } } @@ -103,7 +113,7 @@ export class AdminCourseApprovalService { /** * Get course details for admin review */ - static async getCourseDetail(courseId: number): Promise { + static async getCourseDetail(token: string,courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, @@ -214,6 +224,16 @@ export class AdminCourseApprovalService { }; } catch (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; } } @@ -238,7 +258,7 @@ export class AdminCourseApprovalService { // Update course status prisma.course.update({ where: { id: courseId }, - data: { + data: { status: 'APPROVED', approved_by: decoded.id, approved_at: new Date() @@ -275,6 +295,17 @@ export class AdminCourseApprovalService { }; } catch (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; } } @@ -303,12 +334,12 @@ export class AdminCourseApprovalService { // Update course status back to REJECTED prisma.course.update({ where: { id: courseId }, - data: { + data: { status: 'REJECTED', rejection_reason: comment, approved_by: null, approved_at: null - } + } }), // Create rejection record prisma.courseApproval.create({ @@ -341,6 +372,17 @@ export class AdminCourseApprovalService { }; } catch (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; } } diff --git a/Backend/src/services/ChaptersLesson.service.ts b/Backend/src/services/ChaptersLesson.service.ts index 02ce0a67..003670b1 100644 --- a/Backend/src/services/ChaptersLesson.service.ts +++ b/Backend/src/services/ChaptersLesson.service.ts @@ -142,6 +142,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData }; } catch (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; } } @@ -163,6 +174,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData }; } catch (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; } } @@ -197,6 +219,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter deleted successfully' }; } catch (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; } } @@ -280,6 +313,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] }; } catch (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; } } @@ -354,6 +398,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData }; } catch (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; } } @@ -494,6 +549,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData }; } catch (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; } } @@ -515,6 +581,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData }; } catch (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; } } @@ -605,6 +682,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] }; } catch (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; } } @@ -676,6 +764,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson deleted successfully' }; } catch (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; } } @@ -754,6 +853,17 @@ export class ChaptersLessonService { }; } catch (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; } } @@ -836,6 +946,17 @@ export class ChaptersLessonService { }; } catch (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; } } @@ -917,6 +1038,17 @@ export class ChaptersLessonService { }; } catch (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; } } @@ -993,6 +1125,17 @@ export class ChaptersLessonService { }; } catch (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; } } @@ -1051,6 +1194,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Attachment deleted successfully' }; } catch (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; } } @@ -1127,6 +1281,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData }; } catch (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; } } @@ -1202,6 +1367,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData }; } catch (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; } } @@ -1295,6 +1471,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] }; } catch (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; } } @@ -1343,6 +1530,17 @@ export class ChaptersLessonService { return { code: 200, message: 'Question deleted successfully' }; } catch (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; } } diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index 244e26ac..9925ce80 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -102,6 +102,16 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to create course', { error }); + await auditService.logSync({ + userId: userId, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: 0, // Failed to create, so no ID + metadata: { + operation: 'create_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -200,6 +210,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve course', { error }); + const decoded = jwt.decode(getmyCourse.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: getmyCourse.course_id, + metadata: { + operation: 'get_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -222,6 +243,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to update 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: 'update_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -275,6 +307,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to upload thumbnail', { 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: 'upload_thumbnail', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -291,6 +334,15 @@ export class CoursesInstructorService { id: courseId } }); + await auditService.logSync({ + userId: courseInstructorId.user_id, + action: AuditAction.DELETE, + entityType: 'Course', + entityId: courseId, + metadata: { + operation: 'delete_course' + } + }); return { code: 200, message: 'Course deleted successfully', @@ -298,6 +350,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to delete 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: 'delete_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -319,12 +382,32 @@ export class CoursesInstructorService { status: 'PENDING' } }); + await auditService.logSync({ + userId: decoded.id, + action: AuditAction.UPDATE, + entityType: 'Course', + entityId: sendCourseForReview.course_id, + metadata: { + operation: 'send_course_for_review' + } + }); return { code: 200, message: 'Course sent for review successfully', }; } catch (error) { logger.error('Failed to send course for review', { error }); + const decoded = jwt.decode(sendCourseForReview.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: sendCourseForReview.course_id, + metadata: { + operation: 'send_course_for_review', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -347,6 +430,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to set course to draft', { error }); + const decoded = jwt.decode(setCourseDraft.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: setCourseDraft.course_id, + metadata: { + operation: 'set_course_draft', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -358,8 +452,6 @@ export class CoursesInstructorService { total: number; }> { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; - // Validate instructor access await this.validateCourseInstructor(token, courseId); @@ -384,6 +476,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve course approvals', { 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: 'get_course_approvals', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -445,6 +548,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to search instructors', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'search_instructors', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -490,12 +604,35 @@ 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 { code: 200, message: 'Instructor added to course successfully', }; } catch (error) { logger.error('Failed to add instructor to course', { error }); + const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: addinstructorCourse.course_id, + metadata: { + operation: 'add_instructor_to_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -511,12 +648,36 @@ 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 { code: 200, message: 'Instructor removed from course successfully', }; } catch (error) { logger.error('Failed to remove instructor from course', { error }); + const decoded = jwt.decode(removeinstructorCourse.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: removeinstructorCourse.course_id, + metadata: { + operation: 'remove_instructor_from_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -567,6 +728,18 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve instructors of course', { error }); + const decoded = jwt.decode(listinstructorCourse.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: listinstructorCourse.course_id, + metadata: { + operation: 'list_instructors_of_course', + error: error instanceof Error ? error.message : String(error) + } + }); + throw error; } } @@ -585,12 +758,36 @@ export class CoursesInstructorService { 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 { code: 200, message: 'Primary instructor set successfully', }; } catch (error) { logger.error('Failed to set primary instructor', { error }); + const decoded = jwt.decode(setprimaryCourseInstructor.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: setprimaryCourseInstructor.course_id, + metadata: { + operation: 'set_primary_instructor', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -629,7 +826,6 @@ export class CoursesInstructorService { static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise { try { const { token, course_id, page = 1, limit = 20, search, status } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor await this.validateCourseInstructor(token, course_id); @@ -707,6 +903,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting enrolled students: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'get_enrolled_students', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -758,7 +965,7 @@ export class CoursesInstructorService { // Get all enrolled students who have attempted this quiz const skip = (page - 1) * limit; - + // Get unique users who attempted this quiz const quizAttempts = await prisma.quizAttempt.findMany({ where: { quiz_id: lesson.quiz.id }, @@ -874,6 +1081,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting quiz scores: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'get_quiz_scores', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -885,7 +1103,6 @@ export class CoursesInstructorService { static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise { try { const { token, course_id, lesson_id, student_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor await this.validateCourseInstructor(token, course_id); @@ -988,6 +1205,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting quiz attempt detail: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'get_quiz_attempt_detail', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -1125,6 +1353,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting enrolled student detail: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'get_enrolled_student_detail', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -1181,6 +1420,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting course approval history: ${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: 'get_course_approval_history', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } diff --git a/Backend/src/services/CoursesStudent.service.ts b/Backend/src/services/CoursesStudent.service.ts index 87679ab3..0e9e5b86 100644 --- a/Backend/src/services/CoursesStudent.service.ts +++ b/Backend/src/services/CoursesStudent.service.ts @@ -186,7 +186,20 @@ export class CoursesStudentService { }, }; } 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; } } @@ -261,6 +274,17 @@ export class CoursesStudentService { }; } catch (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; } } @@ -416,6 +440,17 @@ export class CoursesStudentService { }; } catch (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; } } @@ -678,6 +713,17 @@ export class CoursesStudentService { }; } catch (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; } } @@ -866,6 +912,17 @@ export class CoursesStudentService { }; } catch (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; } } @@ -940,6 +997,17 @@ export class CoursesStudentService { }; } catch (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; } } @@ -1037,6 +1105,17 @@ export class CoursesStudentService { }; } catch (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; } } @@ -1168,7 +1247,19 @@ export class CoursesStudentService { }, }; } 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; } } @@ -1213,22 +1304,14 @@ export class CoursesStudentService { }, }); - if (!lesson) { - throw new NotFoundError('Lesson not found'); - } + if (!lesson) throw new NotFoundError('Lesson not found'); - if (lesson.type !== 'QUIZ') { - throw new ValidationError('This lesson is not a quiz'); - } + if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz'); - if (!lesson.quiz) { - throw new NotFoundError('Quiz not found for this lesson'); - } + if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson'); // Verify lesson belongs to the course - if (lesson.chapter.course_id !== course_id) { - throw new NotFoundError('Lesson not found in this course'); - } + if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course'); const quiz = lesson.quiz; @@ -1332,7 +1415,20 @@ export class CoursesStudentService { }, }; } 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; } } @@ -1373,22 +1469,14 @@ export class CoursesStudentService { }, }); - if (!lesson) { - throw new NotFoundError('Lesson not found'); - } + if (!lesson) throw new NotFoundError('Lesson not found'); - if (lesson.type !== 'QUIZ') { - throw new ValidationError('This lesson is not a quiz'); - } + if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz'); - if (!lesson.quiz) { - throw new NotFoundError('Quiz not found for this lesson'); - } + if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson'); // Verify lesson belongs to the course - if (lesson.chapter.course_id !== course_id) { - throw new NotFoundError('Lesson not found in this course'); - } + if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course'); // Get all quiz attempts for this user const attempts = await prisma.quizAttempt.findMany({ @@ -1438,6 +1526,21 @@ export class CoursesStudentService { }; } catch (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; } } diff --git a/Backend/src/services/RecommendedCourses.service.ts b/Backend/src/services/RecommendedCourses.service.ts index b5c977c5..9c185c8a 100644 --- a/Backend/src/services/RecommendedCourses.service.ts +++ b/Backend/src/services/RecommendedCourses.service.ts @@ -18,7 +18,7 @@ export class RecommendedCoursesService { /** * List all approved courses (for admin to manage recommendations) */ - static async listApprovedCourses(): Promise { + static async listApprovedCourses(token: string): Promise { try { const courses = await prisma.course.findMany({ where: { status: 'APPROVED' }, @@ -94,6 +94,19 @@ export class RecommendedCoursesService { }; } catch (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; } } @@ -101,7 +114,7 @@ export class RecommendedCoursesService { /** * Get course by ID (for admin to view details) */ - static async getCourseById(courseId: number): Promise { + static async getCourseById(token: string, courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, @@ -179,6 +192,19 @@ export class RecommendedCoursesService { }; } catch (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; } } @@ -229,6 +255,17 @@ export class RecommendedCoursesService { }; } catch (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; } } diff --git a/Backend/src/services/announcements.service.ts b/Backend/src/services/announcements.service.ts index eb26b3d1..7e8b2d3e 100644 --- a/Backend/src/services/announcements.service.ts +++ b/Backend/src/services/announcements.service.ts @@ -20,6 +20,8 @@ import { } from '../types/announcements.types'; import { CoursesInstructorService } from './CoursesInstructor.service'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class AnnouncementsService { @@ -37,9 +39,7 @@ export class AnnouncementsService { where: { id: decoded.id }, include: { role: true }, }); - if (!user) { - throw new UnauthorizedError('Invalid token'); - } + if (!user) throw new UnauthorizedError('Invalid token'); // Admin can access all courses const isAdmin = user.role.code === 'ADMIN'; @@ -130,6 +130,16 @@ export class AnnouncementsService { }; } catch (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; } } @@ -226,6 +236,16 @@ export class AnnouncementsService { }; } catch (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; } } @@ -300,6 +320,16 @@ export class AnnouncementsService { }; } catch (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; } } @@ -346,6 +376,16 @@ export class AnnouncementsService { }; } catch (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; } } @@ -411,6 +451,16 @@ export class AnnouncementsService { }; } catch (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; } } @@ -458,6 +508,16 @@ export class AnnouncementsService { }; } catch (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; } } diff --git a/Backend/src/services/auth.service.ts b/Backend/src/services/auth.service.ts index ad4bcbbc..66899bed 100644 --- a/Backend/src/services/auth.service.ts +++ b/Backend/src/services/auth.service.ts @@ -83,167 +83,201 @@ export class AuthService { * User registration */ async register(data: RegisterRequest): Promise { - const { username, email, password, first_name, last_name, prefix, phone } = data; + try { + const { username, email, password, first_name, last_name, prefix, phone } = data; - // Check if username already exists - const existingUsername = await prisma.user.findUnique({ - where: { username } - }); + // Check if username already exists + const existingUsername = await prisma.user.findUnique({ + where: { username } + }); - if (existingUsername) { - throw new ValidationError('Username already exists'); - } - - // Check if email already exists - const existingEmail = await prisma.user.findUnique({ - where: { email } - }); - - if (existingEmail) { - throw new ValidationError('Email already exists'); - } - - // Check if phone number already exists in user profiles - const existingPhone = await prisma.userProfile.findFirst({ - where: { phone } - }); - - if (existingPhone) { - throw new ValidationError('Phone number already exists'); - } - - // Get STUDENT role - const studentRole = await prisma.role.findUnique({ - where: { code: 'STUDENT' } - }); - - if (!studentRole) { - logger.error('STUDENT role not found in database'); - throw new Error('System configuration error'); - } - - // Hash password - const hashedPassword = await bcrypt.hash(password, 10); - - // Create user with profile - const user = await prisma.user.create({ - data: { - username, - email, - password: hashedPassword, - role_id: studentRole.id, - profile: { - create: { - prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull, - first_name, - last_name, - phone - } - } - }, - include: { - role: true, - profile: true + if (existingUsername) { + throw new ValidationError('Username already exists'); } - }); - logger.info('New user registered', { userId: user.id, username: user.username }); + // Check if email already exists + const existingEmail = await prisma.user.findUnique({ + where: { email } + }); - // 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' } - }); + if (existingEmail) { + throw new ValidationError('Email already exists'); + } - return { - user: this.formatUserResponseSync(user), - message: 'Registration successful' - }; + // Check if phone number already exists in user profiles + const existingPhone = await prisma.userProfile.findFirst({ + where: { phone } + }); + + if (existingPhone) { + throw new ValidationError('Phone number already exists'); + } + + // Get STUDENT role + const studentRole = await prisma.role.findUnique({ + where: { code: 'STUDENT' } + }); + + if (!studentRole) { + logger.error('STUDENT role not found in database'); + throw new Error('System configuration error'); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create user with profile + const user = await prisma.user.create({ + data: { + username, + email, + password: hashedPassword, + role_id: studentRole.id, + profile: { + create: { + prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull, + first_name, + last_name, + phone + } + } + }, + include: { + role: true, + profile: true + } + }); + + 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' + }; + } 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 { - const { username, email, password, first_name, last_name, prefix, phone } = data; + try { + const { username, email, password, first_name, last_name, prefix, phone } = data; - // Check if username already exists - const existingUsername = await prisma.user.findUnique({ - where: { username } - }); + // Check if username already exists + const existingUsername = await prisma.user.findUnique({ + where: { username } + }); - if (existingUsername) { - throw new ValidationError('Username already exists'); - } - - // Check if email already exists - const existingEmail = await prisma.user.findUnique({ - where: { email } - }); - - if (existingEmail) { - throw new ValidationError('Email already exists'); - } - - // Check if phone number already exists in user profiles - const existingPhone = await prisma.userProfile.findFirst({ - where: { phone } - }); - - if (existingPhone) { - throw new ValidationError('Phone number already exists'); - } - - // Get INSTRUCTOR role - const instructorRole = await prisma.role.findUnique({ - where: { code: 'INSTRUCTOR' } - }); - - if (!instructorRole) { - logger.error('INSTRUCTOR role not found in database'); - throw new Error('System configuration error'); - } - - // Hash password - const hashedPassword = await bcrypt.hash(password, 10); - - // Create user with profile - const user = await prisma.user.create({ - data: { - username, - email, - password: hashedPassword, - role_id: instructorRole.id, - profile: { - create: { - prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull, - first_name, - last_name, - phone - } - } - }, - include: { - role: true, - profile: true + if (existingUsername) { + throw new ValidationError('Username already exists'); } - }); - logger.info('New user registered', { userId: user.id, username: user.username }); + // Check if email already exists + const existingEmail = await prisma.user.findUnique({ + where: { email } + }); - // 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' } - }); + if (existingEmail) { + throw new ValidationError('Email already exists'); + } - return { - user: this.formatUserResponseSync(user), - message: 'Registration successful' - }; + // Check if phone number already exists in user profiles + const existingPhone = await prisma.userProfile.findFirst({ + where: { phone } + }); + + if (existingPhone) { + throw new ValidationError('Phone number already exists'); + } + + // Get INSTRUCTOR role + const instructorRole = await prisma.role.findUnique({ + where: { code: 'INSTRUCTOR' } + }); + + if (!instructorRole) { + logger.error('INSTRUCTOR role not found in database'); + throw new Error('System configuration error'); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create user with profile + const user = await prisma.user.create({ + data: { + username, + email, + password: hashedPassword, + role_id: instructorRole.id, + profile: { + create: { + prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull, + first_name, + last_name, + phone + } + } + }, + include: { + role: true, + profile: true + } + }); + + 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' + }; + } 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; + } } /** diff --git a/Backend/src/services/categories.service.ts b/Backend/src/services/categories.service.ts index 2b9138b9..0e0defa2 100644 --- a/Backend/src/services/categories.service.ts +++ b/Backend/src/services/categories.service.ts @@ -5,6 +5,8 @@ import { logger } from '../config/logger'; import jwt from 'jsonwebtoken'; import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class CategoryService { async listCategories(): Promise { @@ -30,6 +32,13 @@ export class CategoryService { const newCategory = await prisma.category.create({ 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 { code: 200, message: 'Category created successfully', @@ -43,6 +52,16 @@ export class CategoryService { }; } catch (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; } } @@ -54,6 +73,13 @@ export class CategoryService { where: { id }, 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 { code: 200, message: 'Category updated successfully', @@ -67,21 +93,49 @@ export class CategoryService { }; } catch (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; } } - async deleteCategory(id: number): Promise { + async deleteCategory(token: string, id: number): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; const deletedCategory = await prisma.category.delete({ 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 { code: 200, message: 'Category deleted successfully', }; } catch (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; } } diff --git a/Backend/src/services/certificate.service.ts b/Backend/src/services/certificate.service.ts index ab1749ae..4041ec41 100644 --- a/Backend/src/services/certificate.service.ts +++ b/Backend/src/services/certificate.service.ts @@ -16,6 +16,8 @@ import { ListMyCertificatesInput, ListMyCertificatesResponse, } from '../types/certificate.types'; +import { auditService } from './audit.service'; +import { AuditAction } from '@prisma/client'; export class CertificateService { private static TEMPLATE_PATH = path.join(__dirname, '../../assets/templates/Certificate.pdf'); @@ -54,17 +56,11 @@ export class CertificateService { }, }); - if (!enrollment) { - throw new NotFoundError('Enrollment not found'); - } + if (!enrollment) throw new NotFoundError('Enrollment not found'); - if (enrollment.status !== 'COMPLETED') { - throw new ForbiddenError('Course not completed yet'); - } + if (enrollment.status !== 'COMPLETED') throw new ForbiddenError('Course not completed yet'); - if (!enrollment.course.have_certificate) { - throw new ValidationError('This course does not offer certificates'); - } + if (!enrollment.course.have_certificate) throw new ValidationError('This course does not offer certificates'); // Check if certificate already exists 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); return { @@ -135,6 +139,18 @@ export class CertificateService { }; } catch (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; } } @@ -186,6 +202,18 @@ export class CertificateService { }; } catch (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; } } @@ -239,6 +267,17 @@ export class CertificateService { }; } catch (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; } } diff --git a/Backend/src/services/courses.service.ts b/Backend/src/services/courses.service.ts index 252744f8..0700a7fa 100644 --- a/Backend/src/services/courses.service.ts +++ b/Backend/src/services/courses.service.ts @@ -5,6 +5,8 @@ import { logger } from '../config/logger'; import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.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 CoursesService { async ListCourses(input: ListCoursesInput): Promise { @@ -82,6 +84,16 @@ export class CoursesService { }; } catch (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; } } @@ -122,6 +134,16 @@ export class CoursesService { }; } catch (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; } } diff --git a/Backend/src/services/user.service.ts b/Backend/src/services/user.service.ts index da9bc274..918b12e3 100644 --- a/Backend/src/services/user.service.ts +++ b/Backend/src/services/user.service.ts @@ -135,6 +135,17 @@ export class UserService { throw new UnauthorizedError('Token expired'); } 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; } } @@ -186,6 +197,17 @@ export class UserService { throw new UnauthorizedError('Token expired'); } 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; } } @@ -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 const presignedUrl = await this.getAvatarPresignedUrl(filePath); @@ -273,6 +307,18 @@ export class UserService { throw new UnauthorizedError('Token expired'); } 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; } } @@ -385,6 +431,17 @@ export class UserService { if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token'); if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired'); 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; } } @@ -415,6 +472,15 @@ export class UserService { }); 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 { code: 200, 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.TokenExpiredError) throw new UnauthorizedError('Verification link has expired'); 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; } } diff --git a/Backend/src/services/usermanagement.service.ts b/Backend/src/services/usermanagement.service.ts index 658382c2..2bded499 100644 --- a/Backend/src/services/usermanagement.service.ts +++ b/Backend/src/services/usermanagement.service.ts @@ -39,6 +39,16 @@ export class UserManagementService { }; } catch (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; } } @@ -61,6 +71,16 @@ export class UserManagementService { }; } catch (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; } } @@ -95,6 +115,17 @@ export class UserManagementService { }; } catch (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; } } @@ -114,6 +145,16 @@ export class UserManagementService { }; } catch (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; } } @@ -160,6 +201,16 @@ export class UserManagementService { throw new UnauthorizedError('Token expired'); } 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; } } @@ -207,6 +258,16 @@ export class UserManagementService { throw new UnauthorizedError('Token expired'); } 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; } }