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:
@@ -49,6 +49,15 @@
|
|||||||
- [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities)
|
- [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities)
|
||||||
- [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E)
|
- [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E)
|
||||||
|
|
||||||
|
## v2.6.1c (2026-08-17) ✅
|
||||||
|
- [x] Pentest controles uitgevoerd (30/30)
|
||||||
|
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||||
|
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt
|
||||||
|
- [x] Admin gebruikersbeheer opnieuw ontworpen (lijst + zoeken/filter + profiel + wachtwoord)
|
||||||
|
- [x] CLI commando voor admin wachtwoord reset (cli/reset-admin-password.php)
|
||||||
|
- [x] Verslag gemaakt (docs/release-notes/v2.6.1c.md)
|
||||||
|
- [x] Versie verhoogd naar 2.6.1c
|
||||||
|
|
||||||
## v2.6.1 (2026-08-17) ✅
|
## v2.6.1 (2026-08-17) ✅
|
||||||
- [x] Pentest controles uitgevoerd (30/30)
|
- [x] Pentest controles uitgevoerd (30/30)
|
||||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||||
|
|||||||
@@ -432,6 +432,14 @@ class AdminAuth
|
|||||||
file_put_contents($this->lockFile, json_encode($attempts));
|
file_put_contents($this->lockFile, json_encode($attempts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all failed login attempts for a user (public, used by CLI reset).
|
||||||
|
*/
|
||||||
|
public function clearLockout(string $username): void
|
||||||
|
{
|
||||||
|
$this->clearFailedAttempts($username);
|
||||||
|
}
|
||||||
|
|
||||||
private function getFailedAttempts(): array
|
private function getFailedAttempts(): array
|
||||||
{
|
{
|
||||||
if (!file_exists($this->lockFile)) {
|
if (!file_exists($this->lockFile)) {
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
{% extends "layouts/admin.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ ta.edit_profile|default('Profiel bewerken') }}: {{ target_user.username }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-person-gear"></i> {{ ta.edit_profile|default('Profiel bewerken') }}</h2>
|
||||||
|
<a href="/admin/users" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left"></i> {{ ta.back_to_users|default('Terug naar gebruikers') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-person-circle"></i> {{ ta.profile_info|default('Profiel gegevens') }}
|
||||||
|
{% if is_self %}
|
||||||
|
<span class="badge bg-info ms-2">{{ ta.you|default('Jij') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="action" value="profile">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
||||||
|
<input type="text" class="form-control" value="{{ target_user.username }}" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="profile_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
||||||
|
<input type="email" class="form-control" id="profile_email" name="profile_email" value="{{ target_user.email|default('') }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="profile_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
||||||
|
<input type="text" class="form-control" id="profile_author_name" name="profile_author_name" value="{{ target_user.author_name|default('') }}">
|
||||||
|
<small class="form-text text-muted">{{ ta.author_name_help|default('Getoond als auteur op de website.') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="profile_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||||
|
<input type="email" class="form-control" id="profile_author_email" name="profile_author_email" value="{{ target_user.author_email|default('') }}">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-key"></i> {{ ta.change_password|default('Wachtwoord wijzigen') }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}" onsubmit="return validatePassword();">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="action" value="change_password">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_password" class="form-label">{{ ta.new_password|default('Nieuw wachtwoord') }}</label>
|
||||||
|
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="8">
|
||||||
|
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">{{ ta.confirm_password|default('Bevestig wachtwoord') }}</label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="8">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-warning">
|
||||||
|
<i class="bi bi-key"></i> {{ ta.change_password|default('Wachtwoord wijzigen') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-shield-lock"></i> {{ ta.col_role|default('Rol') }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-2"><strong>{{ ta.current_role|default('Huidige rol') }}</strong></p>
|
||||||
|
<p>
|
||||||
|
<span class="badge bg-{{ target_user.role == 'admin' ? 'danger' : (target_user.role == 'content-manager' ? 'primary' : (target_user.role == 'bi-manager' ? 'success' : 'warning')) }}">
|
||||||
|
{{ target_user.role_label|default(target_user.role) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="action" value="change_role">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_role" class="form-label">{{ ta.new_role|default('Nieuwe rol') }}</label>
|
||||||
|
<select class="form-select" id="new_role" name="new_role">
|
||||||
|
{% for roleKey, roleLabel in roles %}
|
||||||
|
<option value="{{ roleKey }}" {{ target_user.role == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="bi bi-check-lg"></i> {{ ta.change|default('Wijzigen') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not is_self %}
|
||||||
|
<div class="card shadow-sm border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> {{ ta.danger_zone|default('Gevarenzone') }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}" onsubmit="return confirm('{{ ta.confirm_delete_user|default('Weet je zeker dat je deze gebruiker wilt verwijderen?') }}')">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<button type="submit" class="btn btn-danger w-100">
|
||||||
|
<i class="bi bi-trash"></i> {{ ta.delete_user|default('Gebruiker verwijderen') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function validatePassword() {
|
||||||
|
var p1 = document.getElementById('new_password').value;
|
||||||
|
var p2 = document.getElementById('confirm_password').value;
|
||||||
|
if (p1 !== p2) {
|
||||||
|
alert('{{ ta.passwords_no_match|default('Wachtwoorden komen niet overeen.') }}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{% extends "layouts/admin.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ ta.new_user|default('Nieuwe gebruiker') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-plus-circle"></i> {{ ta.new_user|default('Nieuwe gebruiker') }}</h2>
|
||||||
|
<a href="/admin/users" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-arrow-left"></i> {{ ta.back_to_users|default('Terug naar gebruikers') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-person-plus"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/admin/users-new">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_username" class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
||||||
|
<input type="text" class="form-control" id="new_username" name="new_username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_password" class="form-label">{{ ta.password|default('Wachtwoord') }}</label>
|
||||||
|
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="8">
|
||||||
|
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
||||||
|
<input type="email" class="form-control" id="new_email" name="new_email">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
||||||
|
<input type="text" class="form-control" id="new_author_name" name="new_author_name">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||||
|
<input type="email" class="form-control" id="new_author_email" name="new_author_email">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_role" class="form-label">{{ ta.col_role|default('Rol') }}</label>
|
||||||
|
<select class="form-select" id="new_role" name="new_role">
|
||||||
|
{% for roleKey, roleLabel in roles %}
|
||||||
|
<option value="{{ roleKey }}" {{ roleKey == 'content-manager' ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
|
||||||
|
</button>
|
||||||
|
<a href="/admin/users" class="btn btn-outline-secondary">
|
||||||
|
{{ ta.cancel|default('Annuleren') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,18 +5,45 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="mb-4"><i class="bi bi-people"></i> {{ ta.users|default('Gebruikers') }}</h2>
|
<h2 class="mb-4"><i class="bi bi-people"></i> {{ ta.users|default('Gebruikers') }}</h2>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="card shadow-sm">
|
||||||
<div class="col-md-7">
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
<div class="card shadow-sm">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<div class="card-header">
|
<i class="bi bi-list"></i> {{ ta.users_list|default('Gebruikerslijst') }}
|
||||||
<i class="bi bi-list"></i> {{ ta.users|default('Gebruikers') }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<a href="/admin/users-new" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-plus-circle"></i> {{ ta.new_user|default('Nieuwe gebruiker') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" action="/admin/users" class="row g-2 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" class="form-control" name="search" value="{{ search|default('') }}" placeholder="{{ ta.search_users|default('Zoek op gebruikersnaam, e-mail of auteur naam...') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select class="form-select" name="role">
|
||||||
|
<option value="">{{ ta.all_roles|default('Alle rollen') }}</option>
|
||||||
|
{% for roleKey, roleLabel in roles %}
|
||||||
|
<option value="{{ roleKey }}" {{ role_filter == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="bi bi-funnel"></i> {{ ta.filter|default('Filteren') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ ta.username|default('Gebruikersnaam') }}</th>
|
<th>{{ ta.username|default('Gebruikersnaam') }}</th>
|
||||||
<th>{{ ta.col_role|default('Rol') }}</th>
|
<th>{{ ta.col_role|default('Rol') }}</th>
|
||||||
|
<th>{{ ta.login_email|default('Login e-mail') }}</th>
|
||||||
<th>{{ ta.col_created|default('Aangemaakt') }}</th>
|
<th>{{ ta.col_created|default('Aangemaakt') }}</th>
|
||||||
<th>{{ ta.col_actions|default('Acties') }}</th>
|
<th>{{ ta.col_actions|default('Acties') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -26,7 +53,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<i class="bi bi-person-circle"></i>
|
<i class="bi bi-person-circle"></i>
|
||||||
{{ username }}
|
<a href="/admin/users-edit?user={{ username }}">{{ username }}</a>
|
||||||
{% if username == user.username %}
|
{% if username == user.username %}
|
||||||
<span class="badge bg-info">{{ ta.you|default('Jij') }}</span>
|
<span class="badge bg-info">{{ ta.you|default('Jij') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -36,152 +63,32 @@
|
|||||||
{{ data.role_label|default(data.role) }}
|
{{ data.role_label|default(data.role) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-muted">{{ data.email|default('-') }}</td>
|
||||||
<td class="text-muted">{{ data.created|default(ta.unknown|default('Onbekend')) }}</td>
|
<td class="text-muted">{{ data.created|default(ta.unknown|default('Onbekend')) }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<a href="/admin/users-edit?user={{ username }}" class="btn btn-sm btn-outline-primary" title="{{ ta.edit_profile|default('Profiel bewerken') }}">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
{% if username != user.username %}
|
{% if username != user.username %}
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#roleModal-{{ username }}">
|
|
||||||
<i class="bi bi-person-gear"></i> {{ ta.col_role|default('Rol') }}
|
|
||||||
</button>
|
|
||||||
<form method="POST" action="/admin/users" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_user|default('Weet je zeker dat je deze gebruiker wilt verwijderen?') }}')">
|
<form method="POST" action="/admin/users" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_user|default('Weet je zeker dat je deze gebruiker wilt verwijderen?') }}')">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<input type="hidden" name="action" value="delete">
|
<input type="hidden" name="action" value="delete">
|
||||||
<input type="hidden" name="delete_username" value="{{ username }}">
|
<input type="hidden" name="delete_username" value="{{ username }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Role change modal -->
|
|
||||||
<div class="modal fade" id="roleModal-{{ username }}" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<form method="POST" action="/admin/users">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
||||||
<input type="hidden" name="action" value="change_role">
|
|
||||||
<input type="hidden" name="role_username" value="{{ username }}">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title"><i class="bi bi-person-gear"></i> {{ ta.change_role|default('Rol wijzigen: ') }}{{ username }}</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">{{ ta.current_role|default('Huidige rol') }}</label>
|
|
||||||
<p><span class="badge bg-secondary">{{ data.role_label|default(data.role) }}</span></p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_role-{{ username }}" class="form-label">{{ ta.new_role|default('Nieuwe rol') }}</label>
|
|
||||||
<select class="form-select" id="new_role-{{ username }}" name="new_role">
|
|
||||||
{% for roleKey, roleLabel in roles %}
|
|
||||||
<option value="{{ roleKey }}" {{ data.role == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
|
||||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.change|default('Wijzigen') }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#profileModal">
|
|
||||||
<i class="bi bi-pencil"></i> {{ ta.edit_profile|default('Profiel bewerken') }}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-muted text-center py-4">{{ ta.no_users|default('Geen gebruikers gevonden.') }}</td>
|
<td colspan="5" class="text-muted text-center py-4">{{ ta.no_users|default('Geen gebruikers gevonden.') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-5">
|
|
||||||
<!-- Profile edit card for current user -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="bi bi-person-circle"></i> {{ ta.my_profile|default('Mijn profiel') }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="/admin/users">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
||||||
<input type="hidden" name="action" value="profile">
|
|
||||||
<input type="hidden" name="profile_username" value="{{ user.username }}">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
|
||||||
<input type="text" class="form-control" value="{{ user.username }}" disabled>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="profile_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
|
||||||
<input type="email" class="form-control" id="profile_email" name="profile_email" value="{{ user.email|default('') }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="profile_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
|
||||||
<input type="text" class="form-control" id="profile_author_name" name="profile_author_name" value="{{ user.author_name|default('') }}">
|
|
||||||
<small class="form-text text-muted">{{ ta.author_name_help|default('Getoond als auteur op de website.') }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="profile_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
|
||||||
<input type="email" class="form-control" id="profile_author_email" name="profile_author_email" value="{{ user.author_email|default('') }}">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
|
||||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- New user card -->
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="bi bi-plus-circle"></i> {{ ta.new_user|default('Nieuwe gebruiker') }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="/admin/users">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
||||||
<input type="hidden" name="action" value="add">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_username" class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
|
||||||
<input type="text" class="form-control" id="new_username" name="new_username" required autofocus>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_password" class="form-label">{{ ta.password|default('Wachtwoord') }}</label>
|
|
||||||
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
|
||||||
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
|
||||||
<input type="email" class="form-control" id="new_email" name="new_email">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
|
||||||
<input type="text" class="form-control" id="new_author_name" name="new_author_name">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
|
||||||
<input type="email" class="form-control" id="new_author_email" name="new_author_email">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_role" class="form-label">{{ ta.col_role|default('Rol') }}</label>
|
|
||||||
<select class="form-select" id="new_role" name="new_role">
|
|
||||||
{% for roleKey, roleLabel in roles %}
|
|
||||||
<option value="{{ roleKey }}" {{ roleKey == 'content-manager' ? 'selected' : '' }}>{{ roleLabel }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
|
||||||
<i class="bi bi-check-lg"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CodePress CMS — Admin wachtwoord reset CLI
|
||||||
|
*
|
||||||
|
* Gebruik:
|
||||||
|
* php cli/reset-admin-password.php [gebruikersnaam] [nieuw_wachtwoord]
|
||||||
|
*
|
||||||
|
* Als geen wachtwoord opgegeven, wordt een random wachtwoord gegenereerd.
|
||||||
|
* Wisst ook brute-force lockout voor de gebruiker.
|
||||||
|
*
|
||||||
|
* Voorbeelden:
|
||||||
|
* php cli/reset-admin-password.php admin
|
||||||
|
* php cli/reset-admin-password.php admin MijnNieuweWachtwoord123
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (php_sapi_name() !== 'cli') {
|
||||||
|
fwrite(STDERR, "Dit script kan alleen via de CLI uitgevoerd worden.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootDir = dirname(__DIR__);
|
||||||
|
require_once $rootDir . '/admin/src/AdminAuth.php';
|
||||||
|
|
||||||
|
$appConfig = require $rootDir . '/admin/config/app.php';
|
||||||
|
|
||||||
|
// Argumenten parsen
|
||||||
|
$username = $argv[1] ?? '';
|
||||||
|
$password = $argv[2] ?? '';
|
||||||
|
|
||||||
|
if ($username === '') {
|
||||||
|
echo "CodePress CMS — Admin wachtwoord reset\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
echo "Gebruik: php cli/reset-admin-password.php [gebruikersnaam] [nieuw_wachtwoord]\n\n";
|
||||||
|
echo "Als geen wachtwoord opgegeven, wordt een random wachtwoord gegenereerd.\n\n";
|
||||||
|
echo "Voorbeelden:\n";
|
||||||
|
echo " php cli/reset-admin-password.php admin\n";
|
||||||
|
echo " php cli/reset-admin-password.php admin MijnNieuweWachtwoord123\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAuth aanmaken (zonder sessie requirements)
|
||||||
|
$auth = new AdminAuth($appConfig);
|
||||||
|
|
||||||
|
// Controleren of de gebruiker bestaat
|
||||||
|
$users = $auth->getUsers();
|
||||||
|
if (!isset($users[$username])) {
|
||||||
|
fwrite(STDERR, "Fout: Gebruiker '$username' niet gevonden.\n");
|
||||||
|
fwrite(STDERR, "Beschikbare gebruikers: " . implode(', ', array_keys($users)) . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wachtwoord genereren als niet opgegeven
|
||||||
|
if ($password === '') {
|
||||||
|
$password = bin2hex(random_bytes(12));
|
||||||
|
$generated = true;
|
||||||
|
} else {
|
||||||
|
$generated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wachtwoord wijzigen
|
||||||
|
$result = $auth->changePassword($username, $password);
|
||||||
|
if (!$result['success']) {
|
||||||
|
fwrite(STDERR, "Fout: " . $result['message'] . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lockout wissen
|
||||||
|
$auth->clearLockout($username);
|
||||||
|
|
||||||
|
echo "CodePress CMS — Admin wachtwoord reset\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
echo "Gebruiker: $username\n";
|
||||||
|
echo "Wachtwoord: $password\n";
|
||||||
|
if ($generated) {
|
||||||
|
echo " (automatisch gegenereerd — bewaar dit veilig)\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
echo "Lockout voor '$username' is gewist.\n";
|
||||||
|
echo "Je kunt nu inloggen via /admin met het nieuwe wachtwoord.\n";
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# CodePress CMS v2.6.1c — Release Notes
|
||||||
|
|
||||||
|
**Release datum:** 2026-08-17
|
||||||
|
**Codename:** Lyra
|
||||||
|
**Status:** stable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nieuwe features
|
||||||
|
|
||||||
|
### Admin gebruikersbeheer volledig opnieuw ontworpen
|
||||||
|
|
||||||
|
De admin/gebruikers pagina is volledig heringericht met drie aparte pagina's:
|
||||||
|
|
||||||
|
#### Gebruikerslijst (`/admin/users`)
|
||||||
|
- Lijst met alle gebruikers (gebruikersnaam, rol, login e-mail, aanmaakdatum, acties)
|
||||||
|
- **Zoekveld** — zoekt op gebruikersnaam, e-mail of auteur naam
|
||||||
|
- **Rol filter** — dropdown om op een specifieke rol te filteren
|
||||||
|
- **"Nieuwe gebruiker" knop** — linkt naar het aanmaakformulier
|
||||||
|
- **Bewerk knop** per gebruiker — linkt naar de profiel pagina
|
||||||
|
- **Verwijder knop** per gebruiker (bevestiging via JavaScript)
|
||||||
|
|
||||||
|
#### Profiel bewerken (`/admin/users-edit?user=<naam>`)
|
||||||
|
- **Profiel gegevens**: login e-mail, auteur naam, auteur e-mail wijzigen
|
||||||
|
- **Wachtwoord wijzigen**: nieuw wachtwoord + bevestiging met JavaScript validatie
|
||||||
|
- **Rol wijzigen**: dropdown met alle rollen, toont huidige rol met gekleurde badge
|
||||||
|
- **Gevarenzone**: gebruiker verwijderen (alleen voor andere gebruikers, niet jezelf)
|
||||||
|
|
||||||
|
#### Nieuwe gebruiker (`/admin/users-new`)
|
||||||
|
- Formulier met gebruikersnaam, wachtwoord, e-mail, auteur naam, auteur e-mail, rol
|
||||||
|
- Na aanmaken → automatische redirect naar profiel pagina van de nieuwe gebruiker
|
||||||
|
|
||||||
|
### CLI commando voor admin wachtwoord reset
|
||||||
|
|
||||||
|
Nieuw CLI script `cli/reset-admin-password.php` om een admin wachtwoord te resetten en brute-force lockout te wissen — handig bij lockout of vergeten wachtwoord:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Met specifiek wachtwoord
|
||||||
|
php cli/reset-admin-password.php admin NieuwWachtwoord123
|
||||||
|
|
||||||
|
# Met automatisch gegenereerd wachtwoord
|
||||||
|
php cli/reset-admin-password.php admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Het commando wijzigt het wachtwoord (als bcrypt hash), wist de lockout en toont het nieuwe wachtwoord.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verbeteringen
|
||||||
|
|
||||||
|
### AdminAuth uitbreiding
|
||||||
|
- Nieuwe public methode `clearLockout(string $username)` — wist failed login attempts (voor CLI gebruik)
|
||||||
|
|
||||||
|
### Handleidingen bijgewerkt
|
||||||
|
- `guide/nl/admin-beheerder/gebruikers.md` — volledig herschreven met nieuwe architectuur (lijst, zoeken/filter, profiel bewerken, CLI reset)
|
||||||
|
- `guide/en/admin-beheerder/gebruikers.md` — Engelse versie bijgewerkt
|
||||||
|
|
||||||
|
### Vertalingen
|
||||||
|
- 15 nieuwe admin vertaalkeys toegevoegd in NL, EN en DE (`users_list`, `search_users`, `all_roles`, `filter`, `back_to_users`, `profile_info`, `change_password`, `new_password`, `confirm_password`, `passwords_no_match`, `danger_zone`, `delete_user`, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beveiliging
|
||||||
|
- Pentest suite: 30/30 tests geslaagd, 0 vulnerabilities
|
||||||
|
- Security headers aanwezig: X-Frame-Options, Content-Security-Policy, X-Content-Type-Options
|
||||||
|
- Geen vulnerabilities gedetecteerd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
- WCAG 2.1 AA: 25/25 tests geslaagd (100%)
|
||||||
|
- ARIA landmarks aanwezig (24 per pagina)
|
||||||
|
- Semantic HTML structuur (header, nav, main, footer)
|
||||||
|
- Skip-to-content links, focus indicators, screen reader support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Systeem vereisten
|
||||||
|
- PHP >= 8.0
|
||||||
|
- Extensies: json, mbstring
|
||||||
|
- Optioneel: opcache (aanbevolen voor performance)
|
||||||
|
- Git (optioneel, voor content versioning feature)
|
||||||
|
- ZipArchive (optioneel, voor ZIP backup/restore feature)
|
||||||
|
- Apache met `mod_rewrite` en `AllowOverride All` (voor clean URLs en asset-serving)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Upgraden
|
||||||
|
1. Pull de nieuwe code
|
||||||
|
2. Test de website
|
||||||
|
3. Het admin wachtwoord kan gereset worden via `php cli/reset-admin-password.php admin <wachtwoord>` indien nodig
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test resultaten samenvatting
|
||||||
|
|
||||||
|
| Test | Resultaat |
|
||||||
|
|------|----------|
|
||||||
|
| Pentest | 30/30 geslaagd, 0 vulnerabilities |
|
||||||
|
| WCAG 2.1 AA Accessibility | 25/25 geslaagd, 100% compliance |
|
||||||
@@ -15,23 +15,63 @@ CodePress has four roles, defined in `AdminAuth::ROLE_PERMISSIONS`:
|
|||||||
|
|
||||||
Roles are displayed with their label via `AdminAuth::ROLE_LABELS`.
|
Roles are displayed with their label via `AdminAuth::ROLE_LABELS`.
|
||||||
|
|
||||||
## Adding a user
|
## Users list (`/admin/users`)
|
||||||
|
|
||||||
1. Go to **Users**
|
The users list shows all users with their username, role, login email and creation date.
|
||||||
2. Enter username
|
|
||||||
3. Choose password (stored as bcrypt hash)
|
|
||||||
4. Select a role
|
|
||||||
5. Click **Add**
|
|
||||||
|
|
||||||
## Editing a user
|
### Search and filter
|
||||||
|
|
||||||
- Change password (new bcrypt hash)
|
- **Search field**: search by username, email or author name
|
||||||
- Change role (immediately affects visible routes and sidebar items)
|
- **Role filter**: filter by a specific role via the dropdown
|
||||||
|
- Click **Filter** to apply the results
|
||||||
|
|
||||||
## Deleting a user
|
### Adding a new user
|
||||||
|
|
||||||
- Not possible for own account
|
1. Click **New user** (top right of the list)
|
||||||
- Confirm with password
|
2. Enter username, password (minimum 8 characters), email, author name and author email
|
||||||
|
3. Select a role
|
||||||
|
4. Click **Add user**
|
||||||
|
5. You will be automatically redirected to the profile page of the new user
|
||||||
|
|
||||||
|
## Editing a profile (`/admin/users-edit?user=<name>`)
|
||||||
|
|
||||||
|
Click on a user in the list to edit their profile. The profile page contains three sections:
|
||||||
|
|
||||||
|
### Profile information
|
||||||
|
- Login email, author name and author email can be changed
|
||||||
|
- The username cannot be changed
|
||||||
|
|
||||||
|
### Change password
|
||||||
|
- Enter a new password (minimum 8 characters)
|
||||||
|
- Confirm the password
|
||||||
|
- The password is stored as a bcrypt hash
|
||||||
|
|
||||||
|
### Change role
|
||||||
|
- Shows the current role with a colored badge
|
||||||
|
- Select a new role from the dropdown
|
||||||
|
- The change immediately affects the visible admin routes and sidebar items
|
||||||
|
|
||||||
|
### Delete user (Danger zone)
|
||||||
|
- Only visible for other users (not for your own account)
|
||||||
|
- Confirmation via JavaScript dialog
|
||||||
|
- After deletion you return to the users list
|
||||||
|
|
||||||
|
## Admin password reset via CLI
|
||||||
|
|
||||||
|
If the admin is locked out (e.g. due to brute-force lockout or forgotten password), the password can be reset via the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset with a specific password
|
||||||
|
php cli/reset-admin-password.php admin NewPassword123
|
||||||
|
|
||||||
|
# Reset with an automatically generated password
|
||||||
|
php cli/reset-admin-password.php admin
|
||||||
|
```
|
||||||
|
|
||||||
|
The command:
|
||||||
|
- Changes the password (as a bcrypt hash)
|
||||||
|
- Clears the brute-force lockout for the user
|
||||||
|
- Displays the new password in the terminal
|
||||||
|
|
||||||
## Access control
|
## Access control
|
||||||
|
|
||||||
|
|||||||
@@ -15,23 +15,63 @@ CodePress kent vier rollen, gedefinieerd in `AdminAuth::ROLE_PERMISSIONS`:
|
|||||||
|
|
||||||
Rollen worden getoond met hun label via `AdminAuth::ROLE_LABELS`.
|
Rollen worden getoond met hun label via `AdminAuth::ROLE_LABELS`.
|
||||||
|
|
||||||
## Gebruiker toevoegen
|
## Gebruikerslijst (`/admin/users`)
|
||||||
|
|
||||||
1. Ga naar **Gebruikers**
|
De gebruikerslijst toont alle gebruikers met hun gebruikersnaam, rol, login e-mail en aanmaakdatum.
|
||||||
2. Vul gebruikersnaam in
|
|
||||||
3. Kies wachtwoord (opgeslagen als bcrypt hash)
|
|
||||||
4. Selecteer een rol
|
|
||||||
5. Klik **Toevoegen**
|
|
||||||
|
|
||||||
## Gebruiker bewerken
|
### Zoeken en filteren
|
||||||
|
|
||||||
- Wachtwoord wijzigen (nieuwe bcrypt hash)
|
- **Zoekveld**: zoek op gebruikersnaam, e-mail of auteur naam
|
||||||
- Rol wijzigen (beïnvloedt direct zichtbare routes en sidebar items)
|
- **Rol filter**: filter op een specifieke rol via de dropdown
|
||||||
|
- Klik op **Filteren** om de resultaten toe te passen
|
||||||
|
|
||||||
## Gebruiker verwijderen
|
### Nieuwe gebruiker aanmaken
|
||||||
|
|
||||||
- Kan niet voor eigen account
|
1. Klik op **Nieuwe gebruiker** (rechtsboven in de lijst)
|
||||||
- Bevestig met wachtwoord
|
2. Vul gebruikersnaam, wachtwoord (minimaal 8 tekens), e-mail, auteur naam en auteur e-mail in
|
||||||
|
3. Selecteer een rol
|
||||||
|
4. Klik **Gebruiker toevoegen**
|
||||||
|
5. Je wordt automatisch doorgestuurd naar de profiel pagina van de nieuwe gebruiker
|
||||||
|
|
||||||
|
## Profiel bewerken (`/admin/users-edit?user=<naam>`)
|
||||||
|
|
||||||
|
Klik op een gebruiker in de lijst om het profiel te bewerken. De profiel pagina bevat drie secties:
|
||||||
|
|
||||||
|
### Profiel gegevens
|
||||||
|
- Login e-mail, auteur naam en auteur e-mail kunnen worden gewijzigd
|
||||||
|
- De gebruikersnaam kan niet worden gewijzigd
|
||||||
|
|
||||||
|
### Wachtwoord wijzigen
|
||||||
|
- Vul een nieuw wachtwoord in (minimaal 8 tekens)
|
||||||
|
- Bevestig het wachtwoord
|
||||||
|
- Het wachtwoord wordt opgeslagen als bcrypt hash
|
||||||
|
|
||||||
|
### Rol wijzigen
|
||||||
|
- Toont de huidige rol met een gekleurde badge
|
||||||
|
- Selecteer een nieuwe rol uit de dropdown
|
||||||
|
- De wijziging beïnvloedt direct de zichtbare admin routes en sidebar items
|
||||||
|
|
||||||
|
### Gebruiker verwijderen (Gevarenzone)
|
||||||
|
- Alleen zichtbaar voor andere gebruikers (niet voor je eigen account)
|
||||||
|
- Bevestiging via JavaScript dialog
|
||||||
|
- Na verwijderen keer je terug naar de gebruikerslijst
|
||||||
|
|
||||||
|
## Admin wachtwoord reset via CLI
|
||||||
|
|
||||||
|
Als de admin is uitgesloten (bijv. door brute-force lockout of vergeten wachtwoord), kan het wachtwoord via de CLI worden gereset:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset met een specifiek wachtwoord
|
||||||
|
php cli/reset-admin-password.php admin NieuwWachtwoord123
|
||||||
|
|
||||||
|
# Reset met een automatisch gegenereerd wachtwoord
|
||||||
|
php cli/reset-admin-password.php admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Het commando:
|
||||||
|
- Wijzigt het wachtwoord (als bcrypt hash)
|
||||||
|
- Wist de brute-force lockout voor de gebruiker
|
||||||
|
- Toont het nieuwe wachtwoord in de terminal
|
||||||
|
|
||||||
## Toegangscontrole
|
## Toegangscontrole
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,19 @@ return [
|
|||||||
'no_users' => 'Keine Benutzer gefunden.',
|
'no_users' => 'Keine Benutzer gefunden.',
|
||||||
'new_user' => 'Neuer Benutzer',
|
'new_user' => 'Neuer Benutzer',
|
||||||
'add_user' => 'Benutzer hinzufügen',
|
'add_user' => 'Benutzer hinzufügen',
|
||||||
|
'users_list' => 'Benutzerliste',
|
||||||
|
'search_users' => 'Nach Benutzername, E-Mail oder Autor-Name suchen...',
|
||||||
|
'all_roles' => 'Alle Rollen',
|
||||||
|
'filter' => 'Filtern',
|
||||||
|
'delete' => 'Löschen',
|
||||||
|
'back_to_users' => 'Zurück zu Benutzer',
|
||||||
|
'profile_info' => 'Profilinformationen',
|
||||||
|
'change_password' => 'Passwort ändern',
|
||||||
|
'new_password' => 'Neues Passwort',
|
||||||
|
'confirm_password' => 'Passwort bestätigen',
|
||||||
|
'passwords_no_match' => 'Passwörter stimmen nicht überein.',
|
||||||
|
'danger_zone' => 'Gefahrenzone',
|
||||||
|
'delete_user' => 'Benutzer löschen',
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
'login_title' => 'CodePress Admin - Anmeldung',
|
'login_title' => 'CodePress Admin - Anmeldung',
|
||||||
|
|||||||
@@ -242,6 +242,19 @@ return [
|
|||||||
'no_users' => 'No users found.',
|
'no_users' => 'No users found.',
|
||||||
'new_user' => 'New user',
|
'new_user' => 'New user',
|
||||||
'add_user' => 'Add user',
|
'add_user' => 'Add user',
|
||||||
|
'users_list' => 'User list',
|
||||||
|
'search_users' => 'Search by username, email or author name...',
|
||||||
|
'all_roles' => 'All roles',
|
||||||
|
'filter' => 'Filter',
|
||||||
|
'delete' => 'Delete',
|
||||||
|
'back_to_users' => 'Back to users',
|
||||||
|
'profile_info' => 'Profile information',
|
||||||
|
'change_password' => 'Change password',
|
||||||
|
'new_password' => 'New password',
|
||||||
|
'confirm_password' => 'Confirm password',
|
||||||
|
'passwords_no_match' => 'Passwords do not match.',
|
||||||
|
'danger_zone' => 'Danger zone',
|
||||||
|
'delete_user' => 'Delete user',
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
'login_title' => 'CodePress Admin - Login',
|
'login_title' => 'CodePress Admin - Login',
|
||||||
|
|||||||
@@ -242,6 +242,19 @@ return [
|
|||||||
'no_users' => 'Geen gebruikers gevonden.',
|
'no_users' => 'Geen gebruikers gevonden.',
|
||||||
'new_user' => 'Nieuwe gebruiker',
|
'new_user' => 'Nieuwe gebruiker',
|
||||||
'add_user' => 'Gebruiker toevoegen',
|
'add_user' => 'Gebruiker toevoegen',
|
||||||
|
'users_list' => 'Gebruikerslijst',
|
||||||
|
'search_users' => 'Zoek op gebruikersnaam, e-mail of auteur naam...',
|
||||||
|
'all_roles' => 'Alle rollen',
|
||||||
|
'filter' => 'Filteren',
|
||||||
|
'delete' => 'Verwijderen',
|
||||||
|
'back_to_users' => 'Terug naar gebruikers',
|
||||||
|
'profile_info' => 'Profiel gegevens',
|
||||||
|
'change_password' => 'Wachtwoord wijzigen',
|
||||||
|
'new_password' => 'Nieuw wachtwoord',
|
||||||
|
'confirm_password' => 'Bevestig wachtwoord',
|
||||||
|
'passwords_no_match' => 'Wachtwoorden komen niet overeen.',
|
||||||
|
'danger_zone' => 'Gevarenzone',
|
||||||
|
'delete_user' => 'Gebruiker verwijderen',
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
'login_title' => 'CodePress Admin - Login',
|
'login_title' => 'CodePress Admin - Login',
|
||||||
|
|||||||
+144
-21
@@ -328,6 +328,14 @@ switch ($route) {
|
|||||||
handleUsers($auth, $appConfig, $twig, $user, $csrf);
|
handleUsers($auth, $appConfig, $twig, $user, $csrf);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'users-edit':
|
||||||
|
handleUsersEdit($auth, $appConfig, $twig, $user, $csrf);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'users-new':
|
||||||
|
handleUsersNew($auth, $appConfig, $twig, $user, $csrf);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'guide':
|
case 'guide':
|
||||||
handleGuide($auth, $appConfig, $twig, $user, $csrf);
|
handleGuide($auth, $appConfig, $twig, $user, $csrf);
|
||||||
break;
|
break;
|
||||||
@@ -1583,27 +1591,7 @@ function handleUsers($auth, $config, $twig, $user, $csrf): void
|
|||||||
} else {
|
} else {
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
if ($action === 'add') {
|
if ($action === 'delete') {
|
||||||
$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') {
|
|
||||||
$deleteUser = $_POST['delete_username'] ?? '';
|
$deleteUser = $_POST['delete_username'] ?? '';
|
||||||
if ($deleteUser !== $user['username']) {
|
if ($deleteUser !== $user['username']) {
|
||||||
$result = $auth->deleteUser($deleteUser);
|
$result = $auth->deleteUser($deleteUser);
|
||||||
@@ -1627,12 +1615,147 @@ function handleUsers($auth, $config, $twig, $user, $csrf): void
|
|||||||
$users = $auth->getUsers();
|
$users = $auth->getUsers();
|
||||||
$roles = AdminAuth::getRoles();
|
$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', [
|
echo $twig->render('pages/users.twig', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'route' => $route,
|
'route' => $route,
|
||||||
'csrf_token' => $csrf,
|
'csrf_token' => $csrf,
|
||||||
'users' => $users,
|
'users' => $users,
|
||||||
'roles' => $roles,
|
'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),
|
'sidebar_color' => getSidebarColor($config),
|
||||||
'needs_editor' => false,
|
'needs_editor' => false,
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'version' => '2.6.1',
|
'version' => '2.6.1c',
|
||||||
'release_date' => '2026-08-17',
|
'release_date' => '2026-08-17',
|
||||||
'codename' => 'Lyra',
|
'codename' => 'Lyra',
|
||||||
'status' => 'stable',
|
'status' => 'stable',
|
||||||
|
|||||||
Reference in New Issue
Block a user