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 CASES_DIR = path.join(__dirname, "cases"); const PROGRESS_DIR = path.join(__dirname, "data", "progress"); // Ensure progress directory exists if (!fs.existsSync(PROGRESS_DIR)) { fs.mkdirSync(PROGRESS_DIR, { recursive: true }); } // Middleware app.use(cors()); app.use(express.json()); // Expose public Supabase config to the frontend 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")); // Verify token by asking Supabase directly — works regardless of JWT algorithm 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) { console.error("[auth] Supabase rejected token, status:", response.status); return res .status(401) .json({ error: "Sitzung abgelaufen. Bitte erneut anmelden." }); } const user = await response.json(); console.log("[auth] OK, userId:", user.id); req.userId = user.id; next(); } catch (err) { console.error("[auth] Supabase request failed:", err.message); res.status(401).json({ error: "Authentifizierung fehlgeschlagen." }); } } // Load task definitions from case files (no user progress mixed in) function loadCaseTasks() { if (!fs.existsSync(CASES_DIR)) return []; const allTasks = []; fs.readdirSync(CASES_DIR).forEach((file) => { if (!file.endsWith(".json")) return; try { const caseData = JSON.parse( fs.readFileSync(path.join(CASES_DIR, file), "utf8"), ); if (Array.isArray(caseData)) { caseData.forEach( ({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => { allTasks.push(task); }, ); } else if (caseData.tasks) { caseData.tasks.forEach( ({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => { allTasks.push({ ...task, chapter: caseData.chapter, chapterDescription: caseData.description, }); }, ); } } catch (err) { console.error("Fehler beim Laden von", file, ":", err.message); } }); return allTasks; } // Load/save per-user progress from data/progress/{userId}.json 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), ); } // API: Get all tasks merged with user's progress app.get("/api/tasks", requireAuth, (req, res) => { const tasks = loadCaseTasks(); const progress = loadProgress(req.userId); res.json({ tasks: tasks.map((task) => ({ ...task, ...progress[task.id] })) }); }); // API: Check answer and save result for the current user 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 task = loadCaseTasks().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; const progress = loadProgress(req.userId); 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}`); });