Lizenz-Gate: Schueler ohne Lizenz gesperrt + Lehrer-Cockpit-Warnung
Teil 1 — Schueler-Lizenz-Gate (gilt fuer alle Klassen-Schueler ohne gueltige Lizenz = zugewiesen, nicht storniert, nicht abgelaufen): - Server-Gate zentral im Front-Controller (index.php + php/lib/license_gate.php): eingeloggte Klassen-Schueler ohne Lizenz sehen bei JEDER Sim-Route statt der Simulation eine Hinweis-Seite "Deine Lehrperson muss dir noch eine gueltige Lizenz zuweisen". Schnell-Ausstieg fuer Nicht-Schueler/Nicht-Sim (kein Cookie -> keine DB-Abfrage). Autodidakt ohne Klasse: nicht lizenzpflichtig. - modules.php?student=1 liefert 'licensed'; Schueler-Cockpit (schueler.html) zeigt oben die Meldung und sperrt die Start-Buttons. Teil 2 — Lehrer-Cockpit: - Rote Warnleiste ganz oben, wenn Schueler:innen ohne gueltige Lizenz in der Klasse sind (+ Link "Jetzt zuweisen"). licenses?class_id=X liefert dazu die Liste 'unlicensed'. - Lizenzen-Lasche: neue Sektion "Ohne gueltige Lizenz" mit Einzel- und Sammel-Zuweisung (auto_assign) — sonst fuehrt die Warnung ins Leere. Kunden-Vereinfachung (Feedback): keine Auto-Erkennung/Dedup mehr (Ansprech- partner koennen wechseln). Kunde = Freitext "Name/Verlag/Schule" + Mailadresse, je Bestellung gespeichert. Editierbares Kundennummer-Feld + Autocomplete entfernt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-15
@@ -43,21 +43,13 @@ function lic_default_valid_until(Database $db): string {
|
||||
return ($start + 1) . '-10-01';
|
||||
}
|
||||
|
||||
/** Kunde finden (nach Kundennummer, dann Email) oder anlegen. Ergaenzt fehlende Felder. */
|
||||
function lic_find_or_create_customer(Database $db, ?string $email, ?string $customerNo, ?string $name): ?int {
|
||||
/** Kunde je Bestellung als Freitext ablegen (Name/Verlag/Schule + Mail) —
|
||||
* KEIN Abgleich/Dedup, da sich Ansprechpartner ändern können. */
|
||||
function lic_create_customer(Database $db, ?string $email, ?string $name): ?int {
|
||||
$email = $email ? trim($email) : null;
|
||||
$customerNo = $customerNo ? trim($customerNo) : null;
|
||||
$name = $name ? trim($name) : null;
|
||||
if (!$email && !$customerNo && !$name) return null;
|
||||
$found = null;
|
||||
if ($customerNo) $found = $db->fetchOne("SELECT id FROM license_customers WHERE customer_no = ? LIMIT 1", [$customerNo]);
|
||||
if (!$found && $email) $found = $db->fetchOne("SELECT id FROM license_customers WHERE email = ? LIMIT 1", [$email]);
|
||||
if ($found) {
|
||||
$db->execute("UPDATE license_customers SET email = COALESCE(email, ?), customer_no = COALESCE(customer_no, ?), name = COALESCE(name, ?) WHERE id = ?",
|
||||
[$email, $customerNo, $name, $found['id']]);
|
||||
return (int)$found['id'];
|
||||
}
|
||||
$db->execute("INSERT INTO license_customers (customer_no, email, name) VALUES (?,?,?)", [$customerNo, $email, $name]);
|
||||
if (!$email && !$name) return null;
|
||||
$db->execute("INSERT INTO license_customers (email, name) VALUES (?,?)", [$email, $name]);
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
@@ -239,7 +231,20 @@ if ($method === 'GET') {
|
||||
[$teacherId]
|
||||
);
|
||||
|
||||
Response::ok(['assigned' => $licenses, 'free' => $freeLicenses]);
|
||||
// Schüler:innen der Klasse OHNE gültige Lizenz (für die rote Cockpit-Warnung).
|
||||
$unlicensed = $db->fetchAll(
|
||||
"SELECT s.id, s.username, s.display_name
|
||||
FROM students s
|
||||
WHERE s.class_id = ? AND s.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM licenses l
|
||||
WHERE l.student_id = s.id AND l.canceled_at IS NULL
|
||||
AND (l.valid_until IS NULL OR l.valid_until >= CURDATE()))
|
||||
ORDER BY s.username",
|
||||
[$classId]
|
||||
);
|
||||
|
||||
Response::ok(['assigned' => $licenses, 'free' => $freeLicenses, 'unlicensed' => $unlicensed]);
|
||||
}
|
||||
|
||||
// === POST ===
|
||||
@@ -381,7 +386,7 @@ if ($method === 'POST') {
|
||||
if (strtotime($validUntil) < strtotime($validFrom)) Response::error('„Gültig bis" liegt vor „Gültig von"');
|
||||
|
||||
$cust = is_array($body['customer'] ?? null) ? $body['customer'] : [];
|
||||
$customerId = lic_find_or_create_customer($db, $cust['email'] ?? null, $cust['customer_no'] ?? null, $cust['name'] ?? null);
|
||||
$customerId = lic_create_customer($db, $cust['email'] ?? null, $cust['name'] ?? null);
|
||||
$schoolYear = lic_school_year_from($validFrom);
|
||||
$token = bin2hex(random_bytes(20));
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ if ($method === 'GET') {
|
||||
'play_url' => $mod['play_url'],
|
||||
'available_from' => $mod['available_from'],
|
||||
'mode' => $mod['status'] === 'geplant' ? 'coming_soon' : 'free',
|
||||
'licensed' => true, // Autodidakt ohne Klasse: nicht lizenzpflichtig
|
||||
];
|
||||
}
|
||||
Response::ok($result);
|
||||
@@ -83,6 +84,17 @@ if ($method === 'GET') {
|
||||
foreach ($rows as $r) $overrides[$r['module_id']] = $r['mode'];
|
||||
}
|
||||
|
||||
// Lizenz-Status des Schülers (fürs Cockpit-Gate). Gültig = zugewiesen,
|
||||
// nicht storniert, nicht abgelaufen (valid_until >= heute; NULL = Alt-Code).
|
||||
$licensed = false;
|
||||
if ($studentRow && !empty($studentRow['id'])) {
|
||||
$lic = $db->fetchOne(
|
||||
"SELECT 1 AS ok FROM licenses WHERE student_id = ? AND canceled_at IS NULL
|
||||
AND (valid_until IS NULL OR valid_until >= CURDATE()) LIMIT 1",
|
||||
[$studentRow['id']]);
|
||||
$licensed = (bool)$lic;
|
||||
}
|
||||
|
||||
// Einzelner Modul-Filter
|
||||
$filterId = $_GET['module_id'] ?? null;
|
||||
|
||||
@@ -117,6 +129,7 @@ if ($method === 'GET') {
|
||||
'dueDate' => $cm['due_date'] ?? null,
|
||||
'paused' => (bool)($cm['paused'] ?? 0),
|
||||
'assignmentCompleted' => $assignmentCompleted,
|
||||
'licensed' => $licensed,
|
||||
];
|
||||
}
|
||||
// Wenn einzelnes Modul, liefere Objekt statt Array
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Lizenz-Gate — eingeloggte Schüler:innen (in einer Klasse) ohne gültige Lizenz
|
||||
* sehen bei Sim-Routen statt der Simulation eine Hinweis-Seite. Zentral im
|
||||
* Front-Controller aufgerufen, damit alle Sim-Wrapper auf einmal geschützt sind.
|
||||
*
|
||||
* „Gültige Lizenz" = dem Kind zugewiesen, NICHT storniert und NICHT abgelaufen
|
||||
* (valid_until >= heute; NULL = Alt-Code ohne Ablauf).
|
||||
*/
|
||||
|
||||
function ggs_student_has_valid_license(PDO $pdo, int $studentId): bool {
|
||||
if ($studentId <= 0) return false;
|
||||
$st = $pdo->prepare(
|
||||
"SELECT 1 FROM licenses
|
||||
WHERE student_id = ? AND canceled_at IS NULL
|
||||
AND (valid_until IS NULL OR valid_until >= CURDATE())
|
||||
LIMIT 1"
|
||||
);
|
||||
$st->execute([$studentId]);
|
||||
return (bool)$st->fetchColumn();
|
||||
}
|
||||
|
||||
/** Ist die Route eine spielbare Sim (play_url ODER module_id, aktiv/beta)? */
|
||||
function ggs_is_sim_route(PDO $pdo, string $route): bool {
|
||||
if ($route === '') return false;
|
||||
$st = $pdo->prepare(
|
||||
"SELECT 1 FROM module_info
|
||||
WHERE (play_url = ? OR module_id = ?) AND status IN ('aktiv','beta') LIMIT 1"
|
||||
);
|
||||
$st->execute([$route, $route]);
|
||||
return (bool)$st->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-Controller-Hook: blockt Sim-Zugriff eingeloggter Klassen-Schüler ohne
|
||||
* gültige Lizenz. Schnell-Ausstieg fuer alle anderen (kein Cookie / keine Sim).
|
||||
*/
|
||||
function ggs_license_gate(string $route): void {
|
||||
$sessionId = $_COOKIE['ggs_session'] ?? null;
|
||||
if (!$sessionId) return; // nur eingeloggte Schüler:innen
|
||||
$pdo = getDB();
|
||||
$st = $pdo->prepare('SELECT student_id, class_id FROM student_sessions WHERE id = ?');
|
||||
$st->execute([$sessionId]);
|
||||
$sess = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$sess || empty($sess['class_id'])) return; // Autodidakt ohne Klasse: nicht im Lizenzmodell
|
||||
if (!ggs_is_sim_route($pdo, $route)) return; // keine Sim-Route
|
||||
$studentId = (int)($sess['student_id'] ?? 0);
|
||||
if (ggs_student_has_valid_license($pdo, $studentId)) return;
|
||||
ggs_render_license_wall(); // beendet die Anfrage
|
||||
}
|
||||
|
||||
function ggs_render_license_wall(): void {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$css = BASE_PATH . '/assets/css/design-system.css';
|
||||
$cockpit = BASE_PATH . '/schueler';
|
||||
echo <<<HTML
|
||||
<!DOCTYPE html><html lang="de"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Lizenz benötigt · GeoGraSim</title>
|
||||
<link rel="stylesheet" href="{$css}">
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;padding:40px 20px;max-width:620px;margin:auto;color:#2a2a2a}
|
||||
h1{color:#1f4e5a;font-size:1.4rem;margin:0 0 .6rem}
|
||||
.lic-card{background:#f2f8f9;border:1px solid #cfe3e8;border-left:5px solid #4a7c8a;padding:26px;border-radius:12px}
|
||||
.lic-card p{line-height:1.6;margin:.5rem 0}
|
||||
.lic-card a{color:#4a7c8a;font-weight:700}
|
||||
.big{font-size:2.2rem;margin-bottom:.2rem}
|
||||
</style></head><body>
|
||||
<div class="lic-card">
|
||||
<div class="big">🔑</div>
|
||||
<h1>Noch keine Lizenz</h1>
|
||||
<p><strong>Deine Lehrperson muss dir noch eine gültige Lizenz zuweisen.</strong></p>
|
||||
<p>Sobald das erledigt ist, kannst du alle freigegebenen Simulationen nutzen.</p>
|
||||
<p style="margin-top:1rem"><a href="{$cockpit}">← Zurück zum Cockpit</a></p>
|
||||
</div></body></html>
|
||||
HTML;
|
||||
exit;
|
||||
}
|
||||
Reference in New Issue
Block a user