/** * Education Controller * Handles education-related endpoints */ import { Request, Response, NextFunction } from 'express'; import { courseService } from '../services/course.service'; import { enrollmentService } from '../services/enrollment.service'; import type { CourseFilters, PaginationOptions } from '../types/education.types'; type AuthRequest = Request; // ============================================================================ // Categories // ============================================================================ export async function getCategories(req: Request, res: Response, next: NextFunction): Promise { try { const categories = await courseService.getCategories(); res.json({ success: true, data: categories, }); } catch (error) { next(error); } } export async function createCategory(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { name, slug, description, icon, parentId, sortOrder } = req.body; if (!name) { res.status(400).json({ success: false, error: { message: 'Category name is required', code: 'VALIDATION_ERROR' }, }); return; } const category = await courseService.createCategory({ name, slug, description, icon, parentId, sortOrder, }); res.status(201).json({ success: true, data: category, }); } catch (error) { next(error); } } // ============================================================================ // Courses // ============================================================================ export async function getCourses(req: Request, res: Response, next: NextFunction): Promise { try { const filters: CourseFilters = { categoryId: req.query.categoryId as string, level: req.query.level as CourseFilters['level'], status: (req.query.status as CourseFilters['status']) || 'published', isFree: req.query.isFree === 'true' ? true : req.query.isFree === 'false' ? false : undefined, search: req.query.search as string, minRating: req.query.minRating ? parseFloat(req.query.minRating as string) : undefined, }; const pagination: PaginationOptions = { page: parseInt(req.query.page as string, 10) || 1, pageSize: Math.min(parseInt(req.query.pageSize as string, 10) || 20, 100), sortBy: req.query.sortBy as string, sortOrder: req.query.sortOrder as 'asc' | 'desc', }; const result = await courseService.getCourses(filters, pagination); res.json({ success: true, ...result, }); } catch (error) { next(error); } } export async function getCourseById(req: Request, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const course = await courseService.getCourseWithDetails(courseId); if (!course) { res.status(404).json({ success: false, error: { message: 'Course not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, data: course, }); } catch (error) { next(error); } } export async function getCourseBySlug(req: Request, res: Response, next: NextFunction): Promise { try { const { slug } = req.params; const course = await courseService.getCourseBySlug(slug); if (!course) { res.status(404).json({ success: false, error: { message: 'Course not found', code: 'NOT_FOUND' }, }); return; } const courseWithDetails = await courseService.getCourseWithDetails(course.id); res.json({ success: true, data: courseWithDetails, }); } catch (error) { next(error); } } export async function createCourse(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const { title, slug, description, shortDescription, thumbnailUrl, previewVideoUrl, categoryId, level, tags, isFree, price, currency, requiresSubscription, minSubscriptionTier, durationMinutes, } = req.body; if (!title) { res.status(400).json({ success: false, error: { message: 'Course title is required', code: 'VALIDATION_ERROR' }, }); return; } const course = await courseService.createCourse({ title, slug, description, shortDescription, thumbnailUrl, previewVideoUrl, categoryId, level, tags, isFree, price, currency, requiresSubscription, minSubscriptionTier, durationMinutes, instructorId: userId, }); res.status(201).json({ success: true, data: course, }); } catch (error) { next(error); } } export async function updateCourse(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const updates = req.body; const course = await courseService.updateCourse(courseId, updates); if (!course) { res.status(404).json({ success: false, error: { message: 'Course not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, data: course, }); } catch (error) { next(error); } } export async function deleteCourse(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const deleted = await courseService.deleteCourse(courseId); if (!deleted) { res.status(404).json({ success: false, error: { message: 'Course not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, message: 'Course deleted successfully', }); } catch (error) { next(error); } } export async function publishCourse(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const course = await courseService.publishCourse(courseId); if (!course) { res.status(404).json({ success: false, error: { message: 'Course not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, data: course, message: 'Course published successfully', }); } catch (error) { next(error); } } export async function getPopularCourses(req: Request, res: Response, next: NextFunction): Promise { try { const limit = Math.min(parseInt(req.query.limit as string, 10) || 10, 50); const courses = await courseService.getPopularCourses(limit); res.json({ success: true, data: courses, }); } catch (error) { next(error); } } export async function getNewCourses(req: Request, res: Response, next: NextFunction): Promise { try { const limit = Math.min(parseInt(req.query.limit as string, 10) || 10, 50); const courses = await courseService.getNewCourses(limit); res.json({ success: true, data: courses, }); } catch (error) { next(error); } } // ============================================================================ // Modules // ============================================================================ export async function getCourseModules(req: Request, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const modules = await courseService.getCourseModules(courseId); res.json({ success: true, data: modules, }); } catch (error) { next(error); } } export async function createModule(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const { title, description, sortOrder, unlockAfterModuleId } = req.body; if (!title) { res.status(400).json({ success: false, error: { message: 'Module title is required', code: 'VALIDATION_ERROR' }, }); return; } const module = await courseService.createModule({ courseId, title, description, sortOrder, unlockAfterModuleId, }); res.status(201).json({ success: true, data: module, }); } catch (error) { next(error); } } export async function deleteModule(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { moduleId } = req.params; const deleted = await courseService.deleteModule(moduleId); if (!deleted) { res.status(404).json({ success: false, error: { message: 'Module not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, message: 'Module deleted successfully', }); } catch (error) { next(error); } } // ============================================================================ // Lessons // ============================================================================ export async function getModuleLessons(req: Request, res: Response, next: NextFunction): Promise { try { const { moduleId } = req.params; const lessons = await courseService.getModuleLessons(moduleId); res.json({ success: true, data: lessons, }); } catch (error) { next(error); } } export async function getLessonById(req: Request, res: Response, next: NextFunction): Promise { try { const { lessonId } = req.params; const lesson = await courseService.getLessonById(lessonId); if (!lesson) { res.status(404).json({ success: false, error: { message: 'Lesson not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, data: lesson, }); } catch (error) { next(error); } } export async function createLesson(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { moduleId } = req.params; const { courseId, title, slug, contentType, videoUrl, videoDurationSeconds, videoProvider, contentMarkdown, resources, sortOrder, isPreview, } = req.body; if (!title || !courseId) { res.status(400).json({ success: false, error: { message: 'Title and courseId are required', code: 'VALIDATION_ERROR' }, }); return; } const lesson = await courseService.createLesson({ moduleId, courseId, title, slug, contentType, videoUrl, videoDurationSeconds, videoProvider, contentMarkdown, resources, sortOrder, isPreview, }); res.status(201).json({ success: true, data: lesson, }); } catch (error) { next(error); } } export async function deleteLesson(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const { lessonId } = req.params; const deleted = await courseService.deleteLesson(lessonId); if (!deleted) { res.status(404).json({ success: false, error: { message: 'Lesson not found', code: 'NOT_FOUND' }, }); return; } res.json({ success: true, message: 'Lesson deleted successfully', }); } catch (error) { next(error); } } // ============================================================================ // Enrollments // ============================================================================ export async function getMyEnrollments(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const enrollments = await enrollmentService.getUserEnrollments(userId); res.json({ success: true, data: enrollments, }); } catch (error) { next(error); } } export async function enrollInCourse(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const { courseId } = req.params; // Check if course exists and is free or user has purchased const course = await courseService.getCourseById(courseId); if (!course) { res.status(404).json({ success: false, error: { message: 'Course not found', code: 'NOT_FOUND' }, }); return; } if (!course.isFree) { res.status(403).json({ success: false, error: { message: 'Payment required for this course', code: 'PAYMENT_REQUIRED' }, }); return; } const enrollment = await enrollmentService.createEnrollment({ userId, courseId, }); res.status(201).json({ success: true, data: enrollment, message: 'Successfully enrolled in course', }); } catch (error) { if ((error as Error).message === 'Already enrolled in this course') { res.status(409).json({ success: false, error: { message: 'Already enrolled in this course', code: 'ALREADY_ENROLLED' }, }); return; } next(error); } } export async function getEnrollmentStatus(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const { courseId } = req.params; const enrollment = await enrollmentService.getEnrollment(userId, courseId); const progress = enrollment ? await enrollmentService.getCourseProgress(userId, courseId) : []; res.json({ success: true, data: { enrolled: !!enrollment, enrollment, lessonProgress: progress, }, }); } catch (error) { next(error); } } // ============================================================================ // Progress // ============================================================================ export async function updateLessonProgress(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const { lessonId } = req.params; const { videoWatchedSeconds, videoCompleted, userNotes } = req.body; const progress = await enrollmentService.updateLessonProgress(userId, lessonId, { videoWatchedSeconds, videoCompleted, userNotes, }); res.json({ success: true, data: progress, }); } catch (error) { if ((error as Error).message === 'Not enrolled in this course') { res.status(403).json({ success: false, error: { message: 'You must be enrolled to track progress', code: 'NOT_ENROLLED' }, }); return; } next(error); } } export async function markLessonComplete(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const { lessonId } = req.params; const progress = await enrollmentService.markLessonComplete(userId, lessonId); res.json({ success: true, data: progress, message: 'Lesson marked as complete', }); } catch (error) { next(error); } } // ============================================================================ // User Stats // ============================================================================ export async function getMyLearningStats(req: AuthRequest, res: Response, next: NextFunction): Promise { try { const userId = req.user?.id; if (!userId) { res.status(401).json({ success: false, error: { message: 'Unauthorized', code: 'UNAUTHORIZED' }, }); return; } const stats = await enrollmentService.getUserLearningStats(userId); res.json({ success: true, data: stats, }); } catch (error) { next(error); } } export async function getCourseStats(req: Request, res: Response, next: NextFunction): Promise { try { const { courseId } = req.params; const stats = await enrollmentService.getCourseEnrollmentStats(courseId); res.json({ success: true, data: stats, }); } catch (error) { next(error); } }