Geizkragen/admin_users.php

359 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;">&larr; Zurück zum Profil</a>
</div>
</div>
</section>
</main>
<?php
/** @brief Bindet das allgemeine Footer-Template ein. */
include 'footer.php';
?>