feature/logging_Service #17
@ -6,6 +6,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Services\Ldap\LdapAuthService;
|
use App\Services\Ldap\LdapAuthService;
|
||||||
|
use App\Services\Logging\LoggingService;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zuständig für alles rund um den Login:
|
* Zuständig für alles rund um den Login:
|
||||||
@ -26,6 +28,9 @@ class AuthController
|
|||||||
/** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */
|
/** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */
|
||||||
private LdapAuthService $ldapAuthService;
|
private LdapAuthService $ldapAuthService;
|
||||||
|
|
||||||
|
/** @var LoggingService Logger für technische Fehler */
|
||||||
|
private LoggingService $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice.
|
* Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice.
|
||||||
*
|
*
|
||||||
@ -39,6 +44,9 @@ class AuthController
|
|||||||
// LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren.
|
// LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren.
|
||||||
// Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service).
|
// Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service).
|
||||||
$this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []);
|
$this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []);
|
||||||
|
|
||||||
|
// LoggingService mit dem Teilbereich "logging" aus der Konfiguration initialisieren.
|
||||||
|
$this->logger = new LoggingService($config['logging'] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,14 +62,14 @@ class AuthController
|
|||||||
|
|
||||||
// Wichtig: Die View erwartet aktuell die Variable $error.
|
// Wichtig: Die View erwartet aktuell die Variable $error.
|
||||||
return [
|
return [
|
||||||
'view' => $viewPath,
|
'view' => $viewPath,
|
||||||
'data' => [
|
'data' => [
|
||||||
'error' => $errorMessage,
|
'error' => $errorMessage,
|
||||||
'loginPage' => true,
|
'loginPage' => true,
|
||||||
],
|
],
|
||||||
'pageTitle' => 'Login',
|
'pageTitle' => 'Login',
|
||||||
// Beim Login ist typischerweise kein Menüpunkt aktiv.
|
// Beim Login ist typischerweise kein Menüpunkt aktiv.
|
||||||
'activeMenu' => null,
|
'activeMenu' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,11 +93,26 @@ class AuthController
|
|||||||
// true = Authentifizierung erfolgreich
|
// true = Authentifizierung erfolgreich
|
||||||
// false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch)
|
// false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch)
|
||||||
$authenticated = $this->ldapAuthService->authenticate($username, $password);
|
$authenticated = $this->ldapAuthService->authenticate($username, $password);
|
||||||
} catch (\Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
// Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration).
|
// HIER ist vorher dein Fehler entstanden:
|
||||||
// In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt.
|
// - showLoginForm() wurde nur aufgerufen, das Ergebnis aber ignoriert
|
||||||
|
// - danach kam ein "return;" ohne Rückgabewert → Rückgabetyp array wurde verletzt
|
||||||
|
|
||||||
|
// Technischen Fehler ausführlich ins Log schreiben
|
||||||
|
$this->logger->logException(
|
||||||
|
'Technischer Fehler bei der Anmeldung.',
|
||||||
|
$exception,
|
||||||
|
[
|
||||||
|
'route' => 'login.submit',
|
||||||
|
'username' => $username,
|
||||||
|
'remote_addr'=> $_SERVER['REMOTE_ADDR'] ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Für den Benutzer nur eine allgemeine, aber verständliche Meldung anzeigen
|
||||||
return $this->showLoginForm(
|
return $this->showLoginForm(
|
||||||
'Technischer Fehler bei der Anmeldung: ' . $exception->getMessage()
|
'Technischer Fehler bei der Anmeldung. Bitte versuchen Sie es später erneut '
|
||||||
|
. 'oder wenden Sie sich an den Administrator.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,15 +130,15 @@ class AuthController
|
|||||||
// Benutzerinformationen in der Session hinterlegen.
|
// Benutzerinformationen in der Session hinterlegen.
|
||||||
// Diese Daten werden später von requireLogin() bzw. im Layout ausgewertet.
|
// Diese Daten werden später von requireLogin() bzw. im Layout ausgewertet.
|
||||||
$_SESSION[$sessionKey] = [
|
$_SESSION[$sessionKey] = [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'login_at' => date('c'), // ISO-8601 Datum/Zeit der Anmeldung
|
'login_at' => date('c'), // ISO-8601 Datum/Zeit der Anmeldung
|
||||||
];
|
];
|
||||||
|
|
||||||
// Nach erfolgreicher Anmeldung zum Dashboard weiterleiten.
|
// Nach erfolgreicher Anmeldung zum Dashboard weiterleiten.
|
||||||
// Kein direkter header()-Aufruf, sondern ein Redirect-Result
|
// Kein direkter header()-Aufruf, sondern ein Redirect-Result
|
||||||
// für die zentrale Steuerung in index.php.
|
// für die zentrale Steuerung in index.php.
|
||||||
return [
|
return [
|
||||||
'redirect' => 'index.php?route=dashboard',
|
'redirect' => 'index.php?route=dashboard',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +158,7 @@ class AuthController
|
|||||||
|
|
||||||
// Redirect-Result zur Login-Seite.
|
// Redirect-Result zur Login-Seite.
|
||||||
return [
|
return [
|
||||||
'redirect' => 'index.php?route=login',
|
'redirect' => 'index.php?route=login',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Services\Ldap\LdapDirectoryService;
|
use App\Services\Ldap\LdapDirectoryService;
|
||||||
|
use App\Services\Logging\LoggingService;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller für die Benutzer- und Gruppenanzeige.
|
* Controller für die Benutzer- und Gruppenanzeige.
|
||||||
@ -30,6 +32,9 @@ class UserManagementController
|
|||||||
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
|
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
|
||||||
private LdapDirectoryService $directoryService;
|
private LdapDirectoryService $directoryService;
|
||||||
|
|
||||||
|
/** @var LoggingService Logger für technische Fehler */
|
||||||
|
private LoggingService $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
||||||
*/
|
*/
|
||||||
@ -43,6 +48,9 @@ class UserManagementController
|
|||||||
|
|
||||||
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
|
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
|
||||||
$this->directoryService = new LdapDirectoryService($ldapConfig);
|
$this->directoryService = new LdapDirectoryService($ldapConfig);
|
||||||
|
|
||||||
|
// Logging-Service initialisieren.
|
||||||
|
$this->logger = new LoggingService($config['logging'] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,26 +70,35 @@ class UserManagementController
|
|||||||
// Benutzer- und Gruppenlisten aus dem AD laden.
|
// Benutzer- und Gruppenlisten aus dem AD laden.
|
||||||
$users = $this->directoryService->getUsers();
|
$users = $this->directoryService->getUsers();
|
||||||
$groups = $this->directoryService->getGroups();
|
$groups = $this->directoryService->getGroups();
|
||||||
} catch (\Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
// Sämtliche technischen Fehler (z. B. Verbindungs- oder Konfigurationsprobleme)
|
// Technische Details ins Log, für den Benutzer eine allgemeine Meldung.
|
||||||
// werden hier in eine für den Benutzer lesbare Fehlermeldung übersetzt.
|
$this->logger->logException(
|
||||||
$error = 'Fehler beim Laden von Benutzern/Gruppen: ' . $exception->getMessage();
|
'Fehler beim Laden von Benutzern/Gruppen.',
|
||||||
|
$exception,
|
||||||
|
[
|
||||||
|
'route' => 'users',
|
||||||
|
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$error = 'Fehler beim Laden von Benutzern/Gruppen. '
|
||||||
|
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pfad zur eigentlichen View-Datei bestimmen.
|
// Pfad zur eigentlichen View-Datei bestimmen.
|
||||||
$viewPath = __DIR__ . '/../../public/views/users.php';
|
$viewPath = __DIR__ . '/../../public/views/users.php';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'view' => $viewPath,
|
'view' => $viewPath,
|
||||||
'data' => [
|
'data' => [
|
||||||
// Die View erwartet aktuell $users, $groups, $error.
|
// Die View erwartet aktuell $users, $groups, $error.
|
||||||
'users' => $users,
|
'users' => $users,
|
||||||
'groups' => $groups,
|
'groups' => $groups,
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
'loginPage' => false,
|
'loginPage' => false,
|
||||||
],
|
],
|
||||||
'pageTitle' => 'Benutzer & Gruppen',
|
'pageTitle' => 'Benutzer & Gruppen',
|
||||||
'activeMenu' => 'users',
|
'activeMenu' => 'users',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
app/Services/Logging/LoggingService.php
Normal file
134
app/Services/Logging/LoggingService.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Logging;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einfacher File-Logger für die AdminTool-Anwendung.
|
||||||
|
*
|
||||||
|
* Ziele:
|
||||||
|
* - Technische Details werden in eine Log-Datei unter public/logs/ geschrieben.
|
||||||
|
* - In der Weboberfläche erscheinen nur verständliche, fachliche Fehlermeldungen.
|
||||||
|
*/
|
||||||
|
class LoggingService
|
||||||
|
{
|
||||||
|
/** @var string Vollständiger Pfad zum Log-Verzeichnis */
|
||||||
|
private string $logDir;
|
||||||
|
|
||||||
|
/** @var string Dateiname der Log-Datei */
|
||||||
|
private string $logFile;
|
||||||
|
|
||||||
|
/** @var int Minimale Log-Stufe, ab der geschrieben wird. */
|
||||||
|
private int $minLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zuordnung der Log-Level zu numerischen Werten zur Filterung.
|
||||||
|
*
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private const array LEVEL_MAP = [
|
||||||
|
'debug' => 100,
|
||||||
|
'info' => 200,
|
||||||
|
'warning' => 300,
|
||||||
|
'error' => 400,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $config Teilkonfiguration "logging" aus config.php
|
||||||
|
*/
|
||||||
|
public function __construct(array $config)
|
||||||
|
{
|
||||||
|
// Standard: public/logs relativ zum Projektroot
|
||||||
|
$baseDir = $config['log_dir'] ?? (__DIR__ . '/../../../public/logs');
|
||||||
|
$fileName = $config['log_file'] ?? 'app.log';
|
||||||
|
$level = strtolower((string)($config['min_level'] ?? 'info'));
|
||||||
|
|
||||||
|
$this->logDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
|
||||||
|
$this->logFile = $fileName;
|
||||||
|
$this->minLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['info'];
|
||||||
|
|
||||||
|
$this->ensureLogDirectoryExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt sicher, dass das Log-Verzeichnis existiert.
|
||||||
|
*/
|
||||||
|
private function ensureLogDirectoryExists(): void
|
||||||
|
{
|
||||||
|
if (is_dir($this->logDir) === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@mkdir($this->logDir, 0775, true) === false && is_dir($this->logDir) === false) {
|
||||||
|
// Wenn das Anlegen fehlschlägt, wenigstens einen Eintrag im PHP-Error-Log hinterlassen.
|
||||||
|
error_log(sprintf('LoggingService: Konnte Log-Verzeichnis "%s" nicht anlegen.', $this->logDir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allgemeiner Log-Eintrag.
|
||||||
|
*
|
||||||
|
* @param string $level Log-Level (debug|info|warning|error)
|
||||||
|
* @param string $message Nachrichtentext
|
||||||
|
* @param array<string, mixed> $context Zusätzliche Kontextinformationen
|
||||||
|
*/
|
||||||
|
public function log(string $level, string $message, array $context = []): void
|
||||||
|
{
|
||||||
|
$level = strtolower($level);
|
||||||
|
$numericLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['error'];
|
||||||
|
|
||||||
|
// Alles unterhalb der minimalen Stufe ignorieren.
|
||||||
|
if ($numericLevel < $this->minLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = new DateTimeImmutable()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$contextJson = $context === []
|
||||||
|
? '{}'
|
||||||
|
: (string)json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$line = sprintf(
|
||||||
|
"[%s] %-7s %s %s%s",
|
||||||
|
$timestamp,
|
||||||
|
strtoupper($level),
|
||||||
|
$message,
|
||||||
|
$contextJson,
|
||||||
|
PHP_EOL
|
||||||
|
);
|
||||||
|
|
||||||
|
$filePath = $this->logDir . DIRECTORY_SEPARATOR . $this->logFile;
|
||||||
|
|
||||||
|
if (@file_put_contents($filePath, $line, FILE_APPEND | LOCK_EX) === false) {
|
||||||
|
// Fallback, damit Fehler beim Logging selbst nicht die App zerschießen.
|
||||||
|
error_log(sprintf('LoggingService: Konnte in Log-Datei "%s" nicht schreiben.', $filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komfortmethode, um Exceptions strukturiert zu loggen.
|
||||||
|
*
|
||||||
|
* @param string $message Kurzer Kontexttext zur Exception
|
||||||
|
* @param Throwable $exception Die geworfene Exception
|
||||||
|
* @param array<string, mixed> $context Zusätzlicher Kontext (Route, Benutzername, Remote-IP, ...)
|
||||||
|
*/
|
||||||
|
public function logException(string $message, Throwable $exception, array $context = []): void
|
||||||
|
{
|
||||||
|
$exceptionContext = [
|
||||||
|
'exception_class' => get_class($exception),
|
||||||
|
'exception_message' => $exception->getMessage(),
|
||||||
|
'file' => $exception->getFile(),
|
||||||
|
'line' => $exception->getLine(),
|
||||||
|
'trace' => $exception->getTraceAsString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$mergedContext = array_merge($context, $exceptionContext);
|
||||||
|
|
||||||
|
$this->log('error', $message, $mergedContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -49,4 +49,14 @@ return [
|
|||||||
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
|
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Logging-Konfiguration
|
||||||
|
'logging' => [
|
||||||
|
// Standard: public/logs relativ zum Projekt-Root
|
||||||
|
'log_dir' => __DIR__ . '/../public/logs',
|
||||||
|
// Name der Logdatei
|
||||||
|
'log_file' => 'app.log',
|
||||||
|
// Minimale Stufe: debug, info, warning, error
|
||||||
|
'min_level' => 'info',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -23,6 +23,10 @@ declare(strict_types=1);
|
|||||||
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt oder eine bestehende wird fortgesetzt.
|
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt oder eine bestehende wird fortgesetzt.
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
// PHP-Fehler nur ins Log schreiben, nicht direkt im Browser anzeigen
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\".
|
* Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\".
|
||||||
* Statt jede Klasse manuell über "require pfad_zur_klasse.php" einzubinden,
|
* Statt jede Klasse manuell über "require pfad_zur_klasse.php" einzubinden,
|
||||||
@ -68,6 +72,61 @@ $config = require $configPath;
|
|||||||
use App\Controllers\AuthController;
|
use App\Controllers\AuthController;
|
||||||
use App\Controllers\DashboardController;
|
use App\Controllers\DashboardController;
|
||||||
use App\Controllers\UserManagementController;
|
use App\Controllers\UserManagementController;
|
||||||
|
use App\Services\Logging\LoggingService;
|
||||||
|
|
||||||
|
// Globalen Logger initialisieren, damit auch Fehler außerhalb der Controller
|
||||||
|
// (z. B. in index.php selbst) sauber protokolliert werden.
|
||||||
|
$globalLogger = new LoggingService($config['logging'] ?? []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globale Fehlerbehandlung:
|
||||||
|
* - PHP-Fehler (Warnings, Notices, ...) werden in den Logger geschrieben.
|
||||||
|
* - Unbehandelte Exceptions werden ebenfalls geloggt und führen zu einer generischen 500er-Meldung.
|
||||||
|
*/
|
||||||
|
set_error_handler(
|
||||||
|
static function (
|
||||||
|
int $severity,
|
||||||
|
string $message,
|
||||||
|
string $file = '',
|
||||||
|
int $line = 0
|
||||||
|
) use ($globalLogger): bool {
|
||||||
|
// Fehler nur loggen, wenn sie durch error_reporting() nicht unterdrückt sind.
|
||||||
|
if ((error_reporting() & $severity) === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$globalLogger->log(
|
||||||
|
'error',
|
||||||
|
'PHP-Fehler: ' . $message,
|
||||||
|
[
|
||||||
|
'severity' => $severity,
|
||||||
|
'file' => $file,
|
||||||
|
'line' => $line,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// false zurückgeben = PHP darf seinen Standard-Handler zusätzlich verwenden
|
||||||
|
// (der Browser sieht wegen display_errors=0 trotzdem nichts).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
set_exception_handler(
|
||||||
|
static function (Throwable $exception) use ($globalLogger): void {
|
||||||
|
$globalLogger->logException(
|
||||||
|
'Unbehandelte Exception in der Anwendung.',
|
||||||
|
$exception,
|
||||||
|
[
|
||||||
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
|
||||||
|
'route' => $_GET['route'] ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
http_response_code(500);
|
||||||
|
echo 'Es ist ein unerwarteter Fehler aufgetreten. '
|
||||||
|
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
|
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
|
||||||
@ -100,7 +159,7 @@ function handleResult(?array $result): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($result['redirect']) === true) {
|
if (isset($result['redirect']) === true) {
|
||||||
header('Location: ' . (string)$result['redirect']);
|
header('Location: ' . $result['redirect']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user