v2.6.1c (Lyra): Admin gebruikersbeheer opnieuw ontworpen + CLI reset

Nieuwe features:
- Admin gebruikerslijst met zoeken/filter op gebruikersnaam en rol
- Profiel bewerken pagina met wachtwoord wijzigen en rol wijzigen
- Nieuwe gebruiker aanmaken via aparte pagina
- CLI commando cli/reset-admin-password.php voor admin wachtword reset + lockout reset

Architectuur:
- /admin/users: lijst met zoekveld, rol filter, bewerk/verwijder acties
- /admin/users-edit?user=<naam>: profiel, wachtwoord, rol, verwijderen
- /admin/users-new: nieuwe gebruiker aanmaken
- AdminAuth::clearLockout() public methode voor CLI gebruik

Documentatie:
- guide/nl/en/admin-beheerder/gebruikers.md herschreven
- 15 nieuwe admin vertaalkeys in NL/EN/DE
- Release notes: docs/release-notes/v2.6.1c.md

Tests:
- Pentest: 30/30 geslaagd, 0 vulnerabilities
- WCAG 2.1 AA: 25/25 geslaagd, 100% compliance
This commit is contained in:
root
2026-08-17 15:10:40 +00:00
parent c2cf08955b
commit 88fbaa5702
14 changed files with 767 additions and 220 deletions
+147 -24
View File
@@ -328,6 +328,14 @@ switch ($route) {
handleUsers($auth, $appConfig, $twig, $user, $csrf);
break;
case 'users-edit':
handleUsersEdit($auth, $appConfig, $twig, $user, $csrf);
break;
case 'users-new':
handleUsersNew($auth, $appConfig, $twig, $user, $csrf);
break;
case 'guide':
handleGuide($auth, $appConfig, $twig, $user, $csrf);
break;
@@ -1575,35 +1583,15 @@ function handleUsers($auth, $config, $twig, $user, $csrf): void
{
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
$message = 'Ongeldige CSRF token.';
$messageType = 'danger';
} else {
$action = $_POST['action'] ?? '';
if ($action === 'add') {
$newUsername = trim($_POST['new_username'] ?? '');
$newPassword = $_POST['new_password'] ?? '';
$newRole = $_POST['new_role'] ?? 'content-manager';
$newEmail = trim($_POST['new_email'] ?? '');
$newAuthorName = trim($_POST['new_author_name'] ?? '');
$newAuthorEmail = trim($_POST['new_author_email'] ?? '');
$result = $auth->addUser($newUsername, $newPassword, $newRole, $newEmail, $newAuthorName, $newAuthorEmail);
$message = $result['message'];
$messageType = $result['success'] ? 'success' : 'danger';
} elseif ($action === 'profile') {
$profileUser = $_POST['profile_username'] ?? '';
$profileEmail = trim($_POST['profile_email'] ?? '');
$profileAuthorName = trim($_POST['profile_author_name'] ?? '');
$profileAuthorEmail = trim($_POST['profile_author_email'] ?? '');
$result = $auth->updateUserProfile($profileUser, $profileEmail, $profileAuthorName, $profileAuthorEmail);
$message = $result['message'];
$messageType = $result['success'] ? 'success' : 'danger';
} elseif ($action === 'delete') {
if ($action === 'delete') {
$deleteUser = $_POST['delete_username'] ?? '';
if ($deleteUser !== $user['username']) {
$result = $auth->deleteUser($deleteUser);
@@ -1622,17 +1610,152 @@ function handleUsers($auth, $config, $twig, $user, $csrf): void
}
}
}
$route = 'users';
$users = $auth->getUsers();
$roles = AdminAuth::getRoles();
// Search and filter
$search = trim($_GET['search'] ?? '');
$roleFilter = $_GET['role'] ?? '';
if ($search !== '') {
$users = array_filter($users, function($u) use ($search) {
return stripos($u['username'], $search) !== false
|| stripos($u['email'] ?? '', $search) !== false
|| stripos($u['author_name'] ?? '', $search) !== false;
});
}
if ($roleFilter !== '') {
$users = array_filter($users, function($u) use ($roleFilter) {
return $u['role'] === $roleFilter;
});
}
echo $twig->render('pages/users.twig', [
'user' => $user,
'route' => $route,
'csrf_token' => $csrf,
'users' => $users,
'roles' => $roles,
'search' => $search,
'role_filter' => $roleFilter,
'sidebar_color' => getSidebarColor($config),
'needs_editor' => false,
'message' => $message,
'message_type' => $messageType,
]);
}
function handleUsersEdit($auth, $config, $twig, $user, $csrf): void
{
$targetUsername = $_GET['user'] ?? '';
$targetUsername = preg_replace('/[^a-zA-Z0-9_\-]/', '', $targetUsername);
if ($targetUsername === '') {
header('Location: /admin/users');
exit;
}
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
$message = 'Ongeldige CSRF token.';
$messageType = 'danger';
} else {
$action = $_POST['action'] ?? '';
if ($action === 'profile') {
$profileEmail = trim($_POST['profile_email'] ?? '');
$profileAuthorName = trim($_POST['profile_author_name'] ?? '');
$profileAuthorEmail = trim($_POST['profile_author_email'] ?? '');
$result = $auth->updateUserProfile($targetUsername, $profileEmail, $profileAuthorName, $profileAuthorEmail);
$message = $result['message'];
$messageType = $result['success'] ? 'success' : 'danger';
} elseif ($action === 'change_role') {
$newRole = $_POST['new_role'] ?? '';
$result = $auth->changeRole($targetUsername, $newRole);
$message = $result['message'];
$messageType = $result['success'] ? 'success' : 'danger';
} elseif ($action === 'change_password') {
$newPassword = $_POST['new_password'] ?? '';
$result = $auth->changePassword($targetUsername, $newPassword);
$message = $result['message'];
$messageType = $result['success'] ? 'success' : 'danger';
} elseif ($action === 'delete') {
if ($targetUsername !== $user['username']) {
$result = $auth->deleteUser($targetUsername);
header('Location: /admin/users?msg=' . urlencode($result['message']));
exit;
} else {
$message = 'Je kunt jezelf niet verwijderen.';
$messageType = 'danger';
}
}
}
}
$users = $auth->getUsers();
if (!isset($users[$targetUsername])) {
header('Location: /admin/users');
exit;
}
$targetUser = $users[$targetUsername];
$roles = AdminAuth::getRoles();
$isSelf = ($targetUsername === $user['username']);
echo $twig->render('pages/users-edit.twig', [
'user' => $user,
'route' => 'users',
'csrf_token' => $csrf,
'target_user' => $targetUser,
'is_self' => $isSelf,
'roles' => $roles,
'sidebar_color' => getSidebarColor($config),
'needs_editor' => false,
'message' => $message,
'message_type' => $messageType,
]);
}
function handleUsersNew($auth, $config, $twig, $user, $csrf): void
{
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
$message = 'Ongeldige CSRF token.';
$messageType = 'danger';
} else {
$newUsername = trim($_POST['new_username'] ?? '');
$newPassword = $_POST['new_password'] ?? '';
$newRole = $_POST['new_role'] ?? 'content-manager';
$newEmail = trim($_POST['new_email'] ?? '');
$newAuthorName = trim($_POST['new_author_name'] ?? '');
$newAuthorEmail = trim($_POST['new_author_email'] ?? '');
$result = $auth->addUser($newUsername, $newPassword, $newRole, $newEmail, $newAuthorName, $newAuthorEmail);
if ($result['success']) {
header('Location: /admin/users-edit?user=' . urlencode($newUsername) . '&msg=' . urlencode($result['message']));
exit;
}
$message = $result['message'];
$messageType = 'danger';
}
}
$roles = AdminAuth::getRoles();
echo $twig->render('pages/users-new.twig', [
'user' => $user,
'route' => 'users',
'csrf_token' => $csrf,
'roles' => $roles,
'sidebar_color' => getSidebarColor($config),
'needs_editor' => false,
'message' => $message,