- Own CSS is parsed on write only. A page whose stored CSS no longer passes a stricter rule answers 200 and opens in the editor; that one block loses its CSS. A document read several times in one request is validated once. - Settings and theme refuse a write from an outdated form. - A backup fails when a media file does not arrive completely and leaves no half directory behind; rotation counts intact backups only. - The archive update takes its backup behind the maintenance page. - A package import puts all media in place before removing the old ones, reports honestly when that fails and keeps a maintenance state that was already on. - Restoring replaces the SQLite file in one move and points at pending migrations. - Six irreversible actions ask first, in a dialog of the admin's own. - Ctrl+S in the editor takes the field being typed in along; Delete no longer removes a block while a dialog is open. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
111 lines
7.2 KiB
Twig
111 lines
7.2 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'users' %}
|
||
{% block title %}Benutzer{% endblock %}
|
||
{% block heading %}Benutzer{% endblock %}
|
||
{% block lead %}Jedes Konto hat genau eine Rolle. Ein Passwort oder eine Rolle zu ändern meldet alle offenen Sitzungen dieses Kontos ab – das ist der Weg, jemanden sofort auszusperren.{% endblock %}
|
||
{% block content %}
|
||
|
||
<details class="card"{% if app.flashes is not empty %} open{% endif %}>
|
||
<summary>Neues Konto anlegen</summary>
|
||
<form action="{{ path('admin_user_create') }}" method="post" class="card-body">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('users') }}">
|
||
<div class="fields">
|
||
<div>
|
||
<label for="new-username">Benutzername</label>
|
||
<input id="new-username" name="username" maxlength="100" required autocomplete="off" pattern="[a-z0-9][a-z0-9._-]{2,99}">
|
||
<p class="field-help">3 bis 100 Zeichen: Kleinbuchstaben, Ziffern, Punkt, Unterstrich, Bindestrich.</p>
|
||
</div>
|
||
<div>
|
||
<label for="new-password">Passwort</label>
|
||
<input id="new-password" name="password" type="password" required autocomplete="new-password" minlength="{{ minPassword }}" maxlength="{{ maxPassword }}">
|
||
<p class="field-help">Mindestens {{ minPassword }} Zeichen, höchstens {{ maxPassword }} Bytes. Gib es der Person auf einem anderen Weg als per E-Mail weiter; sie kann es unter „Mein Konto“ selbst ändern.</p>
|
||
</div>
|
||
<div>
|
||
<label for="new-role">Rolle</label>
|
||
<select id="new-role" name="role">
|
||
{% for value, label in roles %}<option value="{{ value }}"{{ value == 'ROLE_EDITOR' ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||
</select>
|
||
<p class="field-help">{% for value, help in roleHelp %}<strong>{{ roles[value] }}:</strong> {{ help }}{% if not loop.last %}<br>{% endif %}{% endfor %}</p>
|
||
</div>
|
||
</div>
|
||
<button class="primary" type="submit">Konto anlegen</button>
|
||
</form>
|
||
</details>
|
||
|
||
<div class="section-head"><h2>Konten <span class="count">{{ users|length }}</span></h2><span class="muted">Zeiten in UTC.</span></div>
|
||
<div class="rows">
|
||
{% for account in users %}
|
||
<article class="row row-stack">
|
||
<div class="row-line">
|
||
<span class="row-icon" aria-hidden="true">☺</span>
|
||
<div class="row-main">
|
||
<h3>{{ account.username }}{% if account.id == me %} <span class="pill">dein Konto</span>{% endif %}</h3>
|
||
<p>{{ account.roleName }}
|
||
· {% if account.lastLoginAt %}zuletzt angemeldet {{ account.lastLoginAt|date('d.m.Y H:i') }}{% else %}noch nie angemeldet{% endif %}
|
||
{% if account.passwordChangedAt %} · Passwort vom {{ account.passwordChangedAt|date('d.m.Y') }}{% endif %}
|
||
{% if account.createdAt %} · angelegt {{ account.createdAt|date('d.m.Y') }}{% endif %}
|
||
</p>
|
||
</div>
|
||
<span class="pill {{ 'ROLE_ADMIN' == account.role ? 'pill-live' : ('ROLE_PUBLISHER' == account.role ? 'pill-draft' : 'pill-off') }}">{{ account.roleName }}</span>
|
||
</div>
|
||
<details class="sheet">
|
||
<summary aria-label="Einstellungen von {{ account.username }}">Einstellungen</summary>
|
||
<div class="sheet-body">
|
||
<div class="sheet-part">
|
||
<h4>Rolle</h4>
|
||
{% if account.id == me %}
|
||
<p>Die eigene Rolle lässt sich hier nicht ändern. Das soll ein zweiter Administrator tun – sonst kann sich der letzte Administrator selbst die Rechte nehmen.</p>
|
||
{% else %}
|
||
<form action="{{ path('admin_user_role', {id: account.id}) }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('users') }}">
|
||
<div class="field">
|
||
<label for="role-{{ account.id }}">Rolle von {{ account.username }}</label>
|
||
<select id="role-{{ account.id }}" name="role">
|
||
{% for value, label in roles %}<option value="{{ value }}"{{ value == account.role ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||
</select>
|
||
<p class="field-help">Offene Sitzungen dieses Kontos werden dabei abgemeldet.</p>
|
||
</div>
|
||
<button class="secondary" type="submit">Rolle ändern</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="sheet-part">
|
||
<h4>Passwort setzen</h4>
|
||
<p>Setzt ein neues Passwort, ohne das alte zu kennen – für den Fall, dass jemand nicht mehr hineinkommt. Alle offenen Sitzungen dieses Kontos werden abgemeldet.</p>
|
||
<form action="{{ path('admin_user_password', {id: account.id}) }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('users') }}">
|
||
<div class="field">
|
||
<label for="password-{{ account.id }}">Neues Passwort für {{ account.username }}</label>
|
||
<input id="password-{{ account.id }}" name="password" type="password" required autocomplete="new-password" minlength="{{ minPassword }}" maxlength="{{ maxPassword }}">
|
||
</div>
|
||
<button class="secondary" type="submit">Passwort setzen</button>
|
||
</form>
|
||
</div>
|
||
|
||
{% if account.id != me %}
|
||
<div class="sheet-part">
|
||
<h4>Konto löschen</h4>
|
||
<p>Entfernt „{{ account.username }}“. Gespeicherte Inhalte bleiben unberührt – Seiten und Fassungen hängen nicht am Konto. Das eigene Konto lässt sich nicht löschen; deshalb bleibt immer mindestens ein Administrator übrig.</p>
|
||
<form action="{{ path('admin_user_delete', {id: account.id}) }}" method="post" data-confirm-title="Konto „{{ account.username }}“ löschen?" data-confirm-text="Die Person kann sich danach nicht mehr anmelden. Seiten und Fassungen bleiben." data-confirm-label="Konto löschen" data-confirm-variant="danger">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('users') }}">
|
||
<button class="subtle danger-text" type="submit">Konto löschen</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</details>
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-head"><h2>Wenn kein Konto mehr funktioniert</h2><span class="muted">Zugang über die Konsole</span></div>
|
||
<div class="card-body">
|
||
<p class="muted">Auf dem Server legt <code>php bin/console cms:user:create name --role=admin</code> ein Konto an; das Passwort wird verdeckt abgefragt und ein vorhandenes Konto nie überschrieben. Das ist der Weg zurück, wenn niemand mehr hineinkommt – er braucht Dateizugriff auf den Server und lässt sich hier nicht ersetzen.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||
{% endblock %}
|