474 lines
No EOL
22 KiB
TypeScript
474 lines
No EOL
22 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 {
|
|
createCourseResponse,
|
|
ListMyCourseResponse,
|
|
GetMyCourseResponse,
|
|
UpdateMyCourse,
|
|
UpdateMyCourseResponse,
|
|
DeleteMyCourseResponse,
|
|
submitCourseResponse,
|
|
listinstructorCourseResponse,
|
|
addinstructorCourseResponse,
|
|
removeinstructorCourseResponse,
|
|
setprimaryCourseInstructorResponse,
|
|
GetEnrolledStudentsResponse,
|
|
GetEnrolledStudentDetailResponse,
|
|
GetQuizScoresResponse,
|
|
GetQuizAttemptDetailResponse,
|
|
GetCourseApprovalsResponse,
|
|
SearchInstructorResponse,
|
|
GetCourseApprovalHistoryResponse,
|
|
setCourseDraftResponse,
|
|
CloneCourseResponse,
|
|
} 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 {
|
|
|
|
/**
|
|
* ดึงรายการคอร์สทั้งหมดของผู้สอน
|
|
* 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,
|
|
@Query() status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED'
|
|
): Promise<ListMyCourseResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) {
|
|
throw new ValidationError('No token provided');
|
|
}
|
|
return await CoursesInstructorService.listMyCourses({ token, status });
|
|
}
|
|
|
|
/**
|
|
* ค้นหาผู้สอนทั้งหมดในระบบ (ไม่รวมตัวเองและคนที่อยู่ในคอร์สแล้ว)
|
|
* 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');
|
|
|
|
const { error } = UpdateCourseValidator.validate(body.data);
|
|
if (error) throw new ValidationError(error.details[0].message);
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* คัดลอกคอร์ส (Clone Course)
|
|
* Clone an existing course to a new one with copied chapters, lessons, quizzes, and attachments
|
|
* @param courseId - รหัสคอร์สต้นฉบับ / Source Course ID
|
|
* @param body - ชื่อคอร์สใหม่ / New course title
|
|
*/
|
|
@Post('{courseId}/clone')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('201', 'Course cloned successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('403', 'Not an instructor of this course')
|
|
@Response('404', 'Course not found')
|
|
public async cloneCourse(
|
|
@Request() request: any,
|
|
@Path() courseId: number,
|
|
@Body() body: { title: { th: string; en: string } }
|
|
): Promise<CloneCourseResponse> {
|
|
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,
|
|
course_id: courseId,
|
|
title: body.title
|
|
});
|
|
return result;
|
|
}
|
|
/**
|
|
* ส่งคอร์สเพื่อขออนุมัติจากแอดมิน
|
|
* 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 });
|
|
}
|
|
|
|
/**
|
|
* ตั้งค่าคอร์สเป็น DRAFT
|
|
* Set course to draft
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Post('set-draft/{courseId}')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Course set to draft successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('404', 'Course not found')
|
|
public async setCourseDraft(@Request() request: any, @Path() courseId: number): Promise<setCourseDraftResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided')
|
|
return await CoursesInstructorService.setCourseDraft({ 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,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ดึงประวัติการขออนุมัติคอร์ส
|
|
* Get course approval history for instructor to see rejection reasons
|
|
* @param courseId - รหัสคอร์ส / Course ID
|
|
*/
|
|
@Get('{courseId}/approval-history')
|
|
@Security('jwt', ['instructor'])
|
|
@SuccessResponse('200', 'Approval history retrieved successfully')
|
|
@Response('401', 'Invalid or expired token')
|
|
@Response('403', 'Not an instructor of this course')
|
|
@Response('404', 'Course not found')
|
|
public async getCourseApprovalHistory(
|
|
@Request() request: any,
|
|
@Path() courseId: number
|
|
): Promise<GetCourseApprovalHistoryResponse> {
|
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
if (!token) throw new ValidationError('No token provided');
|
|
return await CoursesInstructorService.getCourseApprovalHistory(token, courseId);
|
|
}
|
|
} |