676 lines
17 KiB
TypeScript
676 lines
17 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
try {
|
|
const { courseId } = req.params;
|
|
|
|
const stats = await enrollmentService.getCourseEnrollmentStats(courseId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: stats,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|