diff --git a/Backend/src/controllers/CoursesInstructorController.ts b/Backend/src/controllers/CoursesInstructorController.ts index 5b698807..a0f956c7 100644 --- a/Backend/src/controllers/CoursesInstructorController.ts +++ b/Backend/src/controllers/CoursesInstructorController.ts @@ -5,6 +5,7 @@ import { createCourses, createCourseResponse, GetMyCourseResponse, + ListMyCoursesInput, ListMyCourseResponse, addinstructorCourseResponse, removeinstructorCourseResponse, @@ -41,12 +42,15 @@ export class CoursesInstructorController { @SuccessResponse('200', 'Courses retrieved successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Courses not found') - public async listMyCourses(@Request() request: any): Promise { + public async listMyCourses( + @Request() request: any, + @Query() status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED' + ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) { throw new ValidationError('No token provided'); } - return await CoursesInstructorService.listMyCourses(token); + return await CoursesInstructorService.listMyCourses({ token, status }); } /** diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index 244e26ac..aeeae850 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -10,6 +10,7 @@ import { UpdateCourseInput, createCourseResponse, GetMyCourseResponse, + ListMyCoursesInput, ListMyCourseResponse, addinstructorCourse, addinstructorCourseResponse, @@ -102,16 +103,27 @@ 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; } } - static async listMyCourses(token: string): Promise { + static async listMyCourses(input: ListMyCoursesInput): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number; type: string }; const courseInstructors = await prisma.courseInstructor.findMany({ where: { - user_id: decoded.id + user_id: decoded.id, + course: input.status ? { status: input.status } : undefined }, include: { course: true @@ -143,6 +155,17 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve courses', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: 0, + metadata: { + operation: 'list_my_courses', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -200,6 +223,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: getmyCourse.course_id, + metadata: { + operation: 'get_my_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -222,6 +256,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: courseId, + metadata: { + operation: 'update_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -275,6 +320,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: courseId, + metadata: { + operation: 'upload_thumbnail', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -298,6 +354,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: courseId, + metadata: { + operation: 'delete_course', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -325,6 +392,17 @@ export class CoursesInstructorService { }; } 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 || undefined, + 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 +425,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: setCourseDraft.course_id, + metadata: { + operation: 'set_course_draft', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -384,6 +473,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: courseId, + metadata: { + operation: 'get_course_approvals', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -445,6 +545,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'search_instructors', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -496,6 +607,17 @@ export class CoursesInstructorService { }; } 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 || undefined, + 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; } } @@ -517,6 +639,17 @@ export class CoursesInstructorService { }; } 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 || undefined, + 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 +700,17 @@ 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 || undefined, + 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; } } @@ -591,6 +735,17 @@ export class CoursesInstructorService { }; } 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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: setprimaryCourseInstructor.course_id, + metadata: { + operation: 'set_primary_instructor', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -707,6 +862,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 || undefined, + 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 +924,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 +1040,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 || undefined, + action: AuditAction.ERROR, + entityType: 'Course', + entityId: input.course_id, + metadata: { + operation: 'get_quiz_scores', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -988,6 +1165,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 || undefined, + 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 +1313,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 || undefined, + 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 +1380,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 || undefined, + 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/audit.service.ts b/Backend/src/services/audit.service.ts index e913be7b..52038129 100644 --- a/Backend/src/services/audit.service.ts +++ b/Backend/src/services/audit.service.ts @@ -37,19 +37,23 @@ export class AuditService { * Log พร้อม await (สำหรับ critical actions) */ async logSync(params: CreateAuditLogParams): Promise { - await prisma.auditLog.create({ - data: { - user_id: params.userId, - action: params.action, - entity_type: params.entityType, - entity_id: params.entityId, - old_value: params.oldValue, - new_value: params.newValue, - ip_address: params.ipAddress, - user_agent: params.userAgent, - metadata: params.metadata, - }, - }); + try { + await prisma.auditLog.create({ + data: { + user_id: params.userId, + action: params.action, + entity_type: params.entityType, + entity_id: params.entityId, + old_value: params.oldValue, + new_value: params.newValue, + ip_address: params.ipAddress, + user_agent: params.userAgent, + metadata: params.metadata, + }, + }); + } catch (error) { + logger.error('Failed to create audit log (sync)', { error, params }); + } } /** diff --git a/Backend/src/types/CoursesInstructor.types.ts b/Backend/src/types/CoursesInstructor.types.ts index a10dac44..e31e80fc 100644 --- a/Backend/src/types/CoursesInstructor.types.ts +++ b/Backend/src/types/CoursesInstructor.types.ts @@ -23,6 +23,11 @@ export interface createCourseResponse { data: Course; } +export interface ListMyCoursesInput { + token: string; + status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED'; +} + export interface ListMyCourseResponse { code: number; message: string;