359 lines
19 KiB
PHP
359 lines
19 KiB
PHP
<?php
|
||
/**
|
||
* @file admin_users.php
|
||
* @brief Backend-Skript für die Benutzerverwaltung (Admin-Panel).
|
||
* @author GitHub Copilot
|
||
* @date 2026-04-03
|
||
*
|
||
* @details Ermöglicht es Administratoren, Benutzer anzuzeigen, nach ihnen zu suchen,
|
||
* ihre Rollen zu ändern und sie endgültig zu löschen. Zentrale administrative Komponente
|
||
* des Geizkragen-Projekts.
|
||
*/
|
||
|
||
/**
|
||
* @brief Bindet die Bootstrap-Datei ein, um die grundlegende Konfiguration, Session-Start
|
||
* und Hilfsfunktionen zu laden.
|
||
*/
|
||
require_once __DIR__ . '/lib/bootstrap.php';
|
||
|
||
/**
|
||
* 1) Zugriffskontrolle – nur ADMIN
|
||
* @brief Überprüft, ob der angemeldete Benutzer die "ADMIN"-Rolle besitzt, um unbefugten
|
||
* Zugriff zu verhindern.
|
||
*/
|
||
if (empty($_SESSION['user_id']) || empty($_SESSION['user_roles']) || !in_array('ADMIN', $_SESSION['user_roles'], true)) {
|
||
/// @details Beendet die Skriptausführung mit einer Fehlermeldung, falls die Berechtigungen unzureichend sind.
|
||
/// Das verhindert das Ausführen jeglicher Administrationslogik durch normale Nutzer.
|
||
die("Zugriff verweigert. Nur Administratoren dürfen diese Seite sehen.");
|
||
}
|
||
|
||
/**
|
||
* @var mysqli $conn
|
||
* @brief Datenbankverbindung, um Abfragen auf der Nutzer-Tabelle auszuführen.
|
||
*/
|
||
$conn = db_connect();
|
||
|
||
/**
|
||
* 2) Aktion: Benutzer löschen
|
||
* @brief Verarbeitet Löschanfragen für Benutzer.
|
||
*/
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_user_id'])) {
|
||
/// @var int $deleteId Konvertiert die übergebene Benutzer-ID sicher in einen Integer.
|
||
$deleteId = (int)$_POST['delete_user_id'];
|
||
|
||
/**
|
||
* @brief Vermeidet Selbstlöschung zur Sicherheit.
|
||
*/
|
||
if ($deleteId !== (int)$_SESSION['user_id']) {
|
||
/// Entfernt zunächst die Einträge des Benutzers aus der `userRoles`-Tabelle, um Fremdschlüsselprobleme zu vermeiden.
|
||
$conn->query("DELETE FROM userRoles WHERE userID = $deleteId");
|
||
|
||
/// @var mysqli_stmt $delStmt Bereitet das Lösch-Statement für die `users`-Tabelle vor.
|
||
$delStmt = $conn->prepare("DELETE FROM users WHERE userID = ?");
|
||
/// Bindet die Benutzer-ID (Parameter-Typ i=Integer) an das Statement.
|
||
$delStmt->bind_param("i", $deleteId);
|
||
/// Führt die Löschung in der Datenbank aus.
|
||
$delStmt->execute();
|
||
/// Schließt das Prepared Statement.
|
||
$delStmt->close();
|
||
|
||
/// @var string $successMsg Setzt die Erfolgsmeldung.
|
||
$successMsg = "Benutzer erfolgreich gelöscht.";
|
||
} else {
|
||
/// @var string $errorMsg Setzt die Fehlermeldung (Selbstlöschung unzulässig).
|
||
$errorMsg = "Du kannst dich nicht selbst löschen.";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 2b) Aktion: Rollen aktualisieren
|
||
* @brief Verarbeitet Massen-Aktualisierungen von Benutzerrollen aus dem Formular.
|
||
* @details Diese Aktion wird ausgelöst, wenn der Administrator auf "Alle Rollen speichern" klickt.
|
||
*/
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_all_roles'])) {
|
||
/// @var array $usersRolesData Die neu zugewiesenen Benutzerrollen (ID als Key, Rollen-ID als Value).
|
||
$usersRolesData = isset($_POST['user_roles']) && is_array($_POST['user_roles']) ? $_POST['user_roles'] : [];
|
||
/// @var array $submittedUsers Liste der Benutzer, deren Rollen übermittelt wurden.
|
||
$submittedUsers = isset($_POST['submitted_users']) && is_array($_POST['submitted_users']) ? $_POST['submitted_users'] : [];
|
||
|
||
/// Iteriert über jeden übermittelten Benutzer, um seine Rollenzuweisungen anzupassen.
|
||
foreach ($submittedUsers as $uId) {
|
||
/// @var int $updateId Die ID des aktuellen Benutzers.
|
||
$updateId = (int)$uId;
|
||
|
||
/// Übergeht den Administrator, der die Seite gerade aufruft (Sicherheitsmassnahme).
|
||
if ($updateId === (int)$_SESSION['user_id']) {
|
||
continue;
|
||
}
|
||
|
||
/// @var string $selectedRole Die aus dem Formular ausgewählte Rolle.
|
||
$selectedRole = isset($usersRolesData[$updateId]) ? $usersRolesData[$updateId] : '';
|
||
|
||
/// @var mysqli_stmt $delStmt Entfernt vorab bestehende Rollenzuweisungen des Benutzers aus der DB.
|
||
$delStmt = $conn->prepare("DELETE FROM userRoles WHERE userID = ?");
|
||
$delStmt->bind_param("i", $updateId);
|
||
$delStmt->execute();
|
||
$delStmt->close();
|
||
|
||
/// Falls eine neue Rolle gewählt wurde, fügt diese in die Datenbank ein.
|
||
if (!empty($selectedRole)) {
|
||
/// @var mysqli_stmt $insStmt Bereitet das Insert-Statement für `userRoles` vor.
|
||
$insStmt = $conn->prepare("INSERT INTO userRoles (userID, roleID) VALUES (?, ?)");
|
||
/// @var int $roleIdInt Die Rolle wird sicher auf Integer gecastet.
|
||
$roleIdInt = (int)$selectedRole;
|
||
/// Bindet UserID und Rollen-ID als Integer-Werte an die Query.
|
||
$insStmt->bind_param("ii", $updateId, $roleIdInt);
|
||
$insStmt->execute();
|
||
$insStmt->close();
|
||
}
|
||
}
|
||
/// Setzt Erfolgs-Feedback für den Bereich Rollenverwaltung.
|
||
$successMsg = "Rollen erfolgreich aktualisiert.";
|
||
}
|
||
|
||
/**
|
||
* 2c) Alle verfügbaren Rollen laden
|
||
* @brief Liest alle Rollen aus, um sie in den Dropdowns zur Auswahl anzuzeigen.
|
||
*/
|
||
$allRoles = [];
|
||
/// Startet die Abfrage aller Datensätze der Tabelle `roles`, aufsteigend sortiert nach Namen.
|
||
$rolesQuery = $conn->query("SELECT roleID, name FROM roles ORDER BY name ASC");
|
||
if ($rolesQuery) {
|
||
/// Fügt jede gefundene Rolle dem Array hinzu.
|
||
while ($r = $rolesQuery->fetch_assoc()) {
|
||
$allRoles[] = $r;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 3) Alle Benutzer laden (mit Suche)
|
||
* @brief Verarbeitet Such- und Filterkriterien, um ein Set an Benutzern abzufragen.
|
||
*/
|
||
/// @var string $searchQuery Die übermittelte Suchanfrage.
|
||
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||
/// @var string $searchParam Wird als Parameter für die SQL-Wildcard-Suche vorbereitet.
|
||
$searchParam = '%' . $searchQuery . '%';
|
||
/// @var int $filterRole Ggf. übergebene Rollen-Filterung des Nutzers.
|
||
$filterRole = isset($_GET['role']) ? (int)$_GET['role'] : 0;
|
||
|
||
/// @var string $sql Basis-Select zum Abfragen der Benutzerinformationen und der gruppierten Rollen-IDs.
|
||
$sql = "
|
||
SELECT u.userID, u.email, u.displayname, u.profilePicture, u.isActive,
|
||
GROUP_CONCAT(ur.roleID) as roleIDs
|
||
FROM users u
|
||
LEFT JOIN userRoles ur ON u.userID = ur.userID
|
||
";
|
||
|
||
/// @var array $whereClauses Array, mit dem die WHERE-Bedingungen flexibel aufgebaut werden sollen.
|
||
$whereClauses = [];
|
||
/// @var string $types Zusammenstellung von Typ-Spezifizierern für das `bind_param`.
|
||
$types = "";
|
||
/// @var array $params Liste der Werte, die für Prepared Statements gebunden werden.
|
||
$params = [];
|
||
|
||
/// Sofern die Suchanfrage nicht leer ist, wird nach Displayname und E-Mail gefiltert.
|
||
if ($searchQuery !== '') {
|
||
$whereClauses[] = "(u.displayname LIKE ? OR u.email LIKE ?)";
|
||
$types .= "ss"; // ss referenziert 2 String Parameter
|
||
$params[] = $searchParam;
|
||
$params[] = $searchParam;
|
||
}
|
||
|
||
/// Fügt eine Bedingung hinzu, sofern mit einer bestimmten Rolle gefiltert werden soll.
|
||
if ($filterRole > 0) {
|
||
/**
|
||
* Da wir einen LEFT JOIN mit GROUP_CONCAT haben und auf Rollen filtern wollen,
|
||
* können wir als einfache Lösung einen Subselect für EXISTS machen, damit
|
||
* alle Rollen des Benutzers in GROUP_CONCAT erhalten bleiben,
|
||
* aber nur Nutzer gezeigt werden, die auch die geforderte Rolle haben.
|
||
*/
|
||
$whereClauses[] = "EXISTS (SELECT 1 FROM userRoles sub_ur WHERE sub_ur.userID = u.userID AND sub_ur.roleID = ?)";
|
||
$types .= "i"; // i referenziert einen Integer
|
||
$params[] = $filterRole;
|
||
}
|
||
|
||
/// Erweitert den SQL-String um die zusammengefassten Filter-Clauses.
|
||
if (!empty($whereClauses)) {
|
||
$sql .= " WHERE " . implode(" AND ", $whereClauses);
|
||
}
|
||
|
||
/// Fügt der SQL die finale Gruppierung und Sortierung nach User ID an.
|
||
$sql .= " GROUP BY u.userID ORDER BY u.userID ASC";
|
||
|
||
/// @var mysqli_stmt $stmtUsers Bereitet das komplette Nutzer-Bezugs-Statement vor.
|
||
$stmtUsers = $conn->prepare($sql);
|
||
/// Wenn Bindings existieren, binden wir sie dynamisch via `$params`-Spread-Operator an das SQL-Statement.
|
||
if (!empty($params)) {
|
||
$stmtUsers->bind_param($types, ...$params);
|
||
}
|
||
/// Das vorbereitete SQL ausführen.
|
||
$stmtUsers->execute();
|
||
/// @var mysqli_result $usersResult Empfängt das Result-Set aus der getätigten Fetch-Operation.
|
||
$usersResult = $stmtUsers->get_result();
|
||
|
||
/**
|
||
* @brief Sammelt Filter-Attribute für die Fortführung der URL-Parameter bei Massenänderungen.
|
||
*/
|
||
$formActionParams = [];
|
||
if ($searchQuery !== '') $formActionParams['search'] = $searchQuery;
|
||
if ($filterRole > 0) $formActionParams['role'] = $filterRole;
|
||
|
||
/// @var string $formActionUrl Stellt die Request-URI des Formulars samt GET-Parametern zusammen.
|
||
$formActionUrl = "admin_users.php";
|
||
if (!empty($formActionParams)) {
|
||
$formActionUrl .= "?" . http_build_query($formActionParams);
|
||
}
|
||
|
||
?>
|
||
|
||
<?php
|
||
/** @brief Bindet das allgemeine Header-Template ein.
|
||
* @details Stellt den HTML-Kopf, CSS-Einbindungen und die Navigation bereit.
|
||
*/
|
||
include 'header.php';
|
||
?>
|
||
|
||
<!--
|
||
@brief Hauptcontainer für die Benutzerverwaltungs-Ansicht.
|
||
@details Nutzt die Klasse 'auth' für konsistentes Styling.
|
||
-->
|
||
<main class="auth" role="main">
|
||
<!--
|
||
@brief Abschnitt für das Grid-Layout der Admin-Tabelle.
|
||
@details Ermöglicht horizontales Scrollen auf kleineren Bildschirmen.
|
||
-->
|
||
<section class="auth__grid" style="display: block; max-width: 95%; width: max-content; margin: 40px auto; overflow-x: auto;">
|
||
<div class="auth__card" style="width: 100%; min-width: max-content;">
|
||
<header class="auth__header">
|
||
<h1 class="auth__title">Benutzerverwaltung</h1>
|
||
<p style="text-align: center; color: #94a3b8; font-size: 0.9rem; margin-top: 5px;">Hier siehst du alle registrierten Benutzer.</p>
|
||
</header>
|
||
|
||
<?php
|
||
/** @brief Zeigt ggf. anfallende Erfolgsmeldungen formatiert an. */
|
||
if (!empty($successMsg)): ?>
|
||
<div class="auth__message auth__message--success" style="color: #4ade80; background: #064e3b; padding: 10px; border-radius: 4px; margin-bottom: 15px; text-align: center;"><?= htmlspecialchars($successMsg) ?></div>
|
||
<?php endif; ?>
|
||
<?php
|
||
/** @brief Zeigt ggf. anfallende Fehlermeldungen formatiert an. */
|
||
if (!empty($errorMsg)): ?>
|
||
<div class="auth__message auth__message--error" style="color: #f87171; background: #7f1d1d; padding: 10px; border-radius: 4px; margin-bottom: 15px; text-align: center;"><?= htmlspecialchars($errorMsg) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<form method="get" action="admin_users.php" style="margin-bottom: 20px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||
<input type="text" name="search" placeholder="Suche nach Name oder E-Mail..." value="<?= htmlspecialchars($searchQuery) ?>" style="flex: 1; min-width: 200px; background: #0f172a; color: #f8fafc; border: 1px solid #334155; padding: 10px; border-radius: 4px; font-size: 0.95rem;">
|
||
|
||
<select name="role" style="background: #0f172a; color: #f8fafc; border: 1px solid #334155; padding: 10px; border-radius: 4px; font-size: 0.95rem;">
|
||
<option value="0">Alle Rollen</option>
|
||
<?php foreach ($allRoles as $role): ?>
|
||
<option value="<?= $role['roleID'] ?>" <?= $filterRole === (int)$role['roleID'] ? 'selected' : '' ?>>
|
||
<?= htmlspecialchars($role['name']) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
|
||
<button type="submit" class="auth__submit" style="width: auto; padding: 10px 16px; margin: 0; background-color: #3b82f6;">Filtern</button>
|
||
<?php if ($searchQuery !== '' || $filterRole > 0): ?>
|
||
<a href="admin_users.php" style="color: #94a3b8; text-decoration: none; padding: 10px; font-size: 0.9rem;">Zurücksetzen</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
|
||
<form method="post" action="<?= htmlspecialchars($formActionUrl) ?>">
|
||
<div style="display: flex; justify-content: flex-end; margin-top: 10px;">
|
||
<button type="submit" name="update_all_roles" value="1" style="background-color: #3b82f6; color: white; border: none; padding: 10px 16px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9rem;">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 5px;">
|
||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||
<polyline points="7 3 7 8 15 8"></polyline>
|
||
</svg>
|
||
Alle Rollen speichern
|
||
</button>
|
||
</div>
|
||
|
||
|
||
<div style="margin-top: 20px;">
|
||
<table style="width: 100%; border-collapse: collapse; text-align: left; color: #f8fafc;">
|
||
<thead>
|
||
<tr style="border-bottom: 2px solid #334155;">
|
||
<th style="padding: 12px 10px;">ID</th>
|
||
<th style="padding: 12px 10px;">Profil</th>
|
||
<th style="padding: 12px 10px;">Name</th>
|
||
<th style="padding: 12px 10px;">E-Mail</th>
|
||
<th style="padding: 12px 10px; width: 15%;">Aktuelle Rollen</th>
|
||
<th style="padding: 12px 10px; width: 25%;">Rollen zuweisen</th>
|
||
<th style="padding: 12px 10px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php
|
||
/** @brief Iteriert über die aus der Datenbank empfangene Liste der User. */
|
||
while ($user = $usersResult->fetch_assoc()): ?>
|
||
<?php
|
||
/// @var array $userRoles Explodiert (splittet) die aus den DB-Verknüpfungen abgeleiteten Strings in Arrays.
|
||
$userRoles = !empty($user['roleIDs']) ? explode(',', $user['roleIDs']) : [];
|
||
/// @var bool $isSelf Validiert, ob das bearbeitete Profil zum gerade agierenden Admin-User gehört.
|
||
$isSelf = (int)$user['userID'] === (int)$_SESSION['user_id'];
|
||
?>
|
||
<tr style="border-bottom: 1px solid #1e293b;">
|
||
<td style="padding: 12px 10px;"><?= $user['userID'] ?></td>
|
||
<td style="padding: 12px 10px;">
|
||
<img src="<?= !empty($user['profilePicture']) ? htmlspecialchars($user['profilePicture']) : 'assets/images/placeholder.png' ?>"
|
||
alt="Profil" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; display: block;">
|
||
</td>
|
||
<td style="padding: 12px 10px;"><?= htmlspecialchars($user['displayname']) ?></td>
|
||
<td style="padding: 12px 10px; white-space: nowrap;"><?= htmlspecialchars($user['email']) ?></td>
|
||
|
||
<td style="padding: 12px 10px;">
|
||
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
|
||
<?php foreach ($allRoles as $role): ?>
|
||
<?php if (in_array($role['roleID'], $userRoles)): ?>
|
||
<span style="background-color: #065f46; border: 1px solid #10b981; color: #a7f3d0; padding: 2px 8px; border-radius: 9999px; font-size: 0.75rem; font-weight: bold;"><?= htmlspecialchars($role['name']) ?></span>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($userRoles)): ?>
|
||
<span style="color: #64748b; font-size: 0.75rem; font-style: italic;">Keine</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td>
|
||
|
||
<td style="padding: 12px 10px; min-width: 150px;">
|
||
<?php if (!$isSelf): ?>
|
||
<input type="hidden" name="submitted_users[]" value="<?= $user['userID'] ?>">
|
||
<select name="user_roles[<?= $user['userID'] ?>]" style="background: #0f172a; color: #f8fafc; border: 1px solid #334155; padding: 5px; border-radius: 4px; font-size: 0.85rem; width: 100%;">
|
||
<?php foreach ($allRoles as $role): ?>
|
||
<option value="<?= $role['roleID'] ?>" <?= in_array($role['roleID'], $userRoles) ? 'selected' : '' ?>>
|
||
<?= htmlspecialchars($role['name']) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php else: ?>
|
||
<span style="color: #94a3b8; font-size: 0.85rem; font-style: italic;">Du (Gesperrt)</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
|
||
<td style="padding: 12px 10px;">
|
||
<?php if (!$isSelf): ?>
|
||
<button type="submit" name="delete_user_id" value="<?= $user['userID'] ?>" class="auth__submit" style="background-color: #ef4444; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85rem; width: auto; margin: 0;" onclick="return confirm('Benutzer wirklich löschen? Dies kann nicht rückgängig gemacht werden!');">Löschen</button>
|
||
<?php else: ?>
|
||
<span style="color: #94a3b8; font-size: 0.85rem; padding: 6px 0; display: inline-block;">Das bist du</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endwhile; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="auth__actions" style="margin-top: 30px; text-align: center;">
|
||
<a href="account.php" style="color: #64748b; text-decoration: none; font-size: 0.95rem;">← Zurück zum Profil</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<?php
|
||
/** @brief Bindet das allgemeine Footer-Template ein. */
|
||
include 'footer.php';
|
||
?>
|