Files
geograsim/App/php/api/admin.php
T
Adminator 5b91a5bf84 Admin-Login lokal: 2FA-PIN ohne Mailversand (Dev) + Mailer-Fehler abfangen
Problem: lokal konnte man sich nicht als Admin anmelden — der 2FA-PIN kam nie
an (kein SMTP), und Mailer::sendAdminPin() warf sogar eine Exception, die den
Login komplett abbrach (leere Antwort → „MFA fehlt").

- admin.php: Mailversand nur auf Prod + in try/catch (Mail-Fehler darf den
  Login NIE blocken). Lokal (!IS_PRODUCTION) wird der PIN als `devPin` in der
  Login-Antwort zurückgegeben — auf Prod NIEMALS (2FA bleibt Pflicht).
- admin.html: devPin wird lokal automatisch ins PIN-Feld vorausgefüllt + als
  Hinweis angezeigt.

Verifiziert (temp Test-Admin): login → devPin → verify → authenticated:true,
admin-weltkueche.html erreichbar. Prod unberührt (IS_PRODUCTION-Gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 19:13:22 +02:00

110 lines
3.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* API: Super-Admin Authentifizierung (2FA mit E-Mail-PIN)
* POST /api/admin {action: "login"|"verify_pin"|"status"|"logout"}
*/
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
Response::error('Nur POST erlaubt', 405);
}
$body = json_decode(file_get_contents('php://input'), true);
$action = $body['action'] ?? '';
$db = Database::get();
// === LOGIN (Schritt 1: Username + Passwort → PIN per E-Mail) ===
if ($action === 'login') {
$username = trim($body['username'] ?? '');
$password = $body['password'] ?? '';
if (!$username || !$password) Response::error('Benutzername und Passwort erforderlich');
$admin = $db->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];
}