implement supabase

This commit is contained in:
Lukas Cremer
2026-05-13 21:32:49 +02:00
parent a49130973a
commit 56f41d9f8d
6 changed files with 528 additions and 411 deletions

303
server.js
View File

@@ -1,196 +1,161 @@
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
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 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());
app.use(express.static('public'));
// Lade alle Aufgaben aus dem cases-Ordner
function loadTasks() {
try {
// Erstelle cases-Ordner falls er nicht existiert
if (!fs.existsSync(CASES_DIR)) {
fs.mkdirSync(CASES_DIR, { recursive: true });
console.log('Cases-Ordner erstellt:', CASES_DIR);
return [];
}
const allTasks = [];
const files = fs.readdirSync(CASES_DIR);
files.forEach(file => {
if (file.endsWith('.json')) {
const filePath = path.join(CASES_DIR, file);
try {
const data = fs.readFileSync(filePath, 'utf8');
if (!data || !data.trim()) {
console.warn('Leere Datei übersprungen:', file);
return;
}
const caseData = JSON.parse(data);
// Unterstütze sowohl alte Struktur (Array) als auch neue Struktur (Objekt)
if (Array.isArray(caseData)) {
allTasks.push(...caseData);
} else if (caseData.tasks && Array.isArray(caseData.tasks)) {
caseData.tasks.forEach(task => {
const taskWithChapter = {
...task,
chapter: caseData.chapter,
chapterDescription: caseData.description
};
allTasks.push(taskWithChapter);
});
}
} catch (err) {
console.error('Fehler beim Laden von', file, ':', err.message);
}
}
});
return allTasks;
} catch (error) {
console.error('Fehler beim Laden der Aufgaben:', error);
return [];
}
}
// Finde die Datei, die eine bestimmte Task-ID enthält
function findTaskFile(taskId) {
try {
if (!fs.existsSync(CASES_DIR)) {
return null;
}
const files = fs.readdirSync(CASES_DIR);
for (const file of files) {
if (file.endsWith('.json')) {
const filePath = path.join(CASES_DIR, file);
const data = fs.readFileSync(filePath, 'utf8');
const caseData = JSON.parse(data);
// Unterstütze sowohl alte Struktur (Array) als auch neue Struktur (Objekt)
let tasks = [];
if (Array.isArray(caseData)) {
tasks = caseData;
} else if (caseData.tasks && Array.isArray(caseData.tasks)) {
tasks = caseData.tasks;
}
if (tasks.some(task => task.id === taskId)) {
return { filePath, caseData };
}
}
}
return null;
} catch (error) {
console.error('Fehler beim Finden der Task-Datei:', error);
return null;
}
}
// Speichere Aufgaben in der entsprechenden Datei
function saveTask(taskId, updatedTask) {
try {
const fileInfo = findTaskFile(taskId);
if (!fileInfo) {
throw new Error(`Datei für Task ${taskId} nicht gefunden`);
}
const { filePath, caseData } = fileInfo;
// Entferne chapter und chapterDescription aus der Task (werden beim Laden wieder hinzugefügt)
const { chapter, chapterDescription, ...taskWithoutChapter } = updatedTask;
// Unterstütze sowohl alte Struktur (Array) als auch neue Struktur (Objekt)
if (Array.isArray(caseData)) {
// Alte Struktur: direktes Array
const taskIndex = caseData.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task ${taskId} nicht in Datei gefunden`);
}
caseData[taskIndex] = updatedTask;
fs.writeFileSync(filePath, JSON.stringify(caseData, null, 2));
} else if (caseData.tasks && Array.isArray(caseData.tasks)) {
// Neue Struktur: Objekt mit chapter, description, tasks
const taskIndex = caseData.tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task ${taskId} nicht in Datei gefunden`);
}
caseData.tasks[taskIndex] = taskWithoutChapter;
fs.writeFileSync(filePath, JSON.stringify(caseData, null, 2));
} else {
throw new Error('Ungültige Dateistruktur');
}
return true;
} catch (error) {
console.error('Fehler beim Speichern der Aufgabe:', error);
return false;
}
}
// API: Hole alle Aufgaben
app.get('/api/tasks', (req, res) => {
const tasks = loadTasks();
res.json({ tasks });
// 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"));
// API: Prüfe Antwort
app.post('/api/check-answer', (req, res) => {
// 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' });
return res
.status(400)
.json({ error: "taskId und answer sind erforderlich" });
}
const tasks = loadTasks();
const task = tasks.find(t => t.id === taskId);
const task = loadCaseTasks().find((t) => t.id === taskId);
if (!task) {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
return res.status(404).json({ error: "Aufgabe nicht gefunden" });
}
const userAnswer = parseInt(answer);
const correctAnswer = parseInt(task.answer);
const isCorrect = userAnswer === correctAnswer;
// Initialisiere attempts Counter falls nicht vorhanden
if (task.attempts === undefined) {
task.attempts = 0;
}
// Erhöhe Versuchszähler bei jeder Antwortprüfung
task.attempts += 1;
// Speichere Antwort, isCorrect und Timestamp in der Task
task.userAnswer = userAnswer;
task.isCorrect = isCorrect;
task.answerTimestamp = new Date().toISOString();
// Speichere die aktualisierte Task in der entsprechenden Datei
if (!saveTask(taskId, task)) {
return res.status(500).json({ error: 'Fehler beim Speichern der Antwort' });
}
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: correctAnswer,
points: isCorrect ? task.points : 0
correctAnswer,
points: isCorrect ? task.points : 0,
});
});
// Starte Server
app.listen(PORT, () => {
console.log(`MathQuest Server läuft auf http://localhost:${PORT}`);
});