317 lines
9.2 KiB
JavaScript
317 lines
9.2 KiB
JavaScript
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
|
|
};
|