From 37f6238d05cf264e03266327e329bc74a3f51381 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 18 Aug 2026 16:33:14 +0200 Subject: [PATCH] Auth Phase 2: Session-Zugriffe kanalisieren (verhaltensneutral) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorbereitung für den JWT+Redis-Umschalter: alle verstreuten $_SESSION['teacher_id'|admin_id']-Zugriffe laufen jetzt durch Session::-Helper, damit der Backend-Tausch später EINE Datei (Session.php) ist statt 18 Stellen. - Session.php: neue Admin-Helper adminId()/loginAdmin()/logoutAdmin()/ requireAdmin() (mirror der Teacher-Helper). teacherId()/adminId() starten die Session jetzt lazy (Aufrufer müssen Session::start() nicht mehr). - 18 Fundstellen kanalisiert (admin.php, admin-accounts, admin-modules, licenses, levels, waypoints, foto-upload, admin_gate, save_map). - Country.php + app.php cockpit_href: über Session:: mit class_exists-Fallback (Bootstrap/Lib-Kontext, Session evtl. nicht geladen). Verhaltensneutral: 14/14 Lint OK, Helper-Round-Trip 6/6 PASS, Live-Smoke (admin status/modules/accounts/licenses) weist unauth. Requests wie bisher mit 401/403 ab. AUTH_BACKEND bleibt 'session' — Live unverändert. Co-Authored-By: Claude Opus 4.8 --- App/pages/foto-upload.php | 3 +- App/php/api/admin-accounts.php | 4 +-- App/php/api/admin-modules.php | 2 +- App/php/api/admin.php | 8 ++--- App/php/api/levels.php | 3 +- App/php/api/licenses.php | 6 ++-- App/php/api/waypoints.php | 3 +- App/php/config/app.php | 5 +++- App/php/lib/Country.php | 5 ++-- App/php/lib/Session.php | 36 ++++++++++++++++++++++- App/php/lib/admin_gate.php | 4 +-- App/sims/tourismusregion/api/save_map.php | 3 +- 12 files changed, 55 insertions(+), 27 deletions(-) diff --git a/App/pages/foto-upload.php b/App/pages/foto-upload.php index a4cc548..aeecfd6 100644 --- a/App/pages/foto-upload.php +++ b/App/pages/foto-upload.php @@ -15,8 +15,7 @@ $bp = BASE_PATH; /* ---------- Zugang: eingeloggte:r Admin ODER PIN (Ausnahme, vorläufig) ---------- */ require_once __DIR__ . '/../php/lib/Session.php'; -Session::start(); -$__isAdmin = !empty($_SESSION['admin_id']); +$__isAdmin = (bool)Session::adminId(); /* ---------- PIN-Schranke (im Browser gespeichert, 1 Jahr) ---------- */ $PIN = '0815'; diff --git a/App/php/api/admin-accounts.php b/App/php/api/admin-accounts.php index 18ab7e0..2c6abc1 100644 --- a/App/php/api/admin-accounts.php +++ b/App/php/api/admin-accounts.php @@ -12,13 +12,13 @@ if (session_status() === PHP_SESSION_NONE) session_start(); -if (empty($_SESSION['admin_id'])) { +if (!Session::adminId()) { http_response_code(401); echo json_encode(['error' => 'Nicht eingeloggt']); exit; } -$meId = (int)$_SESSION['admin_id']; +$meId = Session::adminId(); $db = Database::get(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { diff --git a/App/php/api/admin-modules.php b/App/php/api/admin-modules.php index 394e839..fa8b35b 100644 --- a/App/php/api/admin-modules.php +++ b/App/php/api/admin-modules.php @@ -10,7 +10,7 @@ if (session_status() === PHP_SESSION_NONE) session_start(); -if (empty($_SESSION['admin_id'])) { +if (!Session::adminId()) { http_response_code(401); echo json_encode(['error' => 'Nicht eingeloggt']); exit; diff --git a/App/php/api/admin.php b/App/php/api/admin.php index d691241..88b8747 100644 --- a/App/php/api/admin.php +++ b/App/php/api/admin.php @@ -68,7 +68,7 @@ if ($action === 'verify_pin') { $db->execute('UPDATE admin_pins SET used = 1 WHERE id = ?', [$valid['id']]); // Admin-Session aktivieren - $_SESSION['admin_id'] = $adminId; + Session::loginAdmin((int)$adminId); unset($_SESSION['admin_pending']); Response::ok(['message' => 'Anmeldung erfolgreich.']); @@ -76,8 +76,7 @@ if ($action === 'verify_pin') { // === STATUS === if ($action === 'status') { - Session::start(); - $adminId = $_SESSION['admin_id'] ?? null; + $adminId = Session::adminId(); if ($adminId) { $admin = $db->fetchOne('SELECT id, username FROM admin_users WHERE id = ?', [$adminId]); Response::ok(['authenticated' => true, 'admin' => $admin]); @@ -87,8 +86,7 @@ if ($action === 'status') { // === LOGOUT === if ($action === 'logout') { - Session::start(); - unset($_SESSION['admin_id'], $_SESSION['admin_pending']); + Session::logoutAdmin(); Response::ok(); } diff --git a/App/php/api/levels.php b/App/php/api/levels.php index 977de78..29eefd3 100644 --- a/App/php/api/levels.php +++ b/App/php/api/levels.php @@ -25,8 +25,7 @@ if ($method === 'GET') { if ($method === 'POST') { // Admin check - Session::start(); - $adminId = $_SESSION['admin_id'] ?? null; + $adminId = Session::adminId(); $teacherId = Session::teacherId(); if (!$adminId && !$teacherId) Response::error('Nicht autorisiert', 401); diff --git a/App/php/api/licenses.php b/App/php/api/licenses.php index aafc539..4bfaf8e 100644 --- a/App/php/api/licenses.php +++ b/App/php/api/licenses.php @@ -13,8 +13,7 @@ $db = Database::get(); if ($method === 'GET') { // Super-Admin: alle Lizenzen if (isset($_GET['admin']) && $_GET['admin'] === '1') { - Session::start(); - $adminId = $_SESSION['admin_id'] ?? null; + $adminId = Session::adminId(); if (!$adminId) { Response::error('Nicht autorisiert', 401); } @@ -170,8 +169,7 @@ if ($method === 'POST') { // Super-Admin: neue Lizenzen generieren (NUR echter Admin) if ($action === 'generate') { - Session::start(); - if (empty($_SESSION['admin_id'])) Response::error('Nur Admin', 403); + if (!Session::adminId()) 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); diff --git a/App/php/api/waypoints.php b/App/php/api/waypoints.php index 2bb4c37..0725625 100644 --- a/App/php/api/waypoints.php +++ b/App/php/api/waypoints.php @@ -21,8 +21,7 @@ if ($method === 'GET') { } if ($method === 'POST') { - Session::start(); - $adminId = $_SESSION['admin_id'] ?? null; + $adminId = Session::adminId(); $teacherId = Session::teacherId(); if (!$adminId && !$teacherId) Response::error('Nicht autorisiert', 401); diff --git a/App/php/config/app.php b/App/php/config/app.php index 4444e70..c726661 100644 --- a/App/php/config/app.php +++ b/App/php/config/app.php @@ -103,7 +103,10 @@ if (!defined('JWT_REFRESH_TTL')) define('JWT_REFRESH_TTL', 2592000); // Refresh/ */ function cockpit_href(): string { if (session_status() === PHP_SESSION_NONE) @session_start(); - if (!empty($_SESSION['teacher_id'])) return BASE_PATH . '/teacher'; + // über Session:: kanalisiert; Fallback auf $_SESSION, falls die Lib im + // Bootstrap-Kontext (nur app.php) noch nicht geladen ist. + $tid = class_exists('Session') ? Session::teacherId() : ($_SESSION['teacher_id'] ?? null); + if ($tid) return BASE_PATH . '/teacher'; if (!empty($_COOKIE['ggs_session'])) return BASE_PATH . '/schueler'; return BASE_PATH . '/'; } diff --git a/App/php/lib/Country.php b/App/php/lib/Country.php index 463c662..676a98f 100644 --- a/App/php/lib/Country.php +++ b/App/php/lib/Country.php @@ -101,8 +101,9 @@ class Country } } - // 4./5. Lehrperson eingeloggt? - $teacherId = $_SESSION['teacher_id'] ?? null; + // 4./5. Lehrperson eingeloggt? (über Session:: kanalisiert; Fallback + // auf $_SESSION, falls die Session-Lib in diesem Kontext nicht geladen ist) + $teacherId = class_exists('Session') ? Session::teacherId() : ($_SESSION['teacher_id'] ?? null); if ($teacherId) { $val = self::fetchFirst($db, 'SELECT country FROM teachers WHERE id = ? LIMIT 1', diff --git a/App/php/lib/Session.php b/App/php/lib/Session.php index 39c16f7..c104abd 100644 --- a/App/php/lib/Session.php +++ b/App/php/lib/Session.php @@ -67,8 +67,9 @@ class Session { return $id; } - /** Lehrer eingeloggt? */ + /** Lehrer eingeloggt? (startet die Session lazy — Aufrufer müssen nicht) */ public static function teacherId(): ?int { + self::start(); return $_SESSION['teacher_id'] ?? null; } @@ -89,6 +90,39 @@ class Session { return $id; } + // ── Admin (Super-Admin nach 2FA) ──────────────────────────────────── + // Einheitlicher Zugang wie beim Lehrer, damit der spätere JWT+Redis- + // Umschalter (AUTH_BACKEND) an EINER Stelle greift statt an jedem Endpoint. + + /** Admin eingeloggt (2FA abgeschlossen)? */ + public static function adminId(): ?int { + self::start(); + return $_SESSION['admin_id'] ?? null; + } + + /** Admin-Login abschließen (nach erfolgreicher 2FA). */ + public static function loginAdmin(int $adminId): void { + self::start(); + $_SESSION['admin_id'] = $adminId; + } + + /** Admin-Logout — inkl. offener 2FA-Phase (admin_pending). */ + public static function logoutAdmin(): void { + self::start(); + unset($_SESSION['admin_id'], $_SESSION['admin_pending']); + } + + /** Admin-Guard: 403 wenn nicht eingeloggt. */ + public static function requireAdmin(): int { + $id = self::adminId(); + if (!$id) { + http_response_code(403); + echo json_encode(['error' => 'Nur Admin']); + exit; + } + return $id; + } + public static function logout(): void { self::start(); session_destroy(); diff --git a/App/php/lib/admin_gate.php b/App/php/lib/admin_gate.php index 009fd73..4e178cd 100644 --- a/App/php/lib/admin_gate.php +++ b/App/php/lib/admin_gate.php @@ -15,9 +15,7 @@ if (!defined('BASE_PATH')) { if (!class_exists('Session')) { require_once __DIR__ . '/Session.php'; } -Session::start(); - -if (empty($_SESSION['admin_id'])) { +if (!Session::adminId()) { $base = defined('BASE_PATH') ? BASE_PATH : ''; header('Location: ' . $base . '/admin.html'); exit; diff --git a/App/sims/tourismusregion/api/save_map.php b/App/sims/tourismusregion/api/save_map.php index 2925619..0deb3ee 100644 --- a/App/sims/tourismusregion/api/save_map.php +++ b/App/sims/tourismusregion/api/save_map.php @@ -14,9 +14,8 @@ require_once __DIR__ . '/../../../php/config/app.php'; require_once __DIR__ . '/../../../php/lib/Database.php'; require_once __DIR__ . '/../../../php/lib/Response.php'; require_once __DIR__ . '/../../../php/lib/Session.php'; -Session::start(); // Session laden, sonst ist $_SESSION leer → Auth-Check wirft immer 401 // Backend-Editor: Admin ODER eingeloggte Lehrperson darf speichern. -if (empty($_SESSION['admin_id']) && !Session::teacherId()) { +if (!Session::adminId() && !Session::teacherId()) { http_response_code(401); echo json_encode(['ok' => false, 'error' => 'Nicht eingeloggt (Admin oder Lehrperson erforderlich).']); exit;