diff --git a/Backend/src/controllers/CoursesController.ts b/Backend/src/controllers/CoursesController.ts index cfcea7a6..5eef18e0 100644 --- a/Backend/src/controllers/CoursesController.ts +++ b/Backend/src/controllers/CoursesController.ts @@ -2,11 +2,10 @@ import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Contro import { ValidationError } from '../middleware/errorHandler'; import { listCourseResponse } from '../types/courses.types'; import { CoursesService } from '../services/courses.service'; -import { get } from 'http'; @Route('api/courses') @Tags('Courses') -export class CoursesController extends Controller { +export class CoursesController { private coursesService = new CoursesService(); @Get() diff --git a/Backend/src/controllers/CoursesInstructorController.ts b/Backend/src/controllers/CoursesInstructorController.ts index e69de29b..db5b4675 100644 --- a/Backend/src/controllers/CoursesInstructorController.ts +++ b/Backend/src/controllers/CoursesInstructorController.ts @@ -0,0 +1,66 @@ +import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Security, Put, Path, Delete, Request } from 'tsoa'; +import { ValidationError } from '../middleware/errorHandler'; +import { CoursesInstructorService } from '../services/CoursesInstructor.service'; +import { + createCourses, + createCourseResponse, + GetMyCourseResponse, + ListMyCourseResponse, + addinstructorCourse, + addinstructorCourseResponse, + removeinstructorCourse, + removeinstructorCourseResponse, + setprimaryCourseInstructor, + setprimaryCourseInstructorResponse +} 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('') + @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 { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await CoursesInstructorService.listMyCourses(token); + } + + @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 { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await CoursesInstructorService.getmyCourse(token, courseId); + } + + @Post('') + @Security('jwt', ['instructor']) + @SuccessResponse('201', 'Course created successfully') + @Response('400', 'Validation error') + @Response('500', 'Internal server error') + public async createCourse(@Body() body: createCourses, @Request() request: any): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + const { error, value } = CreateCourseValidator.validate(body.data); + if (error) throw new ValidationError(error.details[0].message); + const course = await CoursesInstructorService.createCourse(value, decoded.id); + return course; + } + + +} \ No newline at end of file diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index e69de29b..a08c3831 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -0,0 +1,119 @@ +import { prisma } from '../config/database'; +import { Prisma } from '@prisma/client'; +import { config } from '../config'; +import { logger } from '../config/logger'; +import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; +import jwt from 'jsonwebtoken'; +import { + createCourses, + CreateCourseInput, + createCourseResponse, + GetMyCourseResponse, + ListMyCourseResponse, + addinstructorCourse, + addinstructorCourseResponse, + removeinstructorCourse, + removeinstructorCourseResponse, + setprimaryCourseInstructor, + setprimaryCourseInstructorResponse +} from "../types/CoursesInstructor.types"; + +export class CoursesInstructorService { + static async createCourse(courseData: CreateCourseInput, userId: number): Promise { + try { + // Map custom input to Prisma format + const courseCreated = await prisma.course.create({ + data: { + category_id: courseData.category_id, + title: courseData.title, + slug: courseData.slug, + description: courseData.description, + thumbnail_url: courseData.thumbnail_url, + price: courseData.price || 0, + is_free: courseData.is_free ?? false, + have_certificate: courseData.have_certificate ?? false, + created_by: userId, // Required field from JWT + status: 'DRAFT' // Default status + } + }); + return { + code: 201, + message: 'Course created successfully', + data: courseCreated + }; + } catch (error) { + logger.error('Failed to create course', { error }); + throw error; + } + } + + static async listMyCourses(token: string): Promise { + try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; + const courseInstructors = await prisma.courseInstructor.findMany({ + where: { + user_id: decoded.id + }, + include: { + course: true + } + }); + + const courses = courseInstructors.map(ci => ci.course); + + return { + code: 200, + message: 'Courses retrieved successfully', + data: courses, + total: courses.length + }; + } catch (error) { + logger.error('Failed to retrieve courses', { error }); + throw error; + } + } + + static async getmyCourse(token: string, courseId: number): Promise { + try { + const decoded = jwt.verify(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, + course_id: courseId + }, + include: { + course: { + include: { + chapters: { + include: { + lessons: { + include: { + attachments: true, + progress: true, + quiz: true + } + } + } + } + } + } + } + }); + + if (!courseInstructor) { + throw new ForbiddenError('You are not an instructor of this course'); + } + + return { + code: 200, + message: 'Course retrieved successfully', + data: courseInstructor.course + }; + } catch (error) { + logger.error('Failed to retrieve course', { error }); + throw error; + } + } +} diff --git a/Backend/src/services/courses.service.ts b/Backend/src/services/courses.service.ts index 6c71f3ad..91f9e9ea 100644 --- a/Backend/src/services/courses.service.ts +++ b/Backend/src/services/courses.service.ts @@ -2,7 +2,6 @@ import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; import { config } from '../config'; import { logger } from '../config/logger'; -import jwt from 'jsonwebtoken'; import {listCourseResponse, getCourseResponse } from '../types/courses.types'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; diff --git a/Backend/src/types/CoursesInstructor.types.ts b/Backend/src/types/CoursesInstructor.types.ts index 9e2504d3..2e508f42 100644 --- a/Backend/src/types/CoursesInstructor.types.ts +++ b/Backend/src/types/CoursesInstructor.types.ts @@ -1,7 +1,25 @@ import { Course, Prisma, User } from '@prisma/client'; -export interface createCourses{ - data: Prisma.CourseCreateInput; +// Custom type for TSOA - avoiding complex Prisma types +export interface CreateCourseInput { + category_id?: number; + title: { + th: string; + en: string; + }; + slug: string; + description: { + th: string; + en: string; + }; + thumbnail_url?: string; + price?: number; + is_free?: boolean; + have_certificate?: boolean; +} + +export interface createCourses { + data: CreateCourseInput; } export interface createCourseResponse { @@ -23,7 +41,7 @@ export interface GetMyCourseResponse { data: Course; } -export interface UpdateMyCourse{ +export interface UpdateMyCourse { data: Prisma.CourseUpdateInput; } @@ -43,7 +61,7 @@ export interface submitCourseResponse { message: string; } -export interface submitCourse{ +export interface submitCourse { courseId: number; submitted_by: number; } @@ -56,11 +74,10 @@ export interface submitCourseResponse { export interface listCourseinstructorResponse { code: number; message: string; - data: User[]; - total: number; + data: Course[]; } -export interface addinstructorCourse{ +export interface addinstructorCourse { user_id: number; course_id: number; } @@ -70,7 +87,7 @@ export interface addinstructorCourseResponse { message: string; } -export interface removeinstructorCourse{ +export interface removeinstructorCourse { user_id: number; course_id: number; } @@ -80,7 +97,7 @@ export interface removeinstructorCourseResponse { message: string; } -export interface setprimaryCourseInstructor{ +export interface setprimaryCourseInstructor { user_id: number; course_id: number; } diff --git a/Backend/src/validators/CoursesInstructor.validator.ts b/Backend/src/validators/CoursesInstructor.validator.ts index fc163453..08ab90b5 100644 --- a/Backend/src/validators/CoursesInstructor.validator.ts +++ b/Backend/src/validators/CoursesInstructor.validator.ts @@ -4,3 +4,20 @@ export const addInstructorCourseValidator = Joi.object({ user_id: Joi.number().required(), course_id: Joi.number().required(), }); + +export const CreateCourseValidator = Joi.object({ + category_id: Joi.number().required(), + title: Joi.object({ + th: Joi.string().required(), + en: Joi.string().required(), + }).required(), + slug: Joi.string().required(), + description: Joi.object({ + th: Joi.string().required(), + en: Joi.string().required(), + }).required(), + thumbnail_url: Joi.string().required(), + price: Joi.number().required(), + is_free: Joi.boolean().required(), + have_certificate: Joi.boolean().required(), +});