Compare commits

..

No commits in common. "9e385c0348e9caa0828635b10e3f9d44224fa1e5" and "9ea6c7a528f32d125f6d8da81603c570a315c882" have entirely different histories.

10 changed files with 16 additions and 1019 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@
/.idea/
/.vscode/
/storage/
/config.php
/config.php

View File

@ -1,65 +1,19 @@
# 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.
Ein zentraler Bestandteil der Arbeit mit diesem Repository ist der verbindliche Gitea-Workflow im Wiki.
Ich habe mir auch noch Gedanken zum Git-Workflow gemacht und es nieder geschrieben. So können wir sauber und sicher arbeiten.
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.
**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.
Bitte auch das Wiki beachten und durchlesen, falls nötig!
---
## Ü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
## Aufgaben: ##
| Aufgabe | Benutzer |
| :---- | :---- |
| Benutzer und Gruppen über LDAP anzeigen | Jens E (@blaerf), Stefan W (@viperion) |
@ -69,32 +23,4 @@ Der komplette Ablauf ist im [Gitea-Workflow](https://git.eckertplayground.de/taa
| UI/UX anpassen | Yasin B (@Muchentuchen), Alexander M (@Alexander), Torsten J (@tojacobs) |
---
## 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.
## Infos gibt es im [Wiki](https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/wiki)

View File

@ -1,153 +0,0 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Controllers;
use App\Services\Ldap\LdapDirectoryService;
/**
* Controller für die Benutzer- und Gruppenanzeige.
*
* Aufgaben:
* - holt über den LdapDirectoryService die Listen von Benutzern und Gruppen
* - behandelt technische Fehler und bereitet eine Fehlermeldung für die View auf
* - gibt die Daten an eine View-Datei (public/views/users.php) weiter
*
* WICHTIG:
* - Es werden aktuell nur Daten angezeigt (Read-only).
* - Es findet keine Änderung im Active Directory statt.
*/
class UserManagementController
{
/** @var array<string, mixed> Vollständige Anwendungskonfiguration (aus config.php) */
private array $config;
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
private LdapDirectoryService $directoryService;
/**
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
*/
public function __construct(array $config)
{
// Komplette Konfiguration speichern (falls später weitere Werte benötigt werden).
$this->config = $config;
// LDAP-Konfiguration aus der Gesamt-Konfiguration herausziehen.
$ldapConfig = $config['ldap'] ?? [];
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
$this->directoryService = new LdapDirectoryService($ldapConfig);
}
/**
* Zeigt Benutzer- und Gruppenliste an.
* Wird typischerweise über die Route "users" (index.php?route=users) aufgerufen.
*/
public function show(): void
{
// Standardwerte für die View-Variablen vorbereiten.
$error = null;
$users = [];
$groups = [];
try {
// Benutzer- und Gruppenlisten aus dem AD laden.
$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();
}
// Pfad zur eigentlichen View-Datei bestimmen.
$viewPath = __DIR__ . '/../../public/views/users.php';
// Falls die View-Datei (noch) nicht existiert, Fallback-Ausgabe verwenden.
if (file_exists($viewPath) === false) {
$this->renderInline($users, $groups, $error);
return;
}
// Variablen $users, $groups, $error stehen in der View zur Verfügung,
// weil sie im aktuellen Scope definiert sind.
require $viewPath;
}
/**
* 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

@ -1,37 +1,21 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Services\Ldap;
use RuntimeException;
/**
* Hilfsklasse zum Aufbau einer LDAP/LDAPS-Verbindung.
*
* Aufgaben:
* - liest Server, Port und Timeout aus der LDAP-Konfiguration
* - erstellt eine LDAP-Verbindung
* - setzt die notwendigen Optionen (Protokollversion, Netzwerk-Timeout)
*
* Wichtig:
* - Diese Klasse führt KEIN ldap_bind durch.
* - Das Bind (mit Benutzer- oder Service-Konto) erfolgt in den Fach-Services
* wie LdapAuthService oder LdapDirectoryService.
*/
class LdapConnectionHelper
{
/** @var array<string, mixed> LDAP-spezifische Konfiguration (server, port, timeout, etc.) */
/** @var array<string, mixed> */
private array $config;
/**
* @param array<string, mixed> $ldapConfig Teilbereich "ldap" aus der config.php
* @param array<string, mixed> $ldapConfig
*/
public function __construct(array $ldapConfig)
{
// LDAP-Konfiguration in der Instanz speichern.
// Dadurch steht sie in allen Methoden dieser Klasse zur Verfügung.
$this->config = $ldapConfig;
}
@ -40,43 +24,26 @@ class LdapConnectionHelper
* aber ohne Bind. Den Bind führen die aufrufenden Services durch.
*
* @return resource LDAP-Verbindungs-Handle
*
* @throws RuntimeException wenn der Server nicht konfiguriert ist oder die Verbindung scheitert
*/
public function createConnection()
{
// Server, Port und Timeout aus der Konfiguration lesen.
// Die Null-Koaleszenz-Operatoren (??) setzen Standardwerte, falls Keys fehlen.
$server = (string)($this->config['server'] ?? '');
$port = (int)($this->config['port'] ?? 636);
$timeout = (int)($this->config['timeout'] ?? 5);
// Ohne Server-Adresse kann keine Verbindung aufgebaut werden.
if ($server === '') {
throw new RuntimeException('LDAP-Konfiguration ist unvollständig (server fehlt).');
}
// Verbindung zum LDAP/AD-Server herstellen.
// ldap_connect liefert entweder ein Verbindungs-Handle (Resource) oder false.
$connection = ldap_connect($server, $port);
// Wenn keine Verbindung aufgebaut werden konnte, Exception werfen.
if ($connection === false) {
throw new RuntimeException('LDAP-Verbindung konnte nicht aufgebaut werden.');
}
// LDAP-Version auf 3 setzen, dies ist die gängige Standardversion.
ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3);
// Netzwerk-Timeout setzen, damit Anfragen nicht ewig hängen bleiben.
ldap_set_option($connection, LDAP_OPT_NETWORK_TIMEOUT, $timeout);
// Referral-Following deaktivieren (0 = aus).
// Das verhindert, dass der Client automatisch zu anderen LDAP-Servern weitergeleitet wird.
ldap_set_option($connection, LDAP_OPT_REFERRALS, 0);
// Verbindungs-Handle an aufrufende Services zurückgeben.
// Dort wird dann der eigentliche ldap_bind durchgeführt.
return $connection;
}
}

View File

@ -1,203 +0,0 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Services\Ldap;
use RuntimeException;
/**
* Service zum Lesen von Objekten aus dem Active Directory.
*
* Aktueller Umfang:
* - Liste von Benutzern (sAMAccountName, displayName, mail)
* - Liste von Gruppen (sAMAccountName, cn, description)
*
* Technische Details:
* - Verwendet ein technisches Konto (bind_dn + bind_password) für Lesezugriffe
* - Nutzt LdapConnectionHelper zum Aufbau der Verbindung
*/
class LdapDirectoryService
{
/** @var array<string, mixed> LDAP-Konfiguration (inkl. base_dn, bind_dn, bind_password) */
private array $config;
/** @var LdapConnectionHelper Zentrale Hilfsklasse für den Aufbau von LDAP-Verbindungen */
private LdapConnectionHelper $connectionHelper;
/**
* @param array<string, mixed> $ldapConfig Teilbereich "ldap" aus der config.php
*/
public function __construct(array $ldapConfig)
{
// Vollständige LDAP-Konfiguration speichern.
$this->config = $ldapConfig;
// Gemeinsamen Verbindungs-Helper initialisieren.
// Dieser kümmert sich um ldap_connect und die grundlegenden Optionen.
$this->connectionHelper = new LdapConnectionHelper($ldapConfig);
}
/**
* Stellt eine LDAP-Verbindung her und bindet sich mit dem technischen Konto.
*
* @return resource LDAP-Verbindungs-Handle
*
* @throws RuntimeException wenn Bind-Daten fehlen oder der Bind fehlschlägt
*/
private function connect()
{
// Technischen Bind-DN (Service-Account) aus der Konfiguration lesen.
$bindDn = (string)($this->config['bind_dn'] ?? '');
$bindPassword = (string)($this->config['bind_password'] ?? '');
// Ohne Bind-DN oder Passwort kann kein technischer Bind durchgeführt werden.
if ($bindDn === '' || $bindPassword === '') {
throw new RuntimeException('LDAP-Binddaten für technisches Konto sind nicht konfiguriert.');
}
// Verbindung über den zentralen Helper erstellen (Server, Port, Optionen).
$connection = $this->connectionHelper->createConnection();
// Mit dem technischen Konto binden, um Lesezugriffe durchführen zu können.
// Das @-Zeichen unterdrückt PHP-Warnings bei Fehlversuchen.
if (@ldap_bind($connection, $bindDn, $bindPassword) !== true) {
// Bei fehlgeschlagenem Bind Verbindung wieder schließen.
ldap_unbind($connection);
throw new RuntimeException('LDAP-Bind mit technischem Konto ist fehlgeschlagen.');
}
// Erfolgreich gebundene Verbindung zurückgeben.
return $connection;
}
/**
* Liefert eine Liste von Benutzern aus dem AD.
*
* @return array<int, array<string, string>> Liste von Benutzer-Datensätzen
* [
* [
* 'samaccountname' => 'user1',
* 'displayname' => 'User Eins',
* 'mail' => 'user1@example.local',
* ],
* ...
* ]
*
* @throws RuntimeException bei Konfigurations- oder Verbindungsproblemen
*/
public function getUsers(): array
{
// Base-DN ist der Startpunkt der Suche im Verzeichnisbaum (z. B. DC=...,DC=...).
$baseDn = (string)($this->config['base_dn'] ?? '');
if ($baseDn === '') {
throw new RuntimeException('LDAP base_dn für Benutzersuche ist nicht konfiguriert.');
}
// Verbindung mit technischem Bind herstellen.
$connection = $this->connect();
// Standard-Filter für Benutzer in AD:
// - objectClass=user
// - objectCategory=person
$filter = '(&(objectClass=user)(objectCategory=person))';
// Nur relevante Attribute auslesen, um Datenmenge klein zu halten.
$attributes = ['sAMAccountName', 'displayName', 'mail'];
// LDAP-Suche unterhalb des Base-DN durchführen.
$search = @ldap_search($connection, $baseDn, $filter, $attributes);
if ($search === false) {
// Bei einem Fehler die Verbindung schließen und Exception werfen.
ldap_unbind($connection);
throw new RuntimeException('LDAP-Suche nach Benutzern ist fehlgeschlagen.');
}
// Suchergebnisse in ein PHP-Array umwandeln.
$entries = ldap_get_entries($connection, $search);
// Verbindung wieder schließen, da sie nicht mehr benötigt wird.
ldap_unbind($connection);
// Ergebnisliste für die View vorbereiten.
$users = [];
// Wenn die Struktur unerwartet ist, leere Liste zurückgeben.
if (!is_array($entries) || !isset($entries['count'])) {
return $users;
}
// Jeden einzelnen Eintrag aus dem Ergebnis verarbeiten.
for ($i = 0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
// Attribute sind im Ergebnis verschachtelt, daher Zugriff über [...][0].
$users[] = [
'samaccountname' => isset($entry['samaccountname'][0]) ? (string)$entry['samaccountname'][0] : '',
'displayname' => isset($entry['displayname'][0]) ? (string)$entry['displayname'][0] : '',
'mail' => isset($entry['mail'][0]) ? (string)$entry['mail'][0] : '',
];
}
return $users;
}
/**
* Liefert eine Liste von Gruppen aus dem AD.
*
* @return array<int, array<string, string>> Liste von Gruppen-Datensätzen
*
* @throws RuntimeException bei Konfigurations- oder Verbindungsproblemen
*/
public function getGroups(): array
{
// Base-DN für die Gruppensuche aus der Konfiguration lesen.
$baseDn = (string)($this->config['base_dn'] ?? '');
if ($baseDn === '') {
throw new RuntimeException('LDAP base_dn für Gruppensuche ist nicht konfiguriert.');
}
// Verbindung mit technischem Bind herstellen.
$connection = $this->connect();
// Filter für Gruppenobjekte im AD.
$filter = '(objectClass=group)';
// Attribute, die für die Darstellung relevant sind.
$attributes = ['sAMAccountName', 'cn', 'description'];
// LDAP-Suche ausführen.
$search = @ldap_search($connection, $baseDn, $filter, $attributes);
if ($search === false) {
ldap_unbind($connection);
throw new RuntimeException('LDAP-Suche nach Gruppen ist fehlgeschlagen.');
}
// Ergebnisse holen.
$entries = ldap_get_entries($connection, $search);
// Verbindung schließen.
ldap_unbind($connection);
$groups = [];
// Struktur prüfen, leere Liste zurückgeben, falls unerwartet.
if (!is_array($entries) || !isset($entries['count'])) {
return $groups;
}
// Alle Einträge in ein flacheres Array-Format transformieren.
for ($i = 0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
$groups[] = [
'samaccountname' => isset($entry['samaccountname'][0]) ? (string)$entry['samaccountname'][0] : '',
'cn' => isset($entry['cn'][0]) ? (string)$entry['cn'][0] : '',
'description' => isset($entry['description'][0]) ? (string)$entry['description'][0] : '',
];
}
return $groups;
}
}

View File

@ -18,9 +18,6 @@ return [
// Optional: Timeout in Sekunden
'timeout' => 5,
'bind_dn' => 'CN=Service IIS,OU=WebAppUsers,DC=ITFA-PROJ-DOM,DC=local',
'bind_password' => '$7aE!R$l$D!p1Q9l458K8@O6&',
],
'security' => [

View File

@ -47,7 +47,6 @@ $config = require $configPath;
use App\Controllers\AuthController;
use App\Controllers\DashboardController;
use App\Controllers\UserManagementController;
// Hilfsfunktion für geschützte Routen
function requireLogin(array $config): void
@ -72,9 +71,6 @@ $authController = new AuthController($config);
// Neue Instanz der Klasse DashboardController erstellen (wird bei Bedarf über den Autoloader geladen).
$dashboardController = new DashboardController($config);
// Neue Instanz der Klasse UserManagmentController erstellen (wird bei Bedarf über den Autoloader geladen).
$userManagementController = new UserManagementController($config);
// Anhand des Routing-Ziels (route) entscheiden, welcher Code ausgeführt wird.
switch ($route) {
case 'login':
@ -98,11 +94,6 @@ switch ($route) {
$dashboardController->show();
break;
case 'users':
requireLogin($config);
$userManagementController->show();
break;
default:
http_response_code(404);
echo 'Route nicht gefunden.';

View File

@ -1,6 +1,4 @@
// Call the dataTables jQuery plugin
$(document).ready(function() {
$('#dataTable').DataTable();
$('#usersTable').DataTable();
$('#groupsTable').DataTable();
$('#dataTable').DataTable();
});

View File

@ -36,7 +36,7 @@ declare(strict_types=1);
<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">
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="dashboard.php">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
</div>
@ -48,17 +48,11 @@ declare(strict_types=1);
<!-- Nav Item - Dashboard -->
<li class="nav-item active">
<a class="nav-link" href="../index.php?route=dashboard">
<a class="nav-link" href="dashboard.php">
<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">

View File

@ -1,520 +0,0 @@
<?php
declare(strict_types=1);
/** @var array<int, array<string, string>> $users */
/** @var array<int, array<string, string>> $groups */
/** @var string|null $error */
?>
<!DOCTYPE html>
<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 - 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'); ?>
</p>
<?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 -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Benutzer</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="usersTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Anmeldename</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Anmeldename</th>
<th>Anzeigename</th>
<th>E-Mail</th>
</tr>
</tfoot>
<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>
</div>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Gruppen</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Gruppenname</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Gruppenname</th>
<th>CN</th>
<th>Beschreibung</th>
</tr>
</tfoot>
<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>
</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>
<!-- 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>