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>
129 lines
4.8 KiB
PHP
129 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Mehrere Benutzer, organisiert in Gruppen.
|
|
*
|
|
* Aufbau:
|
|
* - "users" loest das eine Konto aus der settings-Tabelle ab. Die globale
|
|
* Rolle ist entweder "admin" (Benutzer, Gruppen und Domains verwalten)
|
|
* oder "user" (nur das, was die Gruppenmitgliedschaft hergibt).
|
|
* - "user_groups" buendelt Benutzer. Der Name lautet bewusst nicht
|
|
* "groups": das ist seit MySQL 8.0 ein reserviertes Wort.
|
|
* - "group_members" verbindet beides und traegt die Rolle innerhalb der
|
|
* Gruppe: "editor" darf anlegen und aendern, "viewer" nur lesen.
|
|
* - Eine Domain gehoert zu genau einer Gruppe. Ein Kurzlink haengt an
|
|
* seiner Domain und damit ebenfalls an genau einer Gruppe - deshalb
|
|
* braucht die links-Tabelle keine eigene Spalte.
|
|
*
|
|
* Bestandsdaten: alle vorhandenen Domains wandern in die Gruppe
|
|
* "Standard" (Kennung 1), das bisherige Konto aus der settings-Tabelle
|
|
* wird zum ersten Administrator. Erst danach werden die beiden
|
|
* settings-Schluessel geloescht, damit keine veralteten Zugangsdaten
|
|
* zurueckbleiben.
|
|
*/
|
|
|
|
$now = gmdate('Y-m-d H:i:s');
|
|
|
|
/**
|
|
* Uebernimmt das bisherige Einzelkonto. Laeuft ins Leere, wenn noch
|
|
* keines eingerichtet war (frische Installation) - dann legt der
|
|
* Setup-Assistent den ersten Benutzer an.
|
|
*/
|
|
$adoptAdmin = "INSERT INTO users (username, password_hash, role, is_active, created_at)
|
|
SELECT COALESCE(
|
|
(SELECT value FROM settings WHERE key_name = 'admin_user' AND value IS NOT NULL AND value <> ''),
|
|
'admin'
|
|
),
|
|
s.value,
|
|
'admin',
|
|
1,
|
|
'{$now}'
|
|
FROM settings s
|
|
WHERE s.key_name = 'admin_password_hash' AND s.value IS NOT NULL AND s.value <> ''";
|
|
|
|
return [
|
|
'sqlite' => [
|
|
'CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT "user",
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
last_login_at TEXT NULL
|
|
)',
|
|
|
|
'CREATE TABLE IF NOT EXISTS user_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL
|
|
)',
|
|
|
|
'CREATE TABLE IF NOT EXISTS group_members (
|
|
group_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
role TEXT NOT NULL DEFAULT "editor",
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (group_id, user_id)
|
|
)',
|
|
'CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members (user_id)',
|
|
|
|
'ALTER TABLE domains ADD COLUMN group_id INTEGER NOT NULL DEFAULT 0',
|
|
'CREATE INDEX IF NOT EXISTS idx_domains_group ON domains (group_id)',
|
|
|
|
"INSERT INTO user_groups (id, name, created_at) VALUES (1, 'Standard', '{$now}')",
|
|
'UPDATE domains SET group_id = 1',
|
|
|
|
$adoptAdmin,
|
|
|
|
"INSERT INTO group_members (group_id, user_id, role, created_at)
|
|
SELECT 1, id, 'editor', '{$now}' FROM users",
|
|
|
|
"DELETE FROM settings WHERE key_name IN ('admin_user', 'admin_password_hash')",
|
|
],
|
|
|
|
'mysql' => [
|
|
'CREATE TABLE IF NOT EXISTS users (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
username VARCHAR(190) NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role VARCHAR(20) NOT NULL DEFAULT "user",
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL,
|
|
last_login_at DATETIME NULL,
|
|
UNIQUE KEY uniq_users_username (username)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
|
|
|
'CREATE TABLE IF NOT EXISTS user_groups (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
name VARCHAR(190) NOT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
UNIQUE KEY uniq_user_groups_name (name)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
|
|
|
'CREATE TABLE IF NOT EXISTS group_members (
|
|
group_id INT NOT NULL,
|
|
user_id INT NOT NULL,
|
|
role VARCHAR(20) NOT NULL DEFAULT "editor",
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (group_id, user_id),
|
|
KEY idx_group_members_user (user_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
|
|
|
'ALTER TABLE domains ADD COLUMN group_id INT NOT NULL DEFAULT 0',
|
|
'ALTER TABLE domains ADD INDEX idx_domains_group (group_id)',
|
|
|
|
"INSERT INTO user_groups (id, name, created_at) VALUES (1, 'Standard', '{$now}')",
|
|
'UPDATE domains SET group_id = 1',
|
|
|
|
$adoptAdmin,
|
|
|
|
"INSERT INTO group_members (group_id, user_id, role, created_at)
|
|
SELECT 1, id, 'editor', '{$now}' FROM (SELECT id FROM users) AS existing_users",
|
|
|
|
"DELETE FROM settings WHERE key_name IN ('admin_user', 'admin_password_hash')",
|
|
],
|
|
];
|