const router = require('express').Router(); const path = require('path'); const fs = require('fs'); const timer = (time = 300) => (req, res, next) => setTimeout(next, time); // Helper functions const loadJSON = (filename) => { const filePath = path.join(__dirname, 'data', filename); const data = fs.readFileSync(filePath, 'utf8'); return JSON.parse(data); }; const respond = (res, data) => { res.json({ error: null, data }); }; const respondError = (res, message, statusCode = 400) => { res.status(statusCode).json({ error: { message }, data: null }); }; // In-memory storage (resets on server restart) let tasksCache = null; let chainsCache = null; let usersCache = null; let submissionsCache = null; let statsCache = null; const getTasks = () => { if (!tasksCache) tasksCache = loadJSON('tasks.json'); return tasksCache; }; const getChains = () => { if (!chainsCache) chainsCache = loadJSON('chains.json'); return chainsCache; }; const getUsers = () => { if (!usersCache) usersCache = loadJSON('users.json'); return usersCache; }; const getSubmissions = () => { if (!submissionsCache) submissionsCache = loadJSON('submissions.json'); return submissionsCache; }; const getStats = () => { if (!statsCache) statsCache = loadJSON('stats.json'); return statsCache; }; router.use(timer()); // ============= TASKS ============= // GET /api/challenge/tasks router.get('/challenge/tasks', (req, res) => { const tasks = getTasks(); respond(res, tasks); }); // GET /api/challenge/task/:id router.get('/challenge/task/:id', (req, res) => { const tasks = getTasks(); const task = tasks.find(t => t.id === req.params.id); if (!task) { return respondError(res, 'Task not found', 404); } respond(res, task); }); // POST /api/challenge/task router.post('/challenge/task', (req, res) => { const { title, description, hiddenInstructions } = req.body; if (!title || !description) { return respondError(res, 'Title and description are required'); } const tasks = getTasks(); const newTask = { _id: `task_${Date.now()}`, id: `task_${Date.now()}`, title, description, hiddenInstructions: hiddenInstructions || undefined, creator: { sub: 'teacher-123', preferred_username: 'current_teacher', email: 'teacher@example.com' }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; tasks.push(newTask); // Update stats const stats = getStats(); stats.tasks = tasks.length; respond(res, newTask); }); // PUT /api/challenge/task/:id router.put('/challenge/task/:id', (req, res) => { const tasks = getTasks(); const taskIndex = tasks.findIndex(t => t.id === req.params.id); if (taskIndex === -1) { return respondError(res, 'Task not found', 404); } const { title, description, hiddenInstructions } = req.body; const task = tasks[taskIndex]; if (title) task.title = title; if (description) task.description = description; if (hiddenInstructions !== undefined) { task.hiddenInstructions = hiddenInstructions || undefined; } task.updatedAt = new Date().toISOString(); respond(res, task); }); // DELETE /api/challenge/task/:id router.delete('/challenge/task/:id', (req, res) => { const tasks = getTasks(); const taskIndex = tasks.findIndex(t => t.id === req.params.id); if (taskIndex === -1) { return respondError(res, 'Task not found', 404); } tasks.splice(taskIndex, 1); // Update stats const stats = getStats(); stats.tasks = tasks.length; respond(res, { success: true }); }); // ============= CHAINS ============= // GET /api/challenge/chains router.get('/challenge/chains', (req, res) => { const chains = getChains(); respond(res, chains); }); // GET /api/challenge/chain/:id router.get('/challenge/chain/:id', (req, res) => { const chains = getChains(); const chain = chains.find(c => c.id === req.params.id); if (!chain) { return respondError(res, 'Chain not found', 404); } respond(res, chain); }); // POST /api/challenge/chain router.post('/challenge/chain', (req, res) => { const { name, tasks } = req.body; if (!name || !tasks || !Array.isArray(tasks)) { return respondError(res, 'Name and tasks array are required'); } const chains = getChains(); const allTasks = getTasks(); // Populate tasks const populatedTasks = tasks.map(taskId => { const task = allTasks.find(t => t.id === taskId); return task ? { _id: task._id, id: task.id, title: task.title, description: task.description, createdAt: task.createdAt, updatedAt: task.updatedAt } : null; }).filter(t => t !== null); const newChain = { _id: `chain_${Date.now()}`, id: `chain_${Date.now()}`, name, tasks: populatedTasks, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; chains.push(newChain); // Update stats const stats = getStats(); stats.chains = chains.length; respond(res, newChain); }); // PUT /api/challenge/chain/:id router.put('/challenge/chain/:id', (req, res) => { const chains = getChains(); const chainIndex = chains.findIndex(c => c.id === req.params.id); if (chainIndex === -1) { return respondError(res, 'Chain not found', 404); } const { name, tasks } = req.body; const chain = chains[chainIndex]; if (name) chain.name = name; if (tasks && Array.isArray(tasks)) { const allTasks = getTasks(); const populatedTasks = tasks.map(taskId => { const task = allTasks.find(t => t.id === taskId); return task ? { _id: task._id, id: task.id, title: task.title, description: task.description, createdAt: task.createdAt, updatedAt: task.updatedAt } : null; }).filter(t => t !== null); chain.tasks = populatedTasks; } chain.updatedAt = new Date().toISOString(); respond(res, chain); }); // DELETE /api/challenge/chain/:id router.delete('/challenge/chain/:id', (req, res) => { const chains = getChains(); const chainIndex = chains.findIndex(c => c.id === req.params.id); if (chainIndex === -1) { return respondError(res, 'Chain not found', 404); } chains.splice(chainIndex, 1); // Update stats const stats = getStats(); stats.chains = chains.length; respond(res, { success: true }); }); // ============= USERS ============= // GET /api/challenge/users router.get('/challenge/users', (req, res) => { const users = getUsers(); respond(res, users); }); // ============= STATS ============= // GET /api/challenge/stats router.get('/challenge/stats', (req, res) => { const stats = getStats(); respond(res, stats); }); // GET /api/challenge/user/:userId/stats router.get('/challenge/user/:userId/stats', (req, res) => { const users = getUsers(); const submissions = getSubmissions(); const chains = getChains(); const user = users.find(u => u.id === req.params.userId); if (!user) { return respondError(res, 'User not found', 404); } const userSubmissions = submissions.filter(s => s.user.id === req.params.userId); // Calculate stats const completedTasks = new Set(); const taskStats = {}; userSubmissions.forEach(sub => { const taskId = sub.task.id; if (!taskStats[taskId]) { taskStats[taskId] = { taskId: taskId, taskTitle: sub.task.title, attempts: [], totalAttempts: 0, status: 'not_attempted', lastAttemptAt: null }; } taskStats[taskId].attempts.push({ attemptNumber: sub.attemptNumber, status: sub.status, submittedAt: sub.submittedAt, checkedAt: sub.checkedAt, feedback: sub.feedback }); taskStats[taskId].totalAttempts++; taskStats[taskId].status = sub.status; taskStats[taskId].lastAttemptAt = sub.submittedAt; if (sub.status === 'accepted') { completedTasks.add(taskId); } }); const taskStatsArray = Object.values(taskStats); // Chain stats const chainStats = chains.map(chain => { const completedInChain = chain.tasks.filter(t => completedTasks.has(t.id)).length; return { chainId: chain.id, chainName: chain.name, totalTasks: chain.tasks.length, completedTasks: completedInChain, progress: chain.tasks.length > 0 ? (completedInChain / chain.tasks.length * 100) : 0 }; }); const totalCheckTime = userSubmissions .filter(s => s.checkedAt) .reduce((sum, s) => { const submitted = new Date(s.submittedAt).getTime(); const checked = new Date(s.checkedAt).getTime(); return sum + (checked - submitted); }, 0); const userStats = { totalTasksAttempted: taskStatsArray.length, completedTasks: completedTasks.size, inProgressTasks: taskStatsArray.filter(t => t.status === 'in_progress').length, needsRevisionTasks: taskStatsArray.filter(t => t.status === 'needs_revision').length, totalSubmissions: userSubmissions.length, averageCheckTimeMs: userSubmissions.length > 0 ? totalCheckTime / userSubmissions.length : 0, taskStats: taskStatsArray, chainStats: chainStats }; respond(res, userStats); }); // ============= SUBMISSIONS ============= // GET /api/challenge/submissions router.get('/challenge/submissions', (req, res) => { const submissions = getSubmissions(); respond(res, submissions); }); // GET /api/challenge/user/:userId/submissions router.get('/challenge/user/:userId/submissions', (req, res) => { const submissions = getSubmissions(); const taskId = req.query.taskId; let filtered = submissions.filter(s => s.user.id === req.params.userId); if (taskId) { filtered = filtered.filter(s => s.task.id === taskId); } respond(res, filtered); }); module.exports = router;