add diffent lessions
This commit is contained in:
182
server.js
182
server.js
@@ -6,19 +6,17 @@ const path = require("path");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const CASES_DIR = path.join(__dirname, "cases");
|
||||
const LESSONS_DIR = path.join(__dirname, "lessons");
|
||||
const CLASSES_DIR = path.join(__dirname, "classes");
|
||||
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(
|
||||
@@ -28,7 +26,6 @@ app.get("/config.js", (_req, res) => {
|
||||
|
||||
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 ")) {
|
||||
@@ -37,21 +34,14 @@ async function requireAuth(req, res, next) {
|
||||
|
||||
try {
|
||||
const response = await fetch(`${process.env.SUPABASE_URL}/auth/v1/user`, {
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
apikey: process.env.SUPABASE_ANON_KEY,
|
||||
},
|
||||
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." });
|
||||
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) {
|
||||
@@ -60,43 +50,52 @@ async function requireAuth(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
function loadClasses() {
|
||||
if (!fs.existsSync(CLASSES_DIR)) return {};
|
||||
const classes = {};
|
||||
fs.readdirSync(CLASSES_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,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
const cls = JSON.parse(fs.readFileSync(path.join(CLASSES_DIR, file), "utf8"));
|
||||
classes[cls.id] = cls;
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Laden von", file, ":", err.message);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {};
|
||||
@@ -114,24 +113,100 @@ function saveProgress(userId, progress) {
|
||||
);
|
||||
}
|
||||
|
||||
// API: Get all tasks merged with user's progress
|
||||
app.get("/api/tasks", requireAuth, (req, res) => {
|
||||
const tasks = loadCaseTasks();
|
||||
// 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] })) });
|
||||
});
|
||||
|
||||
// API: Check answer and save result for the current user
|
||||
// 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" });
|
||||
return res.status(400).json({ error: "taskId und answer sind erforderlich" });
|
||||
}
|
||||
|
||||
const task = loadCaseTasks().find((t) => t.id === taskId);
|
||||
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" });
|
||||
}
|
||||
@@ -140,7 +215,6 @@ app.post("/api/check-answer", requireAuth, (req, res) => {
|
||||
const correctAnswer = parseInt(task.answer);
|
||||
const isCorrect = userAnswer === correctAnswer;
|
||||
|
||||
const progress = loadProgress(req.userId);
|
||||
progress[taskId] = {
|
||||
attempts: (progress[taskId]?.attempts || 0) + 1,
|
||||
userAnswer,
|
||||
@@ -149,11 +223,7 @@ app.post("/api/check-answer", requireAuth, (req, res) => {
|
||||
};
|
||||
saveProgress(req.userId, progress);
|
||||
|
||||
res.json({
|
||||
correct: isCorrect,
|
||||
correctAnswer,
|
||||
points: isCorrect ? task.points : 0,
|
||||
});
|
||||
res.json({ correct: isCorrect, correctAnswer, points: isCorrect ? task.points : 0 });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
|
||||
Reference in New Issue
Block a user