Files
urlshort-qr-gen/templates/qr/show.php
T
florian.wildeandClaude Opus 5 7f52613821 feat(admin): Verwaltung fuer Benutzer, Gruppen und das eigene Konto
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>
2026-08-14 12:03:20 +02:00

228 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* @var array<string,mixed> $link
* @var string $shortUrl
* @var App\Qr\QrOptions $options
* @var list<int> $sizes
* @var list<string> $levels
* @var string|null $warning
* @var bool $logoIsSvg
* @var bool $imagickAvailable
* @var bool $canEdit In dieser Gruppe nur "Viewer"? Dann ist alles nur lesbar.
* @var App\Security\Csrf $csrf
*/
$id = (int) $link['id'];
$levelLabels = [
'L' => 'L – niedrig (7 %)',
'M' => 'M – mittel (15 %)',
'Q' => 'Q – hoch (25 %)',
'H' => 'H – sehr hoch (30 %)',
];
?>
<div class="page-head">
<div>
<h1>QR-Code</h1>
<p class="muted">
<button type="button" class="linkcopy mono" data-copy="<?= $e($shortUrl) ?>"
title="In die Zwischenablage kopieren"><?= $e($shortUrl) ?></button>
</p>
</div>
<div class="page-head__actions">
<a class="btn btn--ghost" href="<?= $e($url('/admin/links/' . $id . '/stats')) ?>">Statistik</a>
<a class="btn btn--ghost" href="<?= $e($url('/admin/links')) ?>">Zur Liste</a>
</div>
</div>
<div class="qr-layout"
data-qr
data-preview-url="<?= $e($url('/admin/links/' . $id . '/qr/preview')) ?>"
data-download-url="<?= $e($url('/admin/links/' . $id . '/qr/download')) ?>">
<section class="card qr-preview">
<div class="qr-preview__stage" style="--qr-bg: <?= $e($options->background) ?>">
<img id="qr-image" alt="Vorschau des QR-Codes"
src="<?= $e($url('/admin/links/' . $id . '/qr/preview?' . http_build_query($options->toQueryParams()))) ?>">
<div class="qr-preview__spinner" data-qr-spinner hidden aria-hidden="true"></div>
</div>
<p class="muted small center">Der QR-Code verweist auf den Kurzlink – die Ziel-URL kannst du also
später ändern, ohne den Code neu drucken zu müssen.</p>
<div class="qr-preview__warning" data-qr-warning <?= $warning === null ? 'hidden' : '' ?>>
<div class="alert alert--warn" data-qr-warning-text><?= $e($warning ?? '') ?></div>
</div>
<div class="qr-download">
<a class="btn btn--primary" data-qr-download="png" href="#" download>PNG herunterladen</a>
<a class="btn" data-qr-download="svg" href="#" download>SVG herunterladen</a>
</div>
<p class="muted small center">SVG ist vektorbasiert und damit für den Druck in jeder Größe geeignet.</p>
</section>
<div class="qr-controls">
<form method="post" action="<?= $e($url('/admin/links/' . $id . '/qr')) ?>" class="card form" id="qr-form">
<?= $csrf->field() ?>
<h2>Gestaltung</h2>
<div class="grid grid--2 tight">
<div class="field">
<label for="foreground">Vordergrund</label>
<div class="colorpair">
<input type="color" id="foreground" name="foreground" value="<?= $e($options->foreground) ?>"
data-qr-input data-color-sync="foreground_text">
<input type="text" id="foreground_text" value="<?= $e($options->foreground) ?>"
class="mono" maxlength="7" spellcheck="false" aria-label="Vordergrundfarbe als Hex-Wert"
data-color-sync="foreground">
</div>
</div>
<div class="field">
<label for="background">Hintergrund</label>
<div class="colorpair">
<input type="color" id="background" name="background" value="<?= $e($options->background) ?>"
data-qr-input data-color-sync="background_text">
<input type="text" id="background_text" value="<?= $e($options->background) ?>"
class="mono" maxlength="7" spellcheck="false" aria-label="Hintergrundfarbe als Hex-Wert"
data-color-sync="background">
</div>
</div>
</div>
<div class="field">
<span class="label">Voreinstellungen</span>
<div class="swatches">
<button type="button" class="swatch" data-preset="#000000,#ffffff"
style="--fg:#000000;--bg:#ffffff" title="Schwarz auf Weiß"></button>
<button type="button" class="swatch" data-preset="#1f2937,#f9fafb"
style="--fg:#1f2937;--bg:#f9fafb" title="Anthrazit"></button>
<button type="button" class="swatch" data-preset="#0b4f9e,#ffffff"
style="--fg:#0b4f9e;--bg:#ffffff" title="Blau"></button>
<button type="button" class="swatch" data-preset="#14532d,#ffffff"
style="--fg:#14532d;--bg:#ffffff" title="Grün"></button>
<button type="button" class="swatch" data-preset="#7c2d12,#fff7ed"
style="--fg:#7c2d12;--bg:#fff7ed" title="Warmbraun"></button>
<button type="button" class="swatch" data-preset="#4c1d95,#f5f3ff"
style="--fg:#4c1d95;--bg:#f5f3ff" title="Violett"></button>
</div>
</div>
<div class="grid grid--2 tight">
<div class="field">
<label for="size">Größe</label>
<select id="size" name="size" data-qr-input>
<?php foreach ($sizes as $size): ?>
<option value="<?= (int) $size ?>" <?= $options->size === $size ? 'selected' : '' ?>>
<?= (int) $size ?> × <?= (int) $size ?> px
</option>
<?php endforeach; ?>
</select>
<p class="field__hint">Gilt für den PNG-Download; SVG skaliert verlustfrei.</p>
</div>
<div class="field">
<label for="error_correction">Fehlerkorrektur</label>
<select id="error_correction" name="error_correction" data-qr-input
<?= $options->hasLogo() ? 'disabled' : '' ?>>
<?php foreach ($levels as $level): ?>
<option value="<?= $e($level) ?>" <?= $options->errorCorrection === $level ? 'selected' : '' ?>>
<?= $e($levelLabels[$level] ?? $level) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($options->hasLogo()): ?>
<input type="hidden" name="error_correction" value="H">
<p class="field__hint">Wegen des Logos fest auf <strong>H</strong> gesetzt.</p>
<?php else: ?>
<p class="field__hint">Höher = robuster gegen Beschädigung, aber dichteres Muster.</p>
<?php endif; ?>
</div>
</div>
<div class="field">
<label for="margin">Randbreite (Quiet Zone): <output id="margin_out"><?= (int) $options->margin ?></output> Module</label>
<input type="range" id="margin" name="margin" min="0" max="10" step="1"
value="<?= (int) $options->margin ?>" data-qr-input data-output="margin_out">
<p class="field__hint">Die Spezifikation empfiehlt mindestens 4 Module ruhige Fläche rundherum.</p>
</div>
<?php if ($options->hasLogo()): ?>
<div class="field">
<label for="logo_scale">Logogröße: <output id="logo_scale_out"><?= (int) $options->logoScale ?></output> % der Breite</label>
<input type="range" id="logo_scale" name="logo_scale" min="10" max="30" step="1"
value="<?= (int) $options->logoScale ?>" data-qr-input data-output="logo_scale_out">
</div>
<div class="field field--check">
<label class="check">
<input type="checkbox" name="logo_punchout" value="1" data-qr-input
<?= $options->logoPunchout ? 'checked' : '' ?>>
<span>Fläche hinter dem Logo freistellen</span>
</label>
</div>
<?php endif; ?>
<div class="form__actions">
<?php if ($canEdit): ?>
<button type="submit" class="btn btn--primary">Einstellungen speichern</button>
<?php endif; ?>
<button type="reset" class="btn btn--ghost" data-qr-reset>Zurücksetzen</button>
</div>
<p class="field__hint">
<?php if ($canEdit): ?>
Speichern ist optional – Vorschau und Download nutzen immer die aktuellen Werte.
<?php else: ?>
In dieser Gruppe ist nur das Ansehen erlaubt. Ausprobieren und Herunterladen geht
trotzdem – gespeichert wird die Gestaltung aber nicht.
<?php endif; ?>
</p>
</form>
<section class="card">
<h2>Logo in der Mitte</h2>
<?php if ($options->hasLogo()): ?>
<div class="logo-current">
<img src="<?= $e($url('/admin/uploads/' . rawurlencode((string) $options->logoPath))) ?>"
alt="Aktuelles Logo" class="logo-current__img">
<div class="logo-current__meta">
<span class="mono small"><?= $e($options->logoPath) ?></span>
<?php if ($canEdit): ?>
<form method="post" action="<?= $e($url('/admin/links/' . $id . '/qr/logo/delete')) ?>"
data-confirm="Logo wirklich entfernen?">
<?= $csrf->field() ?>
<button type="submit" class="btn btn--sm btn--danger">Logo entfernen</button>
</form>
<?php endif; ?>
</div>
</div>
<?php if ($logoIsSvg && !$imagickAvailable): ?>
<div class="alert alert--warn">
Dieses SVG-Logo kann nur im <strong>SVG-Download</strong> eingebettet werden. Für PNG-Ausgaben
wird die PHP-Erweiterung <code>imagick</code> benötigt – alternativ ein PNG-Logo hochladen.
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($canEdit): ?>
<form method="post" action="<?= $e($url('/admin/links/' . $id . '/qr/logo')) ?>"
enctype="multipart/form-data" class="form">
<?= $csrf->field() ?>
<div class="field">
<label for="logo"><?= $options->hasLogo() ? 'Anderes Logo hochladen' : 'Logo hochladen' ?></label>
<input type="file" id="logo" name="logo" accept="image/png,image/jpeg,image/svg+xml" required>
<p class="field__hint">PNG, JPG oder SVG. Das Logo wird mittig eingesetzt und passend skaliert;
die Fehlerkorrektur wird dabei automatisch auf Level H gesetzt.</p>
</div>
<button type="submit" class="btn">Hochladen</button>
</form>
<?php elseif (!$options->hasLogo()): ?>
<p class="muted">Für diesen Kurzlink ist kein Logo hinterlegt.</p>
<?php endif; ?>
</section>
</div>
</div>