2984d93e7e
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>
203 lines
8.6 KiB
PHP
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);
|