Review-Batch P0: Sicherheit + Schueler-Ergebnisse + Assessments-Aufblaehung
Sicherheit: - progress.php: IDOR geschlossen (student_id-Zweig verlangt Lehrer-Login + Klassenbesitz) - licenses.php: ?admin=1 und generate nur noch fuer echten Admin; Collision-Retry mit Cap - .htaccess: .env/.sh/.sql/.bak Deny (Defense-in-Depth) Schueler-Ergebnisse (kritisch): - sim-metrics.js + .ivm-CSS in schueler.html; renderOwnRunCard-Fallback zeigt jetzt Sterne + grafische Kennzahlen fuer die 9 bisher leeren Sims - sim-metrics.js: klima final_budget als "Mio EUR" statt "EUR" (Faktor 10^6) Skalierung: - assessment.php: Zwischen-Pings (phase running/started) nicht mehr persistiert - heli: 30-s-assessmentPing entfernt (Live-Zustand laeuft ueber Heartbeat) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,16 @@ if (!defined('GGS_MAX_SESSION_MS')) define('GGS_MAX_SESSION_MS', 7200000);
|
||||
$rawDuration = (int)($body['duration'] ?? 0);
|
||||
$durationMs = max(0, min($rawDuration, GGS_MAX_SESSION_MS));
|
||||
|
||||
// Zwischen-Telemetrie eines laufenden Durchgangs NICHT in assessments persistieren.
|
||||
// Der Live-Zustand für das Lehrer-Cockpit kommt über den Heartbeat (live_sessions);
|
||||
// diese 30-s-Pings (phase 'running'/'started') haben früher die Tabelle zugemüllt
|
||||
// (~96 % der Zeilen). Nur echte Abschlüsse/Ergebnisse werden gespeichert.
|
||||
$pl = $body['processLog'] ?? [];
|
||||
$phase = (is_array($pl) && isset($pl['phase'])) ? $pl['phase'] : null;
|
||||
if ($phase === 'running' || $phase === 'started') {
|
||||
Response::ok(['skipped' => true]);
|
||||
}
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO assessments (session_id, sim_id, class_id, process_log, predictions, results, reflections, duration_ms, completed_phases)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
|
||||
@@ -15,8 +15,7 @@ if ($method === 'GET') {
|
||||
if (isset($_GET['admin']) && $_GET['admin'] === '1') {
|
||||
Session::start();
|
||||
$adminId = $_SESSION['admin_id'] ?? null;
|
||||
$teacherId = Session::teacherId();
|
||||
if (!$adminId && !$teacherId) {
|
||||
if (!$adminId) {
|
||||
Response::error('Nicht autorisiert', 401);
|
||||
}
|
||||
|
||||
@@ -169,16 +168,18 @@ if ($method === 'POST') {
|
||||
Response::ok(['licenseId' => (int)$license['id']]);
|
||||
}
|
||||
|
||||
// Super-Admin: neue Lizenzen generieren
|
||||
// Super-Admin: neue Lizenzen generieren (NUR echter Admin)
|
||||
if ($action === 'generate') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
Session::start();
|
||||
if (empty($_SESSION['admin_id'])) Response::error('Nur Admin', 403);
|
||||
$count = min(1000, max(1, (int)($body['count'] ?? 100)));
|
||||
$year = (int)($body['year'] ?? date('Y'));
|
||||
$schoolYear = ($year % 100) . '/' . (($year % 100) + 1);
|
||||
|
||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
$generated = 0;
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$generated = 0; $tries = 0; $maxTries = $count * 10 + 100;
|
||||
while ($generated < $count && $tries < $maxTries) {
|
||||
$tries++;
|
||||
$code = '';
|
||||
for ($j = 0; $j < 3; $j++) {
|
||||
if ($j > 0) $code .= '-';
|
||||
@@ -189,7 +190,7 @@ if ($method === 'POST') {
|
||||
$db->execute('INSERT INTO licenses (code, school_year) VALUES (?, ?)', [$code, $schoolYear]);
|
||||
$generated++;
|
||||
} catch (\Exception $e) {
|
||||
$i--; // Collision, retry
|
||||
// Kollision (unwahrscheinlich) → nächster Versuch; $maxTries schützt vor Endlosschleife
|
||||
}
|
||||
}
|
||||
Response::ok(['generated' => $generated]);
|
||||
|
||||
@@ -19,10 +19,21 @@ $db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$simId = $_GET['sim_id'] ?? '';
|
||||
$studentId = (int)($_GET['student_id'] ?? 0);
|
||||
$reqStudentId = (int)($_GET['student_id'] ?? 0);
|
||||
$studentId = 0;
|
||||
|
||||
// Schüler*in: eigener Fortschritt
|
||||
if ($simId && !$studentId) {
|
||||
if ($reqStudentId) {
|
||||
// Lehrer-Ansicht: nur eingeloggte Lehrperson, und die:der Schüler:in
|
||||
// MUSS zu einer Klasse dieser Lehrperson gehören (sonst IDOR).
|
||||
$teacherId = Session::requireTeacher();
|
||||
$owned = $db->fetchOne(
|
||||
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
|
||||
[$reqStudentId, $teacherId]
|
||||
);
|
||||
if (!$owned) Response::error('Kein Zugriff auf diese:n Schüler:in', 403);
|
||||
$studentId = $reqStudentId;
|
||||
} elseif ($simId) {
|
||||
// Schüler*in: eigener Fortschritt (aus der Session abgeleitet)
|
||||
$sessionId = Session::requireStudent();
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
if ($session) {
|
||||
|
||||
Reference in New Issue
Block a user