162 lines
4.5 KiB
JavaScript
162 lines
4.5 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 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}`);
|
|
});
|