Setzt die Rechte aus dem vorigen Commit in den Oberflaechen um und
ergaenzt die fehlenden Verwaltungsseiten.
Neu:
- /admin/users - Konten anlegen, Rolle und Sperre setzen, Passwort
zuruecksetzen, Gruppenzuordnung je Konto
- /admin/groups - Gruppen anlegen und umbenennen, Mitglieder samt
Rolle pflegen, zugeordnete Domains einsehen
- /admin/profile - eigenes Konto: Gruppenzugehoerigkeit ansehen und
das Passwort aendern. Das fehlte bisher vollstaendig, auch fuer
den einen Administrator.
Domains sind jetzt Administratorsache, weil dort die Gruppe zugewiesen
wird. Kurzlinks, Statistik und QR-Codes richten sich nach der Gruppe:
ein fremder Kurzlink antwortet mit 404 (seine Existenz geht den
Aufrufer nichts an), ein sichtbarer ohne Schreibrecht mit 403.
Geschlossene Luecke: /admin/uploads/{file} pruefte bisher nur, ob
ueberhaupt jemand angemeldet ist. Da alle Logos in einem gemeinsamen
Verzeichnis liegen, war der Dateiname die einzige Huerde. Ausgeliefert
wird jetzt nur noch, was laut Datenbank zu einem Kurzlink aus einer
Gruppe des Aufrufers gehoert.
Die Navigation blendet aus, wofuer die Rechte fehlen - das ist
Bedienkomfort, kein Zugriffsschutz: die Routen pruefen unabhaengig
davon noch einmal.
Der Setup-Assistent legt das erste Konto als Administrator an und
traegt es gleich als Editor der Gruppe "Standard" ein. Ohne das
koennte es zwar verwalten, aber selbst keine Kurzlinks anlegen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
150 lines
7.2 KiB
PHP
150 lines
7.2 KiB
PHP
<?php
|
||
|
||
/**
|
||
* Benutzer anlegen und bearbeiten.
|
||
*
|
||
* @var array<string,mixed>|null $user
|
||
* @var list<array<string,mixed>> $groups
|
||
* @var array<int,string> $memberships Gruppenkennung => Rolle
|
||
* @var array{username:string,role:string,is_active:string} $values
|
||
* @var array<string,string> $errors
|
||
* @var int $minLength
|
||
* @var App\Security\Csrf $csrf
|
||
*/
|
||
|
||
use App\Security\Role;
|
||
|
||
$isEdit = $user !== null;
|
||
$action = $isEdit
|
||
? $url('/admin/users/' . (int) $user['id'] . '/edit')
|
||
: $url('/admin/users/new');
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1><?= $isEdit ? 'Benutzer bearbeiten' : 'Neuer Benutzer' ?></h1>
|
||
<?php if ($isEdit): ?>
|
||
<p class="muted mono"><?= $e($user['username']) ?></p>
|
||
<?php else: ?>
|
||
<p class="muted">Ein weiteres Konto für diese Installation.</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
<a href="<?= $e($url('/admin/users')) ?>" class="btn btn--ghost">Zurück zur Liste</a>
|
||
</div>
|
||
|
||
<div class="card card--form">
|
||
<form method="post" action="<?= $e($action) ?>" class="form">
|
||
<?= $csrf->field() ?>
|
||
|
||
<div class="field <?= isset($errors['username']) ? 'field--error' : '' ?>">
|
||
<label for="username">Benutzername <span class="req">*</span></label>
|
||
<input type="text" id="username" name="username" value="<?= $e($values['username']) ?>"
|
||
required autofocus spellcheck="false" autocapitalize="off" autocomplete="username">
|
||
<p class="field__hint">3–64 Zeichen: Buchstaben, Ziffern sowie <code>. _ - @</code>.
|
||
Groß- und Kleinschreibung wird beim Anmelden nicht unterschieden.</p>
|
||
<?php if (isset($errors['username'])): ?>
|
||
<p class="field__error"><?= $e($errors['username']) ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="field <?= isset($errors['role']) ? 'field--error' : '' ?>">
|
||
<label for="role">Rolle <span class="req">*</span></label>
|
||
<select id="role" name="role">
|
||
<?php foreach (Role::GLOBAL_LABELS as $value => $label): ?>
|
||
<option value="<?= $e($value) ?>" <?= $values['role'] === $value ? 'selected' : '' ?>><?= $e($label) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<p class="field__hint">
|
||
<strong>Administrator:</strong> verwaltet Benutzer, Gruppen und Domains und sieht alle Kurzlinks.<br>
|
||
<strong>Benutzer:</strong> sieht ausschließlich die Kurzlinks seiner Gruppen.
|
||
</p>
|
||
<?php if (isset($errors['role'])): ?>
|
||
<p class="field__error"><?= $e($errors['role']) ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="field field--check">
|
||
<label class="check">
|
||
<input type="checkbox" name="is_active" value="1" <?= $values['is_active'] === '1' ? 'checked' : '' ?>>
|
||
<span>Konto ist aktiv</span>
|
||
</label>
|
||
<p class="field__hint">Ein gesperrtes Konto kann sich nicht anmelden; laufende Sitzungen enden sofort.</p>
|
||
</div>
|
||
|
||
<fieldset class="field">
|
||
<legend>Gruppen</legend>
|
||
<?php if ($groups === []): ?>
|
||
<p class="field__hint">Es gibt noch keine Gruppe.
|
||
<a href="<?= $e($url('/admin/groups/new')) ?>">Jetzt eine anlegen.</a></p>
|
||
<?php else: ?>
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Gruppe</th>
|
||
<th scope="col">Rolle in der Gruppe</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($groups as $group):
|
||
$groupId = (int) $group['id'];
|
||
$selected = $memberships[$groupId] ?? '';
|
||
?>
|
||
<tr>
|
||
<td><?= $e($group['name']) ?>
|
||
<span class="muted small">(<?= (int) $group['domain_count'] ?> Domain(s))</span>
|
||
</td>
|
||
<td>
|
||
<select name="groups[<?= $groupId ?>]" aria-label="Rolle in <?= $e($group['name']) ?>">
|
||
<option value="">kein Mitglied</option>
|
||
<option value="<?= Role::EDITOR ?>" <?= $selected === Role::EDITOR ? 'selected' : '' ?>>
|
||
<?= $e(Role::GROUP_LABELS[Role::EDITOR]) ?>
|
||
</option>
|
||
<option value="<?= Role::VIEWER ?>" <?= $selected === Role::VIEWER ? 'selected' : '' ?>>
|
||
<?= $e(Role::GROUP_LABELS[Role::VIEWER]) ?>
|
||
</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="field__hint">Ein Administrator sieht alle Kurzlinks – für ihn ist die Zuordnung
|
||
nur dann nötig, wenn er auch selbst welche anlegen soll.</p>
|
||
<?php endif; ?>
|
||
</fieldset>
|
||
|
||
<fieldset class="field">
|
||
<legend>Passwort</legend>
|
||
|
||
<div class="field <?= isset($errors['password']) ? 'field--error' : '' ?>">
|
||
<label for="password"><?= $isEdit ? 'Neues Passwort' : 'Passwort' ?>
|
||
<?= $isEdit ? '' : '<span class="req">*</span>' ?></label>
|
||
<input type="password" id="password" name="password" autocomplete="new-password"
|
||
minlength="<?= (int) $minLength ?>" <?= $isEdit ? '' : 'required' ?>>
|
||
<p class="field__hint">
|
||
Mindestens <?= (int) $minLength ?> Zeichen.
|
||
<?= $isEdit ? 'Leer lassen, um das bisherige Passwort zu behalten. Eine Änderung meldet den Betroffenen überall ab.' : '' ?>
|
||
</p>
|
||
<?php if (isset($errors['password'])): ?>
|
||
<p class="field__error"><?= $e($errors['password']) ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="field <?= isset($errors['password_confirm']) ? 'field--error' : '' ?>">
|
||
<label for="password_confirm">Wiederholung</label>
|
||
<input type="password" id="password_confirm" name="password_confirm" autocomplete="new-password"
|
||
<?= $isEdit ? '' : 'required' ?>>
|
||
<?php if (isset($errors['password_confirm'])): ?>
|
||
<p class="field__error"><?= $e($errors['password_confirm']) ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<div class="form__actions">
|
||
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Änderungen speichern' : 'Benutzer anlegen' ?></button>
|
||
<a href="<?= $e($url('/admin/users')) ?>" class="btn btn--ghost">Abbrechen</a>
|
||
</div>
|
||
</form>
|
||
</div>
|