diff --git a/Backend/src/controllers/CoursesInstructorController.ts b/Backend/src/controllers/CoursesInstructorController.ts index 6ea2be56..746c2041 100644 --- a/Backend/src/controllers/CoursesInstructorController.ts +++ b/Backend/src/controllers/CoursesInstructorController.ts @@ -111,6 +111,29 @@ export class CoursesInstructorController { return await CoursesInstructorService.createCourse(value, decoded.id, thumbnail); } + /** + * อัปโหลดรูป thumbnail ของคอร์ส + * Upload course thumbnail image + * @param courseId - รหัสคอร์ส / Course ID + * @param file - ไฟล์รูปภาพ / Image file + */ + @Post('{courseId}/thumbnail') + @Security('jwt', ['instructor']) + @SuccessResponse('200', 'Thumbnail uploaded successfully') + @Response('401', 'Invalid or expired token') + @Response('400', 'Validation error') + public async uploadThumbnail( + @Request() request: any, + @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); + } + /** * ลบคอร์ส (เฉพาะผู้สอนหลักเท่านั้น) * Delete a course (only primary instructor can delete) diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index 6e0d450b..8cb9838e 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -182,7 +182,7 @@ export class CoursesInstructorService { static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise { try { - const courseInstructorId = await this.validateCourseInstructor(token, courseId); + await this.validateCourseInstructor(token, courseId); const course = await prisma.course.update({ where: { @@ -191,23 +191,10 @@ export class CoursesInstructorService { data: courseData }); - // Generate presigned URL for thumbnail - let thumbnail_presigned_url: string | null = null; - if (course.thumbnail_url) { - try { - thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600); - } catch (err) { - logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); - } - } - return { code: 200, message: 'Course updated successfully', - data: { - ...course, - thumbnail_url: thumbnail_presigned_url, - } + data: course }; } catch (error) { logger.error('Failed to update course', { error }); @@ -215,6 +202,59 @@ 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 } }> { + try { + await this.validateCourseInstructor(token, courseId); + + // Get current course to check for existing thumbnail + const currentCourse = await prisma.course.findUnique({ + where: { id: courseId } + }); + + // Delete old thumbnail if exists + if (currentCourse?.thumbnail_url) { + try { + await deleteFile(currentCourse.thumbnail_url); + logger.info(`Deleted old thumbnail: ${currentCourse.thumbnail_url}`); + } catch (error) { + logger.warn(`Failed to delete old thumbnail: ${error}`); + } + } + + // Generate unique filename + const timestamp = Date.now(); + const uniqueId = Math.random().toString(36).substring(2, 15); + const extension = file.originalname.split('.').pop() || 'jpg'; + const safeFilename = `${timestamp}-${uniqueId}.${extension}`; + const filePath = `courses/${courseId}/thumbnail/${safeFilename}`; + + // Upload to MinIO + await uploadFile(filePath, file.buffer, file.mimetype || 'image/jpeg'); + logger.info(`Uploaded thumbnail: ${filePath}`); + + // Update course with new thumbnail path + await prisma.course.update({ + where: { id: courseId }, + data: { thumbnail_url: filePath } + }); + + // Generate presigned URL for response + const presignedUrl = await getPresignedUrl(filePath, 3600); + + return { + code: 200, + message: 'Thumbnail uploaded successfully', + data: { + course_id: courseId, + thumbnail_url: presignedUrl + } + }; + } catch (error) { + logger.error('Failed to upload thumbnail', { error }); + throw error; + } + } + static async deleteCourse(token: string, courseId: number): Promise { try { const courseInstructorId = await this.validateCourseInstructor(token, courseId);