fetchOne('SELECT id, password, email_2fa FROM admin_users WHERE username = ?', [$username]); if (!$admin || !password_verify($password, $admin['password'])) { sleep(1); // Brute-force delay Response::error('Benutzername oder Passwort falsch'); } // PIN generieren (4-stellig) $pin = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT); $db->execute( 'INSERT INTO admin_pins (admin_id, pin, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))', [$admin['id'], $pin] ); // PIN per E-Mail senden — Fehler dürfen den Login NIE abbrechen (lokal geht // kein SMTP raus; früher warf das hier und die Anmeldung schlug fehl → „MFA // fehlt"). Lokal ohnehin überspringen, da wir den PIN als devPin zurückgeben. if (IS_PRODUCTION && class_exists('Mailer') && !empty($admin['email_2fa'])) { try { Mailer::sendAdminPin($admin['email_2fa'], $pin); } catch (\Throwable $e) { error_log('[admin] Mailer::sendAdminPin fehlgeschlagen: ' . $e->getMessage()); } } // Admin-ID in Session speichern (aber noch nicht authentifiziert) Session::start(); $_SESSION['admin_pending'] = (int)$admin['id']; unset($_SESSION['admin_id']); // Sicherstellen dass nicht schon eingeloggt $resp = ['message' => 'PIN wurde an Ihre E-Mail gesendet.', 'email' => maskEmail($admin['email_2fa'])]; // Dev-Umgebung (localhost): es geht keine Mail raus → PIN direkt zurückgeben, // damit man sich lokal anmelden kann. Auf Prod (IS_PRODUCTION) NIEMALS. if (!IS_PRODUCTION) { $resp['devPin'] = $pin; $resp['message'] = 'Dev-Umgebung – kein Mailversand. PIN steht unten.'; } Response::ok($resp); } // === VERIFY PIN (Schritt 2: PIN eingeben → Admin-Session) === if ($action === 'verify_pin') { Session::start(); $adminId = $_SESSION['admin_pending'] ?? null; if (!$adminId) Response::error('Kein ausstehender Login. Bitte erneut anmelden.'); $pin = trim($body['pin'] ?? ''); if (!$pin || strlen($pin) !== 4) Response::error('4-stelliger PIN erforderlich'); $valid = $db->fetchOne( 'SELECT id FROM admin_pins WHERE admin_id = ? AND pin = ? AND expires_at > NOW() AND used = 0 ORDER BY id DESC LIMIT 1', [$adminId, $pin] ); if (!$valid) { sleep(1); Response::error('PIN ungültig oder abgelaufen'); } // PIN als benutzt markieren $db->execute('UPDATE admin_pins SET used = 1 WHERE id = ?', [$valid['id']]); // Admin-Session aktivieren Session::loginAdmin((int)$adminId); unset($_SESSION['admin_pending']); Response::ok(['message' => 'Anmeldung erfolgreich.']); } // === STATUS === if ($action === 'status') { $adminId = Session::adminId(); if ($adminId) { $admin = $db->fetchOne('SELECT id, username FROM admin_users WHERE id = ?', [$adminId]); Response::ok(['authenticated' => true, 'admin' => $admin]); } Response::ok(['authenticated' => false]); } // === LOGOUT === if ($action === 'logout') { Session::logoutAdmin(); Response::ok(); } Response::error('Unbekannte Aktion'); // Helper: E-Mail maskieren (t***@ph-vorarlberg.ac.at) function maskEmail(string $email): string { $parts = explode('@', $email); return substr($parts[0], 0, 1) . '***@' . $parts[1]; }