Benutzerverwaltung in der Oberfläche, Mediathek als eigene Seite

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>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 18:14:50 +02:00
1 parent 4841bf65bc
commit 14031a5880
23 files changed
+1281 -101

No files matched your search

+33
View File
@@ -8,6 +8,9 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
final class MediaLibrary
{
/** Enough to fill a screen, few enough that the browser does not decode a hundred images. */
public const PER_PAGE = 24;
public function __construct(private Connection $db, private string $directory)
{
}
@@ -17,6 +20,36 @@ final class MediaLibrary
return array_map($this->present(...), $this->db->fetchAllAssociative('SELECT * FROM media ORDER BY created_at DESC, id DESC'));
}
/**
* One page of the library, newest first.
*
* The media screen renders the images itself instead of fetching them, so it has to ask for a
* slice: a library of a few hundred images would otherwise be one very long page and a lot of
* thumbnails at once.
*
* @return array{items: list<array<string, mixed>>, total: int, page: int, pages: int, perPage: int}
*/
public function page(int $page, bool $withArchived = false, int $perPage = self::PER_PAGE): array
{
$where = $withArchived ? '' : ' WHERE archived = 0';
$total = (int) $this->db->fetchOne('SELECT COUNT(*) FROM media'.$where);
$perPage = max(1, min(96, $perPage));
$pages = max(1, (int) ceil($total / $perPage));
$page = max(1, min($pages, $page));
$items = $this->db->fetchAllAssociative(
'SELECT * FROM media'.$where.' ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?',
[$perPage, ($page - 1) * $perPage],
);
return [
'items' => array_map($this->present(...), $items),
'total' => $total,
'page' => $page,
'pages' => $pages,
'perPage' => $perPage,
];
}
public function find(string $id): ?array
{
$row = $this->db->fetchAssociative('SELECT * FROM media WHERE id = ?', [$id]);