Merge branch 'feature/UI-Update' into develop

# Conflicts:
#	public/views/login.php
This commit is contained in:
blaerf 2025-11-28 15:45:02 +01:00
commit ab296ad0dc
16 changed files with 772 additions and 1646 deletions

View File

@ -1,19 +1,65 @@
# PHP_AdminTool_Projekt # PHP_AdminTool_Projekt
Admin Tool Projekt für das Fach PHP
--- Dieses Repository enthält das gemeinsame PHP AdminTool, das auf IIS unter Windows betrieben wird und über LDAP/AD, SNMP und PowerShell administrative Aufgaben ermöglicht. Das Projekt wird von mehreren Entwicklern gepflegt und folgt einem klar definierten Workflow, um Qualität, Stabilität und Nachvollziehbarkeit sicherzustellen.
Ich (@blaerf) habe mich mal hingesetzt und einen Windows Server 2025 als VM aufgesetzt. Darauf habe ich die Active Directory und IIS Rolle installiert.
Zudem habe ich alles so konfiguriert, dass wir PHP mit IIS nutzen können und per LDAPS auf das AD per PHP zugreifen können.
Ich habe mir auch noch Gedanken zum Git-Workflow gemacht und es nieder geschrieben. So können wir sauber und sicher arbeiten. Ein zentraler Bestandteil der Arbeit mit diesem Repository ist der verbindliche Gitea-Workflow im Wiki.
Eine Ordner Struktur habe ich mir auch überlegt und schon mal angelegt. Alle Infos habe ich soweit im Wiki zusammen getragen.
Meine Tests habe ich, wie im Git-workflow beschrieben, in ein eigenes branch (structure/first-structure) gepackt.
Bitte auch das Wiki beachten und durchlesen!
**Hinweis:** Der [Gitea-Workflow](https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/wiki) ist zwingend zu lesen und einzuhalten. Er legt fest, wie Branches erstellt, gemerged und getestet werden.
--- ---
## Aufgaben: ## ## Überblick
Das Projekt dient als webbasiertes Administrationswerkzeug und umfasst unter anderem:
- LDAP/ADAnbindung (LDAPS)
- Benutzer- und Gruppenverwaltung
- SNMPAuswertungen
- PowerShellIntegration
- Weboberfläche zur zentralen Administration
Die Anwendung läuft in zwei Umgebungen:
- Produktion (`main`): https://itfa.schraubenfuzzi.de
- Test (`develop`): https://test.itfa.schraubenfuzzi.de
---
## Ordnerstruktur (Auszug)
- `app/` Anwendungscode
- `public/` Webroot
- `config/` Konfigurationen für LDAP, Systemvariablen usw.
- `scripts/` Hilfs- und Bereitstellungsskripte
- `docs/` interne Dokumente
- `.gitignore` ausgeschlossene Dateien
---
## Workflow
Die Entwicklung folgt einem strengen, verbindlichen Ablauf:
1. **FeatureBranch erstellen** (`feature/name`)
2. Feature entwickeln, committen, pushen
3. **PR nach `develop`** für Tests auf der Testinstanz
- wird vom Autor selbst gemerged
- Branch bleibt bestehen
4. Nach bestandenem Test: **PR nach `main`**
- mit Review
- Merge per Squash
- danach wird der FeatureBranch gelöscht
Details stehen im Wiki.
**Wichtiger Hinweis:**
Der komplette Ablauf ist im [Gitea-Workflow](https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/wiki) beschrieben und muss befolgt werden.
---
## Aufgaben
| Aufgabe | Benutzer | | Aufgabe | Benutzer |
| :---- | :---- | | :---- | :---- |
| Benutzer und Gruppen über LDAP anzeigen | Jens E (@blaerf), Stefan W (@viperion) | | Benutzer und Gruppen über LDAP anzeigen | Jens E (@blaerf), Stefan W (@viperion) |
@ -23,4 +69,32 @@ Bitte auch das Wiki beachten und durchlesen!
| UI/UX anpassen | Yasin B (@Muchentuchen), Alexander M (@Alexander), Torsten J (@tojacobs) | | UI/UX anpassen | Yasin B (@Muchentuchen), Alexander M (@Alexander), Torsten J (@tojacobs) |
--- ---
## Infos gibt es im [Wiki](https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/wiki)
## Dokumentation
Alle weiteren Informationen wie Regeln, technische Details, Konzepte und Anleitungen befinden sich im Wiki:
`Wiki → GiteaWorkflow`
`Wiki → Implementierung / LDAP / IIS / Setup`
Dieser Bereich muss von allen Entwicklern gelesen werden, bevor am Projekt gearbeitet wird.
---
## Mitwirken
Wer etwas ändern oder erweitern möchte:
- Branch vom aktuellen Stand erstellen
- Entwickeln, Committen, Pushen
- PR zuerst nach `develop`, später nach bestandenen Tests nach `main`
- Reviewer zuweisen (für `main`)
Nur tested und reviewed Code gelangt in die Produktion.
---
## Ziel
Das AdminTool soll eine wartbare, erweiterbare und zuverlässige Verwaltungsoberfläche bieten, die zentrale Aufgaben des ADUmfelds über eine moderne Weboberfläche ermöglicht.

View File

@ -12,6 +12,11 @@ use App\Services\Ldap\LdapAuthService;
* - Login-Formular anzeigen * - Login-Formular anzeigen
* - Login-Daten verarbeiten (Authentifizierung gegen LDAP/AD) * - Login-Daten verarbeiten (Authentifizierung gegen LDAP/AD)
* - Logout durchführen * - Logout durchführen
*
* NEU:
* - Statt direkt HTML auszugeben oder header()-Redirects zu setzen,
* liefert der Controller "View-Results" zurück, die von index.php
* und einem zentralen Layout verarbeitet werden.
*/ */
class AuthController class AuthController
{ {
@ -39,33 +44,36 @@ class AuthController
/** /**
* Zeigt das Login-Formular an. * Zeigt das Login-Formular an.
* Optional kann eine Fehlermeldung übergeben werden, die in der View dargestellt wird. * Optional kann eine Fehlermeldung übergeben werden, die in der View dargestellt wird.
*
* @return array<string, mixed> View-Result für das zentrale Layout
*/ */
public function showLoginForm(?string $errorMessage = null): void public function showLoginForm(?string $errorMessage = null): array
{ {
// Pfad zur Login-View (Template-Datei) ermitteln. // Pfad zur Login-View (Template-Datei) ermitteln.
$viewPath = __DIR__ . '/../../public/views/login.php'; $viewPath = __DIR__ . '/../../public/views/login.php';
// Variable für die View vorbereiten (wird in der eingebundenen Datei verwendet). // Wichtig: Die View erwartet aktuell die Variable $error.
$error = $errorMessage; return [
'view' => $viewPath,
// Falls die View-Datei (noch) nicht existiert, einen Fallback-HTML-Output verwenden. 'data' => [
if (file_exists($viewPath) === false) { 'error' => $errorMessage,
$this->renderInlineLogin($error); ],
return; 'pageTitle' => 'Login',
} // Beim Login ist typischerweise kein Menüpunkt aktiv.
'activeMenu' => null,
// View-Datei einbinden. Variablen aus dieser Methode (z. B. $error) sind dort verfügbar. ];
require $viewPath;
} }
/** /**
* Verarbeitet das Login-Formular: * Verarbeitet das Login-Formular:
* - Liest Benutzername und Passwort aus $_POST * - Liest Benutzername und Passwort aus $_POST
* - Ruft den LdapAuthService zur Authentifizierung auf * - Ruft den LdapAuthService zur Authentifizierung auf
* - Setzt bei Erfolg die Session und leitet zum Dashboard weiter * - Liefert bei Erfolg ein Redirect-Result zum Dashboard
* - Zeigt bei Fehlern erneut das Login-Formular mit Fehlermeldung an * - Liefert bei Fehlern ein View-Result für das Login-Formular mit Fehlermeldung
*
* @return array<string, mixed> View-Result ODER Redirect-Result
*/ */
public function processLogin(): void public function processLogin(): array
{ {
// Formulardaten aus dem POST-Request lesen. // Formulardaten aus dem POST-Request lesen.
$username = trim($_POST['username'] ?? ''); $username = trim($_POST['username'] ?? '');
@ -79,14 +87,14 @@ class AuthController
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
// Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration). // Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration).
// In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt. // In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt.
$this->showLoginForm('Technischer Fehler bei der Anmeldung: ' . $exception->getMessage()); return $this->showLoginForm(
return; 'Technischer Fehler bei der Anmeldung: ' . $exception->getMessage()
);
} }
// Fachlich fehlgeschlagene Anmeldung (z. B. falsches Passwort). // Fachlich fehlgeschlagene Anmeldung (z. B. falsches Passwort).
if ($authenticated === false) { if ($authenticated === false) {
$this->showLoginForm('Benutzername oder Passwort ist ungültig.'); return $this->showLoginForm('Benutzername oder Passwort ist ungültig.');
return;
} }
// Ab hier ist die Anmeldung erfolgreich. // Ab hier ist die Anmeldung erfolgreich.
@ -96,22 +104,27 @@ class AuthController
$sessionKey = $this->config['security']['session_key_user'] ?? 'admin_user'; $sessionKey = $this->config['security']['session_key_user'] ?? 'admin_user';
// Benutzerinformationen in der Session hinterlegen. // Benutzerinformationen in der Session hinterlegen.
// Diese Daten werden später von requireLogin() 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.
header('Location: index.php?route=dashboard'); // Kein direkter header()-Aufruf, sondern ein Redirect-Result
exit; // für die zentrale Steuerung in index.php.
return [
'redirect' => 'index.php?route=dashboard',
];
} }
/** /**
* Meldet den aktuell eingeloggten Benutzer ab, indem der entsprechende Session-Eintrag entfernt wird, * Meldet den aktuell eingeloggten Benutzer ab, indem der entsprechende Session-Eintrag entfernt wird,
* und leitet anschließend zurück auf die Login-Seite. * und liefert anschließend ein Redirect-Result zurück auf die Login-Seite.
*
* @return array<string, mixed> Redirect-Result
*/ */
public function logout(): void public function logout(): array
{ {
// Session-Key für den eingeloggten Benutzer aus der Konfiguration lesen. // Session-Key für den eingeloggten Benutzer aus der Konfiguration lesen.
$sessionKey = $this->config['security']['session_key_user'] ?? 'admin_user'; $sessionKey = $this->config['security']['session_key_user'] ?? 'admin_user';
@ -119,49 +132,9 @@ class AuthController
// Eintrag aus der Session entfernen → Benutzer gilt als ausgeloggt. // Eintrag aus der Session entfernen → Benutzer gilt als ausgeloggt.
unset($_SESSION[$sessionKey]); unset($_SESSION[$sessionKey]);
// Zur Login-Seite zurückleiten. // Redirect-Result zur Login-Seite.
header('Location: index.php?route=login'); return [
exit; 'redirect' => 'index.php?route=login',
} ];
/**
* Fallback-Ausgabe für das Login-Formular, falls noch keine separate View-Datei existiert.
* Gibt direkt HTML aus (inline-Template).
*/
private function renderInlineLogin(?string $errorMessage): void
{
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AD Admin Tool Login</title>
</head>
<body>
<h1>AD Admin Tool Login</h1>
<?php if ($errorMessage !== null): ?>
<!-- Fehlermeldung ausgeben, HTML-Ausgabe wird dabei sicher maskiert -->
<p style="color: red;"><?php echo htmlspecialchars($errorMessage, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></p>
<?php endif; ?>
<!-- Einfaches Login-Formular, das per POST an die Route "login.submit" gesendet wird -->
<form action="index.php?route=login.submit" method="post">
<div>
<label for="username">Benutzername:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Passwort:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Anmelden</button>
</form>
</body>
</html>
<?php
} }
} }

View File

@ -12,6 +12,9 @@ use App\Services\Snmp\SnmpServerStatusService;
* Zuständig für: * Zuständig für:
* - Abrufen des Serverstatus (über SnmpServerStatusService) * - Abrufen des Serverstatus (über SnmpServerStatusService)
* - Auswählen und Rendern der Dashboard-View * - Auswählen und Rendern der Dashboard-View
*
* NEU:
* - Gibt ein View-Result zurück, das von index.php + Layout gerendert wird.
*/ */
class DashboardController class DashboardController
{ {
@ -42,8 +45,10 @@ class DashboardController
/** /**
* Zeigt das Dashboard an. * Zeigt das Dashboard an.
* Holt die Serverstatus-Daten aus dem SnmpServerStatusService und übergibt sie an die View. * Holt die Serverstatus-Daten aus dem SnmpServerStatusService und übergibt sie an die View.
*
* @return array<string, mixed> View-Result für das zentrale Layout
*/ */
public function show(): void public function show(): array
{ {
// Serverstatus über den SNMP-Service ermitteln. // Serverstatus über den SNMP-Service ermitteln.
// In der aktuellen Version liefert der Service noch Demo-Daten. // In der aktuellen Version liefert der Service noch Demo-Daten.
@ -52,69 +57,15 @@ class DashboardController
// Pfad zur Dashboard-View (Template-Datei) ermitteln. // Pfad zur Dashboard-View (Template-Datei) ermitteln.
$viewPath = __DIR__ . '/../../public/views/dashboard.php'; $viewPath = __DIR__ . '/../../public/views/dashboard.php';
// Falls die View-Datei (noch) nicht existiert, Fallback-HTML direkt aus dem Controller ausgeben. return [
if (file_exists($viewPath) === false) { 'view' => $viewPath,
$this->renderInlineDashboard($serverStatus); 'data' => [
return; // Die View erwartet aktuell $serverStatus.
} 'serverStatus' => $serverStatus,
],
// View-Datei einbinden. Die Variable $serverStatus steht in der View zur Verfügung. 'pageTitle' => 'Dashboard',
require $viewPath; // In der Sidebar soll der Dashboard-Menüpunkt aktiv sein.
} 'activeMenu' => 'dashboard',
];
/**
* Fallback-Dashboard-Ausgabe direkt aus dem Controller (ohne separate View-Datei).
* Nur als Notlösung gedacht, falls die eigentliche View noch nicht vorhanden ist.
*
* @param array<string, mixed> $serverStatus Serverstatus-Daten (Hostname, CPU%, RAM%, etc.)
*/
private function renderInlineDashboard(array $serverStatus): void
{
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AD Admin Tool Dashboard</title>
</head>
<body>
<h1>Dashboard</h1>
<h2>Serverstatus</h2>
<ul>
<li>
Hostname:
<?php echo htmlspecialchars((string)$serverStatus['hostname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</li>
<li>
Betriebssystem:
<?php echo htmlspecialchars((string)$serverStatus['os'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</li>
<li>
Uptime:
<?php echo htmlspecialchars((string)$serverStatus['uptime'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</li>
<li>
CPU-Auslastung:
<?php echo (int)$serverStatus['cpu_usage']; ?>%
</li>
<li>
RAM-Auslastung:
<?php echo (int)$serverStatus['memory_usage']; ?>%
</li>
<li>
Datenträger C:
<?php echo (int)$serverStatus['disk_usage_c']; ?>%
</li>
<li>
Letzte Aktualisierung:
<?php echo htmlspecialchars((string)$serverStatus['last_update'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</li>
</ul>
<p><a href="index.php?route=logout">Logout</a></p>
</body>
</html>
<?php
} }
} }

View File

@ -18,6 +18,9 @@ use App\Services\Ldap\LdapDirectoryService;
* WICHTIG: * WICHTIG:
* - Es werden aktuell nur Daten angezeigt (Read-only). * - Es werden aktuell nur Daten angezeigt (Read-only).
* - Es findet keine Änderung im Active Directory statt. * - Es findet keine Änderung im Active Directory statt.
*
* NEU:
* - Gibt ein View-Result-Array zurück, das von index.php + Layout gerendert wird.
*/ */
class UserManagementController class UserManagementController
{ {
@ -45,8 +48,10 @@ class UserManagementController
/** /**
* Zeigt Benutzer- und Gruppenliste an. * Zeigt Benutzer- und Gruppenliste an.
* Wird typischerweise über die Route "users" (index.php?route=users) aufgerufen. * Wird typischerweise über die Route "users" (index.php?route=users) aufgerufen.
*
* @return array<string, mixed> View-Result für das zentrale Layout
*/ */
public function show(): void public function show(): array
{ {
// Standardwerte für die View-Variablen vorbereiten. // Standardwerte für die View-Variablen vorbereiten.
$error = null; $error = null;
@ -66,88 +71,16 @@ class UserManagementController
// Pfad zur eigentlichen View-Datei bestimmen. // Pfad zur eigentlichen View-Datei bestimmen.
$viewPath = __DIR__ . '/../../public/views/users.php'; $viewPath = __DIR__ . '/../../public/views/users.php';
// Falls die View-Datei (noch) nicht existiert, Fallback-Ausgabe verwenden. return [
if (file_exists($viewPath) === false) { 'view' => $viewPath,
$this->renderInline($users, $groups, $error); 'data' => [
return; // Die View erwartet aktuell $users, $groups, $error.
} 'users' => $users,
'groups' => $groups,
// Variablen $users, $groups, $error stehen in der View zur Verfügung, 'error' => $error,
// weil sie im aktuellen Scope definiert sind. ],
require $viewPath; 'pageTitle' => 'Benutzer & Gruppen',
} 'activeMenu' => 'users',
];
/**
* Fallback-Ausgabe, falls noch keine View-Datei existiert.
*
* @param array<int, array<string, string>> $users Liste der Benutzer-Datensätze
* @param array<int, array<string, string>> $groups Liste der Gruppen-Datensätze
* @param string|null $error Fehlermeldung (falls vorhanden)
*/
private function renderInline(array $users, array $groups, ?string $error): void
{
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AD Admin Tool Benutzer &amp; Gruppen</title>
</head>
<body>
<h1>Benutzer &amp; Gruppen</h1>
<?php if ($error !== null): ?>
<!-- Fehlermeldung ausgeben, HTML-sicher maskiert -->
<p style="color: red;"><?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></p>
<?php endif; ?>
<h2>Benutzer</h2>
<table border="1" cellpadding="4" cellspacing="0">
<thead>
<tr>
<th>Benutzername (sAMAccountName)</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo htmlspecialchars($user['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($user['displayname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($user['mail'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2>Gruppen</h2>
<table border="1" cellpadding="4" cellspacing="0">
<thead>
<tr>
<th>Gruppenname (sAMAccountName)</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td><?php echo htmlspecialchars($group['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($group['cn'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars($group['description'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<a href="index.php?route=dashboard">Zurück zum Dashboard</a> |
<a href="index.php?route=logout">Logout</a>
</p>
</body>
</html>
<?php
} }
} }

View File

@ -29,38 +29,134 @@ class SnmpServerStatusService
// SNMP-Konfiguration in der Instanz speichern. // SNMP-Konfiguration in der Instanz speichern.
$this->config = $snmpConfig; $this->config = $snmpConfig;
} }
/** /**
* Liefert den aktuellen Serverstatus zurück. * Liefert den aktuellen Serverstatus zurück.
* *
* In dieser Version werden Demo-Daten auf Basis der Konfiguration erzeugt.
* Später können hier echte SNMP-Queries (z. B. über die PHP-SNMP-Extension) verwendet werden.
*
* @return array<string, mixed> Assoziatives Array mit Statuswerten (Hostname, CPU%, RAM%, etc.) * @return array<string, mixed> Assoziatives Array mit Statuswerten (Hostname, CPU%, RAM%, etc.)
* *
* @throws RuntimeException wenn die SNMP-Konfiguration unvollständig ist (z. B. host fehlt) * @throws RuntimeException wenn die SNMP-Konfiguration unvollständig ist oder Abfragen fehlschlagen
*/ */
public function getServerStatus(): array public function getServerStatus(): array
{ {
// Hostnamen aus der SNMP-Konfiguration lesen. // --- 1. Konfiguration auslesen ---
$host = (string)($this->config['host'] ?? ''); $host = (string)($this->config['host'] ?? '127.0.0.1');
$community = (string)($this->config['community'] ?? '');
$oids = $this->config['oids'] ?? [];
$timeout = (int)($this->config['timeout'] ?? 1) * 1_000_000; // in Mikrosekunden
$retries = (int)($this->config['retries'] ?? 1);
// Ohne Host ist keine sinnvolle Abfrage möglich -> Konfigurationsfehler. // --- Prüfungen für essentielle Konfig-Werte ---
if ($host === '') { if ($host === '') {
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (host fehlt).'); throw new RuntimeException('SNMP-Konfiguration ist unvollständig (host fehlt).');
} }
if ($community === '') {
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (community fehlt).');
}
if (empty($oids)) {
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (oids fehlen).');
}
// TODO: Später hier echte SNMP-Abfragen einbauen (z. B. snmp2_get/snmp2_walk). // Helper-Funktion zum Bereinigen von SNMP-Antworten (z.B. "INTEGER: 123" -> 123)
// Die OIDs können aus $this->config['oids'] gelesen werden. $cleanSnmpValue = fn($v) => (int)filter_var($v, FILTER_SANITIZE_NUMBER_INT);
// Für den Anfang verwenden wir dieselben Demo-Werte wie vorher im DashboardController.
// --- 2. Uptime abfragen (war vorher nicht implementiert) ---
$uptimeResult = snmpget($host, $community, $oids['uptime'], $timeout, $retries);
if ($uptimeResult === false) {
throw new RuntimeException("SNMP Uptime GET fehlgeschlagen.");
}
// Uptime (timeticks) in ein lesbares Format umwandeln (optional, hier als String)
// Format ist oft "Timeticks: (12345678) 1 day, 10:17:36.78"
// Wir extrahieren den Teil in Klammern (Hundertstelsekunden)
preg_match('/\((.*?)\)/', $uptimeResult, $matches);
$uptimeTicks = (int)($matches[1] ?? 0);
$uptimeSeconds = $uptimeTicks / 100;
$uptimeFormatted = sprintf(
'%d Tage, %02d:%02d:%02d',
floor($uptimeSeconds / 86400),
floor(($uptimeSeconds % 86400) / 3600),
floor(($uptimeSeconds % 3600) / 60),
$uptimeSeconds % 60
);
// --- 3. CPU ---
$cpuValues = snmpwalk($host, $community, $oids['cpu_table'], $timeout, $retries);
if (!is_array($cpuValues) || empty($cpuValues)) {
throw new RuntimeException("SNMP CPU WALK fehlgeschlagen.");
}
$cpuValues = array_map($cleanSnmpValue, $cpuValues);
$cpuAvg = array_sum($cpuValues) / count($cpuValues);
// --- 4. Memory ---
$memTotalResult = snmpget($host, $community, $oids['mem_size'], $timeout, $retries);
if($memTotalResult === false) {
throw new RuntimeException("SNMP MemTotal GET fehlgeschlagen.");
}
// memTotal in Bytes berechnen (korrigierte Reihenfolge von filter/cast)
$memTotal = $cleanSnmpValue($memTotalResult) * 1024; // KB -> Bytes
// Storage-Tabelle (RAM + Disks)
$descr = snmpwalk($host, $community, $oids['storage_descr'], $timeout, $retries);
$units = snmpwalk($host, $community, $oids['storage_units'], $timeout, $retries);
$size = snmpwalk($host, $community, $oids['storage_size'], $timeout, $retries);
$used = snmpwalk($host, $community, $oids['storage_used'], $timeout, $retries);
if ($descr === false || $units === false || $size === false || $used === false) {
throw new RuntimeException("SNMP Storage WALK fehlgeschlagen.");
}
// Werte bereinigen
// Die SNMP-Antwort enthält "STRING: " und Anführungszeichen, die wir entfernen müssen.
$descr = array_map(fn($v) => trim(str_ireplace('STRING:', '', $v), ' "'), $descr);
$units = array_map($cleanSnmpValue, $units); // Ints
$size = array_map($cleanSnmpValue, $size); // Ints
$used = array_map($cleanSnmpValue, $used); // Ints
// RAM
$ramIndex = array_search("Physical Memory", $descr);
if ($ramIndex === false) {
throw new RuntimeException("Konnte 'Physical Memory' in der SNMP Storage-Tabelle nicht finden.");
}
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
$ramPercent = ($memTotal > 0) ? ($ramUsedBytes / $memTotal) * 100 : 0;
// --- 5. Disk C: ---
$cIndex = false;
foreach ($descr as $index => $description) {
// str_starts_with prüft, ob der String mit C:\ beginnt
if (str_starts_with($description, 'C:\\')) {
$cIndex = $index;
break;
}
}
if ($cIndex === false) {
throw new RuntimeException("Konnte Laufwerk 'C:\' in der SNMP Storage-Tabelle nicht finden.");
}
$cUsed = $units[$cIndex] * $used[$cIndex];
$cTotal = $units[$cIndex] * $size[$cIndex];
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
// --- 6. Status-Array zusammenbauen ---
$status = [ $status = [
'hostname' => $host, 'hostname' => $host,
'os' => 'Windows Server 2025 Datacenter', 'os' => 'Windows Server 2025 Datacenter', // TODO: OS dynamisch abfragen
'uptime' => '3 Tage 12 Stunden', 'uptime' => $uptimeFormatted, // Fehlende Variable hinzugefügt
'cpu_usage' => 23, // CPU-Auslastung in Prozent (Demo) 'cpu_usage' => round($cpuAvg),
'memory_usage' => 62, // RAM-Auslastung in Prozent (Demo) 'memory_usage' => round($ramPercent),
'disk_usage_c' => 71, // Datenträger C in Prozent (Demo) 'disk_usage_c' => round($diskCPercent),
'last_update' => date('d.m.Y H:i:s'), // Zeitpunkt der letzten Aktualisierung 'last_update' => date('d.m.Y H:i:s'),
]; ];
return $status; return $status;

View File

@ -29,7 +29,7 @@ return [
], ],
'snmp' => [ 'snmp' => [
'host' => 'itfa-proj-srv.itfa-proj-dom.local', 'host' => '127.0.0.1',
'community' => 'public_ro', // später: sinnvoller Community-String 'community' => 'public_ro', // später: sinnvoller Community-String
'timeout' => 2, // Sekunden 'timeout' => 2, // Sekunden
'retries' => 1, // Anzahl Wiederholungen 'retries' => 1, // Anzahl Wiederholungen
@ -37,9 +37,16 @@ return [
// Platzhalter für OIDs später können wir die auf echte Werte setzen // Platzhalter für OIDs später können wir die auf echte Werte setzen
'oids' => [ 'oids' => [
'uptime' => '1.3.6.1.2.1.1.3.0', 'uptime' => '1.3.6.1.2.1.1.3.0',
'cpu_usage' => '1.3.6.1.4.1.example.cpu.0',
'memory_usage' => '1.3.6.1.4.1.example.memory.0', // CPU pro Kern
'disk_c' => '1.3.6.1.4.1.example.diskc.0', 'cpu_table' => '1.3.6.1.2.1.25.3.3.1.2',
// Memory
'mem_size' => '1.3.6.1.2.1.25.2.2.0',
'storage_descr' => '1.3.6.1.2.1.25.2.3.1.3',
'storage_units' => '1.3.6.1.2.1.25.2.3.1.4',
'storage_size' => '1.3.6.1.2.1.25.2.3.1.5',
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
], ],
], ],
]; ];

View File

@ -13,7 +13,6 @@ session_start();
* im Namespace "App\..." verwendet wird. * im Namespace "App\..." verwendet wird.
*/ */
spl_autoload_register( spl_autoload_register(
// Anonyme Funktion, die den Klassennamen prüft und den Dateipfad zur Klasse ermittelt.
static function (string $class): void { static function (string $class): void {
$prefix = 'App\\'; $prefix = 'App\\';
$baseDir = __DIR__ . '/../app/'; $baseDir = __DIR__ . '/../app/';
@ -32,6 +31,8 @@ spl_autoload_register(
} }
); );
require __DIR__ . '/views/layout.php';
// Pfad zur Konfigurationsdatei prüfen und Konfiguration laden // Pfad zur Konfigurationsdatei prüfen und Konfiguration laden
$configPath = __DIR__ . '/../config/config.php'; $configPath = __DIR__ . '/../config/config.php';
if (file_exists($configPath) === false) { if (file_exists($configPath) === false) {
@ -49,7 +50,12 @@ use App\Controllers\AuthController;
use App\Controllers\DashboardController; use App\Controllers\DashboardController;
use App\Controllers\UserManagementController; use App\Controllers\UserManagementController;
// Hilfsfunktion für geschützte Routen /**
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
* Wenn nicht, wird auf die Login-Seite umgeleitet.
*
* @param array<string, mixed> $config
*/
function requireLogin(array $config): void function requireLogin(array $config): void
{ {
// session_key_user aus dem Config-Array lesen. Wenn nicht gesetzt oder null, Standard "admin_user" verwenden. // session_key_user aus dem Config-Array lesen. Wenn nicht gesetzt oder null, Standard "admin_user" verwenden.
@ -63,44 +69,77 @@ function requireLogin(array $config): void
} }
} }
// Route aus dem GET-Parameter lesen. Wenn nicht gesetzt, Standardroute "login" verwenden. /**
$route = $_GET['route'] ?? 'login'; * Verarbeitet ein View-Result oder Redirect-Result.
*
* @param array<string, mixed>|null $result
*/
function handleResult(?array $result): void
{
if ($result === null) {
return;
}
// Neue Instanz der Klasse AuthController erstellen (wird bei Bedarf über den Autoloader geladen). if (isset($result['redirect']) === true) {
header('Location: ' . (string)$result['redirect']);
exit;
}
$contentView = (string)($result['view'] ?? '');
$viewData = (array)($result['data'] ?? []);
$pageTitle = (string)($result['pageTitle'] ?? '');
$activeMenu = $result['activeMenu'] ?? null;
if ($contentView === '' || file_exists($contentView) === false) {
http_response_code(500);
echo 'Interner Fehler: Content-View wurde nicht gefunden.';
exit;
}
// Hier rufen wir jetzt die Layout-Funktion aus layout.php auf
renderLayout($contentView, $viewData, $pageTitle, is_string($activeMenu) ? $activeMenu : null);
}
// Controller instanziieren
$authController = new AuthController($config); $authController = new AuthController($config);
// Neue Instanz der Klasse DashboardController erstellen (wird bei Bedarf über den Autoloader geladen).
$dashboardController = new DashboardController($config); $dashboardController = new DashboardController($config);
// Neue Instanz der Klasse UserManagmentController erstellen (wird bei Bedarf über den Autoloader geladen).
$userManagementController = new UserManagementController($config); $userManagementController = new UserManagementController($config);
// Anhand des Routing-Ziels (route) entscheiden, welcher Code ausgeführt wird. // Route bestimmen
$route = $_GET['route'] ?? 'login';
switch ($route) { switch ($route) {
case 'login': case 'login':
$authController->showLoginForm(); // Login-Formular anzeigen (ggf. mit Fehlermeldung)
$result = $authController->showLoginForm();
handleResult($result);
break; break;
case 'login.submit': case 'login.submit':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
// Falscher HTTP-Verb: einfach zurück zum Login
header('Location: index.php?route=login'); header('Location: index.php?route=login');
exit; exit;
} }
$authController->processLogin(); $result = $authController->processLogin();
handleResult($result);
break; break;
case 'logout': case 'logout':
$authController->logout(); $result = $authController->logout();
handleResult($result);
break; break;
case 'dashboard': case 'dashboard':
requireLogin($config); requireLogin($config);
$dashboardController->show(); $result = $dashboardController->show();
handleResult($result);
break; break;
case 'users': case 'users':
requireLogin($config); requireLogin($config);
$userManagementController->show(); $result = $userManagementController->show();
handleResult($result);
break; break;
default: default:
@ -108,3 +147,5 @@ switch ($route) {
echo 'Route nicht gefunden.'; echo 'Route nicht gefunden.';
break; break;
} }

View File

@ -1,792 +1,100 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** @var array<string, mixed> $serverStatus */ /** @var array<string, mixed> $serverStatus */
?> ?>
<!DOCTYPE html>
<html lang="en">
<head> <div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Server-Dashboard</h1>
</div>
<meta charset="utf-8"> <div class="row">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>SB Admin 2 - Dashboard</title> <!-- Hostname -->
<!-- Custom fonts for this template-->
<link href="../vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet">
<!-- Custom styles for this template-->
<link href="../css/sb-admin-2.min.css" rel="stylesheet">
</head>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="../index.php?route=dashboard">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
</div>
<div class="sidebar-brand-text mx-3">SB Admin <sup>2</sup></div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item active">
<a class="nav-link" href="../index.php?route=dashboard">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<li class="nav-item active">
<a class="nav-link" href="../index.php?route=users">
<i class="fas fa-fw fa-users"></i>
<span>Users</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTwo"
aria-expanded="true" aria-controls="collapseTwo">
<i class="fas fa-fw fa-cog"></i>
<span>Components</span>
</a>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Components:</h6>
<a class="collapse-item" href="../buttons.php">Buttons</a>
<a class="collapse-item" href="../cards.php">Cards</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUtilities"
aria-expanded="true" aria-controls="collapseUtilities">
<i class="fas fa-fw fa-wrench"></i>
<span>Utilities</span>
</a>
<div id="collapseUtilities" class="collapse" aria-labelledby="headingUtilities"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Utilities:</h6>
<a class="collapse-item" href="../utilities-color.php">Colors</a>
<a class="collapse-item" href="../utilities-border.php">Borders</a>
<a class="collapse-item" href="../utilities-animation.php">Animations</a>
<a class="collapse-item" href="../utilities-other.php">Other</a>
</div>
</div>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Addons
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
aria-expanded="true" aria-controls="collapsePages">
<i class="fas fa-fw fa-folder"></i>
<span>Pages</span>
</a>
<div id="collapsePages" class="collapse" aria-labelledby="headingPages" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Login Screens:</h6>
<a class="collapse-item" href="login.php">Login</a>
<a class="collapse-item" href="../register.php">Register</a>
<a class="collapse-item" href="../forgot-password.php">Forgot Password</a>
<div class="collapse-divider"></div>
<h6 class="collapse-header">Other Pages:</h6>
<a class="collapse-item" href="../404.php">404 Page</a>
<a class="collapse-item" href="../blank.php">Blank Page</a>
</div>
</div>
</li>
<!-- Nav Item - Charts -->
<li class="nav-item">
<a class="nav-link" href="../charts.php">
<i class="fas fa-fw fa-chart-area"></i>
<span>Charts</span></a>
</li>
<!-- Nav Item - Tables -->
<li class="nav-item">
<a class="nav-link" href="../tables.php">
<i class="fas fa-fw fa-table"></i>
<span>Tables</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
<!-- Sidebar Message -->
<div class="sidebar-card d-none d-lg-flex">
<img class="sidebar-card-illustration mb-2" src="../images/undraw_rocket.svg" alt="...">
<p class="text-center mb-2"><strong>SB Admin Pro</strong> is packed with premium features, components, and more!</p>
<a class="btn btn-success btn-sm" href="https://startbootstrap.com/theme/sb-admin-pro">Upgrade to Pro!</a>
</div>
</ul>
<!-- End of Sidebar -->
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Sidebar Toggle (Topbar) -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Search -->
<form
class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..."
aria-label="Search" aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
<li class="nav-item dropdown no-arrow d-sm-none">
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-search fa-fw"></i>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
aria-labelledby="searchDropdown">
<form class="form-inline mr-auto w-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small"
placeholder="Search for..." aria-label="Search"
aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
</div>
</li>
<!-- Nav Item - Alerts -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bell fa-fw"></i>
<!-- Counter - Alerts -->
<span class="badge badge-danger badge-counter">3+</span>
</a>
<!-- Dropdown - Alerts -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Alerts Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-primary">
<i class="fas fa-file-alt text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 12, 2019</div>
<span class="font-weight-bold">A new monthly report is ready to download!</span>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-success">
<i class="fas fa-donate text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 7, 2019</div>
$290.29 has been deposited into your account!
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-warning">
<i class="fas fa-exclamation-triangle text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 2, 2019</div>
Spending Alert: We've noticed unusually high spending for your account.
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
</div>
</li>
<!-- Nav Item - Messages -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages -->
<span class="badge badge-danger badge-counter">7</span>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="messagesDropdown">
<h6 class="dropdown-header">
Message Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_1.svg"
alt="...">
<div class="status-indicator bg-success"></div>
</div>
<div class="font-weight-bold">
<div class="text-truncate">Hi there! I am wondering if you can help me with a
problem I've been having.</div>
<div class="small text-gray-500">Emily Fowler · 58m</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_2.svg"
alt="...">
<div class="status-indicator"></div>
</div>
<div>
<div class="text-truncate">I have the photos that you ordered last month, how
would you like them sent to you?</div>
<div class="small text-gray-500">Jae Chun · 1d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_3.svg"
alt="...">
<div class="status-indicator bg-warning"></div>
</div>
<div>
<div class="text-truncate">Last month's report looks great, I am very happy with
the progress so far, keep up the good work!</div>
<div class="small text-gray-500">Morgan Alvarez · 2d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60"
alt="...">
<div class="status-indicator bg-success"></div>
</div>
<div>
<div class="text-truncate">Am I a good boy? The reason I ask is because someone
told me that people say this to all dogs, even if they aren't good...</div>
<div class="small text-gray-500">Chicken the Dog · 2w</div>
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Douglas McGee</span>
<img class="img-profile rounded-circle"
src="../images/undraw_profile.svg">
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="userDropdown">
<a class="dropdown-item" href="#">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Settings
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
Activity Log
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Dashboard</h1>
<a href="#" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm"><i
class="fas fa-download fa-sm text-white-50"></i> Generate Report</a>
</div>
<!-- Content Row -->
<div class="row">
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2"> <div class="card border-left-primary shadow h-100 py-2">
<div class="card-body"> <div class="card-body">
<div class="row no-gutters align-items-center"> <div class="row no-gutters align-items-center">
<div class="col mr-2"> <div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">CPU <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
</div> Hostname
<div class="row no-gutters align-items-center">
<div class="col-auto">
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800"><?php echo (int)$serverStatus['cpu_usage']; ?>%</div>
</div>
<div class="col">
<div class="progress progress-sm mr-2">
<div class="progress-bar bg-info" role="progressbar"
style=<?php echo"\"width:". (int)$serverStatus['cpu_usage'] . "%\""; ?> aria-valuenow=<?php echo"\"". (int)$serverStatus['cpu_usage'] . "\""; ?> aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div> </div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo htmlspecialchars((string)($serverStatus['hostname'] ?? 'n/a'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i> <i class="fas fa-server fa-2x text-gray-300"></i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Earnings (Monthly) Card Example --> <!-- Betriebssystem -->
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2"> <div class="card border-left-success shadow h-100 py-2">
<div class="card-body"> <div class="card-body">
<div class="row no-gutters align-items-center"> <div class="row no-gutters align-items-center">
<div class="col mr-2"> <div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">RAM <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
</div> Betriebssystem
<div class="row no-gutters align-items-center">
<div class="col-auto">
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800"><?php echo (int)$serverStatus['memory_usage']; ?>%</div>
</div>
<div class="col">
<div class="progress progress-sm mr-2">
<div class="progress-bar bg-info" role="progressbar"
style=<?php echo"\"width:". (int)$serverStatus['memory_usage'] . "%\""; ?> aria-valuenow=<?php echo"\"". (int)$serverStatus['memory_usage'] . "\""; ?> aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div> </div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo htmlspecialchars((string)($serverStatus['os'] ?? 'n/a'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i> <i class="fas fa-desktop fa-2x text-gray-300"></i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Earnings (Monthly) Card Example --> <!-- CPU-Auslastung -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">HDD (C:)
</div>
<div class="row no-gutters align-items-center">
<div class="col-auto">
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800"><?php echo (int)$serverStatus['disk_usage_c']; ?>%</div>
</div>
<div class="col">
<div class="progress progress-sm mr-2">
<div class="progress-bar bg-info" role="progressbar"
style=<?php echo"\"width:". (int)$serverStatus['disk_usage_c'] . "%\""; ?> aria-valuenow=<?php echo"\"". (int)$serverStatus['disk_usage_c'] . "\""; ?> aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
<div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Pending Requests Card Example -->
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2"> <div class="card border-left-warning shadow h-100 py-2">
<div class="card-body"> <div class="card-body">
<div class="row no-gutters align-items-center"> <div class="row no-gutters align-items-center">
<div class="col mr-2"> <div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1"> <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Pending Requests</div> CPU-Auslastung
<div class="h5 mb-0 font-weight-bold text-gray-800">18</div> </div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?> %
</div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<i class="fas fa-comments fa-2x text-gray-300"></i> <i class="fas fa-microchip fa-2x text-gray-300"></i>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Content Row --> <!-- RAM-Auslastung -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="row"> <div class="card border-left-info shadow h-100 py-2">
<!-- Area Chart -->
<div class="col-xl-8 col-lg-7">
<div class="card shadow mb-4">
<!-- Card Header - Dropdown -->
<div
class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">Earnings Overview</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"
aria-labelledby="dropdownMenuLink">
<div class="dropdown-header">Dropdown Header:</div>
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
</div>
<!-- Card Body -->
<div class="card-body"> <div class="card-body">
<div class="chart-area"> <div class="row no-gutters align-items-center">
<canvas id="myAreaChart"></canvas> <div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
RAM-Auslastung
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo (int)($serverStatus['memory_usage'] ?? 0); ?> %
</div> </div>
</div> </div>
</div> <div class="col-auto">
</div> <i class="fas fa-memory fa-2x text-gray-300"></i>
<!-- Pie Chart -->
<div class="col-xl-4 col-lg-5">
<div class="card shadow mb-4">
<!-- Card Header - Dropdown -->
<div
class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">Revenue Sources</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"
aria-labelledby="dropdownMenuLink">
<div class="dropdown-header">Dropdown Header:</div>
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="chart-pie pt-4 pb-2">
<canvas id="myPieChart"></canvas>
</div>
<div class="mt-4 text-center small">
<span class="mr-2">
<i class="fas fa-circle text-primary"></i> Direct
</span>
<span class="mr-2">
<i class="fas fa-circle text-success"></i> Social
</span>
<span class="mr-2">
<i class="fas fa-circle text-info"></i> Referral
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Content Row --> </div>
<div class="row">
<!-- Content Column --> <!-- Hier kannst du später Charts, weitere Karten usw. anhängen -->
<div class="col-lg-6 mb-4">
<!-- Project Card Example -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Projects</h6>
</div>
<div class="card-body">
<h4 class="small font-weight-bold">Server Migration <span
class="float-right">20%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-danger" role="progressbar" style="width: 20%"
aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Sales Tracking <span
class="float-right">40%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-warning" role="progressbar" style="width: 40%"
aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Customer Database <span
class="float-right">60%</span></h4>
<div class="progress mb-4">
<div class="progress-bar" role="progressbar" style="width: 60%"
aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Payout Details <span
class="float-right">80%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-info" role="progressbar" style="width: 80%"
aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Account Setup <span
class="float-right">Complete!</span></h4>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" style="width: 100%"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
<!-- Color System -->
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card bg-primary text-white shadow">
<div class="card-body">
Primary
<div class="text-white-50 small">#4e73df</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-success text-white shadow">
<div class="card-body">
Success
<div class="text-white-50 small">#1cc88a</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-info text-white shadow">
<div class="card-body">
Info
<div class="text-white-50 small">#36b9cc</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-warning text-white shadow">
<div class="card-body">
Warning
<div class="text-white-50 small">#f6c23e</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-danger text-white shadow">
<div class="card-body">
Danger
<div class="text-white-50 small">#e74a3b</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-secondary text-white shadow">
<div class="card-body">
Secondary
<div class="text-white-50 small">#858796</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-light text-black shadow">
<div class="card-body">
Light
<div class="text-black-50 small">#f8f9fc</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card bg-dark text-white shadow">
<div class="card-body">
Dark
<div class="text-white-50 small">#5a5c69</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<!-- Illustrations -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Illustrations</h6>
</div>
<div class="card-body">
<div class="text-center">
<img class="img-fluid px-3 px-sm-4 mt-3 mb-4" style="width: 25rem;"
src="../images/undraw_posting_photo.svg" alt="...">
</div>
<p>Add some quality, svg illustrations to your project courtesy of <a
target="_blank" rel="nofollow" href="https://undraw.co/">unDraw</a>, a
constantly updated collection of beautiful svg images that you can use
completely free and without attribution!</p>
<a target="_blank" rel="nofollow" href="https://undraw.co/">Browse Illustrations on
unDraw &rarr;</a>
</div>
</div>
<!-- Approach -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Development Approach</h6>
</div>
<div class="card-body">
<p>SB Admin 2 makes extensive use of Bootstrap 4 utility classes in order to reduce
CSS bloat and poor page performance. Custom CSS classes are used to create
custom components and custom utility classes.</p>
<p class="mb-0">Before working with this theme, you should become familiar with the
Bootstrap framework, especially the utility classes.</p>
</div>
</div>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>Copyright &copy; Your Website 2021</span>
</div>
</div>
</footer>
<!-- End of Footer -->
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
</a>
<!-- Logout Modal-->
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="login.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="../vendor/jquery/jquery.min.js"></script>
<script src="../vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="../vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="../js/sb-admin-2.min.js"></script>
<!-- Page level plugins -->
<script src="../vendor/chart.js/Chart.min.js"></script>
<!-- Page level custom scripts -->
<script src="../js/demo/chart-area-demo.js"></script>
<script src="../js/demo/chart-pie-demo.js"></script>
</body>
</html>

43
public/views/layout.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* Zentrales Layout für das AD Admin Tool.
*
* Erwartet (aus index.php übergeben):
* - string $contentView Pfad zur Content-View (z. B. public/views/dashboard.php)
* - array<string, mixed> $viewData Daten-Array für die View
* - string $pageTitle Seitentitel
* - string|null $activeMenu Kennung für die Sidebar (z. B. 'dashboard' oder 'users')
*/
// Daten-Array in einzelne Variablen entpacken,
// sodass die Content-Views direkt mit $users, $groups, $serverStatus, $error etc. arbeiten können.
function renderLayout(string $contentView, array $viewData, string $pageTitle, ?string $activeMenu): void
{
// Daten-Array in einzelne Variablen entpacken
foreach ($viewData as $key => $value) {
if (is_string($key) && $key !== '') {
$cleanKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
if ($cleanKey !== '') {
$$cleanKey = $value;
}
}
}
$partialsPath = __DIR__ . '/partials';
require $partialsPath . '/head.php';
require $partialsPath . '/sidebar.php';
require $partialsPath . '/topbar.php';
?>
<!-- Begin Page Content -->
<div class="container-fluid">
<?php require $contentView; ?>
</div>
<!-- /.container-fluid -->
<?php
require $partialsPath . '/footer.php';
require $partialsPath . '/scripts.php';
}

View File

@ -1,92 +1,61 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /** @var string|null $error */
* @var string|null $error
*/
?> ?>
<!DOCTYPE html> <div class="row justify-content-center">
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>SB Admin 2 - Login</title>
<!-- Custom fonts for this template-->
<link href="vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet">
<!-- Custom styles for this template-->
<link href="css/sb-admin-2.min.css" rel="stylesheet">
</head>
<body class="bg-gradient-primary">
<div class="container">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="col-xl-6 col-lg-7 col-md-8">
<div class="card o-hidden border-0 shadow-lg my-5"> <div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0"> <div class="card-body p-0">
<!-- Nested Row within Card Body --> <!-- Nested Row within Card Body -->
<div class="row"> <div class="row">
<div class="col-lg-6 d-none d-lg-block bg-login-image"> <div class="col-lg-12">
<div class="p-1">
<div class="text-left">
<p class="text-gray-900 mb-4">ITFA-Develop</p>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="p-5"> <div class="p-5">
<div class="text-center"> <div class="text-center mb-4">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1> <h1 class="h4 text-gray-900">Willkommen beim AD Admin Tool</h1>
<p class="text-muted mb-0">
Anmeldung gegen Active Directory per LDAP/LDAPS.
</p>
</div> </div>
<?php if ($error !== null): ?> <?php if ($error !== null): ?>
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?> <?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<form class="user" method="post" action="index.php?route=login.submit">
<form class="user" action="index.php?route=login.submit" method="post" novalidate>
<div class="form-group"> <div class="form-group">
<input type="text" name="username" class="form-control form-control-user" <label for="username">Benutzername</label>
id="username" aria-describedby="usernameHelp" <input type="text"
placeholder="Enter Username..."> class="form-control form-control-user"
id="username"
name="username"
placeholder="z. B. admin"
required>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" name="password" class="form-control form-control-user" <label for="password">Passwort</label>
id="password" placeholder="Password"> <input type="password"
</div> class="form-control form-control-user"
<div class="form-group"> id="password"
<div class="custom-control custom-checkbox small"> name="password"
<input type="checkbox" class="custom-control-input" id="customCheck"> placeholder="Passwort"
<label class="custom-control-label" for="customCheck">Remember required>
Me</label>
</div>
</div> </div>
<button type="submit" class="btn btn-primary btn-user btn-block"> <button type="submit" class="btn btn-primary btn-user btn-block">
Anmelden Anmelden
</button> </button>
</form> </form>
<hr> <hr>
<div class="text-center"> <p class="small text-muted mb-0">
<a class="small" href="forgot-password.php">Forgot Password?</a> Die Zugangsdaten werden nicht gespeichert sie werden nur zur Anmeldung am AD verwendet.
</div> </p>
<div class="text-center">
<a class="small" href="register.php">Create an Account!</a>
</div> </div>
</div> </div>
</div> </div>
@ -94,22 +63,4 @@ declare(strict_types=1);
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="vendor/jquery/jquery.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="js/sb-admin-2.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
<?php
// public/views/partials/footer.php
?>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>&copy; AD Admin Tool <?= date('Y') ?></span>
</div>
</div>
</footer>
<!-- End of Footer -->
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->

View File

@ -0,0 +1,23 @@
<?php
// public/views/partials/head.php
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AD Admin Tool<?= isset($pageTitle) ? ' ' . htmlspecialchars($pageTitle, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : '' ?></title>
<!-- Custom fonts for this template-->
<link href="../../vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<!-- Google Fonts oder lokal, je nach Setup -->
<link href="../../css/sb-admin-2.min.css" rel="stylesheet">
<!-- DataTables CSS (falls benötigt) -->
<link href="../../vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
</head>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">

View File

@ -0,0 +1,30 @@
<?php
// public/views/partials/scripts.php
?>
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
</a>
<!-- Bootstrap core JavaScript-->
<script src="../../vendor/jquery/jquery.min.js"></script>
<script src="../../vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="../../vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="../../js/sb-admin-2.min.js"></script>
<!-- Page level plugins -->
<script src="../../vendor/chart.js/Chart.min.js"></script>
<script src="../../vendor/datatables/jquery.dataTables.min.js"></script>
<script src="../../vendor/datatables/dataTables.bootstrap4.min.js"></script>
<!-- Page level custom scripts -->
<script src="../../js/demo/datatables-demo.js"></script>
<script src="../../js/demo/chart-area-demo.js"></script>
<script src="../../js/demo/chart-pie-demo.js"></script>
</body>
</html>

View File

@ -0,0 +1,49 @@
<?php
// public/views/partials/sidebar.php
?>
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="../../index.php?route=dashboard">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-tools"></i>
</div>
<div class="sidebar-brand-text mx-3">AD Admin Tool</div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item<?= (isset($activeMenu) && $activeMenu === 'dashboard') ? ' active' : '' ?>">
<a class="nav-link" href="../../index.php?route=dashboard">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Verzeichnis
</div>
<!-- Nav Item - Benutzer & Gruppen -->
<li class="nav-item<?= (isset($activeMenu) && $activeMenu === 'users') ? ' active' : '' ?>">
<a class="nav-link" href="../../index.php?route=users">
<i class="fas fa-fw fa-users-cog"></i>
<span>Benutzer &amp; Gruppen</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->

View File

@ -0,0 +1,49 @@
<?php
// public/views/partials/topbar.php
?>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Sidebar Toggle (Topbar) -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">
<?= isset($currentUsername)
? htmlspecialchars($currentUsername, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
: 'Administrator' ?>
</span>
<img class="img-profile rounded-circle"
src="../../images/undraw_profile.svg">
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="userDropdown">
<a class="dropdown-item" href="../../index.php?route=logout">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
<!-- Begin Page Content -->
<div class="container-fluid">

View File

@ -1,388 +1,30 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** @var array<int, array<string, string>> $users */ /**
/** @var array<int, array<string, string>> $groups */ * @var array<int, array<string, string>> $users
/** @var string|null $error */ * @var array<int, array<string, string>> $groups
* @var string|null $error
*/
?> ?>
<!DOCTYPE html>
<html lang="en">
<head> <div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Benutzer &amp; Gruppen</h1>
</div>
<meta charset="utf-8"> <?php if ($error !== null): ?>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <div class="alert alert-danger" role="alert">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>SB Admin 2 - Tables</title>
<!-- Custom fonts for this template -->
<link href="../vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet">
<!-- Custom styles for this template -->
<link href="../css/sb-admin-2.min.css" rel="stylesheet">
<!-- Custom styles for this page -->
<link href="../vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
</head>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="../views/dashboard.php">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
</div>
<div class="sidebar-brand-text mx-3">SB Admin <sup>2</sup></div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item">
<a class="nav-link" href="../views/dashboard.php">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTwo"
aria-expanded="true" aria-controls="collapseTwo">
<i class="fas fa-fw fa-cog"></i>
<span>Components</span>
</a>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Components:</h6>
<a class="collapse-item" href="../buttons.php">Buttons</a>
<a class="collapse-item" href="../cards.php">Cards</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUtilities"
aria-expanded="true" aria-controls="collapseUtilities">
<i class="fas fa-fw fa-wrench"></i>
<span>Utilities</span>
</a>
<div id="collapseUtilities" class="collapse" aria-labelledby="headingUtilities"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Custom Utilities:</h6>
<a class="collapse-item" href="../utilities-color.php">Colors</a>
<a class="collapse-item" href="../utilities-border.php">Borders</a>
<a class="collapse-item" href="../utilities-animation.php">Animations</a>
<a class="collapse-item" href="../utilities-other.php">Other</a>
</div>
</div>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Addons
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapsePages"
aria-expanded="true" aria-controls="collapsePages">
<i class="fas fa-fw fa-folder"></i>
<span>Pages</span>
</a>
<div id="collapsePages" class="collapse" aria-labelledby="headingPages" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<h6 class="collapse-header">Login Screens:</h6>
<a class="collapse-item" href="login.php">Login</a>
<a class="collapse-item" href="../register.php">Register</a>
<a class="collapse-item" href="../forgot-password.php">Forgot Password</a>
<div class="collapse-divider"></div>
<h6 class="collapse-header">Other Pages:</h6>
<a class="collapse-item" href="../404.php">404 Page</a>
<a class="collapse-item" href="../blank.php">Blank Page</a>
</div>
</div>
</li>
<!-- Nav Item - Charts -->
<li class="nav-item">
<a class="nav-link" href="../charts.php">
<i class="fas fa-fw fa-chart-area"></i>
<span>Charts</span></a>
</li>
<!-- Nav Item - Tables -->
<li class="nav-item active">
<a class="nav-link" href="../tables.html">
<i class="fas fa-fw fa-table"></i>
<span>Tables</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Sidebar Toggle (Topbar) -->
<form class="form-inline">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
</form>
<!-- Topbar Search -->
<form
class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..."
aria-label="Search" aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
<li class="nav-item dropdown no-arrow d-sm-none">
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-search fa-fw"></i>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
aria-labelledby="searchDropdown">
<form class="form-inline mr-auto w-100 navbar-search">
<div class="input-group">
<input type="text" class="form-control bg-light border-0 small"
placeholder="Search for..." aria-label="Search"
aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
</div>
</li>
<!-- Nav Item - Alerts -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bell fa-fw"></i>
<!-- Counter - Alerts -->
<span class="badge badge-danger badge-counter">3+</span>
</a>
<!-- Dropdown - Alerts -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Alerts Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-primary">
<i class="fas fa-file-alt text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 12, 2019</div>
<span class="font-weight-bold">A new monthly report is ready to download!</span>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-success">
<i class="fas fa-donate text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 7, 2019</div>
$290.29 has been deposited into your account!
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-warning">
<i class="fas fa-exclamation-triangle text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 2, 2019</div>
Spending Alert: We've noticed unusually high spending for your account.
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
</div>
</li>
<!-- Nav Item - Messages -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages -->
<span class="badge badge-danger badge-counter">7</span>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="messagesDropdown">
<h6 class="dropdown-header">
Message Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_1.svg"
alt="...">
<div class="status-indicator bg-success"></div>
</div>
<div class="font-weight-bold">
<div class="text-truncate">Hi there! I am wondering if you can help me with a
problem I've been having.</div>
<div class="small text-gray-500">Emily Fowler · 58m</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_2.svg"
alt="...">
<div class="status-indicator"></div>
</div>
<div>
<div class="text-truncate">I have the photos that you ordered last month, how
would you like them sent to you?</div>
<div class="small text-gray-500">Jae Chun · 1d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="../images/undraw_profile_3.svg"
alt="...">
<div class="status-indicator bg-warning"></div>
</div>
<div>
<div class="text-truncate">Last month's report looks great, I am very happy with
the progress so far, keep up the good work!</div>
<div class="small text-gray-500">Morgan Alvarez · 2d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60"
alt="...">
<div class="status-indicator bg-success"></div>
</div>
<div>
<div class="text-truncate">Am I a good boy? The reason I ask is because someone
told me that people say this to all dogs, even if they aren't good...</div>
<div class="small text-gray-500">Chicken the Dog · 2w</div>
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Douglas McGee</span>
<img class="img-profile rounded-circle"
src="../images/undraw_profile.svg">
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="userDropdown">
<a class="dropdown-item" href="#">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Settings
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
Activity Log
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- Page Heading -->
<h1 class="h3 mb-2 text-gray-800">Benutzer & Gruppen</h1>
<?php if ($error !== null): ?>
<p class="error">
<?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?> <?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</p> </div>
<?php endif; ?> <?php endif; ?>
<p class="mb-4">DataTables is a third party plugin that is used to generate the demo table below.
For more information about DataTables, please visit the <a target="_blank"
href="https://datatables.net">official DataTables documentation</a>.</p>
<!-- DataTales Example --> <p class="mb-4">
<div class="card shadow mb-4"> Die folgenden Tabellen zeigen Benutzern und Gruppen, die über LDAP/LDAPS aus dem Active Directory gelesen wurden.
</p>
<!-- Benutzer-Tabelle -->
<div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Benutzer</h6> <h6 class="m-0 font-weight-bold text-primary">Benutzer</h6>
</div> </div>
@ -391,32 +33,33 @@ declare(strict_types=1);
<table class="table table-bordered" id="usersTable" width="100%" cellspacing="0"> <table class="table table-bordered" id="usersTable" width="100%" cellspacing="0">
<thead> <thead>
<tr> <tr>
<th>Anmeldename</th> <th>Anmeldename (sAMAccountName)</th>
<th>Anzeigename</th> <th>Anzeigename</th>
<th>E-Mail</th> <th>E-Mail</th>
</tr> </tr>
</thead> </thead>
<tfoot>
<tr>
<th>Anmeldename</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</tfoot>
<tbody> <tbody>
<?php foreach ($users as $user): ?> <?php foreach ($users as $user): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($user['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td> <td>
<td><?php echo htmlspecialchars($user['displayname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td> <?php echo htmlspecialchars($user['samaccountname'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
<td><?php echo htmlspecialchars($user['mail'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td> </td>
<td>
<?php echo htmlspecialchars($user['displayname'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</td>
<td>
<?php echo htmlspecialchars($user['mail'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<div class="card shadow mb-4">
<!-- Gruppen-Tabelle -->
<div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Gruppen</h6> <h6 class="m-0 font-weight-bold text-primary">Gruppen</h6>
</div> </div>
@ -425,96 +68,27 @@ declare(strict_types=1);
<table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0"> <table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0">
<thead> <thead>
<tr> <tr>
<th>Gruppenname</th> <th>Gruppenname (sAMAccountName)</th>
<th>CN</th> <th>CN</th>
<th>Beschreibung</th> <th>Beschreibung</th>
</tr> </tr>
</thead> </thead>
<tfoot>
<tr>
<th>Gruppenname</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</tfoot>
<tbody> <tbody>
<?php foreach ($groups as $group): ?> <?php foreach ($groups as $group): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($group['samaccountname'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td> <td>
<td><?php echo htmlspecialchars($group['cn'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td> <?php echo htmlspecialchars($group['samaccountname'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
<td><?php echo htmlspecialchars($group['description'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td> </td>
<td>
<?php echo htmlspecialchars($group['cn'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</td>
<td>
<?php echo htmlspecialchars($group['description'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>Copyright &copy; Your Website 2020</span>
</div>
</div>
</footer>
<!-- End of Footer -->
</div>
<!-- End of Content Wrapper -->
</div> </div>
<!-- End of Page Wrapper -->
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
</a>
<!-- Logout Modal-->
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="login.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="../vendor/jquery/jquery.min.js"></script>
<script src="../vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="../vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="../js/sb-admin-2.min.js"></script>
<!-- Page level plugins -->
<script src="../vendor/datatables/jquery.dataTables.min.js"></script>
<script src="../vendor/datatables/dataTables.bootstrap4.min.js"></script>
<!-- Page level custom scripts -->
<script src="../js/demo/datatables-demo.js"></script>
</body>
</html>