diff --git a/Backend/src/controllers/AdminCourseApprovalController.ts b/Backend/src/controllers/AdminCourseApprovalController.ts index f10b99ca..46cc1028 100644 --- a/Backend/src/controllers/AdminCourseApprovalController.ts +++ b/Backend/src/controllers/AdminCourseApprovalController.ts @@ -24,9 +24,7 @@ export class AdminCourseApprovalController { @Response('401', 'Unauthorized') @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(token); + return await AdminCourseApprovalService.listPendingCourses(request.user.id); } /** @@ -41,9 +39,7 @@ export class AdminCourseApprovalController { @Response('403', 'Forbidden - Admin only') @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(token, courseId); + return await AdminCourseApprovalService.getCourseDetail(request.user.id, courseId); } /** @@ -62,10 +58,7 @@ export class AdminCourseApprovalController { @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.approveCourse(token, courseId, undefined); + return await AdminCourseApprovalService.approveCourse(request.user.id, courseId, undefined); } /** @@ -85,13 +78,10 @@ export class AdminCourseApprovalController { @Path() courseId: number, @Body() body: RejectCourseBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - // Validate body const { error } = RejectCourseValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); - return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment); + return await AdminCourseApprovalService.rejectCourse(request.user.id, courseId, body.comment); } } diff --git a/Backend/src/controllers/AuditController.ts b/Backend/src/controllers/AuditController.ts index a78c8d5a..6f715061 100644 --- a/Backend/src/controllers/AuditController.ts +++ b/Backend/src/controllers/AuditController.ts @@ -40,11 +40,6 @@ export class AuditController { @Query() page?: number, @Query() limit?: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await auditService.getLogs({ userId, action, @@ -72,11 +67,6 @@ export class AuditController { @Request() request: any, @Path() logId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - const log = await auditService.getLogById(logId); if (!log) { throw new ValidationError('Audit log not found'); @@ -94,11 +84,6 @@ export class AuditController { @Response('401', 'Unauthorized') @Response('403', 'Forbidden - Admin only') public async getAuditStats(@Request() request: any): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await auditService.getStats(); } @@ -118,11 +103,6 @@ export class AuditController { @Path() entityType: string, @Path() entityId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await auditService.getEntityHistory(entityType, entityId); } @@ -142,11 +122,6 @@ export class AuditController { @Path() userId: number, @Query() limit?: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await auditService.getUserActivity(userId, limit || 50); } @@ -164,11 +139,6 @@ export class AuditController { @Request() request: any, @Query() days: number = 90 ): Promise<{ deleted: number; message: string }> { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - if (days < 6) { throw new ValidationError('Cannot delete logs newer than 6 days'); } diff --git a/Backend/src/controllers/AuthController.ts b/Backend/src/controllers/AuthController.ts index 96cb0673..b3c8e280 100644 --- a/Backend/src/controllers/AuthController.ts +++ b/Backend/src/controllers/AuthController.ts @@ -33,32 +33,6 @@ export class AuthController { data: { token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', - user: { - id: 1, - username: 'admin', - email: 'admin@elearning.local', - email_verified_at: new Date('2024-01-01T00:00:00Z'), - updated_at: new Date('2024-01-01T00:00:00Z'), - created_at: new Date('2024-01-01T00:00:00Z'), - role: { - code: 'ADMIN', - name: { - th: 'ผู้ดูแลระบบ', - en: 'Administrator' - } - }, - profile: { - prefix: { - th: 'นาย', - en: 'Mr.' - }, - first_name: 'Admin', - last_name: 'User', - phone: null, - avatar_url: null, - birth_date: null - } - } } }) public async login(@Body() body: LoginRequest): Promise { diff --git a/Backend/src/controllers/CategoriesController.ts b/Backend/src/controllers/CategoriesController.ts index 81fd8b86..fd4b0d20 100644 --- a/Backend/src/controllers/CategoriesController.ts +++ b/Backend/src/controllers/CategoriesController.ts @@ -27,13 +27,11 @@ export class CategoriesAdminController { @SuccessResponse('200', 'Category created successfully') @Response('401', 'Invalid or expired token') public async createCategory(@Request() request: any, @Body() body: createCategory): Promise { - const token = request.headers.authorization?.replace('Bearer ', '') || ''; - // Validate body const { error } = CreateCategoryValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); - return await this.categoryService.createCategory(token, body); + return await this.categoryService.createCategory(request.user.id, body); } @Put('{id}') @@ -41,13 +39,11 @@ export class CategoriesAdminController { @SuccessResponse('200', 'Category updated successfully') @Response('401', 'Invalid or expired token') public async updateCategory(@Request() request: any, @Body() body: updateCategory): Promise { - const token = request.headers.authorization?.replace('Bearer ', '') || ''; - // Validate body const { error } = UpdateCategoryValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); - return await this.categoryService.updateCategory(token, body.id, body); + return await this.categoryService.updateCategory(request.user.id, body.id, body); } @Delete('{id}') @@ -55,7 +51,6 @@ export class CategoriesAdminController { @SuccessResponse('200', 'Category deleted successfully') @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(token, id); + return await this.categoryService.deleteCategory(request.user.id, id); } } \ No newline at end of file diff --git a/Backend/src/controllers/CertificateController.ts b/Backend/src/controllers/CertificateController.ts index 13ec2075..a6e3f5bf 100644 --- a/Backend/src/controllers/CertificateController.ts +++ b/Backend/src/controllers/CertificateController.ts @@ -1,5 +1,4 @@ import { Get, Post, Route, Tags, SuccessResponse, Response, Security, Path, Request } from 'tsoa'; -import { ValidationError } from '../middleware/errorHandler'; import { CertificateService } from '../services/certificate.service'; import { GenerateCertificateResponse, @@ -21,9 +20,7 @@ export class CertificateController { @SuccessResponse('200', 'Certificates retrieved successfully') @Response('401', 'Invalid or expired token') public async listMyCertificates(@Request() request: any): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await this.certificateService.listMyCertificates({ token }); + return await this.certificateService.listMyCertificates({ userId: request.user.id }); } /** @@ -37,9 +34,7 @@ export class CertificateController { @Response('401', 'Invalid or expired token') @Response('404', 'Certificate not found') public async getCertificate(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await this.certificateService.getCertificate({ token, course_id: courseId }); + return await this.certificateService.getCertificate({ userId: request.user.id, course_id: courseId }); } /** @@ -54,8 +49,6 @@ export class CertificateController { @Response('401', 'Invalid or expired token') @Response('404', 'Enrollment not found') public async generateCertificate(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await this.certificateService.generateCertificate({ token, course_id: courseId }); + return await this.certificateService.generateCertificate({ userId: request.user.id, course_id: courseId }); } } diff --git a/Backend/src/controllers/ChaptersLessonInstructorController.ts b/Backend/src/controllers/ChaptersLessonInstructorController.ts index 7ba48f5c..213ae095 100644 --- a/Backend/src/controllers/ChaptersLessonInstructorController.ts +++ b/Backend/src/controllers/ChaptersLessonInstructorController.ts @@ -65,14 +65,11 @@ export class ChaptersLessonInstructorController { @Path() courseId: number, @Body() body: CreateChapterBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = CreateChapterValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.createChapter({ - token, + userId: request.user.id, course_id: courseId, title: body.title, description: body.description, @@ -96,14 +93,11 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: UpdateChapterBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = UpdateChapterValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateChapter({ - token, + userId: request.user.id, course_id: courseId, chapter_id: chapterId, ...body, @@ -125,9 +119,7 @@ export class ChaptersLessonInstructorController { @Path() courseId: number, @Path() chapterId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await chaptersLessonService.deleteChapter({ token, course_id: courseId, chapter_id: chapterId }); + return await chaptersLessonService.deleteChapter({ userId: request.user.id, course_id: courseId, chapter_id: chapterId }); } /** @@ -143,14 +135,11 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: ReorderChapterBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = ReorderChapterValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.reorderChapter({ - token, + userId: request.user.id, course_id: courseId, chapter_id: chapterId, sort_order: body.sort_order, @@ -174,9 +163,7 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await chaptersLessonService.getLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); + return await chaptersLessonService.getLesson({ userId: request.user.id, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); } /** @@ -192,14 +179,11 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: CreateLessonBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = CreateLessonValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.createLesson({ - token, + userId: request.user.id, course_id: courseId, chapter_id: chapterId, title: body.title, @@ -223,14 +207,11 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Body() body: UpdateLessonBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = UpdateLessonValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateLesson({ - token, + userId: request.user.id, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId, @@ -258,9 +239,7 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await chaptersLessonService.deleteLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); + return await chaptersLessonService.deleteLesson({ userId: request.user.id, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); } /** @@ -276,14 +255,11 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: ReorderLessonsBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = ReorderLessonsValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.reorderLessons({ - token, + userId: request.user.id, course_id: courseId, chapter_id: chapterId, lesson_id: body.lesson_id, @@ -309,14 +285,11 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Body() body: AddQuestionBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = AddQuestionValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.addQuestion({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, ...body, @@ -338,14 +311,11 @@ export class ChaptersLessonInstructorController { @Path() questionId: number, @Body() body: UpdateQuestionBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = UpdateQuestionValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateQuestion({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, question_id: questionId, @@ -364,14 +334,11 @@ export class ChaptersLessonInstructorController { @Path() questionId: number, @Body() body: ReorderQuestionBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = ReorderQuestionValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.reorderQuestion({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, question_id: questionId, @@ -393,10 +360,8 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Path() questionId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await chaptersLessonService.deleteQuestion({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, question_id: questionId, @@ -417,14 +382,11 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Body() body: UpdateQuizBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = UpdateQuizValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateQuiz({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, ...body, diff --git a/Backend/src/controllers/CoursesInstructorController.ts b/Backend/src/controllers/CoursesInstructorController.ts index 3657e928..4556281c 100644 --- a/Backend/src/controllers/CoursesInstructorController.ts +++ b/Backend/src/controllers/CoursesInstructorController.ts @@ -22,12 +22,10 @@ import { GetCourseApprovalHistoryResponse, setCourseDraftResponse, CloneCourseResponse, + GetAllMyStudentsResponse, } from '../types/CoursesInstructor.types'; import { CreateCourseValidator, UpdateCourseValidator, CloneCourseValidator } from "../validators/CoursesInstructor.validator"; -import jwt from 'jsonwebtoken'; -import { config } from '../config'; - @Route('api/instructors/courses') @Tags('CoursesInstructor') export class CoursesInstructorController { @@ -45,11 +43,7 @@ export class CoursesInstructorController { @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, status }); + return await CoursesInstructorService.listMyCourses({ userId: request.user.id, status }); } /** @@ -67,9 +61,23 @@ export class CoursesInstructorController { @Path() courseId: number, @Query() query: string ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await CoursesInstructorService.searchInstructors({ token, query, course_id: courseId }); + return await CoursesInstructorService.searchInstructors({ userId: request.user.id, query, course_id: courseId }); + } + + /** + * ดึงผู้เรียนทั้งหมดของ instructor ทุกคอร์ส + * Get all students enrolled in all of instructor's courses + * + * @returns รายการผู้เรียนแยกตามคอร์ส พร้อม total_enrolled และ total_completed ต่อคอร์ส + */ + @Get('my-students') + @Security('jwt', ['instructor']) + @SuccessResponse('200', 'Students retrieved successfully') + @Response('401', 'Unauthorized') + public async getMyAllStudents( + @Request() request: any + ): Promise { + return await CoursesInstructorService.getMyAllStudents(request.user.id); } /** @@ -83,11 +91,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async getMyCourse(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await CoursesInstructorService.getmyCourse({ token, course_id: courseId }); + return await CoursesInstructorService.getmyCourse({ userId: request.user.id, course_id: courseId }); } /** @@ -101,13 +105,10 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async updateCourse(@Request() request: any, @Path() courseId: number, @Body() body: UpdateMyCourse): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = UpdateCourseValidator.validate(body.data); if (error) throw new ValidationError(error.details[0].message); - return await CoursesInstructorService.updateCourse(token, courseId, body.data); + return await CoursesInstructorService.updateCourse(request.user.id, courseId, body.data); } /** @@ -126,10 +127,6 @@ export class CoursesInstructorController { @FormField() data: string, @UploadedFile() thumbnail?: Express.Multer.File ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const parsed = JSON.parse(data); const { error, value } = CreateCourseValidator.validate(parsed); if (error) throw new ValidationError(error.details[0].message); @@ -137,7 +134,7 @@ export class CoursesInstructorController { // Validate thumbnail file type if provided if (thumbnail && !thumbnail.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed for thumbnail'); - return await CoursesInstructorService.createCourse(value, decoded.id, thumbnail); + return await CoursesInstructorService.createCourse(value, request.user.id, thumbnail); } /** @@ -156,11 +153,9 @@ export class CoursesInstructorController { @Path() courseId: number, @UploadedFile() file: Express.Multer.File ): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); if (!file.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed'); - return await CoursesInstructorService.uploadThumbnail(token, courseId, file); + return await CoursesInstructorService.uploadThumbnail(request.user.id, courseId, file); } /** @@ -174,9 +169,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async deleteCourse(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.deleteCourse(token, courseId); + return await CoursesInstructorService.deleteCourse(request.user.id, courseId); } /** @@ -196,14 +189,11 @@ export class CoursesInstructorController { @Path() courseId: number, @Body() body: { title: { th: string; en: string } } ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - const { error } = CloneCourseValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); const result = await CoursesInstructorService.cloneCourse({ - token, + userId: request.user.id, course_id: courseId, title: body.title }); @@ -220,9 +210,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async submitCourse(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.sendCourseForReview({ token, course_id: courseId }); + return await CoursesInstructorService.sendCourseForReview({ userId: request.user.id, course_id: courseId }); } /** @@ -236,9 +224,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async setCourseDraft(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.setCourseDraft({ token, course_id: courseId }); + return await CoursesInstructorService.setCourseDraft({ userId: request.user.id, course_id: courseId }); } /** @@ -253,9 +239,7 @@ export class CoursesInstructorController { @Response('403', 'You are not an instructor of this course') @Response('404', 'Course not found') public async getCourseApprovals(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.getCourseApprovals(token, courseId); + return await CoursesInstructorService.getCourseApprovals(request.user.id, courseId); } /** @@ -269,9 +253,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Instructors not found') public async listInstructorCourses(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.listInstructorsOfCourse({ token, course_id: courseId }); + return await CoursesInstructorService.listInstructorsOfCourse({ userId: request.user.id, course_id: courseId }); } /** @@ -286,9 +268,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Instructor not found') public async addInstructor(@Request() request: any, @Path() courseId: number, @Path() emailOrUsername: string): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.addInstructorToCourse({ token, course_id: courseId, email_or_username: emailOrUsername }); + return await CoursesInstructorService.addInstructorToCourse({ userId: request.user.id, course_id: courseId, email_or_username: emailOrUsername }); } /** @@ -303,9 +283,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Instructor not found') public async removeInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.removeInstructorFromCourse({ token, course_id: courseId, user_id: userId }); + return await CoursesInstructorService.removeInstructorFromCourse({ userId: request.user.id, course_id: courseId, user_id: userId }); } /** @@ -320,9 +298,7 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Primary instructor not found') public async setPrimaryInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided') - return await CoursesInstructorService.setPrimaryInstructor({ token, course_id: courseId, user_id: userId }); + return await CoursesInstructorService.setPrimaryInstructor({ userId: request.user.id, course_id: courseId, user_id: userId }); } /** @@ -347,10 +323,8 @@ export class CoursesInstructorController { @Query() search?: string, @Query() status?: 'ENROLLED' | 'COMPLETED' ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getEnrolledStudents({ - token, + userId: request.user.id, course_id: courseId, page, limit, @@ -376,10 +350,8 @@ export class CoursesInstructorController { @Path() courseId: number, @Path() studentId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getEnrolledStudentDetail({ - token, + userId: request.user.id, course_id: courseId, student_id: studentId, }); @@ -410,10 +382,8 @@ export class CoursesInstructorController { @Query() search?: string, @Query() isPassed?: boolean ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getQuizScores({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, page, @@ -442,10 +412,8 @@ export class CoursesInstructorController { @Path() lessonId: number, @Path() studentId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getQuizAttemptDetail({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, student_id: studentId, @@ -467,8 +435,6 @@ export class CoursesInstructorController { @Request() request: any, @Path() courseId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await CoursesInstructorService.getCourseApprovalHistory(token, courseId); + return await CoursesInstructorService.getCourseApprovalHistory(request.user.id, courseId); } } \ No newline at end of file diff --git a/Backend/src/controllers/CoursesStudentController.ts b/Backend/src/controllers/CoursesStudentController.ts index 87a5a613..62e01636 100644 --- a/Backend/src/controllers/CoursesStudentController.ts +++ b/Backend/src/controllers/CoursesStudentController.ts @@ -36,11 +36,7 @@ export class CoursesStudentController { @Response('404', 'Course not found') @Response('409', 'Already enrolled in this course') public async enrollCourse(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.enrollCourse({ token, course_id: courseId }); + return await this.service.enrollCourse({ userId: request.user.id, course_id: courseId }); } /** @@ -60,11 +56,7 @@ export class CoursesStudentController { @Query() limit?: number, @Query() status?: EnrollmentStatus ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.GetEnrolledCourses({ token, page, limit, status }); + return await this.service.GetEnrolledCourses({ userId: request.user.id, page, limit, status }); } /** @@ -79,11 +71,7 @@ export class CoursesStudentController { @Response('403', 'Not enrolled in this course') @Response('404', 'Course not found') public async getCourseLearning(@Request() request: any, @Path() courseId: number): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.getCourseLearning({ token, course_id: courseId }); + return await this.service.getCourseLearning({ userId: request.user.id, course_id: courseId }); } /** @@ -103,11 +91,7 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.getlessonContent({ token, course_id: courseId, lesson_id: lessonId }); + return await this.service.getlessonContent({ userId: request.user.id, course_id: courseId, lesson_id: lessonId }); } /** @@ -126,11 +110,7 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.checkAccessLesson({ token, course_id: courseId, lesson_id: lessonId }); + return await this.service.checkAccessLesson({ userId: request.user.id, course_id: courseId, lesson_id: lessonId }); } /** @@ -149,14 +129,12 @@ export class CoursesStudentController { @Path() lessonId: number, @Body() body: SaveVideoProgressBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); const { error } = SaveVideoProgressValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await this.service.saveVideoProgress({ - token, + userId: request.user.id, lesson_id: lessonId, video_progress_seconds: body.video_progress_seconds, video_duration_seconds: body.video_duration_seconds, @@ -178,11 +156,7 @@ export class CoursesStudentController { @Request() request: any, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.getVideoProgress({ token, lesson_id: lessonId }); + return await this.service.getVideoProgress({ userId: request.user.id, lesson_id: lessonId }); } /** @@ -202,11 +176,7 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.service.completeLesson({ token, lesson_id: lessonId }); + return await this.service.completeLesson({ userId: request.user.id, lesson_id: lessonId }); } /** @@ -227,14 +197,12 @@ export class CoursesStudentController { @Path() lessonId: number, @Body() body: SubmitQuizBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); const { error } = SubmitQuizValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await this.service.submitQuiz({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, answers: body.answers, @@ -258,12 +226,8 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } return await this.service.getQuizAttempts({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, }); diff --git a/Backend/src/controllers/LessonsController.ts b/Backend/src/controllers/LessonsController.ts index 0323f4ab..33cc1cef 100644 --- a/Backend/src/controllers/LessonsController.ts +++ b/Backend/src/controllers/LessonsController.ts @@ -42,8 +42,6 @@ export class LessonsController { @Path() lessonId: number, @UploadedFile() video: Express.Multer.File ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); if (!video) { throw new ValidationError('Video file is required'); @@ -57,7 +55,7 @@ export class LessonsController { }; return await chaptersLessonService.uploadVideo({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, video: videoInfo, @@ -87,8 +85,6 @@ export class LessonsController { @Path() lessonId: number, @UploadedFile() video: Express.Multer.File ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); if (!video) { throw new ValidationError('Video file is required'); @@ -102,7 +98,7 @@ export class LessonsController { }; return await chaptersLessonService.updateVideo({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, video: videoInfo, @@ -132,8 +128,6 @@ export class LessonsController { @Path() lessonId: number, @UploadedFile() attachment: Express.Multer.File ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); if (!attachment) { throw new ValidationError('Attachment file is required'); @@ -147,7 +141,7 @@ export class LessonsController { }; return await chaptersLessonService.uploadAttachment({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, attachment: attachmentInfo, @@ -177,11 +171,9 @@ export class LessonsController { @Path() lessonId: number, @Path() attachmentId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await chaptersLessonService.deleteAttachment({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, attachment_id: attachmentId, @@ -211,14 +203,12 @@ export class LessonsController { @Path() lessonId: number, @Body() body: SetYouTubeVideoBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); const { error } = SetYouTubeVideoValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.setYouTubeVideo({ - token, + userId: request.user.id, course_id: courseId, lesson_id: lessonId, youtube_video_id: body.youtube_video_id, diff --git a/Backend/src/controllers/RecommendedCoursesController.ts b/Backend/src/controllers/RecommendedCoursesController.ts index 720bff7c..fa0b17c5 100644 --- a/Backend/src/controllers/RecommendedCoursesController.ts +++ b/Backend/src/controllers/RecommendedCoursesController.ts @@ -1,5 +1,4 @@ import { Get, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'; -import { ValidationError } from '../middleware/errorHandler'; import { RecommendedCoursesService } from '../services/RecommendedCourses.service'; import { ListApprovedCoursesResponse, @@ -25,9 +24,7 @@ export class RecommendedCoursesController { @Query() search?: string, @Query() categoryId?: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await RecommendedCoursesService.listApprovedCourses(token, { search, categoryId }); + return await RecommendedCoursesService.listApprovedCourses(request.user.id, { search, categoryId }); } /** @@ -43,9 +40,7 @@ export class RecommendedCoursesController { @Response('403', 'Forbidden - Admin only') @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(token, courseId); + return await RecommendedCoursesService.getCourseById(request.user.id, courseId); } /** @@ -65,8 +60,6 @@ export class RecommendedCoursesController { @Path() courseId: number, @Query() is_recommended: boolean ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended); + return await RecommendedCoursesService.toggleRecommended(request.user.id, courseId, is_recommended); } } diff --git a/Backend/src/controllers/UserController.ts b/Backend/src/controllers/UserController.ts index ccbe7c76..72b9a33a 100644 --- a/Backend/src/controllers/UserController.ts +++ b/Backend/src/controllers/UserController.ts @@ -1,12 +1,10 @@ -import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Example, Controller, Security, Request, Put, UploadedFile } from 'tsoa'; +import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Security, Request, Put, UploadedFile } from 'tsoa'; import { ValidationError } from '../middleware/errorHandler'; import { UserService } from '../services/user.service'; import { UserResponse, - ProfileResponse, ProfileUpdate, ProfileUpdateResponse, - ChangePasswordRequest, ChangePasswordResponse, updateAvatarResponse, SendVerifyEmailResponse, @@ -23,8 +21,6 @@ export class UserController { /** * Get current user profile - * @summary Retrieve authenticated user's profile information - * @param request Express request object with JWT token in Authorization header */ @Get('me') @SuccessResponse('200', 'User found') @@ -32,12 +28,7 @@ export class UserController { @Response('401', 'Invalid or expired token') @Security('jwt') public async getMe(@Request() request: any): Promise { - // Extract token from Authorization header - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.userService.getUserProfile(token); + return await this.userService.getUserProfile(request.user.id); } @Put('me') @@ -47,34 +38,20 @@ export class UserController { @Response('400', 'Validation error') public async updateProfile(@Request() request: any, @Body() body: ProfileUpdate): Promise { const { error } = profileUpdateSchema.validate(body); - if (error) { - throw new ValidationError(error.details[0].message); - } - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.userService.updateProfile(token, body); + if (error) throw new ValidationError(error.details[0].message); + return await this.userService.updateProfile(request.user.id, body); } @Get('roles') @Security('jwt') @SuccessResponse('200', 'Roles retrieved successfully') @Response('401', 'Invalid or expired token') - public async getRoles(@Request() request: any): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - return await this.userService.getRoles(token); + public async getRoles(): Promise { + return await this.userService.getRoles(); } /** * Change password - * @summary Change user password using old password - * @param request Express request object with JWT token in Authorization header - * @param body Old password and new password - * @returns Success message */ @Post('change-password') @Security('jwt') @@ -83,22 +60,12 @@ export class UserController { @Response('400', 'Validation error') public async changePassword(@Request() request: any, @Body() body: ChangePassword): Promise { const { error } = changePasswordSchema.validate(body); - if (error) { - throw new ValidationError(error.details[0].message); - } - - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) { - throw new ValidationError('No token provided'); - } - - return await this.userService.changePassword(token, body.oldPassword, body.newPassword); + if (error) throw new ValidationError(error.details[0].message); + return await this.userService.changePassword(request.user.id, body.oldPassword, body.newPassword); } /** * Upload user avatar picture - * @param request Express request object with JWT token in Authorization header - * @param file Avatar image file */ @Post('upload-avatar') @Security('jwt') @@ -109,9 +76,6 @@ export class UserController { @Request() request: any, @UploadedFile() file: Express.Multer.File ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - // Validate file type (images only) if (!file.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed'); @@ -119,13 +83,11 @@ export class UserController { const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) throw new ValidationError('File size must be less than 5MB'); - return await this.userService.uploadAvatarPicture(token, file); + return await this.userService.uploadAvatarPicture(request.user.id, file); } /** * Send verification email to user - * @summary Send email verification link to authenticated user's email - * @param request Express request object with JWT token in Authorization header */ @Post('send-verify-email') @Security('jwt') @@ -133,9 +95,7 @@ export class UserController { @Response('401', 'Invalid or expired token') @Response('400', 'Email already verified') public async sendVerifyEmail(@Request() request: any): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - return await this.userService.sendVerifyEmail(token); + return await this.userService.sendVerifyEmail(request.user.id); } /** diff --git a/Backend/src/controllers/announcementsController.ts b/Backend/src/controllers/announcementsController.ts index 8ac03c70..aa38ca54 100644 --- a/Backend/src/controllers/announcementsController.ts +++ b/Backend/src/controllers/announcementsController.ts @@ -37,10 +37,8 @@ export class AnnouncementsController { @Query() page?: number, @Query() limit?: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await announcementsService.listAnnouncement({ - token, + userId: request.user.id, course_id: courseId, page, limit, @@ -63,9 +61,6 @@ export class AnnouncementsController { @FormField() data: string, @UploadedFiles() files?: Express.Multer.File[] ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - // Parse JSON data field const parsed = JSON.parse(data) as CreateAnnouncementBody; @@ -74,7 +69,7 @@ export class AnnouncementsController { if (error) throw new ValidationError(error.details[0].message); return await announcementsService.createAnnouncement({ - token, + userId: request.user.id, course_id: courseId, title: parsed.title, content: parsed.content, @@ -103,15 +98,12 @@ export class AnnouncementsController { @Path() announcementId: number, @Body() body: UpdateAnnouncementBody ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); - // Validate body const { error } = UpdateAnnouncementValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await announcementsService.updateAnnouncement({ - token, + userId: request.user.id, course_id: courseId, announcement_id: announcementId, title: body.title, @@ -139,10 +131,8 @@ export class AnnouncementsController { @Path() courseId: number, @Path() announcementId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await announcementsService.deleteAnnouncement({ - token, + userId: request.user.id, course_id: courseId, announcement_id: announcementId, }); @@ -166,10 +156,8 @@ export class AnnouncementsController { @Path() announcementId: number, @UploadedFile() file: Express.Multer.File ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await announcementsService.uploadAttachment({ - token, + userId: request.user.id, course_id: courseId, announcement_id: announcementId, file: file as any, @@ -195,10 +183,8 @@ export class AnnouncementsController { @Path() announcementId: number, @Path() attachmentId: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await announcementsService.deleteAttachment({ - token, + userId: request.user.id, course_id: courseId, announcement_id: announcementId, attachment_id: attachmentId, @@ -228,10 +214,8 @@ export class AnnouncementsStudentController { @Query() page?: number, @Query() limit?: number ): Promise { - const token = request.headers.authorization?.replace('Bearer ', ''); - if (!token) throw new ValidationError('No token provided'); return await announcementsService.listAnnouncement({ - token, + userId: request.user.id, course_id: courseId, page, limit, diff --git a/Backend/src/services/AdminCourseApproval.service.ts b/Backend/src/services/AdminCourseApproval.service.ts index 0596034c..808f2747 100644 --- a/Backend/src/services/AdminCourseApproval.service.ts +++ b/Backend/src/services/AdminCourseApproval.service.ts @@ -1,8 +1,6 @@ import { prisma } from '../config/database'; -import { config } from '../config'; import { logger } from '../config/logger'; -import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; -import jwt from 'jsonwebtoken'; +import { ValidationError, NotFoundError } from '../middleware/errorHandler'; import { getPresignedUrl } from '../config/minio'; import { ListPendingCoursesResponse, @@ -18,7 +16,7 @@ export class AdminCourseApprovalService { /** * Get all pending courses for admin review */ - static async listPendingCourses(token: string): Promise { + static async listPendingCourses(userId: number): Promise { try { const courses = await prisma.course.findMany({ where: { status: 'PENDING' }, @@ -96,9 +94,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: 0, @@ -113,7 +110,7 @@ export class AdminCourseApprovalService { /** * Get course details for admin review */ - static async getCourseDetail(token: string, courseId: number): Promise { + static async getCourseDetail(userId: number, courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, @@ -228,9 +225,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -245,9 +241,8 @@ export class AdminCourseApprovalService { /** * Approve a course */ - static async approveCourse(token: string, courseId: number, comment?: string): Promise { + static async approveCourse(userId: number, courseId: number, comment?: string): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { @@ -264,7 +259,7 @@ export class AdminCourseApprovalService { where: { id: courseId }, data: { status: 'APPROVED', - approved_by: decoded.id, + approved_by: userId, approved_at: new Date() } }), @@ -273,7 +268,7 @@ export class AdminCourseApprovalService { data: { course_id: courseId, submitted_by: course.created_by, - reviewed_by: decoded.id, + reviewed_by: userId, action: 'APPROVED', previous_status: course.status, new_status: 'APPROVED', @@ -284,7 +279,7 @@ export class AdminCourseApprovalService { // Audit log - APPROVE_COURSE await auditService.logSync({ - userId: decoded.id, + userId, action: AuditAction.APPROVE_COURSE, entityType: 'Course', entityId: courseId, @@ -299,9 +294,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -317,9 +311,8 @@ export class AdminCourseApprovalService { /** * Reject a course */ - static async rejectCourse(token: string, courseId: number, comment: string): Promise { + static async rejectCourse(userId: number, courseId: number, comment: string): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { @@ -350,7 +343,7 @@ export class AdminCourseApprovalService { data: { course_id: courseId, submitted_by: course.created_by, - reviewed_by: decoded.id, + reviewed_by: userId, action: 'REJECTED', previous_status: course.status, new_status: 'REJECTED', @@ -361,7 +354,7 @@ export class AdminCourseApprovalService { // Audit log - REJECT_COURSE await auditService.logSync({ - userId: decoded.id, + userId, action: AuditAction.REJECT_COURSE, entityType: 'Course', entityId: courseId, @@ -376,9 +369,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, diff --git a/Backend/src/services/ChaptersLesson.service.ts b/Backend/src/services/ChaptersLesson.service.ts index 003670b1..63a68356 100644 --- a/Backend/src/services/ChaptersLesson.service.ts +++ b/Backend/src/services/ChaptersLesson.service.ts @@ -59,14 +59,11 @@ import { AuditAction } from '@prisma/client'; * ตรวจสอบสิทธิ์เข้าถึง Course (สำหรับทั้ง Instructor และ Student) * Returns: { hasAccess: boolean, role: 'INSTRUCTOR' | 'STUDENT' | null, userId: number } */ -async function validateCourseAccess(token: string, course_id: number): Promise<{ +async function validateCourseAccess(userId: number, course_id: number): Promise<{ hasAccess: boolean; role: 'INSTRUCTOR' | 'STUDENT' | null; userId: number; }> { - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; - const userId = decodedToken.id; - const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); @@ -98,9 +95,8 @@ async function validateCourseAccess(token: string, course_id: number): Promise<{ export class ChaptersLessonService { async listChapters(request: ChaptersRequest): Promise { try { - const { token, course_id } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const { userId, course_id } = request; + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } @@ -117,14 +113,13 @@ export class ChaptersLessonService { async createChapter(request: CreateChapterInput): Promise { try { - const { token, course_id, title, description, sort_order } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, title, description, sort_order } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create chapter'); } @@ -132,7 +127,7 @@ export class ChaptersLessonService { // Audit log - CREATE Chapter auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.CREATE, entityType: 'Chapter', entityId: chapter.id, @@ -142,9 +137,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: 0, @@ -159,14 +153,13 @@ export class ChaptersLessonService { async updateChapter(request: UpdateChapterInput): Promise { try { - const { token, course_id, chapter_id, title, description, sort_order } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, chapter_id, title, description, sort_order } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update chapter'); } @@ -174,9 +167,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: request.chapter_id, @@ -191,14 +183,13 @@ export class ChaptersLessonService { async deleteChapter(request: DeleteChapterRequest): Promise { try { - const { token, course_id, chapter_id } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, chapter_id } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to delete chapter'); } @@ -206,7 +197,7 @@ export class ChaptersLessonService { // Audit log - DELETE Chapter auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.DELETE, entityType: 'Chapter', entityId: chapter_id, @@ -219,9 +210,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: request.chapter_id, @@ -236,14 +226,13 @@ export class ChaptersLessonService { async reorderChapter(request: ReorderChapterRequest): Promise { try { - const { token, course_id, chapter_id, sort_order: newSortOrder } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, chapter_id, sort_order: newSortOrder } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to reorder chapter'); } @@ -313,9 +302,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: request.chapter_id, @@ -335,14 +323,13 @@ export class ChaptersLessonService { */ async createLesson(request: CreateLessonInput): Promise { try { - const { token, course_id, chapter_id, title, content, type, sort_order } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, chapter_id, title, content, type, sort_order } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create lesson'); } @@ -354,7 +341,6 @@ export class ChaptersLessonService { // If QUIZ type, create empty Quiz shell if (type === 'QUIZ') { - const userId = decodedToken.id; await prisma.quiz.create({ data: { @@ -376,7 +362,7 @@ export class ChaptersLessonService { // Audit log - CREATE Lesson (QUIZ) auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.CREATE, entityType: 'Lesson', entityId: lesson.id, @@ -388,7 +374,7 @@ export class ChaptersLessonService { // Audit log - CREATE Lesson auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.CREATE, entityType: 'Lesson', entityId: lesson.id, @@ -398,9 +384,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: 0, @@ -419,10 +404,10 @@ export class ChaptersLessonService { */ async getLesson(request: GetLessonRequest): Promise { try { - const { token, course_id, lesson_id } = request; + const { userId, course_id, lesson_id } = request; // Check access for both instructor and enrolled student - const access = await validateCourseAccess(token, course_id); + const access = await validateCourseAccess(userId, course_id); if (!access.hasAccess) { throw new ForbiddenError('You do not have access to this course'); } @@ -549,9 +534,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -566,14 +550,13 @@ export class ChaptersLessonService { async updateLesson(request: UpdateLessonRequest): Promise { try { - const { token, course_id, lesson_id, data } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, data } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update lesson'); } @@ -581,9 +564,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -602,14 +584,13 @@ export class ChaptersLessonService { */ async reorderLessons(request: ReorderLessonsRequest): Promise { try { - const { token, course_id, chapter_id, lesson_id, sort_order: newSortOrder } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, chapter_id, lesson_id, sort_order: newSortOrder } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to reorder lessons'); // Verify chapter exists and belongs to the course @@ -682,9 +663,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -704,14 +684,13 @@ export class ChaptersLessonService { */ async deleteLesson(request: DeleteLessonRequest): Promise { try { - const { token, course_id, lesson_id } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to delete this lesson'); // Fetch lesson with all related data @@ -751,7 +730,7 @@ export class ChaptersLessonService { // Audit log - DELETE Lesson auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.DELETE, entityType: 'Lesson', entityId: lesson_id, @@ -764,9 +743,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -789,14 +767,13 @@ export class ChaptersLessonService { */ async uploadVideo(request: UploadVideoInput): Promise { try { - const { token, course_id, lesson_id, video } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, video } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is VIDEO type @@ -833,7 +810,7 @@ export class ChaptersLessonService { // Audit log - UPLOAD_FILE (Video) auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.UPLOAD_FILE, entityType: 'Lesson', entityId: lesson_id, @@ -853,9 +830,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -874,14 +850,13 @@ export class ChaptersLessonService { */ async updateVideo(request: UpdateVideoInput): Promise { try { - const { token, course_id, lesson_id, video } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, video } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is VIDEO type @@ -946,9 +921,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -967,14 +941,13 @@ export class ChaptersLessonService { */ async setYouTubeVideo(request: SetYouTubeVideoInput): Promise { try { - const { token, course_id, lesson_id, youtube_video_id, video_title } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, youtube_video_id, video_title } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is VIDEO type @@ -1038,9 +1011,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -1059,14 +1031,13 @@ export class ChaptersLessonService { */ async uploadAttachment(request: UploadAttachmentInput): Promise { try { - const { token, course_id, lesson_id, attachment } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, attachment } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists @@ -1101,7 +1072,7 @@ export class ChaptersLessonService { // Audit log - UPLOAD_FILE (Attachment) auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.UPLOAD_FILE, entityType: 'LessonAttachment', entityId: newAttachment.id, @@ -1125,9 +1096,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'LessonAttachment', entityId: request.lesson_id, @@ -1146,14 +1116,13 @@ export class ChaptersLessonService { */ async deleteAttachment(request: DeleteAttachmentInput): Promise { try { - const { token, course_id, lesson_id, attachment_id } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, attachment_id } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists @@ -1184,7 +1153,7 @@ export class ChaptersLessonService { // Audit log - DELETE_FILE (Attachment) auditService.log({ - userId: decodedToken.id, + userId: userId, action: AuditAction.DELETE_FILE, entityType: 'LessonAttachment', entityId: attachment_id, @@ -1194,9 +1163,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'LessonAttachment', entityId: request.attachment_id, @@ -1216,14 +1184,13 @@ export class ChaptersLessonService { */ async addQuestion(request: AddQuestionInput): Promise { try { - const { token, course_id, lesson_id, question, explanation, question_type, sort_order, choices } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, question, explanation, question_type, sort_order, choices } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1281,9 +1248,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: 0, @@ -1303,14 +1269,13 @@ export class ChaptersLessonService { */ async updateQuestion(request: UpdateQuestionInput): Promise { try { - const { token, course_id, lesson_id, question_id, question, explanation, question_type, sort_order, choices } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, question_id, question, explanation, question_type, sort_order, choices } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1367,9 +1332,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: request.question_id, @@ -1384,14 +1348,13 @@ export class ChaptersLessonService { async reorderQuestion(request: ReorderQuestionInput): Promise { try { - const { token, course_id, lesson_id, question_id, sort_order } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, question_id, sort_order } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1471,9 +1434,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: request.question_id, @@ -1493,14 +1455,13 @@ export class ChaptersLessonService { */ async deleteQuestion(request: DeleteQuestionInput): Promise { try { - const { token, course_id, lesson_id, question_id } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, question_id } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1530,9 +1491,8 @@ 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, + userId: request.userId || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: request.question_id, @@ -1680,14 +1640,13 @@ export class ChaptersLessonService { */ async updateQuiz(request: UpdateQuizInput): Promise { try { - const { token, course_id, lesson_id, title, description, passing_score, time_limit, shuffle_questions, shuffle_choices, show_answers_after_completion, is_skippable, allow_multiple_attempts } = request; - const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, title, description, passing_score, time_limit, shuffle_questions, shuffle_choices, show_answers_after_completion, is_skippable, allow_multiple_attempts } = request; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index e1b40d0c..959ef80f 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -1,9 +1,7 @@ import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; -import { config } from '../config'; import { logger } from '../config/logger'; -import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; -import jwt from 'jsonwebtoken'; +import { ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio'; import { CreateCourseInput, @@ -27,6 +25,7 @@ import { SearchInstructorResponse, GetEnrolledStudentsInput, GetEnrolledStudentsResponse, + EnrolledStudentData, GetQuizScoresInput, GetQuizScoresResponse, GetQuizAttemptDetailInput, @@ -38,6 +37,7 @@ import { CloneCourseResponse, setCourseDraft, setCourseDraftResponse, + GetAllMyStudentsResponse, } from "../types/CoursesInstructor.types"; import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; @@ -121,10 +121,9 @@ export class CoursesInstructorService { static async listMyCourses(input: ListMyCoursesInput): Promise { try { - 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: input.userId, course: input.status ? { status: input.status } : undefined }, include: { @@ -157,9 +156,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: 0, @@ -174,12 +172,10 @@ export class CoursesInstructorService { static async getmyCourse(getmyCourse: getmyCourse): Promise { try { - const decoded = jwt.verify(getmyCourse.token, config.jwt.secret) as { id: number; type: string }; - // Check if user is instructor of this course const courseInstructor = await prisma.courseInstructor.findFirst({ where: { - user_id: decoded.id, + user_id: getmyCourse.userId, course_id: getmyCourse.course_id }, include: { @@ -225,9 +221,8 @@ 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, + userId: getmyCourse.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: getmyCourse.course_id, @@ -240,9 +235,9 @@ export class CoursesInstructorService { } } - static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise { + static async updateCourse(userId: number, courseId: number, courseData: UpdateCourseInput): Promise { try { - await this.validateCourseInstructor(token, courseId); + await this.validateCourseInstructor(userId, courseId); const course = await prisma.course.update({ where: { @@ -258,9 +253,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -273,9 +267,9 @@ export class CoursesInstructorService { } } - static async uploadThumbnail(token: string, courseId: number, file: Express.Multer.File): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { + static async uploadThumbnail(userId: number, courseId: number, file: Express.Multer.File): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { try { - await this.validateCourseInstructor(token, courseId); + await this.validateCourseInstructor(userId, courseId); // Get current course to check for existing thumbnail const currentCourse = await prisma.course.findUnique({ @@ -322,9 +316,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -337,9 +330,9 @@ export class CoursesInstructorService { } } - static async deleteCourse(token: string, courseId: number): Promise { + static async deleteCourse(userId: number, courseId: number): Promise { try { - const courseInstructorId = await this.validateCourseInstructor(token, courseId); + const courseInstructorId = await this.validateCourseInstructor(userId, courseId); if (!courseInstructorId.is_primary) { throw new ForbiddenError('You have no permission to delete this course'); } @@ -365,9 +358,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -382,11 +374,10 @@ export class CoursesInstructorService { static async sendCourseForReview(sendCourseForReview: sendCourseForReview): Promise { try { - const decoded = jwt.verify(sendCourseForReview.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseApproval.create({ data: { course_id: sendCourseForReview.course_id, - submitted_by: decoded.id, + submitted_by: sendCourseForReview.userId, } }); await prisma.course.update({ @@ -398,7 +389,7 @@ export class CoursesInstructorService { } }); await auditService.logSync({ - userId: decoded.id, + userId: sendCourseForReview.userId, action: AuditAction.UPDATE, entityType: 'Course', entityId: sendCourseForReview.course_id, @@ -412,9 +403,8 @@ 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, + userId: sendCourseForReview.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: sendCourseForReview.course_id, @@ -429,7 +419,7 @@ export class CoursesInstructorService { static async setCourseDraft(setCourseDraft: setCourseDraft): Promise { try { - await this.validateCourseInstructor(setCourseDraft.token, setCourseDraft.course_id); + await this.validateCourseInstructor(setCourseDraft.userId, setCourseDraft.course_id); await prisma.course.update({ where: { id: setCourseDraft.course_id, @@ -445,9 +435,8 @@ 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, + userId: setCourseDraft.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: setCourseDraft.course_id, @@ -460,7 +449,7 @@ export class CoursesInstructorService { } } - static async getCourseApprovals(token: string, courseId: number): Promise<{ + static async getCourseApprovals(userId: number, courseId: number): Promise<{ code: number; message: string; data: any[]; @@ -468,7 +457,7 @@ export class CoursesInstructorService { }> { try { // Validate instructor access - await this.validateCourseInstructor(token, courseId); + await this.validateCourseInstructor(userId, courseId); const approvals = await prisma.courseApproval.findMany({ where: { course_id: courseId }, @@ -491,9 +480,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -510,8 +498,6 @@ export class CoursesInstructorService { static async searchInstructors(input: SearchInstructorInput): Promise { try { - const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number }; - // Get existing instructors in the course const existingInstructors = await prisma.courseInstructor.findMany({ where: { course_id: input.course_id }, @@ -528,7 +514,7 @@ export class CoursesInstructorService { ], role: { code: 'INSTRUCTOR' }, id: { - notIn: [decoded.id, ...existingInstructorIds], + notIn: [input.userId, ...existingInstructorIds], }, }, include: { @@ -563,9 +549,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -581,7 +566,7 @@ export class CoursesInstructorService { static async addInstructorToCourse(addinstructorCourse: addinstructorCourse): Promise { try { // Validate user is instructor of this course - await this.validateCourseInstructor(addinstructorCourse.token, addinstructorCourse.course_id); + await this.validateCourseInstructor(addinstructorCourse.userId, addinstructorCourse.course_id); // Find user by email or username const user = await prisma.user.findFirst({ @@ -619,9 +604,8 @@ export class CoursesInstructorService { } }); - const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null; await auditService.logSync({ - userId: decoded?.id || 0, + userId: addinstructorCourse.userId, action: AuditAction.CREATE, entityType: 'Course', entityId: addinstructorCourse.course_id, @@ -637,9 +621,8 @@ 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, + userId: addinstructorCourse.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: addinstructorCourse.course_id, @@ -654,7 +637,6 @@ export class CoursesInstructorService { static async removeInstructorFromCourse(removeinstructorCourse: removeinstructorCourse): Promise { try { - const decoded = jwt.verify(removeinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.delete({ where: { course_id_user_id: { @@ -665,7 +647,7 @@ export class CoursesInstructorService { }); await auditService.logSync({ - userId: decoded?.id || 0, + userId: removeinstructorCourse.userId, action: AuditAction.DELETE, entityType: 'Course', entityId: removeinstructorCourse.course_id, @@ -682,9 +664,8 @@ 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, + userId: removeinstructorCourse.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: removeinstructorCourse.course_id, @@ -699,7 +680,6 @@ export class CoursesInstructorService { static async listInstructorsOfCourse(listinstructorCourse: listinstructorCourse): Promise { try { - const decoded = jwt.verify(listinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; const courseInstructors = await prisma.courseInstructor.findMany({ where: { course_id: listinstructorCourse.course_id, @@ -743,9 +723,8 @@ 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, + userId: listinstructorCourse.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: listinstructorCourse.course_id, @@ -760,7 +739,6 @@ export class CoursesInstructorService { static async setPrimaryInstructor(setprimaryCourseInstructor: setprimaryCourseInstructor): Promise { try { - const decoded = jwt.verify(setprimaryCourseInstructor.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.update({ where: { course_id_user_id: { @@ -774,7 +752,7 @@ export class CoursesInstructorService { }); await auditService.logSync({ - userId: decoded?.id || 0, + userId: setprimaryCourseInstructor.userId, action: AuditAction.UPDATE, entityType: 'Course', entityId: setprimaryCourseInstructor.course_id, @@ -791,9 +769,8 @@ 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, + userId: setprimaryCourseInstructor.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: setprimaryCourseInstructor.course_id, @@ -806,11 +783,10 @@ export class CoursesInstructorService { } } - static async validateCourseInstructor(token: string, courseId: number): Promise<{ user_id: number; is_primary: boolean }> { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + static async validateCourseInstructor(userId: number, courseId: number): Promise<{ user_id: number; is_primary: boolean }> { const courseInstructor = await prisma.courseInstructor.findFirst({ where: { - user_id: decoded.id, + user_id: userId, course_id: courseId } }); @@ -839,10 +815,10 @@ export class CoursesInstructorService { */ static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise { try { - const { token, course_id, page = 1, limit = 20, search, status } = input; + const { userId, course_id, page = 1, limit = 20, search, status } = input; // Validate instructor - await this.validateCourseInstructor(token, course_id); + await this.validateCourseInstructor(userId, course_id); // Build where clause const whereClause: any = { course_id }; @@ -917,9 +893,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -938,11 +913,10 @@ export class CoursesInstructorService { */ static async getQuizScores(input: GetQuizScoresInput): Promise { try { - const { token, course_id, lesson_id, page = 1, limit = 20, search, is_passed } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, lesson_id, page = 1, limit = 20, search, is_passed } = input; // Validate instructor - await this.validateCourseInstructor(token, course_id); + await this.validateCourseInstructor(userId, course_id); // Get lesson and verify it's a QUIZ type const lesson = await prisma.lesson.findUnique({ @@ -1095,9 +1069,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1116,10 +1089,10 @@ export class CoursesInstructorService { */ static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise { try { - const { token, course_id, lesson_id, student_id } = input; + const { userId, course_id, lesson_id, student_id } = input; // Validate instructor - await this.validateCourseInstructor(token, course_id); + await this.validateCourseInstructor(userId, course_id); // Get lesson and verify it's a QUIZ type const lesson = await prisma.lesson.findUnique({ @@ -1219,9 +1192,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1240,10 +1212,10 @@ export class CoursesInstructorService { */ static async getEnrolledStudentDetail(input: GetEnrolledStudentDetailInput): Promise { try { - const { token, course_id, student_id } = input; + const { userId, course_id, student_id } = input; // Validate instructor - await this.validateCourseInstructor(token, course_id); + await this.validateCourseInstructor(userId, course_id); // Get student info const student = await prisma.user.findUnique({ @@ -1367,9 +1339,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1386,12 +1357,10 @@ export class CoursesInstructorService { * ดึงประวัติการขออนุมัติคอร์ส * Get course approval history for instructor to see rejection reasons */ - static async getCourseApprovalHistory(token: string, courseId: number): Promise { + static async getCourseApprovalHistory(userId: number, courseId: number): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; - // Validate instructor access - await this.validateCourseInstructor(token, courseId); + await this.validateCourseInstructor(userId, courseId); // Get course with approval history const course = await prisma.course.findUnique({ @@ -1434,9 +1403,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -1454,11 +1422,10 @@ export class CoursesInstructorService { */ static async cloneCourse(input: CloneCourseInput): Promise { try { - const { token, course_id, title } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, title } = input; // Validate instructor - const courseInstructor = await this.validateCourseInstructor(token, course_id); + const courseInstructor = await this.validateCourseInstructor(userId, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not an instructor of this course'); } @@ -1508,7 +1475,7 @@ export class CoursesInstructorService { is_free: originalCourse.is_free, have_certificate: originalCourse.have_certificate, status: 'DRAFT', // Reset status - created_by: decoded.id + created_by: userId } }); @@ -1516,7 +1483,7 @@ export class CoursesInstructorService { await tx.courseInstructor.create({ data: { course_id: createdCourse.id, - user_id: decoded.id, + user_id: userId, is_primary: true } }); @@ -1589,7 +1556,7 @@ export class CoursesInstructorService { shuffle_questions: lesson.quiz.shuffle_questions, shuffle_choices: lesson.quiz.shuffle_choices, show_answers_after_completion: lesson.quiz.show_answers_after_completion, - created_by: decoded.id + created_by: userId } }); @@ -1636,7 +1603,7 @@ export class CoursesInstructorService { }); await auditService.logSync({ - userId: decoded.id, + userId: input.userId, action: AuditAction.CREATE, entityType: 'Course', entityId: newCourse.id, @@ -1658,9 +1625,8 @@ export class CoursesInstructorService { } catch (error) { logger.error(`Error cloning course: ${error}`); - const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1672,4 +1638,45 @@ export class CoursesInstructorService { throw error; } } + + /** + * ดึงผู้เรียนทั้งหมดในทุกคอร์สของ instructor + * Get all enrolled students across all courses the instructor owns/teaches + */ + static async getMyAllStudents(userId: number): Promise { + try { + // หา course IDs ทั้งหมดที่ instructor สอน + const instructorCourses = await prisma.courseInstructor.findMany({ + where: { user_id: userId }, + select: { course_id: true } + }); + + const courseIds = instructorCourses.map(ci => ci.course_id); + + if (courseIds.length === 0) { + return { code: 200, message: 'Students retrieved successfully', total_students: 0, total_completed: 0 }; + } + + // unique students ทั้งหมด + const uniqueStudents = await prisma.enrollment.groupBy({ + by: ['user_id'], + where: { course_id: { in: courseIds } }, + }); + + // จำนวน enrollment ที่ COMPLETED + const totalCompleted = await prisma.enrollment.count({ + where: { course_id: { in: courseIds }, status: 'COMPLETED' } + }); + + return { + code: 200, + message: 'Students retrieved successfully', + total_students: uniqueStudents.length, + total_completed: totalCompleted, + }; + } catch (error) { + logger.error(`Error getting all students: ${error}`); + throw error; + } + } } diff --git a/Backend/src/services/CoursesStudent.service.ts b/Backend/src/services/CoursesStudent.service.ts index 986695b1..2ae599fc 100644 --- a/Backend/src/services/CoursesStudent.service.ts +++ b/Backend/src/services/CoursesStudent.service.ts @@ -133,7 +133,7 @@ export class CoursesStudentService { async enrollCourse(input: EnrollCourseInput): Promise { try { const { course_id } = input; - const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number; type: string }; + const userId = input.userId; const course = await prisma.course.findUnique({ where: { id: course_id }, @@ -146,7 +146,7 @@ export class CoursesStudentService { const existingEnrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -159,7 +159,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.create({ data: { course_id, - user_id: decoded.id, + user_id: userId, status: 'ENROLLED', enrolled_at: new Date(), }, @@ -167,11 +167,11 @@ export class CoursesStudentService { // Audit log - ENROLL auditService.log({ - userId: decoded.id, + userId: userId, action: AuditAction.ENROLL, entityType: 'Enrollment', entityId: enrollment.id, - newValue: { course_id, user_id: decoded.id, status: 'ENROLLED' } + newValue: { course_id, user_id: userId, status: 'ENROLLED' } }); return { @@ -187,9 +187,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(`Error enrolling in course: ${error}`); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -206,13 +206,13 @@ export class CoursesStudentService { async GetEnrolledCourses(input: ListEnrolledCoursesInput): Promise { try { - const { token } = input; + // destructure input const page = input.page ?? 1; const limit = input.limit ?? 20; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const userId = input.userId; const enrollments = await prisma.enrollment.findMany({ where: { - user_id: decoded.id, + user_id: userId, }, include: { course: { @@ -230,7 +230,7 @@ export class CoursesStudentService { }); const total = await prisma.enrollment.count({ where: { - user_id: decoded.id, + user_id: userId, }, }); @@ -274,9 +274,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -290,8 +290,8 @@ export class CoursesStudentService { } async getCourseLearning(input: GetCourseLearningInput): Promise { try { - const { token, course_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { course_id } = input; + const userId = input.userId; // Get course with chapters and lessons (basic info only) const course = await prisma.course.findUnique({ @@ -330,7 +330,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -345,7 +345,7 @@ export class CoursesStudentService { prisma.enrollment.update({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -357,7 +357,7 @@ export class CoursesStudentService { const lessonIds = course.chapters.flatMap(ch => ch.lessons.map(l => l.id)); const lessonProgress = await prisma.lessonProgress.findMany({ where: { - user_id: decoded.id, + user_id: userId, lesson_id: { in: lessonIds }, }, }); @@ -453,9 +453,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -470,8 +470,8 @@ export class CoursesStudentService { async getlessonContent(input: GetLessonContentInput): Promise { try { - const { token, course_id, lesson_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { course_id, lesson_id } = input; + const userId = input.userId; // Import MinIO functions @@ -479,7 +479,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -528,7 +528,7 @@ export class CoursesStudentService { const lessonProgress = await prisma.lessonProgress.findUnique({ where: { user_id_lesson_id: { - user_id: decoded.id, + user_id: userId, lesson_id, }, }, @@ -639,7 +639,7 @@ export class CoursesStudentService { // Get latest quiz attempt for this user latestQuizAttempt = await prisma.quizAttempt.findFirst({ where: { - user_id: decoded.id, + user_id: userId, quiz_id: lesson.quiz.id, }, orderBy: { @@ -726,9 +726,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -744,14 +744,14 @@ export class CoursesStudentService { async checkAccessLesson(input: CheckLessonAccessInput): Promise { try { - const { token, course_id, lesson_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { course_id, lesson_id } = input; + const userId = input.userId; // Check enrollment const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -845,7 +845,7 @@ export class CoursesStudentService { // Get user's progress for prerequisite lessons const prerequisiteProgress = await prisma.lessonProgress.findMany({ where: { - user_id: decoded.id, + user_id: userId, lesson_id: { in: prerequisiteIds }, }, }); @@ -879,7 +879,7 @@ export class CoursesStudentService { // Check if user passed the quiz const quizAttempt = await prisma.quizAttempt.findFirst({ where: { - user_id: decoded.id, + user_id: userId, quiz_id: prereqLesson.quiz.id, is_passed: true, }, @@ -925,9 +925,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -942,8 +942,8 @@ export class CoursesStudentService { async getVideoProgress(input: GetVideoProgressInput): Promise { try { - const { token, lesson_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { lesson_id } = input; + const userId = input.userId; // Get lesson to find course_id const lesson = await prisma.lesson.findUnique({ @@ -966,7 +966,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -980,7 +980,7 @@ export class CoursesStudentService { const progress = await prisma.lessonProgress.findUnique({ where: { user_id_lesson_id: { - user_id: decoded.id, + user_id: userId, lesson_id, }, }, @@ -1010,9 +1010,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -1027,8 +1027,8 @@ export class CoursesStudentService { async saveVideoProgress(input: SaveVideoProgressInput): Promise { try { - const { token, lesson_id, video_progress_seconds, video_duration_seconds } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { lesson_id, video_progress_seconds, video_duration_seconds } = input; + const userId = input.userId; // Get lesson to find course_id const lesson = await prisma.lesson.findUnique({ @@ -1051,7 +1051,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -1074,12 +1074,12 @@ export class CoursesStudentService { const progress = await prisma.lessonProgress.upsert({ where: { user_id_lesson_id: { - user_id: decoded.id, + user_id: userId, lesson_id, }, }, create: { - user_id: decoded.id, + user_id: userId, lesson_id, video_progress_seconds, video_duration_seconds: video_duration_seconds ?? null, @@ -1098,7 +1098,7 @@ export class CoursesStudentService { // If video completed, mark lesson as complete and update enrollment progress let enrollmentProgress: { progress_percentage: number; is_course_completed: boolean } | undefined; if (isCompleted) { - const result = await this.markLessonComplete(decoded.id, lesson_id, course_id); + const result = await this.markLessonComplete(userId, lesson_id, course_id); enrollmentProgress = result.enrollmentProgress; } @@ -1118,9 +1118,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -1135,8 +1135,8 @@ export class CoursesStudentService { async completeLesson(input: CompleteLessonInput): Promise { try { - const { token, lesson_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { lesson_id } = input; + const userId = input.userId; // Get lesson with chapter and course info const lesson = await prisma.lesson.findUnique({ @@ -1185,7 +1185,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -1196,7 +1196,7 @@ export class CoursesStudentService { } // Mark lesson as complete and update enrollment progress - const { lessonProgress, enrollmentProgress } = await this.markLessonComplete(decoded.id, lesson_id, course_id); + const { lessonProgress, enrollmentProgress } = await this.markLessonComplete(userId, lesson_id, course_id); const { progress_percentage: course_progress_percentage, is_course_completed } = enrollmentProgress; // Find next lesson @@ -1225,7 +1225,7 @@ export class CoursesStudentService { // Check if certificate already exists const existingCertificate = await prisma.certificate.findFirst({ where: { - user_id: decoded.id, + user_id: userId, course_id, }, }); @@ -1233,10 +1233,10 @@ export class CoursesStudentService { if (!existingCertificate) { await prisma.certificate.create({ data: { - user_id: decoded.id, + user_id: userId, course_id, enrollment_id: enrollment.id, - file_path: `certificates/${course_id}/${decoded.id}/${Date.now()}.pdf`, + file_path: `certificates/${course_id}/${userId}/${Date.now()}.pdf`, issued_at: new Date(), }, }); @@ -1261,9 +1261,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(`Error completing lesson: ${error}`); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'LessonProgress', entityId: input.lesson_id, @@ -1283,14 +1283,14 @@ export class CoursesStudentService { */ async submitQuiz(input: SubmitQuizInput): Promise { try { - const { token, course_id, lesson_id, answers } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { course_id, lesson_id, answers } = input; + const userId = input.userId; // Check enrollment const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -1331,7 +1331,7 @@ export class CoursesStudentService { // Get previous attempt count const previousAttempts = await prisma.quizAttempt.count({ where: { - user_id: decoded.id, + user_id: userId, quiz_id: quiz.id, }, }); @@ -1384,7 +1384,7 @@ export class CoursesStudentService { const now = new Date(); const quizAttempt = await prisma.quizAttempt.create({ data: { - user_id: decoded.id, + user_id: userId, quiz_id: quiz.id, score: earnedScore, total_questions: quiz.questions.length, @@ -1400,7 +1400,7 @@ export class CoursesStudentService { // If passed, mark lesson as complete and update enrollment progress let enrollmentProgress: { progress_percentage: number; is_course_completed: boolean } | undefined; if (isPassed) { - const result = await this.markLessonComplete(decoded.id, lesson_id, course_id); + const result = await this.markLessonComplete(userId, lesson_id, course_id); enrollmentProgress = result.enrollmentProgress; } @@ -1429,9 +1429,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(`Error submitting quiz: ${error}`); - const decoded = jwt.decode(input.token) as { id: number } | null; + // userId from middleware await auditService.logSync({ - userId: decoded?.id || 0, + userId: input.userId, action: AuditAction.ERROR, entityType: 'QuizAttempt', entityId: 0, @@ -1452,14 +1452,14 @@ export class CoursesStudentService { */ async getQuizAttempts(input: GetQuizAttemptsInput): Promise { try { - const { token, course_id, lesson_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { course_id, lesson_id } = input; + const userId = input.userId; // Check enrollment const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -1494,7 +1494,7 @@ export class CoursesStudentService { // Get all quiz attempts for this user const attempts = await prisma.quizAttempt.findMany({ where: { - user_id: decoded.id, + user_id: userId, quiz_id: lesson.quiz.id, }, orderBy: { attempt_number: 'desc' }, @@ -1539,22 +1539,20 @@ 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) - } - }); - } + // userId from middleware + await auditService.logSync({ + userId: input.userId, + 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; } } -} \ No newline at end of file +} diff --git a/Backend/src/services/RecommendedCourses.service.ts b/Backend/src/services/RecommendedCourses.service.ts index 22440eb2..750131c6 100644 --- a/Backend/src/services/RecommendedCourses.service.ts +++ b/Backend/src/services/RecommendedCourses.service.ts @@ -1,8 +1,6 @@ import { prisma } from '../config/database'; -import { config } from '../config'; import { logger } from '../config/logger'; import { NotFoundError, ValidationError } from '../middleware/errorHandler'; -import jwt from 'jsonwebtoken'; import { getPresignedUrl } from '../config/minio'; import { ListApprovedCoursesResponse, @@ -20,7 +18,7 @@ export class RecommendedCoursesService { * List all approved courses (for admin to manage recommendations) */ static async listApprovedCourses( - token: string, + userId: number, filters?: { search?: string; categoryId?: number } ): Promise { try { @@ -108,19 +106,16 @@ 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) - } - }); - } + await auditService.logSync({ + userId, + action: AuditAction.ERROR, + entityType: 'RecommendedCourses', + entityId: 0, + metadata: { + operation: 'list_approved_courses', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -128,7 +123,7 @@ export class RecommendedCoursesService { /** * Get course by ID (for admin to view details) */ - static async getCourseById(token: string, courseId: number): Promise { + static async getCourseById(userId: number, courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, @@ -213,19 +208,16 @@ 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) - } - }); - } + await auditService.logSync({ + userId, + action: AuditAction.ERROR, + entityType: 'RecommendedCourses', + entityId: 0, + metadata: { + operation: 'get_course_by_id', + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -234,12 +226,11 @@ export class RecommendedCoursesService { * Toggle course recommendation status */ static async toggleRecommended( - token: string, + userId: number, courseId: number, isRecommended: boolean ): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { @@ -257,7 +248,7 @@ export class RecommendedCoursesService { // Audit log await auditService.logSync({ - userId: decoded.id, + userId, action: AuditAction.UPDATE, entityType: 'Course', entityId: courseId, @@ -276,9 +267,8 @@ 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, + userId, action: AuditAction.ERROR, entityType: 'RecommendedCourses', entityId: courseId, diff --git a/Backend/src/services/announcements.service.ts b/Backend/src/services/announcements.service.ts index 7e8b2d3e..36adabca 100644 --- a/Backend/src/services/announcements.service.ts +++ b/Backend/src/services/announcements.service.ts @@ -1,8 +1,6 @@ import { prisma } from '../config/database'; -import { config } from '../config'; import { logger } from '../config/logger'; -import { UnauthorizedError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; -import jwt from 'jsonwebtoken'; +import { ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import { ListAnnouncementResponse, CreateAnnouncementInput, @@ -31,27 +29,26 @@ export class AnnouncementsService { */ async listAnnouncement(input: ListAnnouncementInput): Promise { try { - const { token, course_id, page = 1, limit = 10 } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const { userId, course_id, page = 1, limit = 10 } = input; // Check user access - instructor, admin, or enrolled student const user = await prisma.user.findUnique({ - where: { id: decoded.id }, + where: { id: userId }, include: { role: true }, }); - if (!user) throw new UnauthorizedError('Invalid token'); + if (!user) throw new ForbiddenError('User not found'); // Admin can access all courses const isAdmin = user.role.code === 'ADMIN'; // Check if instructor of this course const isInstructor = await prisma.courseInstructor.findFirst({ - where: { course_id, user_id: decoded.id }, + where: { course_id, user_id: userId }, }); // Check if enrolled student const isEnrolled = await prisma.enrollment.findFirst({ - where: { course_id, user_id: decoded.id }, + where: { course_id, user_id: userId }, }); if (!isAdmin && !isInstructor && !isEnrolled) throw new ForbiddenError('You do not have access to this course announcements'); @@ -61,7 +58,7 @@ export class AnnouncementsService { // Students only see PUBLISHED announcements with published_at <= now const now = new Date(); const whereClause: any = { course_id }; - + if (!(isAdmin || isInstructor)) { // Students: only show PUBLISHED and published_at <= now whereClause.status = 'PUBLISHED'; @@ -130,9 +127,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -150,11 +146,10 @@ export class AnnouncementsService { */ async createAnnouncement(input: CreateAnnouncementInput): Promise { try { - const { token, course_id, title, content, status, is_pinned, published_at, files } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, title, content, status, is_pinned, published_at, files } = input; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(token, course_id); + await CoursesInstructorService.validateCourseInstructor(userId, course_id); // Determine published_at: use provided value or default to now if status is PUBLISHED let finalPublishedAt: Date | null = null; @@ -171,7 +166,7 @@ export class AnnouncementsService { status: status as any, is_pinned, published_at: finalPublishedAt, - created_by: decoded.id, + created_by: userId, }, }); @@ -236,9 +231,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -256,11 +250,10 @@ export class AnnouncementsService { */ async updateAnnouncement(input: UpdateAnnouncementInput): Promise { try { - const { token, course_id, announcement_id, title, content, status, is_pinned, published_at } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, announcement_id, title, content, status, is_pinned, published_at } = input; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(token, course_id); + await CoursesInstructorService.validateCourseInstructor(userId, course_id); // Check announcement exists and belongs to course const existing = await prisma.announcement.findFirst({ @@ -289,7 +282,7 @@ export class AnnouncementsService { status: status as any, is_pinned, published_at: finalPublishedAt, - updated_by: decoded.id, + updated_by: userId, }, include: { attachments: true, @@ -320,9 +313,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -340,11 +332,10 @@ export class AnnouncementsService { */ async deleteAnnouncement(input: DeleteAnnouncementInput): Promise { try { - const { token, course_id, announcement_id } = input; - jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, announcement_id } = input; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(token, course_id); + await CoursesInstructorService.validateCourseInstructor(userId, course_id); // Check announcement exists and belongs to course const existing = await prisma.announcement.findFirst({ @@ -376,9 +367,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -396,11 +386,10 @@ export class AnnouncementsService { */ async uploadAttachment(input: UploadAnnouncementAttachmentInput): Promise { try { - const { token, course_id, announcement_id, file } = input; - jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, announcement_id, file } = input; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(token, course_id); + await CoursesInstructorService.validateCourseInstructor(userId, course_id); // Check announcement exists and belongs to course const existing = await prisma.announcement.findFirst({ @@ -451,9 +440,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -471,11 +459,10 @@ export class AnnouncementsService { */ async deleteAttachment(input: DeleteAnnouncementAttachmentInput): Promise { try { - const { token, course_id, announcement_id, attachment_id } = input; - jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id, announcement_id, attachment_id } = input; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(token, course_id); + await CoursesInstructorService.validateCourseInstructor(userId, course_id); // Check attachment exists and belongs to announcement in this course const attachment = await prisma.announcementAttachment.findFirst({ @@ -508,9 +495,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, diff --git a/Backend/src/services/auth.service.ts b/Backend/src/services/auth.service.ts index 66899bed..9d16d594 100644 --- a/Backend/src/services/auth.service.ts +++ b/Backend/src/services/auth.service.ts @@ -74,7 +74,6 @@ export class AuthService { data: { token, refreshToken, - user: await this.formatUserResponse(user) } }; } diff --git a/Backend/src/services/categories.service.ts b/Backend/src/services/categories.service.ts index 0e0defa2..e18ec617 100644 --- a/Backend/src/services/categories.service.ts +++ b/Backend/src/services/categories.service.ts @@ -1,10 +1,7 @@ import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; -import { config } from '../config'; 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'; @@ -26,14 +23,13 @@ export class CategoryService { } } - async createCategory(token: string, category: createCategory): Promise { + async createCategory(userId: number, category: createCategory): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; const newCategory = await prisma.category.create({ data: category }); auditService.log({ - userId: decoded.id, + userId, action: AuditAction.CREATE, entityType: 'Category', entityId: newCategory.id, @@ -47,13 +43,13 @@ export class CategoryService { name: newCategory.name as { th: string; en: string }, slug: newCategory.slug, description: newCategory.description as { th: string; en: string }, - created_by: decoded.id, + created_by: userId, } }; } catch (error) { logger.error('Failed to create category', { error }); await auditService.logSync({ - userId: 0, + userId, action: AuditAction.ERROR, entityType: 'Category', entityId: 0, @@ -66,15 +62,14 @@ export class CategoryService { } } - async updateCategory(token: string, id: number, category: updateCategory): Promise { + async updateCategory(userId: number, id: number, category: updateCategory): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; const updatedCategory = await prisma.category.update({ where: { id }, data: category }); auditService.log({ - userId: decoded.id, + userId, action: AuditAction.UPDATE, entityType: 'Category', entityId: id, @@ -88,13 +83,13 @@ export class CategoryService { name: updatedCategory.name as { th: string; en: string }, slug: updatedCategory.slug, description: updatedCategory.description as { th: string; en: string }, - updated_by: decoded.id, + updated_by: userId, } }; } catch (error) { logger.error('Failed to update category', { error }); await auditService.logSync({ - userId: 0, + userId, action: AuditAction.ERROR, entityType: 'Category', entityId: 0, @@ -107,14 +102,13 @@ export class CategoryService { } } - async deleteCategory(token: string, id: number): Promise { + async deleteCategory(userId: number, 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, + userId, action: AuditAction.DELETE, entityType: 'Category', entityId: id, @@ -127,7 +121,7 @@ export class CategoryService { } catch (error) { logger.error('Failed to delete category', { error }); await auditService.logSync({ - userId: 0, + userId, action: AuditAction.ERROR, entityType: 'Category', entityId: 0, diff --git a/Backend/src/services/certificate.service.ts b/Backend/src/services/certificate.service.ts index 4041ec41..eafaffa0 100644 --- a/Backend/src/services/certificate.service.ts +++ b/Backend/src/services/certificate.service.ts @@ -1,8 +1,6 @@ import { prisma } from '../config/database'; -import { config } from '../config'; import { logger } from '../config/logger'; import { NotFoundError, ForbiddenError, ValidationError } from '../middleware/errorHandler'; -import jwt from 'jsonwebtoken'; import { PDFDocument, rgb } from 'pdf-lib'; import fontkit from '@pdf-lib/fontkit'; import * as fs from 'fs'; @@ -29,14 +27,13 @@ export class CertificateService { */ async generateCertificate(input: GenerateCertificateInput): Promise { try { - const { token, course_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id } = input; // Check enrollment and completion const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: decoded.id, + user_id: userId, course_id, }, }, @@ -65,7 +62,7 @@ export class CertificateService { // Check if certificate already exists const existingCertificate = await prisma.certificate.findFirst({ where: { - user_id: decoded.id, + user_id: userId, course_id, }, }); @@ -103,13 +100,13 @@ export class CertificateService { // Upload to MinIO const timestamp = Date.now(); - const filePath = `certificates/${course_id}/${decoded.id}/${timestamp}.pdf`; + const filePath = `certificates/${course_id}/${userId}/${timestamp}.pdf`; await uploadFile(filePath, Buffer.from(pdfBytes), 'application/pdf'); // Save to database const certificate = await prisma.certificate.create({ data: { - user_id: decoded.id, + user_id: userId, course_id, enrollment_id: enrollment.id, file_path: filePath, @@ -118,7 +115,7 @@ export class CertificateService { }); auditService.log({ - userId: decoded.id, + userId, action: AuditAction.CREATE, entityType: 'Certificate', entityId: certificate.id, @@ -139,9 +136,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Certificate', entityId: 0, @@ -160,12 +156,11 @@ export class CertificateService { */ async getCertificate(input: GetCertificateInput): Promise { try { - const { token, course_id } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId, course_id } = input; const certificate = await prisma.certificate.findFirst({ where: { - user_id: decoded.id, + user_id: userId, course_id, }, include: { @@ -202,9 +197,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Certificate', entityId: 0, @@ -223,12 +217,11 @@ export class CertificateService { */ async listMyCertificates(input: ListMyCertificatesInput): Promise { try { - const { token } = input; - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { userId } = input; const certificates = await prisma.certificate.findMany({ where: { - user_id: decoded.id, + user_id: userId, }, include: { enrollment: { @@ -267,9 +260,8 @@ 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, + userId: input.userId, action: AuditAction.ERROR, entityType: 'Certificate', entityId: 0, diff --git a/Backend/src/services/user.service.ts b/Backend/src/services/user.service.ts index 69153d51..552c7665 100644 --- a/Backend/src/services/user.service.ts +++ b/Backend/src/services/user.service.ts @@ -24,15 +24,10 @@ import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; export class UserService { - async getUserProfile(token: string): Promise { + async getUserProfile(userId: number): Promise { try { - // Decode JWT token to get user ID - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; - const user = await prisma.user.findUnique({ - where: { - id: decoded.id - }, + where: { id: userId }, include: { profile: true, role: true @@ -68,14 +63,6 @@ export class UserService { } : undefined }; } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { - logger.error('Invalid JWT token:', error); - throw new UnauthorizedError('Invalid token'); - } - if (error instanceof jwt.TokenExpiredError) { - logger.error('JWT token expired:', error); - throw new UnauthorizedError('Token expired'); - } logger.error('Error fetching user profile:', error); throw error; } @@ -84,12 +71,9 @@ export class UserService { /** * Change user password */ - async changePassword(token: string, oldPassword: string, newPassword: string): Promise { + async changePassword(userId: number, oldPassword: string, newPassword: string): Promise { try { - // Decode JWT token to get user ID - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; - - const user = await prisma.user.findUnique({ where: { id: decoded.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('User not found'); // Check if account is deactivated @@ -127,21 +111,12 @@ export class UserService { message: 'Password changed successfully' }; } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { - logger.error('Invalid JWT token:', error); - throw new UnauthorizedError('Invalid token'); - } - if (error instanceof jwt.TokenExpiredError) { - logger.error('JWT token expired:', error); - 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, + userId, action: AuditAction.ERROR, entityType: 'User', - entityId: decoded?.id || 0, + entityId: userId, metadata: { operation: 'change_password', error: error instanceof Error ? error.message : String(error) @@ -154,12 +129,9 @@ export class UserService { /** * Update user profile */ - async updateProfile(token: string, profile: ProfileUpdate): Promise { + async updateProfile(userId: number, profile: ProfileUpdate): Promise { try { - // Decode JWT token to get user ID - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; - - const user = await prisma.user.findUnique({ where: { id: decoded.id } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) throw new UnauthorizedError('User not found'); // Check if account is deactivated @@ -189,21 +161,12 @@ export class UserService { } }; } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { - logger.error('Invalid JWT token:', error); - throw new UnauthorizedError('Invalid token'); - } - if (error instanceof jwt.TokenExpiredError) { - logger.error('JWT token expired:', error); - 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, + userId, action: AuditAction.UPDATE, entityType: 'UserProfile', - entityId: decoded?.id || 0, + entityId: userId, metadata: { operation: 'update_profile', error: error instanceof Error ? error.message : String(error) @@ -213,9 +176,8 @@ export class UserService { } } - async getRoles(token: string): Promise { + async getRoles(): Promise { try { - jwt.verify(token, config.jwt.secret); const roles = await prisma.role.findMany({ select: { id: true, @@ -224,14 +186,6 @@ export class UserService { }); return { roles }; } catch (error) { - if (error instanceof jwt.TokenExpiredError) { - logger.error('JWT token expired:', error); - throw new UnauthorizedError('Token expired'); - } - if (error instanceof jwt.JsonWebTokenError) { - logger.error('Invalid JWT token:', error); - throw new UnauthorizedError('Invalid token'); - } logger.error('Failed to get roles', { error }); throw error; } @@ -240,13 +194,11 @@ export class UserService { /** * Upload avatar picture to MinIO */ - async uploadAvatarPicture(token: string, file: Express.Multer.File): Promise { + async uploadAvatarPicture(userId: number, file: Express.Multer.File): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; - // Check if user exists const user = await prisma.user.findUnique({ - where: { id: decoded.id }, + where: { id: userId }, include: { profile: true } }); @@ -265,7 +217,7 @@ export class UserService { const fileName = file.originalname || 'avatar'; const extension = fileName.split('.').pop() || 'jpg'; const safeFilename = `${timestamp}-${uniqueId}.${extension}`; - const filePath = `avatars/${decoded.id}/${safeFilename}`; + const filePath = `avatars/${userId}/${safeFilename}`; // Delete old avatar if exists if (user.profile?.avatar_url) { @@ -285,13 +237,13 @@ export class UserService { // Update or create profile - store only file path if (user.profile) { await prisma.userProfile.update({ - where: { user_id: decoded.id }, + where: { user_id: userId }, data: { avatar_url: filePath } }); } else { await prisma.userProfile.create({ data: { - user_id: decoded.id, + user_id: userId, avatar_url: filePath, first_name: '', last_name: '' @@ -301,10 +253,10 @@ export class UserService { // Audit log - UPLOAD_AVATAR await auditService.logSync({ - userId: decoded.id, + userId, action: AuditAction.UPLOAD_FILE, entityType: 'User', - entityId: decoded.id, + entityId: userId, metadata: { operation: 'upload_avatar', filePath @@ -318,26 +270,17 @@ export class UserService { code: 200, message: 'Avatar uploaded successfully', data: { - id: decoded.id, + id: userId, avatar_url: presignedUrl } }; } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { - logger.error('Invalid JWT token:', error); - throw new UnauthorizedError('Invalid token'); - } - if (error instanceof jwt.TokenExpiredError) { - logger.error('JWT token expired:', error); - 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, + userId, action: AuditAction.UPLOAD_FILE, entityType: 'UserProfile', - entityId: decoded?.id || 0, + entityId: userId, metadata: { operation: 'upload_avatar', error: error instanceof Error ? error.message : String(error) @@ -390,12 +333,10 @@ export class UserService { /** * Send verification email to user */ - async sendVerifyEmail(token: string): Promise { + async sendVerifyEmail(userId: number): Promise { try { - const decoded = jwt.verify(token, config.jwt.secret) as { id: number; email: string; roleCode: string }; - const user = await prisma.user.findUnique({ - where: { id: decoded.id }, + where: { id: userId }, include: { role: true } }); @@ -453,15 +394,12 @@ export class UserService { message: 'Verification email sent successfully' }; } catch (error) { - 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, + userId, action: AuditAction.ERROR, entityType: 'UserProfile', - entityId: decoded?.id || 0, + entityId: userId, metadata: { operation: 'send_verification_email', error: error instanceof Error ? error.message : String(error) diff --git a/Backend/src/types/ChaptersLesson.typs.ts b/Backend/src/types/ChaptersLesson.typs.ts index 51a26e4b..a2dd9a37 100644 --- a/Backend/src/types/ChaptersLesson.typs.ts +++ b/Backend/src/types/ChaptersLesson.typs.ts @@ -98,18 +98,18 @@ export interface ChapterData { // ============================================ export interface ChaptersRequest { - token: string; + userId: number; course_id: number; } export interface GetChapterRequest { - token: string; + userId: number; course_id: number; chapter_id: number; } export interface CreateChapterInput { - token: string; + userId: number; course_id: number; title: MultiLanguageText; description?: MultiLanguageText; @@ -118,13 +118,13 @@ export interface CreateChapterInput { } export interface CreateChapterRequest { - token: string; + userId: number; course_id: number; data: CreateChapterInput; } export interface UpdateChapterInput { - token: string; + userId: number; course_id: number; chapter_id: number; title?: MultiLanguageText; @@ -134,20 +134,20 @@ export interface UpdateChapterInput { } export interface UpdateChapterRequest { - token: string; + userId: number; course_id: number; chapter_id: number; data: UpdateChapterInput; } export interface DeleteChapterRequest { - token: string; + userId: number; course_id: number; chapter_id: number; } export interface ReorderChapterRequest { - token: string; + userId: number; course_id: number; chapter_id: number; sort_order: number; @@ -199,7 +199,7 @@ export interface ReorderChapterResponse { // ============================================ export interface GetLessonRequest { - token: string; + userId: number; course_id: number; chapter_id: number; lesson_id: number; @@ -216,7 +216,7 @@ export interface UploadedFileInfo { } export interface CreateLessonInput { - token: string; + userId: number; course_id: number; chapter_id: number; title: MultiLanguageText; @@ -293,7 +293,7 @@ export interface QuizChoiceData { } export interface CreateLessonRequest { - token: string; + userId: number; course_id: number; chapter_id: number; data: CreateLessonInput; @@ -311,7 +311,7 @@ export interface UpdateLessonInput { } export interface UpdateLessonRequest { - token: string; + userId: number; course_id: number; chapter_id: number; lesson_id: number; @@ -319,14 +319,14 @@ export interface UpdateLessonRequest { } export interface DeleteLessonRequest { - token: string; + userId: number; course_id: number; chapter_id: number; lesson_id: number; } export interface ReorderLessonsRequest { - token: string; + userId: number; course_id: number; chapter_id: number; lesson_id: number; @@ -365,7 +365,7 @@ export interface UpdateLessonResponse { * Input for uploading video to a lesson */ export interface UploadVideoInput { - token: string; + userId: number; course_id: number; lesson_id: number; video: UploadedFileInfo; @@ -375,7 +375,7 @@ export interface UploadVideoInput { * Input for updating (replacing) video in a lesson */ export interface UpdateVideoInput { - token: string; + userId: number; course_id: number; lesson_id: number; video: UploadedFileInfo; @@ -385,7 +385,7 @@ export interface UpdateVideoInput { * Input for setting YouTube video to a lesson */ export interface SetYouTubeVideoInput { - token: string; + userId: number; course_id: number; lesson_id: number; youtube_video_id: string; @@ -411,7 +411,7 @@ export interface YouTubeVideoResponse { * Input for uploading a single attachment to a lesson */ export interface UploadAttachmentInput { - token: string; + userId: number; course_id: number; lesson_id: number; attachment: UploadedFileInfo; @@ -421,7 +421,7 @@ export interface UploadAttachmentInput { * Input for deleting an attachment from a lesson */ export interface DeleteAttachmentInput { - token: string; + userId: number; course_id: number; lesson_id: number; attachment_id: number; @@ -490,7 +490,7 @@ export interface LessonWithDetailsResponse { * Input for adding quiz to an existing QUIZ lesson */ export interface AddQuizToLessonInput { - token: string; + userId: number; course_id: number; lesson_id: number; quiz_data: { @@ -509,7 +509,7 @@ export interface AddQuizToLessonInput { * Input for adding a single question to a quiz lesson */ export interface AddQuestionInput { - token: string; + userId: number; course_id: number; lesson_id: number; question: MultiLanguageText; @@ -532,7 +532,7 @@ export interface AddQuestionResponse { * Input for updating a question */ export interface UpdateQuestionInput { - token: string; + userId: number; course_id: number; lesson_id: number; question_id: number; @@ -556,14 +556,14 @@ export interface UpdateQuestionResponse { * Input for deleting a question */ export interface DeleteQuestionInput { - token: string; + userId: number; course_id: number; lesson_id: number; question_id: number; } export interface ReorderQuestionInput { - token: string; + userId: number; course_id: number; lesson_id: number; question_id: number; @@ -588,7 +588,7 @@ export interface DeleteQuestionResponse { * Input for updating quiz settings */ export interface UpdateQuizInput { - token: string; + userId: number; course_id: number; lesson_id: number; title?: MultiLanguageText; diff --git a/Backend/src/types/CoursesInstructor.types.ts b/Backend/src/types/CoursesInstructor.types.ts index cc4aa149..8291a1d5 100644 --- a/Backend/src/types/CoursesInstructor.types.ts +++ b/Backend/src/types/CoursesInstructor.types.ts @@ -24,7 +24,7 @@ export interface createCourseResponse { } export interface ListMyCoursesInput { - token: string; + userId: number; status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED'; } @@ -42,7 +42,7 @@ export interface GetMyCourseResponse { } export interface getmyCourse { - token: string; + userId: number; course_id: number; } @@ -94,13 +94,13 @@ export interface listCourseinstructorResponse { } export interface addinstructorCourse { - token: string; + userId: number; email_or_username: string; course_id: number; } export interface SearchInstructorInput { - token: string; + userId: number; query: string; course_id: number; } @@ -145,12 +145,12 @@ export interface listinstructorCourseResponse { } export interface listinstructorCourse { - token: string; + userId: number; course_id: number; } export interface removeinstructorCourse { - token: string; + userId: number; user_id: number; course_id: number; } @@ -161,7 +161,7 @@ export interface removeinstructorCourseResponse { } export interface setprimaryCourseInstructor { - token: string; + userId: number; user_id: number; course_id: number; } @@ -172,12 +172,12 @@ export interface setprimaryCourseInstructorResponse { } export interface sendCourseForReview { - token: string; + userId: number; course_id: number; } export interface setCourseDraft { - token: string; + userId: number; course_id: number; } @@ -220,7 +220,7 @@ export interface GetCourseApprovalsResponse { // ============================================ export interface GetEnrolledStudentsInput { - token: string; + userId: number; course_id: number; page?: number; limit?: number; @@ -254,7 +254,7 @@ export interface GetEnrolledStudentsResponse { // ============================================ export interface GetQuizScoresInput { - token: string; + userId: number; course_id: number; lesson_id: number; page?: number; @@ -305,7 +305,7 @@ export interface GetQuizScoresResponse { // ============================================ export interface GetQuizAttemptDetailInput { - token: string; + userId: number; course_id: number; lesson_id: number; student_id: number; @@ -353,7 +353,7 @@ export interface GetQuizAttemptDetailResponse { // ============================================ export interface GetEnrolledStudentDetailInput { - token: string; + userId: number; course_id: number; student_id: number; } @@ -435,7 +435,7 @@ export interface GetCourseApprovalHistoryResponse { } export interface CloneCourseInput { - token: string; + userId: number; course_id: number; title: MultiLanguageText; } @@ -448,3 +448,14 @@ export interface CloneCourseResponse { title: MultiLanguageText; }; } + +// ============================================ +// Get All Students across all instructor courses +// ============================================ + +export interface GetAllMyStudentsResponse { + code: number; + message: string; + total_students: number; + total_completed: number; +} diff --git a/Backend/src/types/CoursesStudent.types.ts b/Backend/src/types/CoursesStudent.types.ts index 541787c1..2c92850f 100644 --- a/Backend/src/types/CoursesStudent.types.ts +++ b/Backend/src/types/CoursesStudent.types.ts @@ -9,7 +9,7 @@ export type MultiLangText = MultiLanguageText; // ============================================ export interface EnrollCourseInput { - token: string; + userId: number; course_id: number; } @@ -26,7 +26,7 @@ export interface EnrollCourseResponse { } export interface ListEnrolledCoursesInput { - token: string; + userId: number; page?: number; limit?: number; status?: EnrollmentStatus; @@ -64,7 +64,7 @@ export interface ListEnrolledCoursesResponse { // ============================================ export interface GetCourseLearningInput { - token: string; + userId: number; course_id: number; } @@ -126,7 +126,7 @@ export interface GetCourseLearningResponse { // ============================================ export interface GetLessonContentInput { - token: string; + userId: number; course_id: number; lesson_id: number; } @@ -204,7 +204,7 @@ export interface GetLessonContentResponse { // ============================================ export interface CheckLessonAccessInput { - token: string; + userId: number; course_id: number; lesson_id: number; } @@ -236,7 +236,7 @@ export interface CheckLessonAccessResponse { // ============================================ export interface SaveVideoProgressInput { - token: string; + userId: number; lesson_id: number; video_progress_seconds: number; video_duration_seconds?: number; @@ -258,7 +258,7 @@ export interface SaveVideoProgressResponse { } export interface GetVideoProgressInput { - token: string; + userId: number; lesson_id: number; } @@ -281,7 +281,7 @@ export interface GetVideoProgressResponse { // ============================================ export interface MarkLessonCompleteInput { - token: string; + userId: number; course_id: number; lesson_id: number; } @@ -314,7 +314,7 @@ export interface EnrollCourseBody { } export interface CompleteLessonInput { - token: string; + userId: number; lesson_id: number; } @@ -342,7 +342,7 @@ export interface QuizAnswerInput { } export interface SubmitQuizInput { - token: string; + userId: number; course_id: number; lesson_id: number; answers: QuizAnswerInput[]; @@ -384,7 +384,7 @@ export interface SubmitQuizResponse { // ============================================ export interface GetQuizAttemptsInput { - token: string; + userId: number; course_id: number; lesson_id: number; } diff --git a/Backend/src/types/announcements.types.ts b/Backend/src/types/announcements.types.ts index f960a961..b026809e 100644 --- a/Backend/src/types/announcements.types.ts +++ b/Backend/src/types/announcements.types.ts @@ -22,7 +22,7 @@ export interface AnnouncementAttachment { updated_at: Date; } -export interface ListAnnouncementResponse{ +export interface ListAnnouncementResponse { code: number; message: string; data: Announcement[]; @@ -31,15 +31,15 @@ export interface ListAnnouncementResponse{ limit: number; } -export interface ListAnnouncementInput{ - token: string; +export interface ListAnnouncementInput { + userId: number; course_id: number; page?: number; limit?: number; } -export interface CreateAnnouncementInput{ - token: string; +export interface CreateAnnouncementInput { + userId: number; course_id: number; title: MultiLanguageText; content: MultiLanguageText; @@ -49,39 +49,39 @@ export interface CreateAnnouncementInput{ files?: Express.Multer.File[]; } -export interface UploadAnnouncementAttachmentInput{ - token: string; +export interface UploadAnnouncementAttachmentInput { + userId: number; course_id: number; announcement_id: number; file: File; } -export interface UploadAnnouncementAttachmentResponse{ +export interface UploadAnnouncementAttachmentResponse { code: number; message: string; data: AnnouncementAttachment; } -export interface DeleteAnnouncementAttachmentInput{ - token: string; +export interface DeleteAnnouncementAttachmentInput { + userId: number; course_id: number; announcement_id: number; attachment_id: number; } -export interface DeleteAnnouncementAttachmentResponse{ +export interface DeleteAnnouncementAttachmentResponse { code: number; message: string; } -export interface CreateAnnouncementResponse{ +export interface CreateAnnouncementResponse { code: number; message: string; data: Announcement; } -export interface UpdateAnnouncementInput{ - token: string; +export interface UpdateAnnouncementInput { + userId: number; course_id: number; announcement_id: number; title: MultiLanguageText; @@ -92,19 +92,19 @@ export interface UpdateAnnouncementInput{ attachments?: AnnouncementAttachment[]; } -export interface UpdateAnnouncementResponse{ +export interface UpdateAnnouncementResponse { code: number; message: string; data: Announcement; } -export interface DeleteAnnouncementInput{ - token: string; +export interface DeleteAnnouncementInput { + userId: number; course_id: number; announcement_id: number; } -export interface DeleteAnnouncementResponse{ +export interface DeleteAnnouncementResponse { code: number; message: string; } diff --git a/Backend/src/types/auth.types.ts b/Backend/src/types/auth.types.ts index be1fe625..1ec9235a 100644 --- a/Backend/src/types/auth.types.ts +++ b/Backend/src/types/auth.types.ts @@ -28,7 +28,6 @@ export interface LoginResponse { data: { token: string; refreshToken: string; - user: UserResponse; }; } diff --git a/Backend/src/types/certificate.types.ts b/Backend/src/types/certificate.types.ts index a35caa19..ac442f76 100644 --- a/Backend/src/types/certificate.types.ts +++ b/Backend/src/types/certificate.types.ts @@ -3,7 +3,7 @@ // ============================================ export interface GenerateCertificateInput { - token: string; + userId: number; course_id: number; } @@ -19,7 +19,7 @@ export interface GenerateCertificateResponse { } export interface GetCertificateInput { - token: string; + userId: number; course_id: number; } @@ -37,7 +37,7 @@ export interface GetCertificateResponse { } export interface ListMyCertificatesInput { - token: string; + userId: number; } export interface ListMyCertificatesResponse {