Konten gab es bisher nur über die Konsole. Unter "Benutzer" legt ein Administrator jetzt Konten an, ändert Rollen, setzt Passwörter und löscht Konten; unter "Mein Konto" ändert jeder sein eigenes Passwort, aber nur mit dem aktuellen in der Hand. Eine Rolle je Konto: Administrator, Veröffentlicher, Redakteur. Ein Passwort oder eine Rolle zu ändern meldet jede offene Sitzung dieses Kontos ab - dafür war nichts zu bauen, CmsUser::isEqualTo vergleicht Hash und Rollen mit der Datenbank. Genau eine Sitzung überlebt: die, die ihr eigenes Passwort geändert hat. "Jemanden sofort aussperren" ist damit dasselbe wie "Passwort setzen". Es bleibt immer ein Administrator übrig, nicht über eine Zählung, sondern weil niemand sein eigenes Konto löschen oder herabstufen kann und jede Änderung die Administratorrolle voraussetzt. Die zusätzliche Prüfung auf einen zweiten Administrator war damit unerreichbar und ist wieder heraus. Mediathek (Umut: "das sollte direkt laden in pagination"): /admin/media zeigt die Bilder jetzt selbst, 24 je Seite, neueste zuerst, mit Blättern und einem Schalter für archivierte. Hochladen und Bildangaben laufen über gewöhnliche Formulare ohne JavaScript; der Dialog bleibt der Weg, beim Bearbeiten ein Bild auszuwählen. Die Bild-ID steht in jeder Karte, weil Beitragsbild und Teilen-Vorschau genau diese ID erwarten. Die Migrationsliste der Testfixtures stand dreimal da und steht jetzt einmal in tests/TestSchema.php. 217 PHP-Tests / 845 Assertions unter PHP 8.2.12 und 8.4.22. axe deckt jetzt elf Bildschirme ab statt drei - alle Verwaltungsseiten, nicht nur Anmeldung, Seitenliste und Editor -, null Verstöße. Nicht geprüft: kein Screenreader; Blättern und Upload sind mit zwei Bildern geprüft, nicht mit einer großen Bibliothek. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
55 lines
3.5 KiB
Twig
55 lines
3.5 KiB
Twig
{# The frame around every management screen. One sidebar, always the same, so each area is one
|
|
click away and the current one is visible. Screens fill in heading, lead, actions and content. #}
|
|
{% extends 'admin/base.html.twig' %}
|
|
{% block body_class %}admin-shell{% endblock %}
|
|
{% block body %}
|
|
<header class="shell-nav">
|
|
<a class="brand" href="{{ path('admin_pages') }}"><span class="brand-icon" aria-hidden="true">k.</span>kant<span>cms</span></a>
|
|
<nav aria-label="Verwaltung">
|
|
<p class="nav-label" id="nav-content">INHALT</p>
|
|
<ul class="nav-list" aria-labelledby="nav-content">
|
|
{% set entries = [
|
|
{id: 'pages', route: 'admin_pages', icon: '▤', label: 'Seiten'},
|
|
{id: 'partials', route: 'admin_partials', icon: '◫', label: 'Bereiche'},
|
|
{id: 'menus', route: 'admin_menus', icon: '☰', label: 'Menüs'},
|
|
{id: 'media', route: 'admin_media', icon: '▩', label: 'Mediathek'},
|
|
{id: 'forms', route: 'admin_forms', icon: '✉', label: 'Formulare'}
|
|
] %}
|
|
{% for entry in entries %}
|
|
<li><a class="nav-item" href="{{ path(entry.route) }}"{{ nav|default('') == entry.id ? ' aria-current="page"' : '' }}><span class="nav-icon" aria-hidden="true">{{ entry.icon }}</span>{{ entry.label }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
<p class="nav-label" id="nav-site">WEBSITE</p>
|
|
<ul class="nav-list" aria-labelledby="nav-site">
|
|
<li><a class="nav-item" href="{{ path('admin_theme') }}"{{ nav|default('') == 'theme' ? ' aria-current="page"' : '' }}><span class="nav-icon" aria-hidden="true">◐</span>Design</a></li>
|
|
<li><a class="nav-item" href="{{ path('admin_settings') }}"{{ nav|default('') == 'settings' ? ' aria-current="page"' : '' }}><span class="nav-icon" aria-hidden="true">⚙</span>Einstellungen</a></li>
|
|
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_users') }}"{{ nav|default('') == 'users' ? ' aria-current="page"' : '' }}><span class="nav-icon" aria-hidden="true">☺</span>Benutzer</a></li>{% endif %}
|
|
<li><a class="nav-item nav-out" href="/">Website ansehen</a></li>
|
|
<li><a class="nav-item nav-out" href="/beitraege">Beitragsübersicht</a></li>
|
|
</ul>
|
|
</nav>
|
|
<div class="shell-foot">
|
|
<a class="shell-user" href="{{ path('admin_account') }}"{{ nav|default('') == 'account' ? ' aria-current="page"' : '' }}><span class="shell-avatar" aria-hidden="true">{{ app.user.userIdentifier|first|upper }}</span><strong>{{ app.user.userIdentifier }}</strong></a>
|
|
<form action="{{ path('admin_logout') }}" method="post">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('logout') }}">
|
|
<button class="subtle" type="submit">Abmelden</button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
<main id="admin-main" class="shell-main" tabindex="-1">
|
|
<div class="shell-top">
|
|
<div class="shell-heading">
|
|
<h1>{% block heading %}Verwaltung{% endblock %}</h1>
|
|
<p class="muted">{% block lead %}{% endblock %}</p>
|
|
</div>
|
|
<div class="shell-actions">{% block actions %}{% endblock %}</div>
|
|
</div>
|
|
<div class="shell-body stack">
|
|
{% for type in ['error', 'success'] %}{% for message in app.flashes(type) %}
|
|
<p class="notice {{ type == 'error' ? 'error' : 'ok' }}" role="{{ type == 'error' ? 'alert' : 'status' }}">{{ message }}</p>
|
|
{% endfor %}{% endfor %}
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
{% endblock %}
|