Docker Compose mit drei Services (nginx auf Port 8000), Migrationsmechanik, Update- und Backup-Skript. Anwendung: Router mit CSRF-Prüfung, Anmeldung und Ersteinrichtung, zentraler Einstellungsdialog mit Reitern, ein- und ausschaltbares Debug-Log mit Maskierung von Zugangsdaten. Layout nach Vorbild des FinanzManagers: feste Navigation links, Werkzeugleiste oben, Tab-Leiste; Farbschema hell/dunkel/System. Tests: 51 PHP-Prüfungen und 7 Playwright-Tests, alle grün.
122 lines
3.5 KiB
PHP
122 lines
3.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Finanzmanager;
|
||
|
||
use Finanzmanager\Models\Benutzer;
|
||
|
||
class Auth
|
||
{
|
||
/** Absolutes Session-Timeout. */
|
||
private const SESSION_LAUFZEIT = 86400;
|
||
|
||
public static function startSession(): void
|
||
{
|
||
if (session_status() !== PHP_SESSION_NONE) {
|
||
return;
|
||
}
|
||
|
||
session_set_cookie_params(self::cookieParams());
|
||
session_start();
|
||
}
|
||
|
||
/**
|
||
* Cookie-Attribute als reine Funktion, getrennt von startSession() – so
|
||
* lässt sich die Secure-Erkennung testen, ohne eine Session zu starten.
|
||
*
|
||
* Secure richtet sich nach dem Request (HTTPS bzw. X-Forwarded-Proto hinter
|
||
* einem Reverse-Proxy), damit der lokale Betrieb auf http://localhost:8000
|
||
* ohne TLS funktioniert. SESSION_COOKIE_SECURE erzwingt (1) oder
|
||
* unterdrückt (0) das Attribut.
|
||
*/
|
||
public static function cookieParams(): array
|
||
{
|
||
$override = getenv('SESSION_COOKIE_SECURE');
|
||
if ($override === '1' || $override === '0') {
|
||
$secure = $override === '1';
|
||
} else {
|
||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
|
||
}
|
||
|
||
return [
|
||
'lifetime' => 0,
|
||
'path' => '/',
|
||
'domain' => '',
|
||
'secure' => $secure,
|
||
'httponly' => true,
|
||
'samesite' => 'Lax',
|
||
];
|
||
}
|
||
|
||
public static function verlange(): void
|
||
{
|
||
self::startSession();
|
||
|
||
$benutzerId = $_SESSION['user_id'] ?? null;
|
||
$anmeldung = $_SESSION['login_time'] ?? 0;
|
||
|
||
if (!$benutzerId || (time() - (int)$anmeldung) > self::SESSION_LAUFZEIT) {
|
||
$_SESSION = [];
|
||
session_destroy();
|
||
View::redirect('/login');
|
||
}
|
||
}
|
||
|
||
public static function anmelden(int $benutzerId): void
|
||
{
|
||
self::startSession();
|
||
session_regenerate_id(true);
|
||
$_SESSION['user_id'] = $benutzerId;
|
||
$_SESSION['login_time'] = time();
|
||
}
|
||
|
||
public static function abmelden(): void
|
||
{
|
||
self::startSession();
|
||
$_SESSION = [];
|
||
if (ini_get('session.use_cookies')) {
|
||
$p = session_get_cookie_params();
|
||
setcookie(session_name(), '', time() - 42000,
|
||
$p['path'], $p['domain'], (bool)$p['secure'], (bool)$p['httponly']);
|
||
}
|
||
session_destroy();
|
||
}
|
||
|
||
public static function benutzerId(): ?int
|
||
{
|
||
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||
}
|
||
|
||
public static function benutzer(): ?array
|
||
{
|
||
$id = self::benutzerId();
|
||
return $id === null ? null : Benutzer::find($id);
|
||
}
|
||
|
||
public static function csrfToken(): string
|
||
{
|
||
self::startSession();
|
||
if (empty($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
}
|
||
return $_SESSION['csrf_token'];
|
||
}
|
||
|
||
public static function pruefeCsrfToken(string $token): void
|
||
{
|
||
self::startSession();
|
||
if (!self::istGueltigerCsrfToken($token)) {
|
||
http_response_code(403);
|
||
exit('CSRF-Token ungültig.');
|
||
}
|
||
}
|
||
|
||
/** Reiner Vergleich ohne Seiteneffekt, damit isoliert testbar. */
|
||
public static function istGueltigerCsrfToken(string $token): bool
|
||
{
|
||
$gespeichert = $_SESSION['csrf_token'] ?? '';
|
||
return $gespeichert !== '' && hash_equals($gespeichert, $token);
|
||
}
|
||
}
|