Compare commits

...

1 Commits

Author SHA1 Message Date
c490d38457 develop (#24)
Reviewed-on: https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/pulls/24
Co-authored-by: blaerf <blaerf@gmx.de>
Co-committed-by: blaerf <blaerf@gmx.de>
2025-12-05 09:20:21 +00:00
8 changed files with 296 additions and 49 deletions

View File

@ -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',
];
}
}

View File

@ -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<string, mixed> $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',
];
}
}

View File

@ -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) {

View File

@ -0,0 +1,133 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Services\Logging;
use DateTimeImmutable;
/**
* 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 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);
}
}

View File

@ -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',
],
];

View File

@ -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;
}

View File

@ -31,7 +31,7 @@ declare(strict_types=1);
<div class="text-center mb-4">
<h1 class="h4 text-gray-900">Willkommen beim AD Admin Tool</h1>
<p class="text-muted mb-0">
Dies ist unser Testserver für den <code>main</code> Branch.
Dies ist unser Testserver für den <code>develop</code> Branch.
</p>
</div>

View File

@ -44,7 +44,7 @@ declare(strict_types=1);
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="usersTable" width="100%" cellspacing="0">
<table class="table table-bordered" id="usersTable">
<thead>
<tr>
<th><input type="checkbox" name="selectAllUsers"></th>
@ -83,7 +83,7 @@ declare(strict_types=1);
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0">
<table class="table table-bordered" id="groupsTable">
<thead>
<tr>
<th>Gruppenname (sAMAccountName)</th>