elearning/Backend/src/controllers/CoursesInstructorController.ts

443 lines
21 KiB
TypeScript
Raw Normal View History

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,
ListMyCoursesInput,
ListMyCourseResponse,
addinstructorCourseResponse,
removeinstructorCourseResponse,
setprimaryCourseInstructorResponse,
UpdateMyCourse,
UpdateMyCourseResponse,
DeleteMyCourseResponse,
submitCourseResponse,
2026-01-23 13:16:41 +07:00
listinstructorCourseResponse,
GetCourseApprovalsResponse,
SearchInstructorResponse,
GetEnrolledStudentsResponse,
GetQuizScoresResponse,
GetQuizAttemptDetailResponse,
GetEnrolledStudentDetailResponse,
GetCourseApprovalHistoryResponse,
setCourseDraftResponse,
} 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,
@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');
}
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 });
}
/**
* 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 });
}
2026-01-23 13:16:41 +07:00
/**
*
* 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')
2026-01-23 13:16:41 +07:00
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);
}
}