diff --git a/README_RECHTESYSTEM.md b/README_RECHTESYSTEM.md new file mode 100644 index 0000000..02068e2 --- /dev/null +++ b/README_RECHTESYSTEM.md @@ -0,0 +1,172 @@ +# Rechtesystem + +Diese Anleitung beschreibt das aktuelle Rechtesystem fuer die Seiten unter `public/`. + +## Grundidee + +Aktuell gilt: + +- `admin` und `master` haben Vollzugriff auf alle Seiten unter `public/` +- normale Benutzer sehen standardmaessig keine geschuetzten Seiten +- einzelne Seiten koennen pro Benutzer durch einen `master` freigegeben werden +- das Header-Menue passt sich an die hinterlegten Seitenrechte an + +## Rollen + +Es gibt zurzeit diese Rollen: + +- `user` + Normale Benutzerrolle ohne automatischen Zugriff auf die geschuetzten Seiten +- `admin` + Vollzugriff auf alle Seiten unter `public/` +- `master` + Vollzugriff auf alle Seiten unter `public/` plus Verwaltung der Seitenfreigaben anderer Benutzer + +Die Rollen-Helfer liegen in [auth_helpers.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php). + +## Zentrale Logik + +Die zentrale Rechtebasis besteht aus drei Bausteinen: + +- [auth_helpers.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php) + Enthält Rollen-Helfer, Allowlist und die Liste der freigebbaren Seiten +- [header.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/header.php) + Prueft beim Aufruf einer Seite den Zugriff und baut das Menue passend zu den Rechten +- [admin_users.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/admin_users.php) + Enthält die Benutzerverwaltung und die Seitenmatrix fuer `master` + +## Datenbank + +Fuer die Seitenfreigaben wird diese Tabelle verwendet: + +- `app_user_page_permissions` + +Wichtige Spalten: + +- `user_id` +- `page_key` + +`page_key` entspricht dem relativen Seitenpfad unter `public/`, zum Beispiel: + +- `moonphase.php` +- `tcrb_lightcurve.php` +- `geocron/index.php` + +Wichtig: + +- Nicht nur der Dateiname zaehlt. +- Unterordner-Seiten werden mit vollem relativen Pfad gespeichert. + +## Oeffentliche Ausnahmen + +Einige Seiten bleiben ohne Seitenfreigabe erreichbar. Diese stehen in der Allowlist in [auth_helpers.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php): + +- `login.php` +- `register.php` +- `logout.php` + +## Wie der Zugriff geprueft wird + +Beim Laden einer Seite mit gemeinsamem Header passiert Folgendes: + +1. `header.php` ermittelt den aktuellen `page_key` +2. Wenn der Benutzer `admin` oder `master` ist, ist der Zugriff erlaubt +3. Wenn die Seite in der Allowlist steht, ist der Zugriff erlaubt +4. Sonst wird in `app_user_page_permissions` geprueft, ob der Benutzer genau diese Seite sehen darf +5. Wenn nein, kommt `403 Zugriff verweigert` + +Wichtig: + +- Eine einzelne Seitenfreigabe macht den Benutzer nicht zu `admin` +- dadurch bleibt das Menue auf seine wirklich erlaubten Seiten beschraenkt + +## Wie das Menue funktioniert + +Das Header-Menue wird ebenfalls in [header.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/header.php) aufgebaut. + +Dabei gilt: + +- `admin` und `master` sehen das volle Menue +- normale Benutzer sehen nur die Menuepunkte, deren `href` in ihren Seitenfreigaben vorhanden ist +- auch der Kontobereich unten im Menue wird entsprechend gefiltert + +Damit das funktioniert, muessen Menuelink und `page_key` exakt zusammenpassen. + +Beispiel: + +- Menuelink: `geocron/index.php` +- Datenbankeintrag: `geocron/index.php` + +## Wie ein Master Seiten freigibt + +Die Freigaben werden in [admin_users.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/admin_users.php) verwaltet. + +Ablauf: + +1. Als `master` anmelden +2. `Benutzerverwaltung` oeffnen +3. Im Bereich `Seitenfreigaben` den gewuenschten Benutzer waehlen +4. Haken bei den erlaubten Seiten setzen +5. Speichern + +Besonderheiten: + +- `admin` und `master` brauchen keine Einzelhaken, weil ihre Rolle bereits Vollzugriff gibt +- nur ein `master` darf Seitenfreigaben bearbeiten + +## Wo die freigebaren Seiten definiert sind + +Die Liste der freigebaren Seiten steht zentral in [auth_helpers.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php) in: + +- `manageablePublicPages()` + +Nur Seiten, die dort eingetragen sind, koennen: + +- in der Matrix erscheinen +- einzeln fuer Benutzer freigegeben werden +- sauber im Menue gefiltert werden + +## Neue Seite ins Rechtesystem aufnehmen + +Wenn eine neue Seite unter `public/` dazukommt, sind in der Regel diese Schritte noetig: + +1. Seite anlegen +2. Seite so bauen, dass sie den gemeinsamen Header nutzt +3. In `manageablePublicPages()` eintragen +4. Falls noetig als Menuepunkt in [header.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/header.php) eintragen +5. Darauf achten, dass `href` und `page_key` exakt gleich sind + +Beispiel: + +```php +'beispielseite.php' => 'Beispielseite', +``` + +Oder fuer Unterordner: + +```php +'tools/beispiel.php' => 'Beispielseite', +``` + +## Wichtige Regel fuer Unterordner + +Bei Unterordnern niemals nur `index.php` verwenden. + +Richtig: + +- `geocron/index.php` + +Falsch: + +- `index.php` + +Die Seitenerkennung dafuer macht `currentPublicPageKey()` in [auth_helpers.php](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php). + +## Aktueller Stand + +Der aktuelle Zustand ist bewusst streng: + +- standardmaessig nur `admin` oder `master` +- normale Benutzer nur ueber explizite Seitenfreigaben + +Spaeter kann entschieden werden, welche Seiten wieder standardmaessig fuer normale Benutzer sichtbar sein sollen. diff --git a/config/rechtesystem.json b/config/rechtesystem.json new file mode 100644 index 0000000..3e54956 --- /dev/null +++ b/config/rechtesystem.json @@ -0,0 +1,159 @@ +{ + "version": 1, + "scope": "public", + "roles": { + "user": { + "label": "Normale Benutzerrolle", + "full_public_access": false, + "can_manage_page_permissions": false + }, + "admin": { + "label": "Administrator", + "full_public_access": true, + "can_manage_page_permissions": false + }, + "master": { + "label": "Master", + "full_public_access": true, + "can_manage_page_permissions": true + } + }, + "public_access_allowlist": [ + "login.php", + "register.php", + "logout.php" + ], + "manageable_pages": { + "index.php": { + "label": "Startseite", + "menu_group": null + }, + "settings.php": { + "label": "Einstellungen", + "menu_group": "Konto" + }, + "my_favorites.php": { + "label": "Favoriten", + "menu_group": "Konto" + }, + "moonphase.php": { + "label": "Mondphase", + "menu_group": "Himmel & Planeten" + }, + "planetensichtbarkeit.php": { + "label": "Planetensichtbarkeit", + "menu_group": "Himmel & Planeten" + }, + "tcrb_lightcurve.php": { + "label": "T CrB Lichtkurve", + "menu_group": "Himmel & Planeten" + }, + "solarsystem.php": { + "label": "Sonnensystem", + "menu_group": "Himmel & Planeten" + }, + "jupitersystem.php": { + "label": "Jupitersystem", + "menu_group": "Himmel & Planeten" + }, + "tagbogen.php": { + "label": "Tagbogen", + "menu_group": "Himmel & Planeten" + }, + "sternenhimmel.php": { + "label": "Sternenhimmel", + "menu_group": "Himmel & Planeten" + }, + "meteorshowers.php": { + "label": "Meteorstroeme", + "menu_group": "Himmel & Planeten" + }, + "geocron/index.php": { + "label": "Geocron", + "menu_group": "Himmel & Planeten" + }, + "geocron3d.php": { + "label": "Geocron 3D", + "menu_group": "Himmel & Planeten" + }, + "mondfinsternis.php": { + "label": "Mondfinsternis", + "menu_group": "Finsternisse & Bedeckungen" + }, + "sonnenfinsternis.php": { + "label": "Sonnenfinsternis", + "menu_group": "Finsternisse & Bedeckungen" + }, + "bedeckung.php": { + "label": "Bedeckungsphaenomene", + "menu_group": "Finsternisse & Bedeckungen" + }, + "sternbedeckungen.php": { + "label": "Mond-Stern-Bedeckungen", + "menu_group": "Finsternisse & Bedeckungen" + }, + "monatsvorhersage.php": { + "label": "Monatsvorhersage", + "menu_group": "Administration" + }, + "wochenvorhersage.php": { + "label": "Wochenvorhersage", + "menu_group": null + }, + "satellitenhimmel.php": { + "label": "Satellitenhimmel", + "menu_group": "Konto" + } + }, + "menu": { + "main_groups": [ + { + "title": "Himmel & Planeten", + "links": [ + "moonphase.php", + "planetensichtbarkeit.php", + "tcrb_lightcurve.php", + "solarsystem.php", + "jupitersystem.php", + "tagbogen.php", + "sternenhimmel.php", + "meteorshowers.php", + "geocron/index.php", + "geocron3d.php" + ] + }, + { + "title": "Finsternisse & Bedeckungen", + "links": [ + "mondfinsternis.php", + "sonnenfinsternis.php", + "bedeckung.php", + "sternbedeckungen.php" + ] + } + ], + "account_links": [ + "settings.php", + "my_favorites.php", + "satellitenhimmel.php" + ], + "admin_links": [ + "admin_users.php", + "monatsvorhersage.php", + "telegram/broadcast_test.php" + ] + }, + "database": { + "page_permissions_table": "app_user_page_permissions", + "page_permissions_columns": [ + "user_id", + "page_key" + ] + }, + "rules": { + "default_policy": "deny", + "single_page_permission_does_not_grant_admin": true, + "menu_visibility_follows_page_permissions": true, + "subfolder_pages_use_relative_path_keys": true + } +} diff --git a/config/skyview.sql b/config/skyview.sql index 5ad536e..8b64982 100644 --- a/config/skyview.sql +++ b/config/skyview.sql @@ -47,6 +47,21 @@ CREATE TABLE IF NOT EXISTS `app_users` ( -- Daten-Export vom Benutzer nicht ausgewählt +-- Exportiere Struktur von Tabelle d0467f49.app_user_page_permissions +CREATE TABLE IF NOT EXISTS `app_user_page_permissions` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `page_key` varchar(100) NOT NULL, + `created_at` datetime NOT NULL DEFAULT current_timestamp(), + `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `uq_app_user_page_permissions_user_page` (`user_id`,`page_key`), + KEY `idx_app_user_page_permissions_page_key` (`page_key`), + CONSTRAINT `fk_app_user_page_permissions_user_id` FOREIGN KEY (`user_id`) REFERENCES `app_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Daten-Export vom Benutzer nicht ausgewählt + -- Exportiere Struktur von Tabelle d0467f49.app_user_locations CREATE TABLE IF NOT EXISTS `app_user_locations` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, diff --git a/php-server.err.log b/php-server.err.log index 4e1b150..21c09ce 100644 --- a/php-server.err.log +++ b/php-server.err.log @@ -528,3 +528,274 @@ [Sun Apr 5 10:15:53 2026] 127.0.0.1:61416 Accepted [Sun Apr 5 10:18:37 2026] 127.0.0.1:61416 Closed without sending a request; it was probably just an unused speculative preconnection [Sun Apr 5 10:18:37 2026] 127.0.0.1:61416 Closing +[Sun Apr 5 10:42:17 2026] 127.0.0.1:64278 Accepted +[Sun Apr 5 10:42:17 2026] 127.0.0.1:64278 [200]: GET /moonphase.php +[Sun Apr 5 10:42:17 2026] 127.0.0.1:64278 Closing +[Sun Apr 5 10:42:17 2026] 127.0.0.1:54179 Accepted +[Sun Apr 5 10:42:17 2026] 127.0.0.1:54179 [200]: GET /css/style.css +[Sun Apr 5 10:42:17 2026] 127.0.0.1:54179 Closing +[Sun Apr 5 10:42:18 2026] 127.0.0.1:56188 Accepted +[Sun Apr 5 10:42:18 2026] 127.0.0.1:54704 Accepted +[Sun Apr 5 10:42:18 2026] 127.0.0.1:56188 [200]: GET /js/three.module.min.js +[Sun Apr 5 10:42:18 2026] 127.0.0.1:54704 [200]: GET /images/star16x16.png +[Sun Apr 5 10:42:18 2026] 127.0.0.1:54704 Closing +[Sun Apr 5 10:42:18 2026] 127.0.0.1:56188 Closing +[Sun Apr 5 10:42:18 2026] 127.0.0.1:54315 Accepted +[Sun Apr 5 10:42:18 2026] 127.0.0.1:54315 [200]: GET /js/three.core.min.js +[Sun Apr 5 10:42:18 2026] 127.0.0.1:54315 Closing +[Sun Apr 5 10:42:18 2026] 127.0.0.1:64368 Accepted +[Sun Apr 5 10:42:18 2026] 127.0.0.1:64368 [200]: POST /moonphase.php +[Sun Apr 5 10:42:18 2026] 127.0.0.1:64368 Closing +[Sun Apr 5 10:42:18 2026] 127.0.0.1:53158 Accepted +[Sun Apr 5 10:42:18 2026] 127.0.0.1:49519 Accepted +[Sun Apr 5 10:42:18 2026] 127.0.0.1:53158 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:42:18 2026] 127.0.0.1:53158 Closing +[Sun Apr 5 10:42:18 2026] 127.0.0.1:49519 [200]: GET /images/moons/moon_8192.jpg +[Sun Apr 5 10:42:19 2026] 127.0.0.1:49910 Accepted +[Sun Apr 5 10:42:19 2026] 127.0.0.1:49910 [200]: GET /images/moons/moon_displacement_16bit.jpg +[Sun Apr 5 10:42:19 2026] 127.0.0.1:49910 Closing +[Sun Apr 5 10:42:19 2026] 127.0.0.1:49519 Closing +[Sun Apr 5 10:42:20 2026] 127.0.0.1:54809 Accepted +[Sun Apr 5 10:42:21 2026] 127.0.0.1:54809 [200]: GET /favicon.ico +[Sun Apr 5 10:42:21 2026] 127.0.0.1:54809 Closing +[Sun Apr 5 10:42:21 2026] 127.0.0.1:61361 Accepted +[Sun Apr 5 10:44:00 2026] 127.0.0.1:61361 Closed without sending a request; it was probably just an unused speculative preconnection +[Sun Apr 5 10:44:00 2026] 127.0.0.1:61361 Closing +[Sun Apr 5 10:51:22 2026] 127.0.0.1:51086 Accepted +[Sun Apr 5 10:51:23 2026] 127.0.0.1:51086 [200]: GET /tcrb_lightcurve.php +[Sun Apr 5 10:51:23 2026] 127.0.0.1:51086 Closing +[Sun Apr 5 10:51:23 2026] 127.0.0.1:53270 Accepted +[Sun Apr 5 10:51:23 2026] 127.0.0.1:53270 [200]: GET /css/style.css +[Sun Apr 5 10:51:23 2026] 127.0.0.1:53270 Closing +[Sun Apr 5 10:51:23 2026] 127.0.0.1:49254 Accepted +[Sun Apr 5 10:51:23 2026] 127.0.0.1:54706 Accepted +[Sun Apr 5 10:51:23 2026] 127.0.0.1:49254 [200]: GET /images/star16x16.png +[Sun Apr 5 10:51:23 2026] 127.0.0.1:54706 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:51:23 2026] 127.0.0.1:49254 Closing +[Sun Apr 5 10:51:23 2026] 127.0.0.1:54706 Closing +[Sun Apr 5 10:51:23 2026] 127.0.0.1:54380 Accepted +[Sun Apr 5 10:51:24 2026] 127.0.0.1:54380 [200]: GET /favicon.ico +[Sun Apr 5 10:51:24 2026] 127.0.0.1:54380 Closing +[Sun Apr 5 10:51:24 2026] 127.0.0.1:58138 Accepted +[Sun Apr 5 10:51:34 2026] 127.0.0.1:58138 [302]: GET /logout.php +[Sun Apr 5 10:51:34 2026] 127.0.0.1:58138 Closing +[Sun Apr 5 10:51:34 2026] 127.0.0.1:52689 Accepted +[Sun Apr 5 10:51:34 2026] 127.0.0.1:52689 [200]: GET /login.php +[Sun Apr 5 10:51:34 2026] 127.0.0.1:52689 Closing +[Sun Apr 5 10:51:34 2026] 127.0.0.1:49930 Accepted +[Sun Apr 5 10:51:34 2026] 127.0.0.1:49930 [200]: GET /css/style.css +[Sun Apr 5 10:51:34 2026] 127.0.0.1:49930 Closing +[Sun Apr 5 10:51:34 2026] 127.0.0.1:53553 Accepted +[Sun Apr 5 10:51:34 2026] 127.0.0.1:53553 [200]: GET /images/star16x16.png +[Sun Apr 5 10:51:34 2026] 127.0.0.1:53553 Closing +[Sun Apr 5 10:51:34 2026] 127.0.0.1:59673 Accepted +[Sun Apr 5 10:51:34 2026] 127.0.0.1:61110 Accepted +[Sun Apr 5 10:51:34 2026] 127.0.0.1:59673 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:51:34 2026] 127.0.0.1:59673 Closing +[Sun Apr 5 10:51:34 2026] 127.0.0.1:61110 [302]: GET /favicon.ico +[Sun Apr 5 10:51:34 2026] 127.0.0.1:61110 Closing +[Sun Apr 5 10:51:35 2026] 127.0.0.1:49921 Accepted +[Sun Apr 5 10:51:35 2026] 127.0.0.1:49921 [200]: GET /login.php +[Sun Apr 5 10:51:35 2026] 127.0.0.1:49921 Closing +[Sun Apr 5 10:51:35 2026] 127.0.0.1:54437 Accepted +[Sun Apr 5 10:51:40 2026] 127.0.0.1:54437 [302]: GET /bedeckung.php +[Sun Apr 5 10:51:40 2026] 127.0.0.1:54437 Closing +[Sun Apr 5 10:51:40 2026] 127.0.0.1:51986 Accepted +[Sun Apr 5 10:51:40 2026] 127.0.0.1:51986 [200]: GET /login.php +[Sun Apr 5 10:51:40 2026] 127.0.0.1:51986 Closing +[Sun Apr 5 10:51:41 2026] 127.0.0.1:61653 Accepted +[Sun Apr 5 10:51:41 2026] 127.0.0.1:61653 [200]: GET /css/style.css +[Sun Apr 5 10:51:41 2026] 127.0.0.1:61653 Closing +[Sun Apr 5 10:51:41 2026] 127.0.0.1:53964 Accepted +[Sun Apr 5 10:51:41 2026] 127.0.0.1:53964 [200]: GET /images/star16x16.png +[Sun Apr 5 10:51:41 2026] 127.0.0.1:53964 Closing +[Sun Apr 5 10:51:41 2026] 127.0.0.1:65320 Accepted +[Sun Apr 5 10:51:41 2026] 127.0.0.1:51650 Accepted +[Sun Apr 5 10:51:41 2026] 127.0.0.1:65320 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:51:41 2026] 127.0.0.1:65320 Closing +[Sun Apr 5 10:51:41 2026] 127.0.0.1:51650 [302]: GET /favicon.ico +[Sun Apr 5 10:51:41 2026] 127.0.0.1:51650 Closing +[Sun Apr 5 10:51:41 2026] 127.0.0.1:59582 Accepted +[Sun Apr 5 10:51:41 2026] 127.0.0.1:59582 [200]: GET /login.php +[Sun Apr 5 10:51:41 2026] 127.0.0.1:59582 Closing +[Sun Apr 5 10:51:41 2026] 127.0.0.1:57266 Accepted +[Sun Apr 5 10:51:43 2026] 127.0.0.1:57266 [302]: GET /tcrb_lightcurve.php +[Sun Apr 5 10:51:43 2026] 127.0.0.1:57266 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:63119 Accepted +[Sun Apr 5 10:51:44 2026] 127.0.0.1:63119 [200]: GET /login.php +[Sun Apr 5 10:51:44 2026] 127.0.0.1:63119 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:52558 Accepted +[Sun Apr 5 10:51:44 2026] 127.0.0.1:52558 [200]: GET /css/style.css +[Sun Apr 5 10:51:44 2026] 127.0.0.1:52558 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:60412 Accepted +[Sun Apr 5 10:51:44 2026] 127.0.0.1:60412 [200]: GET /images/star16x16.png +[Sun Apr 5 10:51:44 2026] 127.0.0.1:60412 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:64866 Accepted +[Sun Apr 5 10:51:44 2026] 127.0.0.1:54717 Accepted +[Sun Apr 5 10:51:44 2026] 127.0.0.1:64866 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:51:44 2026] 127.0.0.1:64866 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:54717 [302]: GET /favicon.ico +[Sun Apr 5 10:51:44 2026] 127.0.0.1:54717 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:53140 Accepted +[Sun Apr 5 10:51:44 2026] 127.0.0.1:53140 [200]: GET /login.php +[Sun Apr 5 10:51:44 2026] 127.0.0.1:53140 Closing +[Sun Apr 5 10:51:44 2026] 127.0.0.1:59107 Accepted +[Sun Apr 5 10:51:50 2026] 127.0.0.1:59107 [200]: GET /register.php +[Sun Apr 5 10:51:50 2026] 127.0.0.1:59107 Closing +[Sun Apr 5 10:51:51 2026] 127.0.0.1:49407 Accepted +[Sun Apr 5 10:51:51 2026] 127.0.0.1:49407 [200]: GET /css/style.css +[Sun Apr 5 10:51:51 2026] 127.0.0.1:49407 Closing +[Sun Apr 5 10:51:51 2026] 127.0.0.1:55522 Accepted +[Sun Apr 5 10:51:51 2026] 127.0.0.1:55522 [200]: GET /images/star16x16.png +[Sun Apr 5 10:51:51 2026] 127.0.0.1:55522 Closing +[Sun Apr 5 10:51:51 2026] 127.0.0.1:55160 Accepted +[Sun Apr 5 10:51:51 2026] 127.0.0.1:62368 Accepted +[Sun Apr 5 10:51:51 2026] 127.0.0.1:55160 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:51:51 2026] 127.0.0.1:55160 Closing +[Sun Apr 5 10:51:51 2026] 127.0.0.1:62368 [302]: GET /favicon.ico +[Sun Apr 5 10:51:51 2026] 127.0.0.1:62368 Closing +[Sun Apr 5 10:51:51 2026] 127.0.0.1:57638 Accepted +[Sun Apr 5 10:51:51 2026] 127.0.0.1:57638 [200]: GET /login.php +[Sun Apr 5 10:51:51 2026] 127.0.0.1:57638 Closing +[Sun Apr 5 10:51:51 2026] 127.0.0.1:59720 Accepted +[Sun Apr 5 10:52:27 2026] PHP Fatal error: Uncaught Error: Call to undefined function mb_strlen() in C:\Users\ich\source\repos\skyview.astronomiemuseum.de\public\register.php:80 +Stack trace: +#0 {main} + thrown in C:\Users\ich\source\repos\skyview.astronomiemuseum.de\public\register.php on line 80 +[Sun Apr 5 10:52:27 2026] 127.0.0.1:59720 [200]: POST /register.php - Uncaught Error: Call to undefined function mb_strlen() in C:\Users\ich\source\repos\skyview.astronomiemuseum.de\public\register.php:80 +Stack trace: +#0 {main} + thrown in C:\Users\ich\source\repos\skyview.astronomiemuseum.de\public\register.php on line 80 +[Sun Apr 5 10:52:27 2026] 127.0.0.1:59720 Closing +[Sun Apr 5 10:52:27 2026] 127.0.0.1:57967 Accepted +[Sun Apr 5 10:52:27 2026] 127.0.0.1:57967 [302]: GET /favicon.ico +[Sun Apr 5 10:52:27 2026] 127.0.0.1:57967 Closing +[Sun Apr 5 10:52:28 2026] 127.0.0.1:59506 Accepted +[Sun Apr 5 10:52:28 2026] 127.0.0.1:59506 [200]: GET /login.php +[Sun Apr 5 10:52:28 2026] 127.0.0.1:59506 Closing +[Sun Apr 5 10:52:28 2026] 127.0.0.1:52354 Accepted +[Sun Apr 5 10:53:34 2026] 127.0.0.1:52354 Closed without sending a request; it was probably just an unused speculative preconnection +[Sun Apr 5 10:53:34 2026] 127.0.0.1:52354 Closing +[Sun Apr 5 10:59:42 2026] 127.0.0.1:49712 Accepted +[Sun Apr 5 10:59:42 2026] 127.0.0.1:49712 [302]: GET / +[Sun Apr 5 10:59:42 2026] 127.0.0.1:49712 Closing +[Sun Apr 5 10:59:42 2026] 127.0.0.1:60156 Accepted +[Sun Apr 5 10:59:42 2026] 127.0.0.1:60156 [200]: GET /login.php +[Sun Apr 5 10:59:42 2026] 127.0.0.1:60156 Closing +[Sun Apr 5 10:59:42 2026] 127.0.0.1:60139 Accepted +[Sun Apr 5 10:59:42 2026] 127.0.0.1:60139 [200]: GET /css/style.css +[Sun Apr 5 10:59:42 2026] 127.0.0.1:60139 Closing +[Sun Apr 5 10:59:43 2026] 127.0.0.1:50258 Accepted +[Sun Apr 5 10:59:43 2026] 127.0.0.1:55895 Accepted +[Sun Apr 5 10:59:43 2026] 127.0.0.1:50258 [200]: GET /images/star16x16.png +[Sun Apr 5 10:59:43 2026] 127.0.0.1:55895 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:59:43 2026] 127.0.0.1:50258 Closing +[Sun Apr 5 10:59:43 2026] 127.0.0.1:55895 Closing +[Sun Apr 5 10:59:43 2026] 127.0.0.1:61134 Accepted +[Sun Apr 5 10:59:43 2026] 127.0.0.1:61134 [302]: GET /favicon.ico +[Sun Apr 5 10:59:43 2026] 127.0.0.1:61134 Closing +[Sun Apr 5 10:59:43 2026] 127.0.0.1:64912 Accepted +[Sun Apr 5 10:59:43 2026] 127.0.0.1:64912 [200]: GET /login.php +[Sun Apr 5 10:59:43 2026] 127.0.0.1:64912 Closing +[Sun Apr 5 10:59:43 2026] 127.0.0.1:50335 Accepted +[Sun Apr 5 10:59:52 2026] 127.0.0.1:50335 [302]: POST /login.php +[Sun Apr 5 10:59:52 2026] 127.0.0.1:50335 Closing +[Sun Apr 5 10:59:52 2026] 127.0.0.1:64873 Accepted +[Sun Apr 5 10:59:53 2026] 127.0.0.1:64873 [200]: GET /index.php +[Sun Apr 5 10:59:53 2026] 127.0.0.1:64873 Closing +[Sun Apr 5 10:59:53 2026] 127.0.0.1:50230 Accepted +[Sun Apr 5 10:59:53 2026] 127.0.0.1:50230 [200]: GET /css/style.css +[Sun Apr 5 10:59:53 2026] 127.0.0.1:50230 Closing +[Sun Apr 5 10:59:54 2026] 127.0.0.1:52984 Accepted +[Sun Apr 5 10:59:54 2026] 127.0.0.1:53538 Accepted +[Sun Apr 5 10:59:54 2026] 127.0.0.1:52984 [200]: GET /images/star16x16.png +[Sun Apr 5 10:59:54 2026] 127.0.0.1:53538 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 10:59:54 2026] 127.0.0.1:52984 Closing +[Sun Apr 5 10:59:54 2026] 127.0.0.1:53538 Closing +[Sun Apr 5 10:59:54 2026] 127.0.0.1:55187 Accepted +[Sun Apr 5 10:59:55 2026] 127.0.0.1:55187 [200]: GET /favicon.ico +[Sun Apr 5 10:59:55 2026] 127.0.0.1:55187 Closing +[Sun Apr 5 10:59:55 2026] 127.0.0.1:52923 Accepted +[Sun Apr 5 11:00:00 2026] 127.0.0.1:52923 [200]: GET /admin_users.php +[Sun Apr 5 11:00:00 2026] 127.0.0.1:52923 Closing +[Sun Apr 5 11:00:01 2026] 127.0.0.1:51769 Accepted +[Sun Apr 5 11:00:01 2026] 127.0.0.1:51769 [200]: GET /css/style.css +[Sun Apr 5 11:00:01 2026] 127.0.0.1:51769 Closing +[Sun Apr 5 11:00:01 2026] 127.0.0.1:61907 Accepted +[Sun Apr 5 11:00:01 2026] 127.0.0.1:61907 [200]: GET /images/star16x16.png +[Sun Apr 5 11:00:01 2026] 127.0.0.1:61907 Closing +[Sun Apr 5 11:00:01 2026] 127.0.0.1:54942 Accepted +[Sun Apr 5 11:00:01 2026] 127.0.0.1:56778 Accepted +[Sun Apr 5 11:00:01 2026] 127.0.0.1:54942 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:00:01 2026] 127.0.0.1:54942 Closing +[Sun Apr 5 11:00:02 2026] 127.0.0.1:56778 [200]: GET /favicon.ico +[Sun Apr 5 11:00:02 2026] 127.0.0.1:56778 Closing +[Sun Apr 5 11:03:03 2026] 127.0.0.1:62953 Accepted +[Sun Apr 5 11:03:04 2026] 127.0.0.1:62953 [200]: GET /admin_users.php +[Sun Apr 5 11:03:04 2026] 127.0.0.1:62953 Closing +[Sun Apr 5 11:03:04 2026] 127.0.0.1:51691 Accepted +[Sun Apr 5 11:03:04 2026] 127.0.0.1:51691 [200]: GET /css/style.css +[Sun Apr 5 11:03:04 2026] 127.0.0.1:51691 Closing +[Sun Apr 5 11:03:04 2026] 127.0.0.1:52469 Accepted +[Sun Apr 5 11:03:04 2026] 127.0.0.1:57404 Accepted +[Sun Apr 5 11:03:04 2026] 127.0.0.1:52469 [200]: GET /images/star16x16.png +[Sun Apr 5 11:03:04 2026] 127.0.0.1:57404 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:03:04 2026] 127.0.0.1:52469 Closing +[Sun Apr 5 11:03:04 2026] 127.0.0.1:57404 Closing +[Sun Apr 5 11:03:04 2026] 127.0.0.1:49325 Accepted +[Sun Apr 5 11:03:05 2026] 127.0.0.1:49325 [200]: GET /favicon.ico +[Sun Apr 5 11:03:05 2026] 127.0.0.1:49325 Closing +[Sun Apr 5 11:03:05 2026] 127.0.0.1:51057 Accepted +[Sun Apr 5 11:05:18 2026] 127.0.0.1:51057 Closed without sending a request; it was probably just an unused speculative preconnection +[Sun Apr 5 11:05:18 2026] 127.0.0.1:51057 Closing +[Sun Apr 5 11:05:18 2026] 127.0.0.1:60059 Accepted +[Sun Apr 5 11:05:18 2026] 127.0.0.1:60059 [200]: GET /admin_users.php +[Sun Apr 5 11:05:18 2026] 127.0.0.1:60059 Closing +[Sun Apr 5 11:05:18 2026] 127.0.0.1:63636 Accepted +[Sun Apr 5 11:05:18 2026] 127.0.0.1:63636 [200]: GET /css/style.css +[Sun Apr 5 11:05:18 2026] 127.0.0.1:63636 Closing +[Sun Apr 5 11:05:18 2026] 127.0.0.1:60668 Accepted +[Sun Apr 5 11:05:18 2026] 127.0.0.1:60668 [200]: GET /images/star16x16.png +[Sun Apr 5 11:05:18 2026] 127.0.0.1:60668 Closing +[Sun Apr 5 11:05:18 2026] 127.0.0.1:57182 Accepted +[Sun Apr 5 11:05:18 2026] 127.0.0.1:52683 Accepted +[Sun Apr 5 11:05:18 2026] 127.0.0.1:57182 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:05:18 2026] 127.0.0.1:57182 Closing +[Sun Apr 5 11:05:19 2026] 127.0.0.1:52683 [200]: GET /favicon.ico +[Sun Apr 5 11:05:19 2026] 127.0.0.1:52683 Closing +[Sun Apr 5 11:05:37 2026] 127.0.0.1:52585 Accepted +[Sun Apr 5 11:05:37 2026] 127.0.0.1:52585 [302]: POST /admin_users.php +[Sun Apr 5 11:05:37 2026] 127.0.0.1:52585 Closing +[Sun Apr 5 11:05:37 2026] 127.0.0.1:61429 Accepted +[Sun Apr 5 11:05:37 2026] 127.0.0.1:61429 [200]: GET /admin_users.php +[Sun Apr 5 11:05:37 2026] 127.0.0.1:61429 Closing +[Sun Apr 5 11:05:38 2026] 127.0.0.1:64587 Accepted +[Sun Apr 5 11:05:38 2026] 127.0.0.1:64587 [200]: GET /css/style.css +[Sun Apr 5 11:05:38 2026] 127.0.0.1:64587 Closing +[Sun Apr 5 11:05:38 2026] 127.0.0.1:57024 Accepted +[Sun Apr 5 11:05:38 2026] 127.0.0.1:57024 [200]: GET /images/star16x16.png +[Sun Apr 5 11:05:38 2026] 127.0.0.1:57024 Closing +[Sun Apr 5 11:05:38 2026] 127.0.0.1:56396 Accepted +[Sun Apr 5 11:05:38 2026] 127.0.0.1:60199 Accepted +[Sun Apr 5 11:05:38 2026] 127.0.0.1:56396 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:05:38 2026] 127.0.0.1:56396 Closing +[Sun Apr 5 11:05:39 2026] 127.0.0.1:60199 [200]: GET /favicon.ico +[Sun Apr 5 11:05:39 2026] 127.0.0.1:60199 Closing +[Sun Apr 5 11:12:43 2026] 127.0.0.1:55123 Accepted +[Sun Apr 5 11:12:43 2026] 127.0.0.1:55123 [302]: POST /admin_users.php +[Sun Apr 5 11:12:43 2026] 127.0.0.1:55123 Closing +[Sun Apr 5 11:12:43 2026] 127.0.0.1:63224 Accepted +[Sun Apr 5 11:12:43 2026] 127.0.0.1:63224 [200]: GET /admin_users.php +[Sun Apr 5 11:12:43 2026] 127.0.0.1:63224 Closing +[Sun Apr 5 11:12:43 2026] 127.0.0.1:56558 Accepted +[Sun Apr 5 11:12:43 2026] 127.0.0.1:56558 [200]: GET /css/style.css +[Sun Apr 5 11:12:43 2026] 127.0.0.1:56558 Closing +[Sun Apr 5 11:12:44 2026] 127.0.0.1:64536 Accepted +[Sun Apr 5 11:12:44 2026] 127.0.0.1:64536 [200]: GET /images/star16x16.png +[Sun Apr 5 11:12:44 2026] 127.0.0.1:64536 Closing +[Sun Apr 5 11:12:44 2026] 127.0.0.1:54192 Accepted +[Sun Apr 5 11:12:44 2026] 127.0.0.1:55784 Accepted +[Sun Apr 5 11:12:44 2026] 127.0.0.1:54192 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:12:44 2026] 127.0.0.1:54192 Closing +[Sun Apr 5 11:12:45 2026] 127.0.0.1:55784 [200]: GET /favicon.ico +[Sun Apr 5 11:12:45 2026] 127.0.0.1:55784 Closing diff --git a/public/admin_users.php b/public/admin_users.php index 5d9d8d0..9cb6e55 100644 --- a/public/admin_users.php +++ b/public/admin_users.php @@ -10,6 +10,8 @@ if (!isset($_SESSION['user_id'])) { exit; } +require_once __DIR__ . '/auth_helpers.php'; + $config = require __DIR__ . '/../config/database.php'; $dsn = sprintf( @@ -49,6 +51,28 @@ function redirectToAdminUsers(): void exit; } +function isPrivilegedRole(?string $role): bool +{ + return isAdminLikeRole($role); +} + +function ensureUserPagePermissionsTable(PDO $pdo): void +{ + $pdo->exec(" + CREATE TABLE IF NOT EXISTS `app_user_page_permissions` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `page_key` varchar(100) NOT NULL, + `created_at` datetime NOT NULL DEFAULT current_timestamp(), + `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `uq_app_user_page_permissions_user_page` (`user_id`, `page_key`), + KEY `idx_app_user_page_permissions_page_key` (`page_key`), + CONSTRAINT `fk_app_user_page_permissions_user_id` FOREIGN KEY (`user_id`) REFERENCES `app_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + "); +} + function sendActivationNotification(string $username, string $email, ?string $displayName): void { $subject = 'Dein SkyView-Konto wurde freigeschaltet'; @@ -99,7 +123,7 @@ $currentUser = $stmtCurrentUser->fetch(); if ( !$currentUser || - $currentUser['role'] !== 'admin' || + !isAdminLikeRole((string) $currentUser['role']) || (int)$currentUser['is_active'] !== 1 ) { $_SESSION = []; @@ -108,6 +132,13 @@ if ( exit; } +$currentUserIsMaster = isMasterRole((string) ($currentUser['role'] ?? '')); +$manageablePages = manageablePublicPages(); + +if ($currentUserIsMaster) { + ensureUserPagePermissionsTable($pdo); +} + if (isset($_SESSION['admin_users_flash_success'])) { $successMessage = (string)$_SESSION['admin_users_flash_success']; unset($_SESSION['admin_users_flash_success']); @@ -140,6 +171,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $targetUser = $stmtTargetUser->fetch(); } + if ($action === 'update_page_permissions' && !$currentUserIsMaster) { + $errors[] = 'Nur ein Master-Benutzer darf Seitenfreigaben verwalten.'; + } + if (!$targetUser) { $errors[] = 'Das ausgewählte Benutzerkonto wurde nicht gefunden.'; } @@ -148,40 +183,66 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $errors[] = 'Das eigene Administrationskonto kann hier nicht deaktiviert, geloescht oder umgestellt werden.'; } + $currentUserIsMaster = isMasterRole((string) ($currentUser['role'] ?? '')); + $targetUserIsMaster = $targetUser ? isMasterRole((string) ($targetUser['role'] ?? '')) : false; + $requestedRole = (string) ($_POST['role'] ?? 'user'); + if (empty($errors) && $targetUser) { $stmtActiveAdminCount = $pdo->query(" SELECT COUNT(*) FROM `app_users` - WHERE `role` = 'admin' AND `is_active` = 1 + WHERE `role` IN ('admin', 'master') AND `is_active` = 1 "); $activeAdminCount = (int)$stmtActiveAdminCount->fetchColumn(); + $stmtMasterCount = $pdo->query(" + SELECT COUNT(*) + FROM `app_users` + WHERE `role` = 'master' + "); + $masterCount = (int)$stmtMasterCount->fetchColumn(); + + if ( + !$currentUserIsMaster && + $targetUserIsMaster + ) { + $errors[] = 'Nur ein Master-Benutzer darf die Rolle master vergeben oder aendern.'; + } + + if ( + !$currentUserIsMaster && + $requestedRole === 'master' && + $masterCount > 0 + ) { + $errors[] = 'Nur ein Master-Benutzer darf die Rolle master vergeben oder aendern.'; + } + if ( $action === 'deactivate_user' && - $targetUser['role'] === 'admin' && + isPrivilegedRole((string) $targetUser['role']) && (int)$targetUser['is_active'] === 1 && $activeAdminCount <= 1 ) { - $errors[] = 'Der letzte aktive Admin kann nicht deaktiviert werden.'; + $errors[] = 'Der letzte aktive privilegierte Benutzer kann nicht deaktiviert werden.'; } if ( $action === 'change_role' && - $targetUser['role'] === 'admin' && - ($_POST['role'] ?? '') !== 'admin' && + isPrivilegedRole((string) $targetUser['role']) && + !isPrivilegedRole($requestedRole) && (int)$targetUser['is_active'] === 1 && $activeAdminCount <= 1 ) { - $errors[] = 'Der letzte aktive Admin kann nicht zur Benutzerrolle herabgestuft werden.'; + $errors[] = 'Der letzte aktive privilegierte Benutzer kann nicht zur Benutzerrolle herabgestuft werden.'; } if ( $action === 'delete_user' && - $targetUser['role'] === 'admin' && + isPrivilegedRole((string) $targetUser['role']) && (int)$targetUser['is_active'] === 1 && $activeAdminCount <= 1 ) { - $errors[] = 'Der letzte aktive Admin kann nicht geloescht werden.'; + $errors[] = 'Der letzte aktive privilegierte Benutzer kann nicht geloescht werden.'; } } @@ -221,9 +282,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if ($action === 'change_role') { - $newRole = (string)($_POST['role'] ?? 'user'); + $newRole = $requestedRole; - if (!in_array($newRole, ['user', 'admin'], true)) { + if (!in_array($newRole, ['user', 'admin', 'master'], true)) { $errors[] = 'Die gewählte Rolle ist ungültig.'; } else { $stmtUpdate = $pdo->prepare(" @@ -251,6 +312,59 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $_SESSION['admin_users_flash_success'] = 'Benutzerkonto wurde geloescht. Zugehoerige Standorte und Satelliten wurden automatisch entfernt.'; redirectToAdminUsers(); } + + if ($action === 'update_page_permissions' && $currentUserIsMaster) { + $submittedPages = array_values(array_unique(array_filter(array_map( + static fn ($value): string => trim((string) $value), + is_array($_POST['page_keys'] ?? null) ? $_POST['page_keys'] : [] + ), static fn (string $value): bool => $value !== ''))); + + $submittedPages = array_values(array_intersect($submittedPages, array_keys($manageablePages))); + + $pdo->beginTransaction(); + try { + $stmtDeletePermissions = $pdo->prepare(" + DELETE FROM `app_user_page_permissions` + WHERE `user_id` = :user_id + "); + $stmtDeletePermissions->execute([ + ':user_id' => $targetUserId, + ]); + + if ($submittedPages !== []) { + $stmtInsertPermission = $pdo->prepare(" + INSERT INTO `app_user_page_permissions` ( + `user_id`, + `page_key`, + `created_at`, + `updated_at` + ) VALUES ( + :user_id, + :page_key, + NOW(), + NOW() + ) + "); + + foreach ($submittedPages as $pageKey) { + $stmtInsertPermission->execute([ + ':user_id' => $targetUserId, + ':page_key' => $pageKey, + ]); + } + } + + $pdo->commit(); + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + + $_SESSION['admin_users_flash_success'] = 'Die Seitenfreigaben wurden aktualisiert.'; + redirectToAdminUsers(); + } } if (!empty($errors)) { @@ -264,6 +378,7 @@ $stats = [ 'active' => 0, 'pending' => 0, 'admins' => 0, + 'masters' => 0, ]; $stmtStats = $pdo->query(" @@ -271,7 +386,8 @@ $stmtStats = $pdo->query(" COUNT(*) AS total_users, SUM(CASE WHEN `is_active` = 1 THEN 1 ELSE 0 END) AS active_users, SUM(CASE WHEN `is_active` = 0 THEN 1 ELSE 0 END) AS pending_users, - SUM(CASE WHEN `role` = 'admin' THEN 1 ELSE 0 END) AS admin_users + SUM(CASE WHEN `role` = 'admin' THEN 1 ELSE 0 END) AS admin_users, + SUM(CASE WHEN `role` = 'master' THEN 1 ELSE 0 END) AS master_users FROM `app_users` "); $statsRow = $stmtStats->fetch(); @@ -280,6 +396,7 @@ if ($statsRow) { $stats['active'] = (int)$statsRow['active_users']; $stats['pending'] = (int)$statsRow['pending_users']; $stats['admins'] = (int)$statsRow['admin_users']; + $stats['masters'] = (int)$statsRow['master_users']; } $stmtUsers = $pdo->query(" @@ -297,13 +414,36 @@ $stmtUsers = $pdo->query(" ORDER BY `is_active` ASC, `role` DESC, `created_at` DESC, `username` ASC "); $users = $stmtUsers->fetchAll(); +$pagePermissionsByUser = []; + +if ($currentUserIsMaster) { + $stmtPagePermissions = $pdo->query(" + SELECT `user_id`, `page_key` + FROM `app_user_page_permissions` + ORDER BY `user_id` ASC, `page_key` ASC + "); + + foreach ($stmtPagePermissions as $permissionRow) { + $permissionUserId = (int) ($permissionRow['user_id'] ?? 0); + $permissionPageKey = (string) ($permissionRow['page_key'] ?? ''); + if ($permissionUserId < 1 || $permissionPageKey === '') { + continue; + } + + if (!isset($pagePermissionsByUser[$permissionUserId])) { + $pagePermissionsByUser[$permissionUserId] = []; + } + + $pagePermissionsByUser[$permissionUserId][$permissionPageKey] = true; + } +} ?>
Hier kannst du Benutzerkonten freischalten, deaktivieren, loeschen und zwischen user und admin wechseln.
+Hier kannst du Benutzerkonten freischalten, deaktivieren, loeschen und zwischen user, admin und master wechseln.
= h($successMessage) ?>
@@ -334,6 +474,10 @@ $users = $stmtUsers->fetchAll(); Admins = $stats['admins'] ?>Nur Master-Benutzer duerfen hier festlegen, welche Seiten normale Nutzer im Menue sehen und direkt aufrufen duerfen.
+| Benutzer | + $pageLabel): ?> += h($pageLabel) ?> | + +Speichern | +
|---|---|---|
|
+
+ = h($userRow['display_name'] ?: $userRow['username']) ?> + = h($userRow['username']) ?> (= h($userRow['role']) ?>) + |
+ $pageLabel): ?>
+ + + = $isPrivilegedUser ? 'disabled' : '' ?> + > + | + ++ + Vollzugriff ueber Rolle + + + + | +