diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index 02e9905..005ea59 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace App\Controllers; use App\Services\Ldap\LdapAuthService; +use App\Services\Logging\LoggingService; /** * Zuständig für alles rund um den Login: @@ -26,6 +27,9 @@ class AuthController /** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */ private LdapAuthService $ldapAuthService; + /** @var LoggingService Logger für technische Fehler */ + private LoggingService $logger; + /** * Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice. * @@ -39,6 +43,9 @@ class AuthController // 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). $this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []); + + // LoggingService mit dem Teilbereich "logging" aus der Konfiguration initialisieren. + $this->logger = new LoggingService($config['logging'] ?? []); } /** @@ -54,14 +61,14 @@ class AuthController // Wichtig: Die View erwartet aktuell die Variable $error. return [ - 'view' => $viewPath, - 'data' => [ - 'error' => $errorMessage, - 'loginPage' => true, - ], - 'pageTitle' => 'Login', + 'view' => $viewPath, + 'data' => [ + 'error' => $errorMessage, + 'loginPage' => true, + ], + 'pageTitle' => 'Login', // Beim Login ist typischerweise kein Menüpunkt aktiv. - 'activeMenu' => null, + 'activeMenu' => null, ]; } @@ -86,10 +93,25 @@ class AuthController // false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch) $authenticated = $this->ldapAuthService->authenticate($username, $password); } catch (\Throwable $exception) { - // Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration). - // In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt. + // HIER ist vorher dein Fehler entstanden: + // - 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( - '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 +129,15 @@ class AuthController // Benutzerinformationen in der Session hinterlegen. // Diese Daten werden später von requireLogin() bzw. im Layout ausgewertet. $_SESSION[$sessionKey] = [ - 'username' => $username, - 'login_at' => date('c'), // ISO-8601 Datum/Zeit der Anmeldung + 'username' => $username, + 'login_at' => date('c'), // ISO-8601 Datum/Zeit der Anmeldung ]; // Nach erfolgreicher Anmeldung zum Dashboard weiterleiten. // Kein direkter header()-Aufruf, sondern ein Redirect-Result // für die zentrale Steuerung in index.php. return [ - 'redirect' => 'index.php?route=dashboard', + 'redirect' => 'index.php?route=dashboard', ]; } @@ -135,7 +157,7 @@ class AuthController // Redirect-Result zur Login-Seite. return [ - 'redirect' => 'index.php?route=login', + 'redirect' => 'index.php?route=login', ]; } } diff --git a/app/Controllers/UserManagementController.php b/app/Controllers/UserManagementController.php index 02d197a..9e35640 100644 --- a/app/Controllers/UserManagementController.php +++ b/app/Controllers/UserManagementController.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace App\Controllers; use App\Services\Ldap\LdapDirectoryService; +use App\Services\Logging\LoggingService; /** * Controller für die Benutzer- und Gruppenanzeige. @@ -30,6 +31,9 @@ class UserManagementController /** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */ private LdapDirectoryService $directoryService; + /** @var LoggingService Logger für technische Fehler */ + private LoggingService $logger; + /** * @param array $config Vollständige Konfiguration aus config.php */ @@ -43,6 +47,9 @@ class UserManagementController // Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt. $this->directoryService = new LdapDirectoryService($ldapConfig); + + // Logging-Service initialisieren. + $this->logger = new LoggingService($config['logging'] ?? []); } /** @@ -63,25 +70,34 @@ class UserManagementController $users = $this->directoryService->getUsers(); $groups = $this->directoryService->getGroups(); } catch (\Throwable $exception) { - // Sämtliche technischen Fehler (z. B. Verbindungs- oder Konfigurationsprobleme) - // werden hier in eine für den Benutzer lesbare Fehlermeldung übersetzt. - $error = 'Fehler beim Laden von Benutzern/Gruppen: ' . $exception->getMessage(); + // Technische Details ins Log, für den Benutzer eine allgemeine Meldung. + $this->logger->logException( + '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. $viewPath = __DIR__ . '/../../public/views/users.php'; return [ - 'view' => $viewPath, - 'data' => [ - // Die View erwartet aktuell $users, $groups, $error. - 'users' => $users, - 'groups' => $groups, - 'error' => $error, - 'loginPage' => false, - ], - 'pageTitle' => 'Benutzer & Gruppen', - 'activeMenu' => 'users', + 'view' => $viewPath, + 'data' => [ + // Die View erwartet aktuell $users, $groups, $error. + 'users' => $users, + 'groups' => $groups, + 'error' => $error, + 'loginPage' => false, + ], + 'pageTitle' => 'Benutzer & Gruppen', + 'activeMenu' => 'users', ]; } } diff --git a/app/Services/Ldap/LdapConnectionHelper.php b/app/Services/Ldap/LdapConnectionHelper.php index abd5b99..87788f1 100644 --- a/app/Services/Ldap/LdapConnectionHelper.php +++ b/app/Services/Ldap/LdapConnectionHelper.php @@ -58,7 +58,8 @@ class LdapConnectionHelper // Verbindung zum LDAP/AD-Server herstellen. // ldap_connect liefert entweder ein Verbindungs-Handle (Resource) oder false. - $connection = ldap_connect($server, $port); + $uri = "ldap://".$server . ':' . $port; + $connection = ldap_connect($uri); // Wenn keine Verbindung aufgebaut werden konnte, Exception werfen. if ($connection === false) { diff --git a/app/Services/Logging/LoggingService.php b/app/Services/Logging/LoggingService.php new file mode 100644 index 0000000..433d1b5 --- /dev/null +++ b/app/Services/Logging/LoggingService.php @@ -0,0 +1,133 @@ + + */ + private const LEVEL_MAP = [ + 'debug' => 100, + 'info' => 200, + 'warning' => 300, + 'error' => 400, + ]; + + /** + * @param array $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 $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 $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); + } +} diff --git a/config/config.php b/config/config.php index a84872e..aebf7fe 100644 --- a/config/config.php +++ b/config/config.php @@ -49,4 +49,14 @@ return [ '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', + ], ]; diff --git a/public/index.php b/public/index.php index d7a3fc1..70dbe3c 100644 --- a/public/index.php +++ b/public/index.php @@ -19,10 +19,15 @@ declare(strict_types=1); * - Alle neuen Routen sollten über den Switch-Block am Ende ergänzt werden. */ - -// 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(); +// PHP-Fehler erfassen, aber veraltete Hinweise (E_DEPRECATED) ignorieren, +// weil sie sonst im Zusammenspiel mit IIS/fastcgi zu 500-Fehlern führen können. +error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED); +ini_set('display_errors', '0'); + /* * Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\". * Statt jede Klasse manuell über "require pfad_zur_klasse.php" einzubinden, @@ -31,7 +36,7 @@ session_start(); */ spl_autoload_register( static function (string $class): void { - $prefix = 'App\\'; + $prefix = 'App\\'; $baseDir = __DIR__ . '/../app/'; $len = strlen($prefix); @@ -40,7 +45,7 @@ spl_autoload_register( } $relativeClass = substr($class, $len); - $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; + $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file) === true) { require $file; @@ -48,11 +53,11 @@ spl_autoload_register( } ); +// Layout-Funktion einbinden (renderLayout) require __DIR__ . '/views/layout.php'; // Die Konfigurationsdatei liefert ein assoziatives Array mit den Teilbereichen -// "ldap", "snmp" und "security" (u. a. Session-Keys, Timeout-Einstellungen, OIDs). - +// "ldap", "snmp", "security" und "logging" (u. a. Session-Keys, Timeout-Einstellungen, OIDs, Log-Pfade). $configPath = __DIR__ . '/../config/config.php'; if (file_exists($configPath) === false) { // Fail fast: ohne Konfiguration macht die App keinen Sinn @@ -68,6 +73,61 @@ $config = require $configPath; use App\Controllers\AuthController; use App\Controllers\DashboardController; 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. @@ -99,20 +159,24 @@ function handleResult(?array $result): void return; } + // Redirect-Result if (isset($result['redirect']) === true) { header('Location: ' . (string)$result['redirect']); exit; } + // View-Result $contentView = (string)($result['view'] ?? ''); $viewData = (array)($result['data'] ?? []); + // Standard: Wir gehen davon aus, dass es KEINE Loginseite ist, // außer der Controller sagt explizit etwas anderes. - if (!array_key_exists('loginPage', $viewData)) { + if (array_key_exists('loginPage', $viewData) === false) { $viewData['loginPage'] = false; } - $pageTitle = (string)($result['pageTitle'] ?? ''); - $activeMenu = $result['activeMenu'] ?? null; + + $pageTitle = (string)($result['pageTitle'] ?? ''); + $activeMenu = $result['activeMenu'] ?? null; if ($contentView === '' || file_exists($contentView) === false) { http_response_code(500); @@ -121,30 +185,33 @@ function handleResult(?array $result): void } // Hier rufen wir jetzt die Layout-Funktion aus layout.php auf - renderLayout($contentView, $viewData, $pageTitle, is_string($activeMenu) ? $activeMenu : null); + renderLayout( + $contentView, + $viewData, + $pageTitle, + is_string($activeMenu) ? $activeMenu : null + ); } // Zentrale Controller der Anwendung initialisieren und ihnen die vollständige Konfiguration übergeben. // Die Controller holen sich daraus bei Bedarf ihre spezifischen Teilkonfigurationen (z. B. "ldap" oder "snmp"). - -$authController = new AuthController($config); -$dashboardController = new DashboardController($config); +// Jeder Controller erzeugt intern seinen eigenen LoggingService aus $config['logging']. +$authController = new AuthController($config); +$dashboardController = new DashboardController($config); $userManagementController = new UserManagementController($config); // Route aus dem Query-Parameter lesen. Standardroute ist "login", // sodass nicht angemeldete Benutzer automatisch auf die Login-Seite geführt werden. - $route = $_GET['route'] ?? 'login'; // Einfache Router-Logik: Jede Route ruft eine Controller-Methode auf und // übergibt deren View-Result an handleResult(). Neue Seiten werden hier ergänzt. - switch ($route) { case 'login': // Login-Formular anzeigen (ggf. mit Fehlermeldung) $result = $authController->showLoginForm(); handleResult($result); - break; + break; case 'login.submit': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { @@ -178,5 +245,3 @@ switch ($route) { echo 'Route nicht gefunden.'; break; } - - diff --git a/public/views/login.php b/public/views/login.php index 3050ab6..2eaa1a9 100644 --- a/public/views/login.php +++ b/public/views/login.php @@ -31,7 +31,7 @@ declare(strict_types=1);

Willkommen beim AD Admin Tool

- Dies ist unser Testserver für den main Branch. + Dies ist unser Testserver für den develop Branch.

diff --git a/public/views/users.php b/public/views/users.php index e666ee1..e95e779 100644 --- a/public/views/users.php +++ b/public/views/users.php @@ -44,7 +44,7 @@ declare(strict_types=1);
- +
@@ -83,7 +83,7 @@ declare(strict_types=1);
-
+
Gruppenname (sAMAccountName)