Files
geograsim/App/php/api/licenses.php
T
Adminator 2984d93e7e 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>
2026-08-15 16:21:25 +02:00

203 lines
8.6 KiB
PHP

<?php
/**
* API: Lizenzverwaltung
* GET /api/licenses?class_id=X → Lizenzen der Klasse (Lehrer)
* GET /api/licenses?admin=1 → Alle Lizenzen (Super-Admin)
* POST /api/licenses {action} → Lizenz einlösen/zuweisen/generieren
*/
$method = $_SERVER['REQUEST_METHOD'];
$db = Database::get();
// === GET ===
if ($method === 'GET') {
// Super-Admin: alle Lizenzen
if (isset($_GET['admin']) && $_GET['admin'] === '1') {
Session::start();
$adminId = $_SESSION['admin_id'] ?? null;
if (!$adminId) {
Response::error('Nicht autorisiert', 401);
}
$year = $_GET['year'] ?? null;
$status = $_GET['status'] ?? null; // 'free', 'used'
$where = '1=1';
$params = [];
if ($year) { $where .= ' AND l.school_year = ?'; $params[] = $year; }
if ($status === 'free') { $where .= ' AND l.student_id IS NULL'; }
if ($status === 'used') { $where .= ' AND l.student_id IS NOT NULL'; }
$total = $db->fetchOne("SELECT COUNT(*) as c FROM licenses l WHERE $where", $params);
$free = $db->fetchOne("SELECT COUNT(*) as c FROM licenses l WHERE student_id IS NULL" . ($year ? " AND school_year = ?" : ""), $year ? [$year] : []);
$used = $db->fetchOne("SELECT COUNT(*) as c FROM licenses l WHERE student_id IS NOT NULL" . ($year ? " AND school_year = ?" : ""), $year ? [$year] : []);
$limit = (int)($_GET['limit'] ?? 100);
$offset = (int)($_GET['offset'] ?? 0);
$licenses = $db->fetchAll(
"SELECT l.id, l.code, l.school_year, l.student_id, l.teacher_id, l.redeemed_at, l.created_at,
s.username as student_name, t.display_name as teacher_name
FROM licenses l
LEFT JOIN students s ON s.id = l.student_id
LEFT JOIN teachers t ON t.id = l.teacher_id
WHERE $where
ORDER BY l.id DESC
LIMIT $limit OFFSET $offset",
$params
);
Response::ok([
'total' => (int)$total['c'],
'free' => (int)$free['c'],
'used' => (int)$used['c'],
'licenses' => $licenses
]);
}
// Lehrer: Lizenzen einer Klasse
$teacherId = Session::requireTeacher();
$classId = (int)($_GET['class_id'] ?? 0);
if (!$classId) Response::error('class_id erforderlich');
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
if (!$class) Response::error('Klasse nicht gefunden', 404);
$licenses = $db->fetchAll(
'SELECT l.id, l.code, l.school_year, l.student_id, l.redeemed_at,
s.username as student_name, s.display_name as student_display
FROM licenses l
JOIN students s ON s.id = l.student_id
WHERE s.class_id = ?
ORDER BY s.username',
[$classId]
);
// Auch freie Lizenzen des Lehrers (noch nicht zugewiesen)
$freeLicenses = $db->fetchAll(
'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL ORDER BY code',
[$teacherId]
);
Response::ok(['assigned' => $licenses, 'free' => $freeLicenses]);
}
// === POST ===
if ($method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true);
$action = $body['action'] ?? '';
// Lizenz einlösen (Lehrer gibt Code ein → wird ihm zugeordnet)
if ($action === 'redeem') {
$teacherId = Session::requireTeacher();
$code = strtoupper(trim($body['code'] ?? ''));
if (!$code) Response::error('Lizenzcode erforderlich');
$license = $db->fetchOne('SELECT id, student_id, teacher_id FROM licenses WHERE code = ?', [$code]);
if (!$license) Response::error('Lizenzcode ungültig');
if ($license['student_id']) Response::error('Diese Lizenz ist bereits bereits zugewiesen');
if ($license['teacher_id'] && $license['teacher_id'] != $teacherId) Response::error('Diese Lizenz gehört einer anderen Lehrperson');
$db->execute('UPDATE licenses SET teacher_id = ?, redeemed_at = NOW() WHERE id = ?', [$teacherId, $license['id']]);
Response::ok(['licenseId' => (int)$license['id']]);
}
// Mehrere Lizenzen auf einmal einlösen
if ($action === 'redeem_batch') {
$teacherId = Session::requireTeacher();
$codes = $body['codes'] ?? [];
if (!is_array($codes) || empty($codes)) Response::error('Lizenzcodes erforderlich');
$redeemed = 0;
$errors = [];
foreach ($codes as $code) {
$code = strtoupper(trim($code));
if (!$code) continue;
$license = $db->fetchOne('SELECT id, student_id, teacher_id FROM licenses WHERE code = ?', [$code]);
if (!$license) { $errors[] = "$code: ungültig"; continue; }
if ($license['student_id']) { $errors[] = "$code: bereits zugewiesen"; continue; }
if ($license['teacher_id'] && $license['teacher_id'] != $teacherId) { $errors[] = "$code: gehört anderer Lehrperson"; continue; }
$db->execute('UPDATE licenses SET teacher_id = ?, redeemed_at = NOW() WHERE id = ?', [$teacherId, $license['id']]);
$redeemed++;
}
Response::ok(['redeemed' => $redeemed, 'errors' => $errors]);
}
// Lizenz einem Schüler zuweisen
if ($action === 'assign') {
$teacherId = Session::requireTeacher();
$licenseId = (int)($body['licenseId'] ?? 0);
$studentId = (int)($body['studentId'] ?? 0);
if (!$licenseId || !$studentId) Response::error('licenseId und studentId erforderlich');
$license = $db->fetchOne('SELECT id, teacher_id FROM licenses WHERE id = ? AND student_id IS NULL', [$licenseId]);
if (!$license) Response::error('Lizenz nicht gefunden oder bereits zugewiesen');
if ($license['teacher_id'] && $license['teacher_id'] != $teacherId) Response::error('Lizenz gehört anderer Lehrperson');
// Prüfen dass Schüler dem Lehrer gehört
$student = $db->fetchOne(
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
[$studentId, $teacherId]
);
if (!$student) Response::error('Schüler*in nicht gefunden');
$db->execute('UPDATE licenses SET student_id = ?, teacher_id = ? WHERE id = ?', [$studentId, $teacherId, $licenseId]);
Response::ok();
}
// Auto-Assign: nächste freie Lizenz einem Schüler zuweisen
if ($action === 'auto_assign') {
$teacherId = Session::requireTeacher();
$studentId = (int)($body['studentId'] ?? 0);
if (!$studentId) Response::error('studentId erforderlich');
$student = $db->fetchOne(
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
[$studentId, $teacherId]
);
if (!$student) Response::error('Schüler*in nicht gefunden');
// Nächste freie Lizenz des Lehrers
$license = $db->fetchOne(
'SELECT id FROM licenses WHERE teacher_id = ? AND student_id IS NULL ORDER BY id LIMIT 1',
[$teacherId]
);
if (!$license) Response::error('Keine freien Lizenzen verfügbar');
$db->execute('UPDATE licenses SET student_id = ? WHERE id = ?', [$studentId, $license['id']]);
Response::ok(['licenseId' => (int)$license['id']]);
}
// Super-Admin: neue Lizenzen generieren (NUR echter Admin)
if ($action === 'generate') {
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; $tries = 0; $maxTries = $count * 10 + 100;
while ($generated < $count && $tries < $maxTries) {
$tries++;
$code = '';
for ($j = 0; $j < 3; $j++) {
if ($j > 0) $code .= '-';
for ($k = 0; $k < 4; $k++) $code .= $chars[random_int(0, strlen($chars) - 1)];
}
$code .= '-' . $year;
try {
$db->execute('INSERT INTO licenses (code, school_year) VALUES (?, ?)', [$code, $schoolYear]);
$generated++;
} catch (\Exception $e) {
// Kollision (unwahrscheinlich) → nächster Versuch; $maxTries schützt vor Endlosschleife
}
}
Response::ok(['generated' => $generated]);
}
Response::error('Unbekannte Aktion');
}
Response::error('Methode nicht erlaubt', 405);