const { ApiError } = require('../middleware/errorHandler'); const lessonQueries = require('../models/queries/lesson.queries'); const progressQueries = require('../models/queries/progress.queries'); const lessonLoader = require('../services/lessonLoader.service'); const scoringService = require('../services/scoring.service'); /** * Get lessons for an event (participant view) * GET /api/participant/event/:eventId/lessons */ const getEventLessons = async (req, res) => { const { eventId } = req.params; const participantId = req.participant.id; const lessons = await lessonQueries.getEventLessonsWithProgress(eventId, participantId); res.json({ success: true, data: lessons.map(lesson => ({ eventLessonId: lesson.event_lesson_id, lessonId: lesson.id, lessonKey: lesson.lesson_key, title: lesson.title, description: lesson.description, difficultyLevel: lesson.difficulty_level, estimatedDuration: lesson.estimated_duration, orderIndex: lesson.order_index, maxPoints: lesson.max_points, weight: lesson.weight, isRequired: lesson.is_required, isUnlocked: lesson.is_unlocked, progress: lesson.progress_id ? { status: lesson.status, score: lesson.score, attempts: lesson.attempts, startedAt: lesson.started_at, completedAt: lesson.completed_at } : null })) }); }; /** * Get lesson content * GET /api/participant/lesson/:eventLessonId */ const getLessonContent = async (req, res) => { const { eventLessonId } = req.params; const participantId = req.participant.id; // Get event lesson details const eventLesson = await lessonQueries.getEventLessonById(eventLessonId); if (!eventLesson) { throw new ApiError(404, 'Lesson not found'); } // Check if lesson is unlocked const isUnlocked = await progressQueries.isLessonUnlocked(participantId, eventLessonId); if (!isUnlocked) { throw new ApiError(403, 'This lesson is locked. Complete previous lessons first.'); } // Load lesson content from module const content = await lessonLoader.getLessonContent(eventLesson.lesson_key); // Get progress if exists const progress = await progressQueries.getLessonProgress(participantId, eventLessonId); res.json({ success: true, data: { eventLessonId, ...content, maxPoints: eventLesson.max_points, weight: eventLesson.weight, progress: progress ? { id: progress.id, status: progress.status, score: progress.score, currentStep: progress.current_step, attempts: progress.attempts } : null } }); }; /** * Start a lesson * POST /api/participant/lesson/:eventLessonId/start */ const startLesson = async (req, res) => { const { eventLessonId } = req.params; const participantId = req.participant.id; // Check if lesson is unlocked const isUnlocked = await progressQueries.isLessonUnlocked(participantId, eventLessonId); if (!isUnlocked) { throw new ApiError(403, 'This lesson is locked'); } // Start or resume progress const progress = await progressQueries.startLesson(participantId, eventLessonId); res.json({ success: true, message: 'Lesson started', data: { progressId: progress.id, status: progress.status, startedAt: progress.started_at } }); }; /** * Submit an answer * POST /api/participant/lesson/:eventLessonId/answer */ const submitAnswer = async (req, res) => { const { eventLessonId } = req.params; const { questionId, answer } = req.body; const participantId = req.participant.id; if (!questionId || answer === undefined) { throw new ApiError(400, 'Question ID and answer are required'); } // Get event lesson const eventLesson = await lessonQueries.getEventLessonById(eventLessonId); if (!eventLesson) { throw new ApiError(404, 'Lesson not found'); } // Get or create progress let progress = await progressQueries.getLessonProgress(participantId, eventLessonId); if (!progress) { // Auto-start lesson if not started progress = await progressQueries.startLesson(participantId, eventLessonId); } // Validate answer using lesson module const validation = await lessonLoader.validateAnswer( eventLesson.lesson_key, questionId, answer ); // Save answer to database await progressQueries.saveAnswer( progress.id, questionId, answer, validation.isCorrect, validation.pointsAwarded, validation.feedback ); // Update score const newScore = await progressQueries.updateScore(progress.id, validation.pointsAwarded); res.json({ success: true, data: { isCorrect: validation.isCorrect, isPartial: validation.isPartial || false, pointsAwarded: validation.pointsAwarded, feedback: validation.feedback, totalScore: newScore } }); }; /** * Complete a lesson * POST /api/participant/lesson/:eventLessonId/complete */ const completeLesson = async (req, res) => { const { eventLessonId } = req.params; const participantId = req.participant.id; // Get progress const progress = await progressQueries.getLessonProgress(participantId, eventLessonId); if (!progress) { throw new ApiError(404, 'Lesson progress not found. Start the lesson first.'); } if (progress.status === 'completed') { throw new ApiError(400, 'Lesson already completed'); } // Mark as completed const updated = await progressQueries.completeLesson(progress.id); // Calculate lesson score details const scoreDetails = await scoringService.calculateLessonScore(progress.id); // Check if passed const passed = await scoringService.checkLessonPassed(progress.id); res.json({ success: true, message: 'Lesson completed', data: { completedAt: updated.completed_at, finalScore: updated.score, maxPoints: scoreDetails.maxPoints, percentage: scoreDetails.percentage, passed } }); }; /** * Get interactive component data * GET /api/lessons/:lessonKey/interactive/:stepId */ const getInteractiveData = async (req, res) => { const { lessonKey, stepId } = req.params; const data = await lessonLoader.getInteractiveData(lessonKey, stepId); res.json({ success: true, data }); }; /** * Execute lesson-specific action (e.g., SQL query) * POST /api/lesson/:eventLessonId/action/:action */ const executeLessonAction = async (req, res) => { const { eventLessonId, action } = req.params; const participantId = req.participant.id; const actionData = req.body; // Get progress to ensure lesson is started const progress = await progressQueries.getLessonProgress(participantId, eventLessonId); if (!progress) { throw new ApiError(404, 'Lesson not started. Start the lesson first.'); } // Get event lesson details to find lesson key const eventLesson = await lessonQueries.getEventLessonById(eventLessonId); const lessonKey = eventLesson.lesson_key; // Load lesson module const lessonModule = await lessonLoader.loadLesson(lessonKey); // Execute action based on type let result; if (action === 'execute-query' && lessonModule.executeVulnerableQuery) { // SQL Injection demo const { searchTerm, mode } = actionData; if (mode === 'safe' && lessonModule.executeSafeQuery) { result = lessonModule.executeSafeQuery(searchTerm); } else { result = await lessonModule.executeVulnerableQuery(searchTerm, participantId, eventLessonId); } } else if (action === 'start-timer') { // Start timer for interactive step if (lessonModule.startTimer) { result = await lessonModule.startTimer(participantId, eventLessonId); } else if (lessonModule.startStepTimer) { const { stepId } = actionData; result = await lessonModule.startStepTimer(participantId, stepId, eventLessonId); } else { throw new ApiError(400, 'Timer not supported for this lesson'); } } else if (action === 'get-hint' && lessonModule.getHint) { // Request hint (with point deduction) result = await lessonModule.getHint(participantId, eventLessonId); } else if (action === 'test-xss' && lessonModule.testXSSPayload) { // XSS Deeplink demo const { payload, stepId } = actionData; result = await lessonModule.testXSSPayload(participantId, payload, stepId || 'xss-demo', eventLessonId); } else if (action === 'add-comment' && lessonModule.addComment) { // Forum Script Injection demo const { author, content, stepId } = actionData; result = await lessonModule.addComment(participantId, author, content, stepId || 'forum-demo', eventLessonId); } else if (action === 'test-password' && lessonModule.testPassword) { // Social Engineering Password demo const { password } = actionData; result = await lessonModule.testPassword(participantId, password, eventLessonId); } else if (action === 'fetch-profile' && lessonModule.fetchUserProfile) { // IDOR demo const { userId } = actionData; result = await lessonModule.fetchUserProfile(userId, participantId, eventLessonId); } else { throw new ApiError(400, `Unsupported action: ${action}`); } res.json({ success: true, data: result }); }; module.exports = { getEventLessons, getLessonContent, startLesson, submitAnswer, completeLesson, getInteractiveData, executeLessonAction };