Rectesystem eingebaut

This commit is contained in:
2026-04-05 11:19:20 +02:00
parent cfd25d3b54
commit eb35501712
9 changed files with 1046 additions and 25 deletions
+172
View File
@@ -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.
+159
View File
@@ -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
}
}
+15
View File
@@ -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,
+271
View File
@@ -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
+221 -14
View File
@@ -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;
}
}
?>
<?php require __DIR__ . '/header.php'; ?>
<div class="admin-users-layout">
<section class="card admin-users-summary-card">
<h2>Uebersicht</h2>
<p class="hint">Hier kannst du Benutzerkonten freischalten, deaktivieren, loeschen und zwischen <strong>user</strong> und <strong>admin</strong> wechseln.</p>
<p class="hint">Hier kannst du Benutzerkonten freischalten, deaktivieren, loeschen und zwischen <strong>user</strong>, <strong>admin</strong> und <strong>master</strong> wechseln.</p>
<?php if ($successMessage !== ''): ?>
<p class="msg-success"><?= h($successMessage) ?></p>
@@ -334,6 +474,10 @@ $users = $stmtUsers->fetchAll();
<span class="admin-stat-label">Admins</span>
<strong><?= $stats['admins'] ?></strong>
</div>
<div class="admin-stat-box">
<span class="admin-stat-label">Master</span>
<strong><?= $stats['masters'] ?></strong>
</div>
</div>
</section>
@@ -375,7 +519,7 @@ $users = $stmtUsers->fetchAll();
</span>
</td>
<td>
<span class="role-badge <?= $userRow['role'] === 'admin' ? 'role-badge-admin' : 'role-badge-user' ?>">
<span class="role-badge <?= in_array($userRow['role'], ['admin', 'master'], true) ? 'role-badge-admin' : 'role-badge-user' ?>">
<?= h($userRow['role']) ?>
</span>
</td>
@@ -403,6 +547,9 @@ $users = $stmtUsers->fetchAll();
<select name="role"<?= $isSelf ? ' disabled' : '' ?>>
<option value="user"<?= $userRow['role'] === 'user' ? ' selected' : '' ?>>user</option>
<option value="admin"<?= $userRow['role'] === 'admin' ? ' selected' : '' ?>>admin</option>
<?php if ($currentUserIsMaster || $userRow['role'] === 'master'): ?>
<option value="master"<?= $userRow['role'] === 'master' ? ' selected' : '' ?>>master</option>
<?php endif; ?>
</select>
<button type="submit" class="btn btn-secondary btn-sm"<?= $isSelf ? ' disabled' : '' ?>>Rolle speichern</button>
</form>
@@ -421,6 +568,66 @@ $users = $stmtUsers->fetchAll();
</table>
</div>
</section>
<?php if ($currentUserIsMaster): ?>
<section class="card admin-users-table-card">
<h2>Seitenfreigaben</h2>
<p class="hint">Nur Master-Benutzer duerfen hier festlegen, welche Seiten normale Nutzer im Menue sehen und direkt aufrufen duerfen.</p>
<div class="admin-users-table-wrap">
<table class="data-table admin-users-table">
<thead>
<tr>
<th>Benutzer</th>
<?php foreach ($manageablePages as $pageKey => $pageLabel): ?>
<th><?= h($pageLabel) ?></th>
<?php endforeach; ?>
<th>Speichern</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $userRow): ?>
<?php
$permissionUserId = (int) $userRow['id'];
$userPermissions = $pagePermissionsByUser[$permissionUserId] ?? [];
$isPrivilegedUser = isPrivilegedRole((string) ($userRow['role'] ?? ''));
$permissionFormId = 'page-permissions-user-' . $permissionUserId;
?>
<tr>
<td>
<form id="<?= h($permissionFormId) ?>" method="post" action="">
<input type="hidden" name="csrf_token" value="<?= h($_SESSION['admin_users_csrf']) ?>">
<input type="hidden" name="action" value="update_page_permissions">
<input type="hidden" name="user_id" value="<?= $permissionUserId ?>">
</form>
<strong><?= h($userRow['display_name'] ?: $userRow['username']) ?></strong><br>
<small><?= h($userRow['username']) ?> (<?= h($userRow['role']) ?>)</small>
</td>
<?php foreach ($manageablePages as $pageKey => $pageLabel): ?>
<td>
<input
type="checkbox"
name="page_keys[]"
value="<?= h($pageKey) ?>"
form="<?= h($permissionFormId) ?>"
<?= isset($userPermissions[$pageKey]) ? 'checked' : '' ?>
<?= $isPrivilegedUser ? 'disabled' : '' ?>
>
</td>
<?php endforeach; ?>
<td>
<?php if ($isPrivilegedUser): ?>
<span class="hint">Vollzugriff ueber Rolle</span>
<?php else: ?>
<button type="submit" class="btn btn-secondary btn-sm" form="<?= h($permissionFormId) ?>">Freigaben speichern</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<?php endif; ?>
</div>
<dialog class="admin-dialog" id="activateNotifyDialog">
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
function currentUserRole(): string
{
return trim((string) ($_SESSION['role'] ?? ''));
}
function isMasterRole(?string $role): bool
{
return trim((string) $role) === 'master';
}
function isAdminLikeRole(?string $role): bool
{
$normalizedRole = trim((string) $role);
return $normalizedRole === 'admin' || $normalizedRole === 'master';
}
function currentUserIsAdminLike(): bool
{
return isAdminLikeRole(currentUserRole());
}
function publicAccessAllowlist(): array
{
return [
'login.php',
'register.php',
'logout.php',
];
}
function currentPublicPageKey(): string
{
$scriptName = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? ''));
$scriptName = ltrim($scriptName, '/');
if ($scriptName === '') {
return '';
}
$publicMarker = 'public/';
$publicPosition = strpos($scriptName, $publicMarker);
if ($publicPosition !== false) {
return substr($scriptName, $publicPosition + strlen($publicMarker));
}
return $scriptName;
}
function manageablePublicPages(): array
{
return [
'index.php' => 'Startseite',
'settings.php' => 'Einstellungen',
'my_favorites.php' => 'Favoriten',
'moonphase.php' => 'Mondphase',
'planetensichtbarkeit.php' => 'Planetensichtbarkeit',
'tcrb_lightcurve.php' => 'T CrB Lichtkurve',
'solarsystem.php' => 'Sonnensystem',
'jupitersystem.php' => 'Jupitersystem',
'tagbogen.php' => 'Tagbogen',
'sternenhimmel.php' => 'Sternenhimmel',
'meteorshowers.php' => 'Meteorstroeme',
'geocron/index.php' => 'Geocron',
'geocron3d.php' => 'Geocron 3D',
'mondfinsternis.php' => 'Mondfinsternis',
'sonnenfinsternis.php' => 'Sonnenfinsternis',
'bedeckung.php' => 'Bedeckungsphaenomene',
'sternbedeckungen.php' => 'Mond-Stern-Bedeckungen',
'monatsvorhersage.php' => 'Monatsvorhersage',
'wochenvorhersage.php' => 'Wochenvorhersage',
'satellitenhimmel.php' => 'Satellitenhimmel',
];
}
+125 -9
View File
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/auth_helpers.php';
$title = isset($pageTitle) && is_string($pageTitle) && $pageTitle !== ''
? $pageTitle
: 'Skyview & AstroTools — Astronomiemuseum der Sternwarte Sonneberg';
@@ -14,10 +15,14 @@ $headerIntroSub = isset($headerIntroSub) && is_string($headerIntroSub) ? trim($h
$showHeaderIntro = $headerIntroPre !== '' || $headerIntroTitle !== '' || $headerIntroSub !== '';
$publicBasePath = isset($publicBasePath) && is_string($publicBasePath) ? rtrim($publicBasePath, '/') . '/' : '';
$pdoHeader = isset($pdo) ? $pdo : null;
$isAdmin = $loggedIn && (($_SESSION['role'] ?? '') === 'admin');
$hasFullAdminAccess = $loggedIn && currentUserIsAdminLike();
$canAccessCurrentPage = $hasFullAdminAccess;
$pendingApprovals = 0;
$publicScriptName = currentPublicPageKey();
$publicAccessAllowlist = publicAccessAllowlist();
$manageablePublicPages = manageablePublicPages();
if (($loggedIn || $isAdmin) && $pdoHeader === null) {
if ($loggedIn && $pdoHeader === null) {
$cfgHeader = require __DIR__ . '/../config/database.php';
$pdoHeader = new PDO(
sprintf('mysql:host=%s;dbname=%s;charset=%s', $cfgHeader['host'], $cfgHeader['dbname'], $cfgHeader['charset'] ?? 'utf8mb4'),
@@ -26,6 +31,67 @@ if (($loggedIn || $isAdmin) && $pdoHeader === null) {
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
}
if (!in_array($publicScriptName, $publicAccessAllowlist, true) && !$canAccessCurrentPage) {
if (!$loggedIn) {
header('Location: ' . $publicBasePath . 'login.php');
exit;
}
$userHasPagePermission = false;
if ($pdoHeader !== null && isset($manageablePublicPages[$publicScriptName], $_SESSION['user_id'])) {
try {
$stmtPagePermission = $pdoHeader->prepare("
SELECT 1
FROM `app_user_page_permissions`
WHERE `user_id` = :user_id
AND `page_key` = :page_key
LIMIT 1
");
$stmtPagePermission->execute([
':user_id' => (int) $_SESSION['user_id'],
':page_key' => $publicScriptName,
]);
$userHasPagePermission = (bool) $stmtPagePermission->fetchColumn();
} catch (Throwable $e) {
$userHasPagePermission = false;
}
}
if ($userHasPagePermission) {
$canAccessCurrentPage = true;
}
}
if (!in_array($publicScriptName, $publicAccessAllowlist, true) && !$canAccessCurrentPage) {
if (!$loggedIn) {
header('Location: ' . $publicBasePath . 'login.php');
exit;
}
http_response_code(403);
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zugriff verweigert</title>
<link rel="stylesheet" href="<?= htmlspecialchars($publicBasePath . 'css/style.css', ENT_QUOTES, 'UTF-8') ?>">
</head>
<body>
<main class="container" style="padding-top: 4rem; padding-bottom: 4rem;">
<section class="card">
<h1>Zugriff verweigert</h1>
<p>Diese Seite ist aktuell nur fuer Benutzer mit der Rolle <strong>admin</strong> oder <strong>master</strong> freigegeben.</p>
<p><a class="btn btn-primary" href="<?= htmlspecialchars($publicBasePath . 'logout.php', ENT_QUOTES, 'UTF-8') ?>">Abmelden</a></p>
</section>
</main>
</body>
</html>
<?php
exit;
}
// Standard-Standort des eingeloggten Benutzers laden
$currentLocation = null;
if (!empty($defaultLocation)) {
@@ -43,7 +109,7 @@ if (!empty($defaultLocation)) {
$currentLocation = $stmtLoc->fetch() ?: null;
}
if ($isAdmin && $pdoHeader !== null) {
if ($hasFullAdminAccess && $pdoHeader !== null) {
$stmtPendingUsers = $pdoHeader->query('SELECT COUNT(*) FROM app_users WHERE is_active = 0');
$pendingApprovals = (int)$stmtPendingUsers->fetchColumn();
}
@@ -53,7 +119,6 @@ $menuGroups = [
'title' => 'Himmel &amp; Planeten',
'links' => [
['href' => 'moonphase.php', 'label' => 'Mondphase'],
['href' => 'index_py.php', 'label' => 'Sonne & Mond'],
['href' => 'planetensichtbarkeit.php', 'label' => 'Planetensichtbarkeit'],
['href' => 'tcrb_lightcurve.php', 'label' => 'T CrB Lichtkurve'],
['href' => 'solarsystem.php', 'label' => 'Sonnensystem'],
@@ -81,6 +146,57 @@ $adminLinks = [
['href' => 'monatsvorhersage.php', 'label' => 'Monatsvorhersage'],
['href' => 'telegram/broadcast_test.php', 'label' => 'Telegram-Test'],
];
$accountLinks = [
['href' => 'settings.php', 'label' => 'Einstellungen'],
['href' => 'my_favorites.php', 'label' => 'Favoriten'],
['href' => 'satellitenhimmel.php', 'label' => 'Satellitenhimmel'],
];
if ($loggedIn && !$hasFullAdminAccess && $pdoHeader !== null) {
$allowedPageKeys = [];
try {
$stmtAllowedPages = $pdoHeader->prepare("
SELECT `page_key`
FROM `app_user_page_permissions`
WHERE `user_id` = :user_id
");
$stmtAllowedPages->execute([
':user_id' => (int) ($_SESSION['user_id'] ?? 0),
]);
foreach ($stmtAllowedPages as $allowedPageRow) {
$pageKey = trim((string) ($allowedPageRow['page_key'] ?? ''));
if ($pageKey !== '') {
$allowedPageKeys[$pageKey] = true;
}
}
} catch (Throwable $e) {
$allowedPageKeys = [];
}
foreach ($menuGroups as &$group) {
$group['links'] = array_values(array_filter(
$group['links'],
static function (array $link) use ($allowedPageKeys): bool {
$href = trim((string) ($link['href'] ?? ''));
return $href !== '' && isset($allowedPageKeys[$href]);
}
));
}
unset($group);
$menuGroups = array_values(array_filter(
$menuGroups,
static fn (array $group): bool => !empty($group['links'])
));
$accountLinks = array_values(array_filter(
$accountLinks,
static function (array $link) use ($allowedPageKeys): bool {
$href = trim((string) ($link['href'] ?? ''));
return $href !== '' && isset($allowedPageKeys[$href]);
}
));
}
?>
<!DOCTYPE html>
<html lang="de">
@@ -165,7 +281,7 @@ $adminLinks = [
<a href="<?= htmlspecialchars($publicBasePath . 'index.php', ENT_QUOTES, 'UTF-8') ?>">Skyview &amp; AstroTools</a>
<span class="header-sub"><a href="https://www.astronomiemuseum.de" target="_blank" rel="noopener">Astronomiemuseum der Sternwarte Sonneberg</a></span>
</div>
<?php if ($isAdmin && $pendingApprovals > 0): ?>
<?php if ($hasFullAdminAccess && $pendingApprovals > 0): ?>
<a href="<?= htmlspecialchars($publicBasePath . 'admin_users.php', ENT_QUOTES, 'UTF-8') ?>" class="admin-alert" aria-live="polite">
<span class="admin-alert-icon" aria-hidden="true">!</span>
<span class="admin-alert-text">Achtung: <?= $pendingApprovals ?> <?= $pendingApprovals === 1 ? 'Benutzer wartet' : 'Benutzer warten' ?> auf Freischaltung</span>
@@ -195,16 +311,16 @@ $adminLinks = [
<div class="menu-group">
<div class="menu-title">Konto</div>
<?php if ($loggedIn): ?>
<a href="<?= htmlspecialchars($publicBasePath . 'settings.php', ENT_QUOTES, 'UTF-8') ?>">Einstellungen</a>
<a href="<?= htmlspecialchars($publicBasePath . 'my_favorites.php', ENT_QUOTES, 'UTF-8') ?>">Favoriten</a>
<a href="<?= htmlspecialchars($publicBasePath . 'satellitenhimmel.php', ENT_QUOTES, 'UTF-8') ?>">Satellitenhimmel</a>
<?php foreach ($accountLinks as $accountLink): ?>
<a href="<?= htmlspecialchars($publicBasePath . $accountLink['href'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($accountLink['label'], ENT_QUOTES, 'UTF-8') ?></a>
<?php endforeach; ?>
<a href="<?= htmlspecialchars($publicBasePath . 'logout.php', ENT_QUOTES, 'UTF-8') ?>" class="nav-logout">Abmelden</a>
<?php else: ?>
<a href="<?= htmlspecialchars($publicBasePath . 'login.php', ENT_QUOTES, 'UTF-8') ?>">Anmelden</a>
<a href="<?= htmlspecialchars($publicBasePath . 'register.php', ENT_QUOTES, 'UTF-8') ?>">Registrieren</a>
<?php endif; ?>
</div>
<?php if ($isAdmin): ?>
<?php if ($hasFullAdminAccess): ?>
<div class="menu-group">
<div class="menu-title">Administration</div>
<?php foreach ($adminLinks as $adminLink): ?>
+3 -1
View File
@@ -5,8 +5,10 @@ ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
require_once __DIR__ . '/auth_helpers.php';
$loggedIn = isset($_SESSION['user_id']);
$isAdmin = $loggedIn && (($_SESSION['role'] ?? '') === 'admin');
$isAdmin = $loggedIn && currentUserIsAdminLike();
// AJAX-Handler: GRS-Kalibrierung lesen/speichern
if (isset($_GET['grs_api'])) {
+3 -1
View File
@@ -10,6 +10,8 @@ if (!isset($_SESSION['user_id'])) {
exit;
}
require_once __DIR__ . '/../auth_helpers.php';
$config = require __DIR__ . '/../../config/database.php';
$telegramConfig = require __DIR__ . '/../../config/telegram.php';
@@ -255,7 +257,7 @@ $currentUser = $stmtCurrentUser->fetch();
if (
!$currentUser ||
$currentUser['role'] !== 'admin' ||
!isAdminLikeRole((string) $currentUser['role']) ||
(int) $currentUser['is_active'] !== 1
) {
$_SESSION = [];