medienkompetenz-lernplattform/backend/src/controllers/lesson.controller.js
Marius Rometsch a439873394 Add lessons
2026-02-08 19:47:21 +01:00

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