trading-platform/apps/backend/src/modules/education/controllers/education.controller.ts

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);
}
}