setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); } catch (PDOException $e) { http_response_code(500); echo json_encode(['error' => 'Datenbankverbindung fehlgeschlagen']); exit; } // 3. TABELLEN INITIALISIERUNG $pdo->exec(" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, salt TEXT NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT 'student', xp INTEGER DEFAULT 0, level INTEGER DEFAULT 1, streak_days INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS chapter_progress ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, chapter_id TEXT NOT NULL, code_draft TEXT, subgoals_json TEXT, is_solved INTEGER DEFAULT 0, solved_at TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, chapter_id) ); CREATE TABLE IF NOT EXISTS classrooms ( id INTEGER PRIMARY KEY AUTOINCREMENT, teacher_id INTEGER NOT NULL, name TEXT NOT NULL, invite_code TEXT UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS class_enrollments ( id INTEGER PRIMARY KEY AUTOINCREMENT, classroom_id INTEGER NOT NULL, user_id INTEGER NOT NULL, joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(classroom_id, user_id) ); "); // 4. AUTH HELPER MIT SICHERER HMAC-VERIFIKATION function get_auth_user(string $secret_key): ?array { $auth = ''; if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $auth = $_SERVER['HTTP_AUTHORIZATION']; } elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { $auth = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } elseif (function_exists('getallheaders')) { $headers = getallheaders(); $auth = $headers['Authorization'] ?? $headers['authorization'] ?? ''; } if (strpos($auth, 'Bearer ') === 0) { $token = substr($auth, 7); $parts = explode('.', $token); if (count($parts) === 2) { $payload_raw = base64_decode($parts[0], true); if ($payload_raw === false) return null; $expected_sig = hash_hmac('sha256', $parts[0], $secret_key); if (!hash_equals($expected_sig, $parts[1])) { return null; // Ungültige Signatur } $payload = json_decode($payload_raw, true); if ($payload && isset($payload['uid']) && isset($payload['exp'])) { if ($payload['exp'] < time()) { return null; // Abgelaufen } return $payload; } } } return null; } function create_token(int $uid, string $email, string $role, string $secret_key): string { $payload_data = [ 'uid' => $uid, 'email' => $email, 'role' => $role, 'exp' => time() + (86400 * 30) ]; $payload = base64_encode((string)json_encode($payload_data)); $sig = hash_hmac('sha256', $payload, $secret_key); return $payload . '.' . $sig; } $path = $_SERVER['PATH_INFO'] ?? $_GET['action'] ?? ''; $raw_input = file_get_contents('php://input'); $body = $raw_input ? (json_decode($raw_input, true) ?? []) : []; // 1. REGISTER if ($path === '/auth/register') { $email = strtolower(trim((string)($body['email'] ?? ''))); $password = (string)($body['password'] ?? ''); $name = trim((string)($body['name'] ?? '')); $role_input = $body['role'] ?? 'solo'; $role = in_array($role_input, ['student', 'teacher', 'solo'], true) ? $role_input : 'solo'; if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($password) < 6 || strlen($name) < 1) { http_response_code(400); echo json_encode(['error' => 'Ungültige Eingabedaten']); exit; } $salt = bin2hex(random_bytes(16)); $hash = hash_pbkdf2('sha256', $password, $salt, 100000); try { $stmt = $pdo->prepare("INSERT INTO users (email, password_hash, salt, name, role) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$email, $hash, $salt, $name, $role]); $uid = (int)$pdo->lastInsertId(); $token = create_token($uid, $email, $role, $secret_key); http_response_code(201); echo json_encode(['token' => $token, 'user' => ['id' => $uid, 'email' => $email, 'name' => $name, 'role' => $role, 'xp' => 0, 'level' => 1]]); } catch (PDOException $e) { http_response_code(409); echo json_encode(['error' => 'E-Mail existiert bereits']); } exit; } // 2. LOGIN (mit Constant-Time Dummy PBKDF2) if ($path === '/auth/login') { $email = strtolower(trim((string)($body['email'] ?? ''))); $password = (string)($body['password'] ?? ''); $stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?"); $stmt->execute([$email]); $user = $stmt->fetch(); if ($user) { $hash = hash_pbkdf2('sha256', $password, $user['salt'], 100000); if (hash_equals($hash, $user['password_hash'])) { $token = create_token((int)$user['id'], $user['email'], $user['role'], $secret_key); echo json_encode([ 'token' => $token, 'user' => [ 'id' => (int)$user['id'], 'email' => $user['email'], 'name' => $user['name'], 'role' => $user['role'], 'xp' => (int)$user['xp'], 'level' => (int)$user['level'] ] ]); exit; } } else { hash_pbkdf2('sha256', $password, 'dummy_salt_fixed', 100000); } http_response_code(401); echo json_encode(['error' => 'Ungültige Anmeldedaten']); exit; } // 3. PROGRESS SAVE if ($path === '/progress/save') { $user = get_auth_user($secret_key); if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; } $chapter_id = substr(trim((string)($body['chapter_id'] ?? '')), 0, 120); if (!$chapter_id || strpos($chapter_id, '..') !== false || strpos($chapter_id, '\\') !== false) { http_response_code(400); echo json_encode(['error' => 'Ungültiges chapter_id']); exit; } $code_draft = substr((string)($body['code_draft'] ?? ''), 0, 100000); $stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, code_draft) VALUES (?, ?, ?) ON CONFLICT(user_id, chapter_id) DO UPDATE SET code_draft = excluded.code_draft, updated_at = CURRENT_TIMESTAMP"); $stmt->execute([$user['uid'], $chapter_id, $code_draft]); echo json_encode(['status' => 'saved']); exit; } // 4. PROGRESS SOLVE (mit Anti-Cheat Cap) if ($path === '/progress/solve') { $user = get_auth_user($secret_key); if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; } $chapter_id = substr(trim((string)($body['chapter_id'] ?? '')), 0, 120); $xp_reward = min(max(0, (int)($body['xp_reward'] ?? 100)), 100); $stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, is_solved, solved_at) VALUES (?, ?, 1, CURRENT_TIMESTAMP) ON CONFLICT(user_id, chapter_id) DO UPDATE SET is_solved = 1, solved_at = CURRENT_TIMESTAMP"); $stmt->execute([$user['uid'], $chapter_id]); $stmt = $pdo->prepare("UPDATE users SET xp = xp + ?, level = ((xp + ?) / 100) + 1 WHERE id = ?"); $stmt->execute([$xp_reward, $xp_reward, $user['uid']]); echo json_encode(['status' => 'solved', 'earned_xp' => $xp_reward]); exit; } http_response_code(404); echo json_encode(['error' => 'Endpunkt nicht gefunden']);