Files
Adminator c950f954d1 Admin-ToDo/Review-Seite + Staging-Self-Service-Admin-Login
- admin-todo.html: mobile-freundliche ToDo/Review-Liste (Prioritaeten, Haekchen +
  Notizfelder je Punkt, localStorage, Notizen-Export), admin-gated, noindex. In
  admin-licenses.html Nav + Backend-Werkzeuge verlinkt.
- admin.php: devPin auch bei STAGING=1 (v3, Mail bewusst tot) -> Self-Service-
  Admin-Login ohne Mailversand. Prod unveraendert (STAGING dort nie definiert,
  2FA-Mail-Pflicht bleibt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 11:31:02 +02:00

112 lines
4.1 KiB
PHP
Raw Permalink 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) ODER Staging (v3, STAGING=1 in .env.production, Mail
// bewusst tot): PIN direkt zurückgeben, damit man sich ohne Mailversand anmelden
// kann. Auf Prod (IS_PRODUCTION ohne STAGING) NIEMALS — dort greift 2FA per Mail.
$isStaging = defined('STAGING') && (STAGING === '1' || STAGING === 1 || STAGING === true);
if (!IS_PRODUCTION || $isStaging) {
$resp['devPin'] = $pin;
$resp['message'] = ($isStaging ? 'Staging' : '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];
}