Files
urlshort-qr-gen/src/Repository/SettingRepository.php
T
florian.wildeandClaude Opus 5 cad05e5fd5 feat(auth): Benutzer, Gruppen und Rechte als Datenmodell
Loest das eine fest verdrahtete Admin-Konto durch beliebig viele
Konten ab, die in Gruppen organisiert sind.

Die Zugriffskette lautet Benutzer -> Gruppe -> Domain -> Kurzlink:
eine Domain gehoert zu genau einer Gruppe, ein Kurzlink haengt an
seiner Domain. Wer Mitglied einer Gruppe ist, sieht damit alle
Kurzlinks aller Domains dieser Gruppe. Ein Kurzlink hat bewusst
keinen persoenlichen Eigentuemer - sonst wuerde das Loeschen eines
Kontos gedruckte QR-Codes unbrauchbar machen.

Zwei Rollenebenen: global "admin" (verwaltet Benutzer, Gruppen und
Domains, sieht alles) bzw. "user", innerhalb einer Gruppe "editor"
(anlegen und aendern) bzw. "viewer" (nur lesen).

AccessPolicy beantwortet je Request, welche Domains sichtbar bzw.
beschreibbar sind; die Repositories nehmen diese Liste als Filter.
Dabei ist die Unterscheidung wesentlich: null heisst "keine
Einschraenkung" (Administrator), ein leeres Array heisst "nichts" -
und darf gerade nicht zu einer Abfrage ohne Bedingung werden.

Der Zugang aus der .env (ADMIN_USER / ADMIN_PASSWORD_HASH) bleibt
gueltig, steht aber ab jetzt als Notfallzugang neben den regulaeren
Konten, statt sie zu ueberstimmen. Er hat Administratorrechte und
existiert nur fuer den Fall, dass das letzte Administratorkonto
gesperrt wurde oder sein Passwort verloren ging.

Migration 003 uebernimmt das Bestandskonto aus der settings-Tabelle
als Benutzer Nr. 1 mit unveraendertem Hash, legt die Gruppe
"Standard" an, ordnet ihr alle vorhandenen Domains zu und raeumt
die beiden alten settings-Schluessel auf.

Nebenbei geschlossen: das Rate-Limiting am Login zaehlte nur nach
IP-Adresse. Es zaehlt jetzt auch je Benutzername, sonst reicht ein
Wechsel der Adresse, um alle Konten der Reihe nach durchzuprobieren.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:02:59 +02:00

73 lines
1.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Repository;
use App\Database;
/**
* Key-Value-Speicher fuer Einstellungen, die zur Laufzeit gesetzt werden.
*
* Hier lagen bis zur Version mit mehreren Benutzern auch die Zugangsdaten
* des einen Admin-Kontos ("admin_user", "admin_password_hash"). Sie stehen
* jetzt in der users-Tabelle; die Migration 003 raeumt die beiden
* Schluessel auf.
*/
final class SettingRepository
{
public const APP_SECRET = 'app_secret';
public function __construct(private readonly Database $db)
{
}
public function get(string $key, ?string $default = null): ?string
{
$row = $this->db->fetchOne('SELECT value FROM settings WHERE key_name = ?', [$key]);
if ($row === null || $row['value'] === null || $row['value'] === '') {
return $default;
}
return (string) $row['value'];
}
/**
* Installationsgeheimnis fuer signierte Tokens. Wird beim ersten
* Bedarf erzeugt und bleibt danach unveraendert.
*/
public function secret(): string
{
$secret = $this->get(self::APP_SECRET);
if ($secret === null || strlen($secret) < 32) {
$secret = bin2hex(random_bytes(32));
$this->set(self::APP_SECRET, $secret);
}
return $secret;
}
public function set(string $key, string $value): void
{
$now = gmdate('Y-m-d H:i:s');
$exists = $this->db->fetchValue('SELECT 1 FROM settings WHERE key_name = ?', [$key]);
if ($exists !== null) {
$this->db->execute(
'UPDATE settings SET value = ?, updated_at = ? WHERE key_name = ?',
[$value, $now, $key],
);
return;
}
$this->db->execute(
'INSERT INTO settings (key_name, value, updated_at) VALUES (?, ?, ?)',
[$key, $value, $now],
);
}
}