Files
mathquest/server.js
2026-05-13 22:49:40 +02:00

232 lines
7.1 KiB
JavaScript

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