Add search and status filter parameters to getEnrolledStudents endpoint to filter students by name/email/username and enrollment status. Add search and isPassed filter parameters to getQuizScores endpoint to filter quiz results by student details and pass status. Remove separate searchStudents endpoint as its functionality is now integrated into getEnrolledStudents. Add setYouTubeVideo endpoint to
401 lines
No EOL
19 KiB
TypeScript
401 lines
No EOL
19 KiB
TypeScript
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Security, Put, Path, Delete, Request, Example, FormField, UploadedFile, Query } from 'tsoa';
|
|
import { ValidationError } from '../middleware/errorHandler';
|
|
import { CoursesInstructorService } from '../services/CoursesInstructor.service';
|
|
import {
|
|
createCourses,
|
|
createCourseResponse,
|
|
GetMyCourseResponse,
|
|
ListMyCourseResponse,
|
|
addinstructorCourseResponse,
|
|
removeinstructorCourseResponse,
|
|
setprimaryCourseInstructorResponse,
|
|
UpdateMyCourse,
|
|
UpdateMyCourseResponse,
|
|
DeleteMyCourseResponse,
|
|
submitCourseResponse,
|
|
listinstructorCourseResponse,
|
|
GetCourseApprovalsResponse,
|
|
SearchInstructorResponse,
|
|
GetEnrolledStudentsResponse,
|
|
GetQuizScoresResponse,
|
|
GetQuizAttemptDetailResponse,
|
|
GetEnrolledStudentDetailResponse,
|
|
} from '../types/CoursesInstructor.types';
|
|
import { CreateCourseValidator } from "../validators/CoursesInstructor.validator";
|
|
|
|
import jwt from 'jsonwebtoken';
|
|
import { config } from '../config';
|
|
|
|
@Route('api/instructors/courses')
|
|
@Tags('CoursesInstructor')
|
|
export class CoursesInstructorController {
|
|
|
|
/**
|
|
* ดึงรายการคอร์สทั้งหมดของผู้สอน
|
|
* Get all courses where the authenticated user is an instructor
|
|
*/
|
|
@Get('')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Courses retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Courses not found')
|
|
public async listMyCourses(@Request() request: any): Promise<ListMyCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) {
|
|
throw new ValidationError('No token provided');
|
|
}
|
|
return await CoursesInstructorService.listMyCourses(token);
|
|
}
|
|
|
|
/**
|
|
* ค้นหาผู้สอนทั้งหมดในระบบ (ไม่รวมตัวเองและคนที่อยู่ในคอร์สแล้ว)
|
|
* Search all instructors in database (excluding self and existing course instructors)
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param query - คำค้นหา (email หรือ username) / Search query (email or username)
|
|
*/
|
|
@Get('{courseId}/search-instructors')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Instructors found')
|
|
@Response('401', 'Invalid or expired token')
|
|
public async searchInstructors(
|
|
@Request() request: any,
|
|
@Path() courseId: number,
|
|
@Query() query: string
|
|
): Promise<SearchInstructorResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided');
|
|
return await CoursesInstructorService.searchInstructors({ token, query, course_id: courseId });
|
|
}
|
|
|
|
/**
|
|
* ดึงข้อมูลคอร์สเฉพาะของผู้สอน (พร้อมบทเรียนและเนื้อหา)
|
|
* Get detailed course information including chapters, lessons, attachments, and quizzes
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Get('{courseId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Course retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Course not found')
|
|
public async getMyCourse(@Request() request: any, @Path() courseId: number): Promise<GetMyCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) {
|
|
throw new ValidationError('No token provided');
|
|
}
|
|
return await CoursesInstructorService.getmyCourse({ token, course_id: courseId });
|
|
}
|
|
|
|
/**
|
|
* แก้ไขข้อมูลคอร์ส
|
|
* Update course information (only for course instructors)
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Put('{courseId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Course updated successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Course not found')
|
|
public async updateCourse(@Request() request: any, @Path() courseId: number, @Body() body: UpdateMyCourse): Promise<UpdateMyCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) {
|
|
throw new ValidationError('No token provided');
|
|
}
|
|
return await CoursesInstructorService.updateCourse(token, courseId, body.data);
|
|
}
|
|
|
|
/**
|
|
* สร้างคอร์สใหม่ (พร้อมอัปโหลดรูป thumbnail)
|
|
* Create a new course with optional thumbnail upload (status will be DRAFT by default)
|
|
* @param data - JSON string containing course data
|
|
* @param thumbnail - Optional thumbnail image file
|
|
*/
|
|
@Post('')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('201', 'Course created successfully')
|
|
@Response('400', 'Validation error')
|
|
@Response('500', 'Internal server error')
|
|
public async createCourse(
|
|
@Request() request: any,
|
|
@FormField() data: string,
|
|
@UploadedFile() thumbnail?: Express.Multer.File
|
|
): Promise<createCourseResponse> {
|
|
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);
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* อัปโหลดรูป 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)
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Delete('{courseId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Course deleted successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Course not found')
|
|
public async deleteCourse(@Request() request: any, @Path() courseId: number): Promise<DeleteMyCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided')
|
|
return await CoursesInstructorService.deleteCourse(token, courseId);
|
|
}
|
|
|
|
/**
|
|
* ส่งคอร์สเพื่อขออนุมัติจากแอดมิน
|
|
* Submit course for admin review and approval
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Post('send-review/{courseId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Course submitted successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Course not found')
|
|
public async submitCourse(@Request() request: any, @Path() courseId: number): Promise<submitCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided')
|
|
return await CoursesInstructorService.sendCourseForReview({ token, course_id: courseId });
|
|
}
|
|
|
|
/**
|
|
* ดึงประวัติการส่งอนุมัติคอร์สทั้งหมด
|
|
* Get all course approval history
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Get('{courseId}/approvals')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Course approvals retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@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<GetCourseApprovalsResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided')
|
|
return await CoursesInstructorService.getCourseApprovals(token, courseId);
|
|
}
|
|
|
|
/**
|
|
* ดึงรายชื่อผู้สอนทั้งหมดในคอร์ส
|
|
* Get list of all instructors in a specific course
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Get('listinstructor/{courseId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Instructors retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Instructors not found')
|
|
public async listInstructorCourses(@Request() request: any, @Path() courseId: number): Promise<listinstructorCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided')
|
|
return await CoursesInstructorService.listInstructorsOfCourse({ token, course_id: courseId });
|
|
}
|
|
|
|
/**
|
|
* เพิ่มผู้สอนเข้าในคอร์ส (ใช้ email หรือ username)
|
|
* Add a new instructor to the course (using email or username)
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param emailOrUsername - email หรือ username ของผู้สอน / Instructor's email or username
|
|
*/
|
|
@Post('add-instructor/{courseId}/{emailOrUsername}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Instructor added successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Instructor not found')
|
|
public async addInstructor(@Request() request: any, @Path() courseId: number, @Path() emailOrUsername: string): Promise<addinstructorCourseResponse> {
|
|
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 });
|
|
}
|
|
|
|
/**
|
|
* ลบผู้สอนออกจากคอร์ส
|
|
* Remove an instructor from the course
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param userId - รหัสผู้ใช้ที่ต้องการลบออกจากผู้สอน / User ID to remove from instructors
|
|
*/
|
|
@Delete('remove-instructor/{courseId}/{userId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Instructor removed successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Instructor not found')
|
|
public async removeInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise<removeinstructorCourseResponse> {
|
|
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 });
|
|
}
|
|
|
|
/**
|
|
* กำหนดผู้สอนหลักของคอร์ส
|
|
* Set a user as the primary instructor of the course
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param userId - รหัสผู้ใช้ที่ต้องการตั้งเป็นผู้สอนหลัก / User ID to set as primary instructor
|
|
*/
|
|
@Put('set-primary-instructor/{courseId}/{userId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Primary instructor set successfully')
|
|
@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<setprimaryCourseInstructorResponse> {
|
|
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 });
|
|
}
|
|
|
|
/**
|
|
* ดึงรายชื่อนักเรียนที่ลงทะเบียนในคอร์สพร้อม progress
|
|
* Get all enrolled students with their progress
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param page - หน้าที่ต้องการ / Page number
|
|
* @param limit - จำนวนต่อหน้า / Items per page
|
|
* @param search - ค้นหาด้วย firstname, lastname, email, username
|
|
* @param status - กรองตามสถานะ (ENROLLED, COMPLETED)
|
|
*/
|
|
@Get('{courseId}/students')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Enrolled students retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('403', 'Not an instructor of this course')
|
|
public async getEnrolledStudents(
|
|
@Request() request: any,
|
|
@Path() courseId: number,
|
|
@Query() page?: number,
|
|
@Query() limit?: number,
|
|
@Query() search?: string,
|
|
@Query() status?: 'ENROLLED' | 'COMPLETED'
|
|
): Promise<GetEnrolledStudentsResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided');
|
|
return await CoursesInstructorService.getEnrolledStudents({
|
|
token,
|
|
course_id: courseId,
|
|
page,
|
|
limit,
|
|
search,
|
|
status,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ดึงรายละเอียดการเรียนของนักเรียนแต่ละคน (progress ของแต่ละ lesson)
|
|
* Get enrolled student detail with lesson progress
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param studentId - รหัสนักเรียน / Student ID
|
|
*/
|
|
@Get('{courseId}/students/{studentId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Enrolled student detail retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('403', 'Not an instructor of this course')
|
|
@Response('404', 'Student not found or not enrolled')
|
|
public async getEnrolledStudentDetail(
|
|
@Request() request: any,
|
|
@Path() courseId: number,
|
|
@Path() studentId: number
|
|
): Promise<GetEnrolledStudentDetailResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided');
|
|
return await CoursesInstructorService.getEnrolledStudentDetail({
|
|
token,
|
|
course_id: courseId,
|
|
student_id: studentId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ดึงคะแนนสอบของนักเรียนทุกคนในแต่ละ lesson quiz
|
|
* Get quiz scores of all students for a specific lesson
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param lessonId - รหัสบทเรียน / Lesson ID
|
|
* @param page - หน้าที่ต้องการ / Page number
|
|
* @param limit - จำนวนต่อหน้า / Items per page
|
|
* @param search - ค้นหาด้วย firstname, lastname, email, username
|
|
* @param isPassed - กรองตามผลสอบ (true = ผ่าน, false = ไม่ผ่าน)
|
|
*/
|
|
@Get('{courseId}/lessons/{lessonId}/quiz/scores')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Quiz scores retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('403', 'Not an instructor of this course')
|
|
@Response('404', 'Lesson or quiz not found')
|
|
public async getQuizScores(
|
|
@Request() request: any,
|
|
@Path() courseId: number,
|
|
@Path() lessonId: number,
|
|
@Query() page?: number,
|
|
@Query() limit?: number,
|
|
@Query() search?: string,
|
|
@Query() isPassed?: boolean
|
|
): Promise<GetQuizScoresResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided');
|
|
return await CoursesInstructorService.getQuizScores({
|
|
token,
|
|
course_id: courseId,
|
|
lesson_id: lessonId,
|
|
page,
|
|
limit,
|
|
search,
|
|
is_passed: isPassed,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ดูรายละเอียดการทำข้อสอบล่าสุดของนักเรียนแต่ละคน
|
|
* Get latest quiz attempt detail for a specific student
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
* @param lessonId - รหัสบทเรียน / Lesson ID
|
|
* @param studentId - รหัสนักเรียน / Student ID
|
|
*/
|
|
@Get('{courseId}/lessons/{lessonId}/quiz/students/{studentId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Quiz attempt detail retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('403', 'Not an instructor of this course')
|
|
@Response('404', 'Student or quiz attempt not found')
|
|
public async getQuizAttemptDetail(
|
|
@Request() request: any,
|
|
@Path() courseId: number,
|
|
@Path() lessonId: number,
|
|
@Path() studentId: number
|
|
): Promise<GetQuizAttemptDetailResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided');
|
|
return await CoursesInstructorService.getQuizAttemptDetail({
|
|
token,
|
|
course_id: courseId,
|
|
lesson_id: lessonId,
|
|
student_id: studentId,
|
|
});
|
|
}
|
|
} |