require("dotenv").config(); const express = require("express"); const cors = require("cors"); const fs = require("fs"); const path = require("path"); const app = express(); const PORT = process.env.PORT || 3000; const LESSONS_DIR = path.join(__dirname, "lessons"); const CLASSES_DIR = path.join(__dirname, "classes"); const PROGRESS_DIR = path.join(__dirname, "data", "progress"); if (!fs.existsSync(PROGRESS_DIR)) { fs.mkdirSync(PROGRESS_DIR, { recursive: true }); } app.use(cors()); app.use(express.json()); app.get("/config.js", (_req, res) => { res.type("application/javascript"); res.send( `window.SUPABASE_URL = '${process.env.SUPABASE_URL}'; window.SUPABASE_ANON_KEY = '${process.env.SUPABASE_ANON_KEY}';`, ); }); app.use(express.static("public")); async function requireAuth(req, res, next) { const auth = req.headers.authorization; if (!auth || !auth.startsWith("Bearer ")) { return res.status(401).json({ error: "Nicht angemeldet" }); } try { const response = await fetch(`${process.env.SUPABASE_URL}/auth/v1/user`, { headers: { Authorization: auth, apikey: process.env.SUPABASE_ANON_KEY }, }); if (!response.ok) { return res.status(401).json({ error: "Sitzung abgelaufen. Bitte erneut anmelden." }); } const user = await response.json(); req.userId = user.id; next(); } catch (err) { console.error("[auth] Supabase request failed:", err.message); res.status(401).json({ error: "Authentifizierung fehlgeschlagen." }); } } function loadClasses() { if (!fs.existsSync(CLASSES_DIR)) return {}; const classes = {}; fs.readdirSync(CLASSES_DIR).forEach((file) => { if (!file.endsWith(".json")) return; try { const cls = JSON.parse(fs.readFileSync(path.join(CLASSES_DIR, file), "utf8")); classes[cls.id] = cls; } catch (err) { console.error("Fehler beim Laden von Klasse", file, ":", err.message); } }); return classes; } function loadLessonsForClass(classId) { const classes = loadClasses(); const cls = classes[classId]; if (!cls) return []; const allTasks = []; (cls.lessons || []).forEach((lessonId) => { const file = path.join(LESSONS_DIR, `${lessonId}.json`); if (!fs.existsSync(file)) return; try { const lessonData = JSON.parse(fs.readFileSync(file, "utf8")); if (Array.isArray(lessonData)) { lessonData.forEach(({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => { allTasks.push(task); }); } else if (lessonData.tasks) { lessonData.tasks.forEach(({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => { allTasks.push({ ...task, chapter: lessonData.chapter, chapterDescription: lessonData.description, }); }); } } catch (err) { console.error("Fehler beim Laden von Lektion", lessonId, ":", err.message); } }); return allTasks; } function loadProgress(userId) { const file = path.join(PROGRESS_DIR, `${userId}.json`); if (!fs.existsSync(file)) return {}; try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return {}; } } function saveProgress(userId, progress) { fs.writeFileSync( path.join(PROGRESS_DIR, `${userId}.json`), JSON.stringify(progress, null, 2), ); } // GET /api/classes — list available classes app.get("/api/classes", requireAuth, (_req, res) => { const classes = loadClasses(); res.json({ classes: Object.values(classes).map(({ id, name, icon, description, theme }) => ({ id, name, icon, description, theme, })), }); }); // GET /api/me — user's active class and completed classes app.get("/api/me", requireAuth, (req, res) => { const progress = loadProgress(req.userId); const meta = progress._meta || { activeClass: null, completedClasses: [] }; res.json(meta); }); // POST /api/set-class — set user's active class app.post("/api/set-class", requireAuth, (req, res) => { const { classId } = req.body; const classes = loadClasses(); if (!classId || !classes[classId]) { return res.status(400).json({ error: "Unbekannte Klasse" }); } const progress = loadProgress(req.userId); if (!progress._meta) progress._meta = { activeClass: null, completedClasses: [] }; progress._meta.activeClass = classId; saveProgress(req.userId, progress); const { id, name, icon, theme } = classes[classId]; res.json({ ok: true, class: { id, name, icon, theme } }); }); // POST /api/complete-class — mark active class as done and clear it app.post("/api/complete-class", requireAuth, (req, res) => { const progress = loadProgress(req.userId); if (!progress._meta) progress._meta = { activeClass: null, completedClasses: [] }; const { activeClass } = progress._meta; if (activeClass && !progress._meta.completedClasses.includes(activeClass)) { progress._meta.completedClasses.push(activeClass); } progress._meta.activeClass = null; saveProgress(req.userId, progress); res.json({ ok: true }); }); // POST /api/reset-class-progress — delete all task progress for the active class app.post("/api/reset-class-progress", requireAuth, (req, res) => { const progress = loadProgress(req.userId); const activeClass = progress._meta?.activeClass; if (!activeClass) { return res.status(400).json({ error: "Keine Klasse ausgewählt" }); } const taskIds = new Set(loadLessonsForClass(activeClass).map((t) => t.id)); Object.keys(progress).forEach((key) => { if (taskIds.has(key)) delete progress[key]; }); saveProgress(req.userId, progress); res.json({ ok: true }); }); // GET /api/tasks — tasks for user's active class app.get("/api/tasks", requireAuth, (req, res) => { const progress = loadProgress(req.userId); const activeClass = progress._meta?.activeClass; if (!activeClass) { return res.status(400).json({ error: "Keine Klasse ausgewählt", noClass: true }); } const tasks = loadLessonsForClass(activeClass); res.json({ tasks: tasks.map((task) => ({ ...task, ...progress[task.id] })) }); }); // POST /api/check-answer app.post("/api/check-answer", requireAuth, (req, res) => { const { taskId, answer } = req.body; if (!taskId || answer === undefined) { return res.status(400).json({ error: "taskId und answer sind erforderlich" }); } const progress = loadProgress(req.userId); const activeClass = progress._meta?.activeClass; if (!activeClass) { return res.status(400).json({ error: "Keine Klasse ausgewählt" }); } const task = loadLessonsForClass(activeClass).find((t) => t.id === taskId); if (!task) { return res.status(404).json({ error: "Aufgabe nicht gefunden" }); } const userAnswer = parseInt(answer); const correctAnswer = parseInt(task.answer); const isCorrect = userAnswer === correctAnswer; progress[taskId] = { attempts: (progress[taskId]?.attempts || 0) + 1, userAnswer, isCorrect, answerTimestamp: new Date().toISOString(), }; saveProgress(req.userId, progress); res.json({ correct: isCorrect, correctAnswer, points: isCorrect ? task.points : 0 }); }); app.listen(PORT, () => { console.log(`MathQuest Server läuft auf http://localhost:${PORT}`); });