adasdadad
This commit is contained in:
5
config/app_access.php
Normal file
5
config/app_access.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [];
|
||||||
@@ -23,6 +23,10 @@ Custom-App-Assets werden nicht direkt aus `public/` geladen, sondern ueber den g
|
|||||||
- aktuelle Beispiele: `Admin Apps`, `User Management`, `User Self Management`, `Cron Tool`
|
- aktuelle Beispiele: `Admin Apps`, `User Management`, `User Self Management`, `Cron Tool`
|
||||||
- `Custom Apps` sind installierbare Fachanwendungen
|
- `Custom Apps` sind installierbare Fachanwendungen
|
||||||
- aktuelle Beispiele: `Mining-Checker`, `Waehrungs-Checker`, `Boersenchecker`, `Pi-hole`
|
- aktuelle Beispiele: `Mining-Checker`, `Waehrungs-Checker`, `Boersenchecker`, `Pi-hole`
|
||||||
|
- eine `App` ist die allgemeine Anwendung; ein Benutzer aktiviert sie mindestens fuer das Menue und kann fuer geeignete Apps zusaetzlich ein Desktop-Icon einblenden
|
||||||
|
- `Tray-Apps` sind kleine Schnellzugriffe oder Statusanzeigen neben der Uhr und koennen eigenstaendig oder zusammen mit der Haupt-App genutzt werden
|
||||||
|
- `Widgets` sind kleine Desktop-Elemente ohne klassische Fensterfunktionen und koennen eigenstaendig oder Teil einer App sein
|
||||||
|
- aktuell werden Widgets noch uebergangsweise im rechten Infobereich gerendert; das Zielbild ist spaeter eine freier positionierbare Darstellung auf dem Desktop
|
||||||
|
|
||||||
Diese Trennung wird ueber App-Metadaten gesteuert:
|
Diese Trennung wird ueber App-Metadaten gesteuert:
|
||||||
|
|
||||||
@@ -45,7 +49,7 @@ Wichtige Bausteine:
|
|||||||
- `show_on_desktop`
|
- `show_on_desktop`
|
||||||
- `show_in_start_menu`
|
- `show_in_start_menu`
|
||||||
- `widgets`
|
- `widgets`
|
||||||
- beschreibt Widget-Funktionen, die nur fuer installierte Module verfuegbar sind
|
- beschreibt Widget-Funktionen, die fuer Benutzer mit App-Zugriff separat aktiviert werden koennen
|
||||||
- `cron_jobs`
|
- `cron_jobs`
|
||||||
- beschreibt Cron-Endpunkte fuer die zentrale Cron-Verwaltung
|
- beschreibt Cron-Endpunkte fuer die zentrale Cron-Verwaltung
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ Der Desktop besteht aus mehreren Hauptbereichen:
|
|||||||
- `Infobereich`
|
- `Infobereich`
|
||||||
Rechter Seitenbereich fuer eingeblendete Informationskarten.
|
Rechter Seitenbereich fuer eingeblendete Informationskarten.
|
||||||
|
|
||||||
- `Widget-Bereich`
|
- `Tray-Bereich`
|
||||||
Bereich bei Uhr und Systemleiste fuer kleine Mini-Funktionen.
|
Bereich bei Uhr und Systemleiste fuer kleine Tray-Apps und Mini-Funktionen.
|
||||||
|
|
||||||
## Startmenue verwenden
|
## Startmenue verwenden
|
||||||
|
|
||||||
@@ -58,10 +58,12 @@ Dort koennen derzeit insbesondere verwaltet werden:
|
|||||||
|
|
||||||
- `Desktop Type` beziehungsweise Skin-Auswahl
|
- `Desktop Type` beziehungsweise Skin-Auswahl
|
||||||
- persoenliche Benutzerdaten
|
- persoenliche Benutzerdaten
|
||||||
- sichtbare Apps
|
- aktivierte `Apps` im Menue
|
||||||
- Inhalte des `Infobereichs`
|
- optionale `Desktop-Icons` fuer geeignete Apps
|
||||||
|
- aktivierte `Tray-Apps`
|
||||||
|
- aktivierte `Widgets`
|
||||||
|
|
||||||
Administratoren nutzen zusaetzlich `Admin Apps` fuer den globalen App-Bestand, die Widget-Uebersicht und erste Integrations-Einstellungen wie den Gitea-Deploy-Status.
|
Administratoren nutzen zusaetzlich `Admin Apps` fuer den globalen App-Bestand, Installationswege, LDAP-Gruppenberechtigungen und erste Integrations-Einstellungen wie den Gitea-Deploy-Status.
|
||||||
|
|
||||||
## Module
|
## Module
|
||||||
|
|
||||||
@@ -74,10 +76,12 @@ Aktuell gilt:
|
|||||||
- Modul-Businesslogik bleibt im Modul und wird nicht in den Desktop-Core verschoben
|
- Modul-Businesslogik bleibt im Modul und wird nicht in den Desktop-Core verschoben
|
||||||
- gemeinsame Desktop-Mechaniken wie Fenster, Asset-Einbindung und Zugriffsschutz werden global bereitgestellt
|
- gemeinsame Desktop-Mechaniken wie Fenster, Asset-Einbindung und Zugriffsschutz werden global bereitgestellt
|
||||||
|
|
||||||
## Widgets und Cronjobs
|
## Tray-Apps, Widgets und Cronjobs
|
||||||
|
|
||||||
- Widgets koennen von Modulen bereitgestellt werden, wenn das Modul diese Funktion im Manifest hinterlegt.
|
- `Tray-Apps` liegen im Bereich neben der Uhr und sind fuer Schnellaktionen oder kleine Visualisierungen gedacht.
|
||||||
- Widget-Funktionen stehen nur dann zur Auswahl, wenn die zugehoerige App fuer den Benutzer installiert oder aktiviert ist.
|
- `Widgets` koennen von Modulen bereitgestellt werden, wenn das Modul diese Funktion im Manifest hinterlegt.
|
||||||
|
- Widgets sind fachlich kleine Desktop-Elemente ohne Fensterfunktionen; aktuell erscheinen sie technisch noch uebergangsweise im rechten `Infobereich`.
|
||||||
|
- Widget- und Tray-Funktionen stehen nur dann zur Auswahl, wenn der Benutzer auf die zugrundeliegende App Zugriff hat.
|
||||||
- Das `Cron Tool` ist ein globales `Systemtool` fuer Administratoren.
|
- Das `Cron Tool` ist ein globales `Systemtool` fuer Administratoren.
|
||||||
- Dort erscheinen Modul-Cronjobs automatisch, sobald ein Modul sie in `module.json` hinterlegt.
|
- Dort erscheinen Modul-Cronjobs automatisch, sobald ein Modul sie in `module.json` hinterlegt.
|
||||||
- Cron-Aufrufe laufen auf demselben Host und verwenden dieselbe Projektbasis wie der Desktop.
|
- Cron-Aufrufe laufen auf demselben Host und verwenden dieselbe Projektbasis wie der Desktop.
|
||||||
@@ -89,7 +93,7 @@ Der `Waehrungs-Checker` verwaltet gespeicherte Wechselkurse, Kurs-Historie, Umre
|
|||||||
- das Oeffnen der App loest keinen externen Abruf aus
|
- das Oeffnen der App loest keinen externen Abruf aus
|
||||||
- ein manueller Abruf ist nur erlaubt, wenn die letzten gespeicherten Kurse aelter als die konfigurierte Sperrzeit sind
|
- ein manueller Abruf ist nur erlaubt, wenn die letzten gespeicherten Kurse aelter als die konfigurierte Sperrzeit sind
|
||||||
- mit `force` kann ein Abruf bewusst erzwungen werden
|
- mit `force` kann ein Abruf bewusst erzwungen werden
|
||||||
- das zugehoerige Widget im `Widget-Bereich` nutzt dieselbe Regel wie App und API
|
- das zugehoerige Widget nutzt dieselbe Regel wie App und API
|
||||||
|
|
||||||
## Benutzerdaten
|
## Benutzerdaten
|
||||||
|
|
||||||
@@ -116,14 +120,15 @@ Der `Infobereich` ist der rechte Desktopbereich.
|
|||||||
|
|
||||||
- Inhalte koennen benutzerbezogen ein- oder ausgeblendet werden.
|
- Inhalte koennen benutzerbezogen ein- oder ausgeblendet werden.
|
||||||
- Aenderungen sollen gespeichert bleiben.
|
- Aenderungen sollen gespeichert bleiben.
|
||||||
- Der `Infobereich` ist nicht identisch mit dem `Widget-Bereich`.
|
- Der `Infobereich` ist nicht identisch mit dem `Tray-Bereich`.
|
||||||
|
- Widgets werden aktuell noch dort eingeblendet, sollen spaeter aber frei auf dem Desktop positionierbar sein.
|
||||||
|
|
||||||
## Begriffsregel
|
## Begriffsregel
|
||||||
|
|
||||||
Fuer sprachliche Konsistenz gilt:
|
Fuer sprachliche Konsistenz gilt:
|
||||||
|
|
||||||
- rechte Desktopseite: `Infobereich`
|
- rechte Desktopseite: `Infobereich`
|
||||||
- Mini-Apps an Uhr/Systemleiste: `Widget-Bereich`
|
- Mini-Apps an Uhr/Systemleiste: `Tray-Bereich`
|
||||||
- linke Startmenuespalte: `User Setting Bereich`
|
- linke Startmenuespalte: `User Setting Bereich`
|
||||||
- mittlere Startmenuespalte: `Funktion-Bereich`
|
- mittlere Startmenuespalte: `Funktion-Bereich`
|
||||||
- rechte Startmenuespalte: `Auswahlbereich`
|
- rechte Startmenuespalte: `Auswahlbereich`
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ Diese Benennungen gelten projektweit und sollen in UI, Doku und Weiterentwicklun
|
|||||||
- `Infobereich`
|
- `Infobereich`
|
||||||
Der rechte Bereich auf dem Desktop mit eingeblendeten Informationskarten oder kleinen Bereichsmodulen.
|
Der rechte Bereich auf dem Desktop mit eingeblendeten Informationskarten oder kleinen Bereichsmodulen.
|
||||||
|
|
||||||
- `Widget-Bereich`
|
- `Tray-Bereich`
|
||||||
Der Bereich bei der Uhr bzw. Systemleiste fuer kleine Widgets oder Mini-Apps.
|
Der Bereich bei der Uhr bzw. Systemleiste fuer kleine Tray-Apps oder Mini-Funktionen.
|
||||||
|
|
||||||
- `Setup-Bereich`
|
- `Setup-Bereich`
|
||||||
Der zentrale Einstellungsbereich fuer Benutzer, Desktop-Typ, Apps und Bereichsauswahl. Aktuell umgesetzt ueber `User Self Management`.
|
Der zentrale Einstellungsbereich fuer Benutzer, Desktop-Typ, Apps und Bereichsauswahl. Aktuell umgesetzt ueber `User Self Management`.
|
||||||
@@ -56,13 +56,16 @@ Diese Benennungen gelten projektweit und sollen in UI, Doku und Weiterentwicklun
|
|||||||
### Systemobjekte
|
### Systemobjekte
|
||||||
|
|
||||||
- `App`
|
- `App`
|
||||||
Ein bereitgestelltes System-Tool oder Modul. Eine App kann als Desktop-App, Menue-App, Systembar-Eintrag oder Quelle fuer Widgets/Funktionen auftreten.
|
Eine allgemeine Anwendung. Eine App erscheint standardmaessig als Menue-Eintrag, kann zusaetzlich ein Desktop-Icon bekommen und weitere Darstellungen wie Tray-App oder Widget bereitstellen. Admins koennen die LDAP-Gruppenberechtigung pro App verwalten.
|
||||||
|
|
||||||
|
- `Tray-App`
|
||||||
|
Eine App oder Teilfunktion im Bereich neben der Uhr. Tray-Apps sind fuer Schnellaktionen oder Visualisierungen gedacht und koennen eigenstaendig oder zusammen mit der Haupt-App genutzt werden.
|
||||||
|
|
||||||
- `API`
|
- `API`
|
||||||
Die serverseitige Programmschnittstelle der Desktop-Anwendung. Sie wird im aktuellen Projekt auf demselben Host wie die Desktop-Shell unter `/api/v1/...` bereitgestellt.
|
Die serverseitige Programmschnittstelle der Desktop-Anwendung. Sie wird im aktuellen Projekt auf demselben Host wie die Desktop-Shell unter `/api/v1/...` bereitgestellt.
|
||||||
|
|
||||||
- `Widget`
|
- `Widget`
|
||||||
Eine kleine Funktion oder Mini-Anzeige. Widgets sind nicht automatisch identisch mit Apps. Eine App kann Widgets bereitstellen, muss es aber nicht.
|
Ein kleines Desktop-Element ohne klassische Fensterfunktionen wie Maximieren, Minimieren, Schliessen oder Groessenaenderung. Widgets koennen eigenstaendig oder Teil einer App sein. Zielbild ist eine frei positionierbare Flaeche auf dem Desktop; aktuell werden Widgets uebergangsweise noch im rechten Infobereich gerendert.
|
||||||
|
|
||||||
- `Desktop Type`
|
- `Desktop Type`
|
||||||
Benutzerseitige Auswahl des Desktop-/Skin-Typs. Aktuell vorgesehen: `Apple`, `Windows`, `Linux`.
|
Benutzerseitige Auswahl des Desktop-/Skin-Typs. Aktuell vorgesehen: `Apple`, `Windows`, `Linux`.
|
||||||
@@ -81,14 +84,15 @@ Stand dieser Datei:
|
|||||||
- Fensterverwaltung mit Oeffnen, Fokussieren, Minimieren, Maximieren und Verschieben
|
- Fensterverwaltung mit Oeffnen, Fokussieren, Minimieren, Maximieren und Verschieben
|
||||||
- konsistentes Standard-Design fuer Fensterinhalte, sofern eine App kein eigenes UI mitbringt
|
- konsistentes Standard-Design fuer Fensterinhalte, sofern eine App kein eigenes UI mitbringt
|
||||||
- Startmenue mit `User Setting Bereich`, `Funktion-Bereich` und `Auswahlbereich`
|
- Startmenue mit `User Setting Bereich`, `Funktion-Bereich` und `Auswahlbereich`
|
||||||
- rechter `Infobereich` mit benutzerbezogener Sichtbarkeit
|
- rechter `Infobereich` mit benutzerbezogener Sichtbarkeit als aktueller Uebergangsplatz fuer Widgets
|
||||||
- `User Self Management` als Setup-App
|
- `User Self Management` als Setup-App
|
||||||
- `Admin Apps` als zentrale Admin-App fuer App-Bestand, Widgets und erste Integrations-Einstellungen
|
- `Admin Apps` als zentrale Admin-App fuer App-Bestand, Widgets und erste Integrations-Einstellungen
|
||||||
|
- getrennte Nutzersteuerung fuer `Apps`, `Tray-Apps`, `Widgets` und optionale `Desktop-Icons`
|
||||||
- erster echter App-Pfad fuer installierbare Fach-Apps mit Discovery aus `custom/apps/<app>/`
|
- erster echter App-Pfad fuer installierbare Fach-Apps mit Discovery aus `custom/apps/<app>/`
|
||||||
- `Mining-Checker` als erstes angebundenes klassisches Modul
|
- `Mining-Checker` als erstes angebundenes klassisches Modul
|
||||||
- `Waehrungs-Checker` als zweites angebundenes klassisches Modul mit Desktop-Widget fuer Kursaktualisierung
|
- `Waehrungs-Checker` als zweites angebundenes klassisches Modul mit Desktop-Widget fuer Kursaktualisierung
|
||||||
- versionierte API auf demselben Host unter `(staging.)desktop.kusche.berlin/api/v1/...`
|
- versionierte API auf demselben Host unter `(staging.)desktop.kusche.berlin/api/v1/...`
|
||||||
- Benutzereinstellungen fuer Desktop-Skin, App-Auswahl, Infobereich und Profildaten in einer eigenen Datenbanktabelle mit `_user_data`-Suffix
|
- Benutzereinstellungen fuer Desktop-Skin, App-Auswahl, Tray-Apps, Widgets und Profildaten in einer eigenen Datenbanktabelle mit `_user_data`-Suffix
|
||||||
- vorhandene JSON-Dateien dienen nur noch als Fallback oder Uebergang fuer lokale Entwicklung und Altbestaende
|
- vorhandene JSON-Dateien dienen nur noch als Fallback oder Uebergang fuer lokale Entwicklung und Altbestaende
|
||||||
- LDAP-Synchronisierung fuer Standardfelder wie Name, E-Mail, Telefon, Titel und Ort
|
- LDAP-Synchronisierung fuer Standardfelder wie Name, E-Mail, Telefon, Titel und Ort
|
||||||
- `Geburtsdatum` aktuell bewusst nur lokal gespeichert
|
- `Geburtsdatum` aktuell bewusst nur lokal gespeichert
|
||||||
@@ -146,7 +150,7 @@ Fuer einen spaeteren Hilfebereich sollen Inhalte aus dieser Datei in Themenblcke
|
|||||||
- `Desktop bedienen`
|
- `Desktop bedienen`
|
||||||
- `Menue verstehen`
|
- `Menue verstehen`
|
||||||
- `Fenster verwenden`
|
- `Fenster verwenden`
|
||||||
- `Infobereich konfigurieren`
|
- `Infobereich und Widgets konfigurieren`
|
||||||
- `Apps und Desktop Type verwalten`
|
- `Apps und Desktop Type verwalten`
|
||||||
- `Module verstehen und starten`
|
- `Module verstehen und starten`
|
||||||
- `API-Pfade und Versionen verstehen`
|
- `API-Pfade und Versionen verstehen`
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ Aktuell wichtig:
|
|||||||
- der Boersenchecker ist als importierte Desktop-App unter `custom/apps/boersenchecker/` angebunden
|
- der Boersenchecker ist als importierte Desktop-App unter `custom/apps/boersenchecker/` angebunden
|
||||||
- Modul-Assets koennen ueber einen gemeinsamen Auslieferungsweg aus dem Modul selbst geladen werden
|
- Modul-Assets koennen ueber einen gemeinsamen Auslieferungsweg aus dem Modul selbst geladen werden
|
||||||
- die API bleibt vorerst auf demselben Host und wird nicht auf `api.desktop.kusche.berlin` ausgelagert
|
- die API bleibt vorerst auf demselben Host und wird nicht auf `api.desktop.kusche.berlin` ausgelagert
|
||||||
- das gemeinsame Admin-Debug-Widget neben der Uhr ist der Standard fuer Live-Debugging in Desktop und Native-Apps
|
- die gemeinsame Admin-Debug-Tray-Funktion neben der Uhr ist der Standard fuer Live-Debugging in Desktop und Native-Apps
|
||||||
- der Waehrungs-Checker stellt zusaetzlich ein Desktop-Widget fuer manuelle Kursaktualisierung bereit
|
- der Waehrungs-Checker stellt zusaetzlich ein Desktop-Widget fuer manuelle Kursaktualisierung bereit
|
||||||
- der Gitea-Deploy-Status liegt als optionales Addon unter `system/addons/gitea-deploy-status/` und wird serverseitig ueber `config/gitea.php` angebunden
|
- der Gitea-Deploy-Status liegt als optionales Addon unter `system/addons/gitea-deploy-status/` und wird serverseitig ueber `config/gitea.php` angebunden
|
||||||
- `Admin Apps` ist als globales `Systemtool` fuer App-Bestand, Widgets und erste Integrations-Einstellungen vorhanden
|
- `Admin Apps` ist als globales `Systemtool` fuer App-Bestand, Widgets und erste Integrations-Einstellungen vorhanden
|
||||||
|
- LDAP-Gruppenberechtigungen fuer Apps werden zentral ueber die Desktop-App-Verwaltung gepflegt
|
||||||
- das Oeffnen des Waehrungs-Checkers darf keinen externen Kursabruf ausloesen; Refreshes laufen nur nach Altersregel oder mit `force`
|
- das Oeffnen des Waehrungs-Checkers darf keinen externen Kursabruf ausloesen; Refreshes laufen nur nach Altersregel oder mit `force`
|
||||||
- `Systemtools` und installierbare `Module` werden getrennt behandelt; Systemtools sind nicht Teil der Benutzer-Installationsauswahl
|
- `Systemtools` und installierbare `Module` werden getrennt behandelt; Systemtools sind nicht Teil der Benutzer-Installationsauswahl
|
||||||
- das `Cron Tool` ist als globales Systemtool vorhanden und sammelt Modul-Cronjobs automatisch ueber Manifest-Metadaten
|
- das `Cron Tool` ist als globales Systemtool vorhanden und sammelt Modul-Cronjobs automatisch ueber Manifest-Metadaten
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Verbindlich ist:
|
|||||||
- globale Debug-Infrastruktur zentral halten und ueber alle Apps wiederverwenden
|
- globale Debug-Infrastruktur zentral halten und ueber alle Apps wiederverwenden
|
||||||
- Begriffe in UI und Doku konsistent nach `CONTENT.md` verwenden
|
- Begriffe in UI und Doku konsistent nach `CONTENT.md` verwenden
|
||||||
- Apps als bereitgestellte Systemfunktionen denken, nicht nur als sichtbare Fenster
|
- Apps als bereitgestellte Systemfunktionen denken, nicht nur als sichtbare Fenster
|
||||||
- Unterschiede zwischen `App`, `Infobereich` und `Widget-Bereich` sauber trennen
|
- Unterschiede zwischen `App`, `Tray-App`, `Widget`, `Infobereich` und `Tray-Bereich` sauber trennen
|
||||||
- lokale Persistenz und spaetere Backend-Synchronisierung getrennt vorbereiten
|
- lokale Persistenz und spaetere Backend-Synchronisierung getrennt vorbereiten
|
||||||
- Nutzerdaten und Desktop-Preferences bevorzugt datenbankbasiert speichern, nicht dateibasiert
|
- Nutzerdaten und Desktop-Preferences bevorzugt datenbankbasiert speichern, nicht dateibasiert
|
||||||
- `README.md`-Dateien in Teilbereichen aktuell halten
|
- `README.md`-Dateien in Teilbereichen aktuell halten
|
||||||
@@ -38,6 +38,7 @@ Verbindlich ist:
|
|||||||
- importierte Module mit eigener API ueber konsistente `public/api/<modul>/index.php`-Shims an die gemeinsame Desktop-Auth anbinden
|
- importierte Module mit eigener API ueber konsistente `public/api/<modul>/index.php`-Shims an die gemeinsame Desktop-Auth anbinden
|
||||||
- importierte Module mit eigener Fensterseite ueber konsistente `public/apps/<modul>/index.php`-Shims anbinden
|
- importierte Module mit eigener Fensterseite ueber konsistente `public/apps/<modul>/index.php`-Shims anbinden
|
||||||
- Widget-Logik einer App muss dieselben fachlichen Regeln wie die App-API verwenden, nicht eigene Sonderwege
|
- Widget-Logik einer App muss dieselben fachlichen Regeln wie die App-API verwenden, nicht eigene Sonderwege
|
||||||
|
- App-Berechtigungen fuer LDAP-Gruppen muessen zentral und pro App nachvollziehbar pflegbar bleiben
|
||||||
- Systemtools duerfen nicht als installierbare Module modelliert werden
|
- Systemtools duerfen nicht als installierbare Module modelliert werden
|
||||||
- neue Modul-Widgets und Modul-Cronjobs sollen ueber Manifest-Metadaten registriert werden statt ueber versteckte Sonderlisten
|
- neue Modul-Widgets und Modul-Cronjobs sollen ueber Manifest-Metadaten registriert werden statt ueber versteckte Sonderlisten
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ Verbindlich ist:
|
|||||||
- die Definition globaler Debug- und Shell-Werkzeuge soll in `system/shell/` liegen und nicht lose in `public/` oder verstreut im JS
|
- die Definition globaler Debug- und Shell-Werkzeuge soll in `system/shell/` liegen und nicht lose in `public/` oder verstreut im JS
|
||||||
- API und Desktop teilen sich im aktuellen Zielbild denselben Host; offizielle Basis ist `/api/v1/...`
|
- API und Desktop teilen sich im aktuellen Zielbild denselben Host; offizielle Basis ist `/api/v1/...`
|
||||||
- App, API und Widget einer Fachfunktion muessen dieselben Sperr- und Force-Regeln fuer externe Abrufe nutzen
|
- App, API und Widget einer Fachfunktion muessen dieselben Sperr- und Force-Regeln fuer externe Abrufe nutzen
|
||||||
|
- Nutzersteuerung fuer Menue-App, Tray-App, Desktop-Icon und Widget muss getrennt speicherbar bleiben
|
||||||
|
- Widgets sollen fachlich als Desktop-Elemente gedacht werden; die aktuelle Einblendung im rechten Infobereich ist nur ein Uebergang und darf spaeter sauber ersetzt werden
|
||||||
- die Trennung `core` / `system_tool` / `module` ist in Metadaten, Startmenue und Benutzer-Setup konsistent zu halten
|
- die Trennung `core` / `system_tool` / `module` ist in Metadaten, Startmenue und Benutzer-Setup konsistent zu halten
|
||||||
- Cron-Endpunkte muessen auf demselben Host laufen und duerfen keine separate Standard-API-Domain voraussetzen
|
- Cron-Endpunkte muessen auf demselben Host laufen und duerfen keine separate Standard-API-Domain voraussetzen
|
||||||
- Skins definieren Darstellung und Interaktionsdetails, nicht die Fachlogik
|
- Skins definieren Darstellung und Interaktionsdetails, nicht die Fachlogik
|
||||||
|
|||||||
@@ -1913,7 +1913,7 @@ if (payloadNode) {
|
|||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
const apps = payload.apps.filter((app) => {
|
const apps = payload.apps.filter((app) => {
|
||||||
if (app.show_in_start_menu === false) {
|
if (app.show_in_start_menu === false || app.user_show_in_start_menu === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2000,7 +2000,7 @@ if (payloadNode) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
payload.apps
|
payload.apps
|
||||||
.filter((app) => app.show_on_desktop !== false)
|
.filter((app) => app.user_show_on_desktop === true)
|
||||||
.forEach((app) => {
|
.forEach((app) => {
|
||||||
const icon = document.createElement('button');
|
const icon = document.createElement('button');
|
||||||
icon.type = 'button';
|
icon.type = 'button';
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ final class AdminAppsService
|
|||||||
require_once AppPaths::systemAddonPath($this->projectRoot, 'gitea-deploy-status') . '/GiteaDeployStatusService.php';
|
require_once AppPaths::systemAddonPath($this->projectRoot, 'gitea-deploy-status') . '/GiteaDeployStatusService.php';
|
||||||
|
|
||||||
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
|
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
|
||||||
$appRegistry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
$accessOverrides = AppAccessConfig::load($this->projectRoot);
|
||||||
|
$appRegistry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps(), $accessOverrides);
|
||||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
||||||
|
|
||||||
$apps = $this->normalizeApps($appRegistry->all());
|
$apps = $this->normalizeApps($appRegistry->all());
|
||||||
@@ -37,6 +38,8 @@ final class AdminAppsService
|
|||||||
'meta' => [
|
'meta' => [
|
||||||
'environment' => $currentEnvironment,
|
'environment' => $currentEnvironment,
|
||||||
'gitea_config_path' => $this->environmentConfigPath('gitea', $currentEnvironment),
|
'gitea_config_path' => $this->environmentConfigPath('gitea', $currentEnvironment),
|
||||||
|
'app_access_config_path' => $this->environmentConfigPath('app_access', $currentEnvironment),
|
||||||
|
'available_groups' => AppAccessConfig::availableGroups($this->projectRoot),
|
||||||
],
|
],
|
||||||
'stats' => [
|
'stats' => [
|
||||||
'apps_total' => count($apps),
|
'apps_total' => count($apps),
|
||||||
@@ -85,6 +88,15 @@ final class AdminAppsService
|
|||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $requiredGroups
|
||||||
|
* @return array<string, array{required_groups: array<int, string>}>
|
||||||
|
*/
|
||||||
|
public function saveAppAccess(string $appId, array $requiredGroups): array
|
||||||
|
{
|
||||||
|
return AppAccessConfig::saveRequiredGroups($this->projectRoot, $appId, $requiredGroups);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $apps
|
* @param array<int, array<string, mixed>> $apps
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
@@ -107,6 +119,7 @@ final class AdminAppsService
|
|||||||
'supports_tray' => !empty($app['supports_tray']),
|
'supports_tray' => !empty($app['supports_tray']),
|
||||||
'enabled_by_default' => !empty($app['enabled_by_default']),
|
'enabled_by_default' => !empty($app['enabled_by_default']),
|
||||||
'required_groups' => $requiredGroups,
|
'required_groups' => $requiredGroups,
|
||||||
|
'required_groups_display' => $requiredGroups === [] ? 'Alle sichtbaren Benutzer' : implode(', ', $requiredGroups),
|
||||||
'admin_only' => in_array('administrators', array_map(
|
'admin_only' => in_array('administrators', array_map(
|
||||||
static fn (string $group): string => strtolower(trim($group, '/')),
|
static fn (string $group): string => strtolower(trim($group, '/')),
|
||||||
$requiredGroups
|
$requiredGroups
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ final class App
|
|||||||
$keycloakConfig = ConfigLoader::load($this->projectRoot, 'keycloak');
|
$keycloakConfig = ConfigLoader::load($this->projectRoot, 'keycloak');
|
||||||
$auth = new KeycloakAuth($keycloakConfig);
|
$auth = new KeycloakAuth($keycloakConfig);
|
||||||
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
|
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($this->projectRoot));
|
||||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
$registry = new AppRegistry(
|
||||||
|
$this->projectRoot . '/config/apps.php',
|
||||||
|
$moduleRegistry->desktopApps(),
|
||||||
|
AppAccessConfig::load($this->projectRoot)
|
||||||
|
);
|
||||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
||||||
$shellRegistry = new ShellAppRegistry(AppPaths::systemShellRoot($this->projectRoot));
|
$shellRegistry = new ShellAppRegistry(AppPaths::systemShellRoot($this->projectRoot));
|
||||||
$giteaStatus = new \SystemAddons\GiteaDeployStatus\GiteaDeployStatusService(ConfigLoader::load($this->projectRoot, 'gitea'));
|
$giteaStatus = new \SystemAddons\GiteaDeployStatus\GiteaDeployStatusService(ConfigLoader::load($this->projectRoot, 'gitea'));
|
||||||
@@ -40,7 +44,10 @@ final class App
|
|||||||
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
||||||
? $_SESSION['desktop_auth']['user']
|
? $_SESSION['desktop_auth']['user']
|
||||||
: [];
|
: [];
|
||||||
$authGroups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
|
$authGroups = array_values(array_map(
|
||||||
|
static fn (string $group): string => strtolower(trim($group, '/')),
|
||||||
|
array_map('strval', (array) ($authUser['groups'] ?? []))
|
||||||
|
));
|
||||||
$isAdmin = in_array('administrators', $authGroups, true);
|
$isAdmin = in_array('administrators', $authGroups, true);
|
||||||
$visibleApps = AppVisibility::filterByGroups($registry->all(), $authGroups);
|
$visibleApps = AppVisibility::filterByGroups($registry->all(), $authGroups);
|
||||||
$userSelfManagement = new UserSelfManagementService($this->projectRoot);
|
$userSelfManagement = new UserSelfManagementService($this->projectRoot);
|
||||||
@@ -53,22 +60,47 @@ final class App
|
|||||||
);
|
);
|
||||||
$userSettings = $resolvedSkin['settings'];
|
$userSettings = $resolvedSkin['settings'];
|
||||||
$activeSkin = (string) $resolvedSkin['skin'];
|
$activeSkin = (string) $resolvedSkin['skin'];
|
||||||
$enabledApps = AppVisibility::filterByEnabledIds(
|
$menuAppIds = array_values(array_map('strval', (array) ($userSettings['apps']['enabled_ids'] ?? [])));
|
||||||
|
$desktopIconIds = array_values(array_map('strval', (array) ($userSettings['apps']['desktop_icon_ids'] ?? [])));
|
||||||
|
$trayAppIds = array_values(array_map('strval', (array) ($userSettings['tray']['active_ids'] ?? [])));
|
||||||
|
$visibleAppIds = array_values(array_filter(array_map(
|
||||||
|
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
|
||||||
|
$visibleApps
|
||||||
|
)));
|
||||||
|
$availableWidgets = $widgetRegistry->availableForApps($visibleAppIds);
|
||||||
|
$activeWidgetIds = array_values(array_map('strval', (array) ($userSettings['widgets']['active_ids'] ?? DesktopState::defaultWidgetIds())));
|
||||||
|
$activeWidgets = $widgetRegistry->active($activeWidgetIds, $visibleAppIds);
|
||||||
|
$widgetLaunchAppIds = array_values(array_filter(array_unique(array_map(
|
||||||
|
static fn (array $widget): string => (string) (($widget['launch_app_id'] ?? '') ?: ($widget['source_app_id'] ?? '')),
|
||||||
|
$activeWidgets
|
||||||
|
))));
|
||||||
|
$payloadAppIds = array_values(array_unique(array_filter(array_merge(
|
||||||
|
$menuAppIds,
|
||||||
|
$desktopIconIds,
|
||||||
|
$trayAppIds,
|
||||||
|
$widgetLaunchAppIds,
|
||||||
|
[UserSelfManagementService::APP_ID]
|
||||||
|
))));
|
||||||
|
$payloadApps = AppVisibility::filterByEnabledIds(
|
||||||
$visibleApps,
|
$visibleApps,
|
||||||
array_values(array_map('strval', (array) ($userSettings['apps']['enabled_ids'] ?? []))),
|
$payloadAppIds,
|
||||||
[UserSelfManagementService::APP_ID]
|
[UserSelfManagementService::APP_ID]
|
||||||
);
|
);
|
||||||
$apps = AppIconResolver::decorate($enabledApps, $activeSkin);
|
$apps = AppIconResolver::decorate(array_map(static function (array $app) use ($menuAppIds, $desktopIconIds, $trayAppIds): array {
|
||||||
|
$appId = (string) ($app['app_id'] ?? '');
|
||||||
|
$installable = !array_key_exists('installable', $app) || (bool) $app['installable'];
|
||||||
|
$app['user_show_in_start_menu'] = $installable
|
||||||
|
? in_array($appId, $menuAppIds, true)
|
||||||
|
: (($app['show_in_start_menu'] ?? true) !== false);
|
||||||
|
$app['user_show_on_desktop'] = in_array($appId, $desktopIconIds, true);
|
||||||
|
$app['user_show_in_tray'] = in_array($appId, $trayAppIds, true);
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}, $payloadApps), $activeSkin);
|
||||||
$windows = (new WindowManager($registry))->defaultWindows();
|
$windows = (new WindowManager($registry))->defaultWindows();
|
||||||
$enabledAppIds = array_values(array_filter(array_map(
|
|
||||||
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
|
|
||||||
$enabledApps
|
|
||||||
)));
|
|
||||||
$availableWidgets = $widgetRegistry->availableForApps($enabledAppIds);
|
|
||||||
$activeWidgetIds = array_values(array_map('strval', (array) ($userSettings['widgets']['active_ids'] ?? DesktopState::defaultWidgetIds())));
|
|
||||||
$trayItems = DesktopState::trayItems();
|
$trayItems = DesktopState::trayItems();
|
||||||
foreach ($enabledApps as $app) {
|
foreach ($apps as $app) {
|
||||||
if (empty($app['supports_tray']) || (string) ($app['app_id'] ?? '') !== 'fx-rates') {
|
if (empty($app['supports_tray']) || empty($app['user_show_in_tray'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +152,7 @@ final class App
|
|||||||
'apps' => $apps,
|
'apps' => $apps,
|
||||||
'windows' => $windows,
|
'windows' => $windows,
|
||||||
'widgets' => [
|
'widgets' => [
|
||||||
'active' => $widgetRegistry->active($activeWidgetIds, $enabledAppIds),
|
'active' => $activeWidgets,
|
||||||
'registry' => $availableWidgets,
|
'registry' => $availableWidgets,
|
||||||
'active_ids' => $activeWidgetIds,
|
'active_ids' => $activeWidgetIds,
|
||||||
],
|
],
|
||||||
|
|||||||
161
src/App/AppAccessConfig.php
Normal file
161
src/App/AppAccessConfig.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class AppAccessConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, array{required_groups: array<int, string>}>
|
||||||
|
*/
|
||||||
|
public static function load(string $projectRoot): array
|
||||||
|
{
|
||||||
|
$config = ConfigLoader::load($projectRoot, 'app_access');
|
||||||
|
$normalized = [];
|
||||||
|
|
||||||
|
foreach ($config as $appId => $definition) {
|
||||||
|
$appId = trim((string) $appId);
|
||||||
|
if ($appId === '' || !is_array($definition)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized[$appId] = [
|
||||||
|
'required_groups' => self::sanitizeGroups($definition['required_groups'] ?? []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{id: string, label: string, dn: string}>
|
||||||
|
*/
|
||||||
|
public static function availableGroups(string $projectRoot): array
|
||||||
|
{
|
||||||
|
$registration = ConfigLoader::load($projectRoot, 'registration');
|
||||||
|
$groups = [];
|
||||||
|
|
||||||
|
foreach ((array) ($registration['ldap']['available_groups'] ?? []) as $entry) {
|
||||||
|
if (!is_array($entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dn = trim((string) ($entry['dn'] ?? ''));
|
||||||
|
$label = trim((string) ($entry['label'] ?? ''));
|
||||||
|
$id = self::ldapGroupId($dn);
|
||||||
|
|
||||||
|
if ($id === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[$id] = [
|
||||||
|
'id' => $id,
|
||||||
|
'label' => $label !== '' ? $label : $id,
|
||||||
|
'dn' => $dn,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($groups);
|
||||||
|
|
||||||
|
return array_values($groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $requiredGroups
|
||||||
|
* @return array<string, array{required_groups: array<int, string>}>
|
||||||
|
*/
|
||||||
|
public static function saveRequiredGroups(string $projectRoot, string $appId, array $requiredGroups): array
|
||||||
|
{
|
||||||
|
$appId = trim($appId);
|
||||||
|
if ($appId === '') {
|
||||||
|
throw new \RuntimeException('App-ID fehlt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = self::load($projectRoot);
|
||||||
|
$current[$appId] = [
|
||||||
|
'required_groups' => self::sanitizeGroups($requiredGroups),
|
||||||
|
];
|
||||||
|
|
||||||
|
self::writeEnvironmentConfig($projectRoot, 'app_access', ConfigLoader::currentEnvironment(), $current);
|
||||||
|
|
||||||
|
return $current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ldapGroupId(string $dn): string
|
||||||
|
{
|
||||||
|
if ($dn === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/cn=([^,]+)/i', $dn, $matches) !== 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower(trim((string) ($matches[1] ?? '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $groups
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private static function sanitizeGroups(mixed $groups): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ((array) $groups as $group) {
|
||||||
|
$group = strtolower(trim((string) $group, " /\t\n\r\0\x0B"));
|
||||||
|
if ($group === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$group] = $group;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
*/
|
||||||
|
private static function writeEnvironmentConfig(string $projectRoot, string $name, string $environment, array $config): void
|
||||||
|
{
|
||||||
|
$directory = rtrim($projectRoot, '/') . '/config/' . $environment;
|
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||||
|
throw new \RuntimeException('Konfigurationsverzeichnis konnte nicht angelegt werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $directory . '/' . $name . '.php';
|
||||||
|
$content = "<?php\n\n";
|
||||||
|
$content .= "declare(strict_types=1);\n\n";
|
||||||
|
$content .= 'return ' . self::exportPhpValue($config) . ";\n";
|
||||||
|
|
||||||
|
if (file_put_contents($path, $content) === false) {
|
||||||
|
throw new \RuntimeException('Konfigurationsdatei konnte nicht geschrieben werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function exportPhpValue(mixed $value, int $depth = 0): string
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
if ($value === []) {
|
||||||
|
return '[]';
|
||||||
|
}
|
||||||
|
|
||||||
|
$indent = str_repeat(' ', $depth);
|
||||||
|
$childIndent = str_repeat(' ', $depth + 1);
|
||||||
|
$lines = ['['];
|
||||||
|
|
||||||
|
foreach ($value as $key => $item) {
|
||||||
|
$prefix = is_int($key) ? '' : var_export($key, true) . ' => ';
|
||||||
|
$lines[] = $childIndent . $prefix . self::exportPhpValue($item, $depth + 1) . ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = $indent . ']';
|
||||||
|
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return var_export($value, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,21 +105,30 @@ final class UserSelfManagementService
|
|||||||
{
|
{
|
||||||
$settings = $this->load($authUser, $visibleApps, $widgets, $availableSkins);
|
$settings = $this->load($authUser, $visibleApps, $widgets, $availableSkins);
|
||||||
$enabledAppIds = array_values(array_map('strval', (array) ($settings['apps']['enabled_ids'] ?? [])));
|
$enabledAppIds = array_values(array_map('strval', (array) ($settings['apps']['enabled_ids'] ?? [])));
|
||||||
$enabledAppLookup = array_fill_keys($enabledAppIds, true);
|
$desktopIconIds = array_values(array_map('strval', (array) ($settings['apps']['desktop_icon_ids'] ?? [])));
|
||||||
|
$trayAppIds = array_values(array_map('strval', (array) ($settings['tray']['active_ids'] ?? [])));
|
||||||
|
$visibleAppLookup = array_fill_keys(array_values(array_filter(array_map(
|
||||||
|
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
|
||||||
|
$visibleApps
|
||||||
|
))), true);
|
||||||
$activeWidgetIds = array_values(array_map('strval', (array) ($settings['widgets']['active_ids'] ?? [])));
|
$activeWidgetIds = array_values(array_map('strval', (array) ($settings['widgets']['active_ids'] ?? [])));
|
||||||
$installableApps = array_values(array_filter(
|
$installableApps = array_values(array_filter(
|
||||||
$visibleApps,
|
$visibleApps,
|
||||||
static fn (array $app): bool => !array_key_exists('installable', $app) || (bool) $app['installable']
|
static fn (array $app): bool => !array_key_exists('installable', $app) || (bool) $app['installable']
|
||||||
));
|
));
|
||||||
|
$trayApps = array_values(array_filter(
|
||||||
|
$visibleApps,
|
||||||
|
static fn (array $app): bool => !empty($app['supports_tray'])
|
||||||
|
));
|
||||||
$availableWidgets = array_values(array_filter(
|
$availableWidgets = array_values(array_filter(
|
||||||
$widgets,
|
$widgets,
|
||||||
static function (array $widget) use ($enabledAppLookup): bool {
|
static function (array $widget) use ($visibleAppLookup): bool {
|
||||||
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
|
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
|
||||||
if ($sourceAppId === '') {
|
if ($sourceAppId === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isset($enabledAppLookup[$sourceAppId]);
|
return isset($visibleAppLookup[$sourceAppId]);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -142,9 +151,20 @@ final class UserSelfManagementService
|
|||||||
'app_scope' => (string) ($app['app_scope'] ?? 'module'),
|
'app_scope' => (string) ($app['app_scope'] ?? 'module'),
|
||||||
'required' => in_array((string) ($app['app_id'] ?? ''), [self::APP_ID], true),
|
'required' => in_array((string) ($app['app_id'] ?? ''), [self::APP_ID], true),
|
||||||
'enabled' => in_array((string) ($app['app_id'] ?? ''), $enabledAppIds, true),
|
'enabled' => in_array((string) ($app['app_id'] ?? ''), $enabledAppIds, true),
|
||||||
|
'can_show_on_desktop' => ($app['show_on_desktop'] ?? false) !== false,
|
||||||
|
'desktop_icon_enabled' => in_array((string) ($app['app_id'] ?? ''), $desktopIconIds, true),
|
||||||
],
|
],
|
||||||
$installableApps
|
$installableApps
|
||||||
),
|
),
|
||||||
|
'tray_apps' => array_map(
|
||||||
|
static fn (array $app): array => [
|
||||||
|
'app_id' => (string) ($app['app_id'] ?? ''),
|
||||||
|
'title' => (string) ($app['title'] ?? ''),
|
||||||
|
'summary' => (string) ($app['summary'] ?? ''),
|
||||||
|
'enabled' => in_array((string) ($app['app_id'] ?? ''), $trayAppIds, true),
|
||||||
|
],
|
||||||
|
$trayApps
|
||||||
|
),
|
||||||
'widgets' => array_map(
|
'widgets' => array_map(
|
||||||
static fn (array $widget): array => [
|
static fn (array $widget): array => [
|
||||||
'widget_id' => (string) ($widget['widget_id'] ?? ''),
|
'widget_id' => (string) ($widget['widget_id'] ?? ''),
|
||||||
@@ -153,6 +173,7 @@ final class UserSelfManagementService
|
|||||||
'default_enabled' => (bool) ($widget['default_enabled'] ?? false),
|
'default_enabled' => (bool) ($widget['default_enabled'] ?? false),
|
||||||
'enabled' => in_array((string) ($widget['widget_id'] ?? ''), $activeWidgetIds, true),
|
'enabled' => in_array((string) ($widget['widget_id'] ?? ''), $activeWidgetIds, true),
|
||||||
'source_app_id' => (string) ($widget['source_app_id'] ?? ''),
|
'source_app_id' => (string) ($widget['source_app_id'] ?? ''),
|
||||||
|
'widget_type' => (string) ($widget['widget_type'] ?? 'default'),
|
||||||
],
|
],
|
||||||
$availableWidgets
|
$availableWidgets
|
||||||
),
|
),
|
||||||
@@ -220,12 +241,44 @@ final class UserSelfManagementService
|
|||||||
static fn (array $app): ?string => ($app['enabled_by_default'] ?? false) ? (string) ($app['app_id'] ?? '') : null,
|
static fn (array $app): ?string => ($app['enabled_by_default'] ?? false) ? (string) ($app['app_id'] ?? '') : null,
|
||||||
$visibleApps
|
$visibleApps
|
||||||
)));
|
)));
|
||||||
|
$desktopIconAllowedIds = array_values(array_filter(array_map(
|
||||||
|
static fn (array $app): ?string => ((!array_key_exists('installable', $app) || (bool) $app['installable']) && (($app['show_on_desktop'] ?? false) !== false))
|
||||||
|
? (string) ($app['app_id'] ?? '')
|
||||||
|
: null,
|
||||||
|
$visibleApps
|
||||||
|
)));
|
||||||
|
$defaultDesktopIconIds = array_values(array_filter(array_map(
|
||||||
|
static fn (array $app): ?string => ((!array_key_exists('installable', $app) || (bool) $app['installable']) && !empty($app['show_on_desktop']))
|
||||||
|
? (string) ($app['app_id'] ?? '')
|
||||||
|
: null,
|
||||||
|
$visibleApps
|
||||||
|
)));
|
||||||
|
$trayAllowedIds = array_values(array_filter(array_map(
|
||||||
|
static fn (array $app): ?string => !empty($app['supports_tray']) ? (string) ($app['app_id'] ?? '') : null,
|
||||||
|
$visibleApps
|
||||||
|
)));
|
||||||
|
$defaultTrayIds = array_values(array_filter(array_map(
|
||||||
|
static fn (array $app): ?string => (!empty($app['supports_tray']) && !empty($app['enabled_by_default'])) ? (string) ($app['app_id'] ?? '') : null,
|
||||||
|
$visibleApps
|
||||||
|
)));
|
||||||
$defaultWidgetIds = array_values(array_filter(array_map(
|
$defaultWidgetIds = array_values(array_filter(array_map(
|
||||||
static fn (array $widget): ?string => ($widget['default_enabled'] ?? false) ? (string) ($widget['widget_id'] ?? '') : null,
|
static fn (array $widget): ?string => ($widget['default_enabled'] ?? false) ? (string) ($widget['widget_id'] ?? '') : null,
|
||||||
$widgets
|
$widgets
|
||||||
)));
|
)));
|
||||||
$allWidgetIds = array_values(array_filter(array_map(
|
$visibleWidgetIds = array_values(array_filter(array_map(
|
||||||
static fn (array $widget): string => (string) ($widget['widget_id'] ?? ''),
|
static function (array $widget) use ($visibleAppIds): ?string {
|
||||||
|
$widgetId = (string) ($widget['widget_id'] ?? '');
|
||||||
|
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
|
||||||
|
if ($widgetId === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceAppId !== '' && !in_array($sourceAppId, $visibleAppIds, true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $widgetId;
|
||||||
|
},
|
||||||
$widgets
|
$widgets
|
||||||
)));
|
)));
|
||||||
$storedEnabledAppIds = array_key_exists('enabled_ids', (array) ($settings['apps'] ?? []))
|
$storedEnabledAppIds = array_key_exists('enabled_ids', (array) ($settings['apps'] ?? []))
|
||||||
@@ -244,12 +297,34 @@ final class UserSelfManagementService
|
|||||||
$visibleAppIds,
|
$visibleAppIds,
|
||||||
[self::APP_ID]
|
[self::APP_ID]
|
||||||
);
|
);
|
||||||
|
$storedDesktopIconIds = array_key_exists('desktop_icon_ids', (array) ($settings['apps'] ?? []))
|
||||||
|
? $this->sanitizeSelection($settings['apps']['desktop_icon_ids'] ?? [], $desktopIconAllowedIds)
|
||||||
|
: $defaultDesktopIconIds;
|
||||||
|
$desktopIconIds = $this->sanitizeSelection(
|
||||||
|
$storedDesktopIconIds,
|
||||||
|
array_values(array_intersect($desktopIconAllowedIds, $enabledAppIds))
|
||||||
|
);
|
||||||
|
$storedTrayIds = array_key_exists('active_ids', (array) ($settings['tray'] ?? []))
|
||||||
|
? $this->sanitizeSelection($settings['tray']['active_ids'] ?? [], $trayAllowedIds)
|
||||||
|
: $defaultTrayIds;
|
||||||
|
$knownTrayIds = $this->sanitizeSelection(
|
||||||
|
$settings['tray']['known_ids'] ?? [],
|
||||||
|
$trayAllowedIds
|
||||||
|
);
|
||||||
|
$newDefaultTrayIds = array_values(array_filter(
|
||||||
|
$defaultTrayIds,
|
||||||
|
static fn (string $appId): bool => !in_array($appId, $knownTrayIds, true)
|
||||||
|
));
|
||||||
|
$trayIds = $this->sanitizeSelection(
|
||||||
|
array_merge($storedTrayIds, $newDefaultTrayIds),
|
||||||
|
$trayAllowedIds
|
||||||
|
);
|
||||||
$storedActiveWidgetIds = array_key_exists('active_ids', (array) ($settings['widgets'] ?? []))
|
$storedActiveWidgetIds = array_key_exists('active_ids', (array) ($settings['widgets'] ?? []))
|
||||||
? $this->sanitizeSelection($settings['widgets']['active_ids'] ?? [], $allWidgetIds)
|
? $this->sanitizeSelection($settings['widgets']['active_ids'] ?? [], $visibleWidgetIds)
|
||||||
: $defaultWidgetIds;
|
: $defaultWidgetIds;
|
||||||
$knownWidgetIds = $this->sanitizeSelection(
|
$knownWidgetIds = $this->sanitizeSelection(
|
||||||
$settings['widgets']['known_ids'] ?? [],
|
$settings['widgets']['known_ids'] ?? [],
|
||||||
$allWidgetIds
|
$visibleWidgetIds
|
||||||
);
|
);
|
||||||
$newDefaultWidgetIds = array_values(array_filter(
|
$newDefaultWidgetIds = array_values(array_filter(
|
||||||
$defaultWidgetIds,
|
$defaultWidgetIds,
|
||||||
@@ -257,14 +332,14 @@ final class UserSelfManagementService
|
|||||||
));
|
));
|
||||||
$activeWidgetIds = $this->sanitizeSelection(
|
$activeWidgetIds = $this->sanitizeSelection(
|
||||||
array_merge($storedActiveWidgetIds, $newDefaultWidgetIds),
|
array_merge($storedActiveWidgetIds, $newDefaultWidgetIds),
|
||||||
array_values(array_filter($allWidgetIds, function (string $widgetId) use ($widgets, $enabledAppIds): bool {
|
array_values(array_filter($visibleWidgetIds, function (string $widgetId) use ($widgets, $visibleAppIds): bool {
|
||||||
foreach ($widgets as $widget) {
|
foreach ($widgets as $widget) {
|
||||||
if ((string) ($widget['widget_id'] ?? '') !== $widgetId) {
|
if ((string) ($widget['widget_id'] ?? '') !== $widgetId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
|
$sourceAppId = trim((string) ($widget['source_app_id'] ?? ''));
|
||||||
return $sourceAppId === '' || in_array($sourceAppId, $enabledAppIds, true);
|
return $sourceAppId === '' || in_array($sourceAppId, $visibleAppIds, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -297,11 +372,16 @@ final class UserSelfManagementService
|
|||||||
],
|
],
|
||||||
'apps' => [
|
'apps' => [
|
||||||
'enabled_ids' => $enabledAppIds,
|
'enabled_ids' => $enabledAppIds,
|
||||||
|
'desktop_icon_ids' => $desktopIconIds,
|
||||||
'known_ids' => $visibleAppIds,
|
'known_ids' => $visibleAppIds,
|
||||||
],
|
],
|
||||||
|
'tray' => [
|
||||||
|
'active_ids' => $trayIds,
|
||||||
|
'known_ids' => $trayAllowedIds,
|
||||||
|
],
|
||||||
'widgets' => [
|
'widgets' => [
|
||||||
'active_ids' => $activeWidgetIds,
|
'active_ids' => $activeWidgetIds,
|
||||||
'known_ids' => $allWidgetIds,
|
'known_ids' => $visibleWidgetIds,
|
||||||
],
|
],
|
||||||
'integration' => [
|
'integration' => [
|
||||||
'ldap_write_status' => 'planned',
|
'ldap_write_status' => 'planned',
|
||||||
|
|||||||
@@ -11,12 +11,27 @@ final class AppRegistry
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $additionalApps
|
* @param array<int, array<string, mixed>> $additionalApps
|
||||||
|
* @param array<string, array<string, mixed>> $accessOverrides
|
||||||
*/
|
*/
|
||||||
public function __construct(string $configPath, array $additionalApps = [])
|
public function __construct(string $configPath, array $additionalApps = [], array $accessOverrides = [])
|
||||||
{
|
{
|
||||||
/** @var array<int, array<string, mixed>> $apps */
|
/** @var array<int, array<string, mixed>> $apps */
|
||||||
$apps = require $configPath;
|
$apps = require $configPath;
|
||||||
$this->apps = array_values(array_merge($apps, $additionalApps));
|
$mergedApps = array_values(array_merge($apps, $additionalApps));
|
||||||
|
|
||||||
|
$this->apps = array_map(static function (array $app) use ($accessOverrides): array {
|
||||||
|
$appId = (string) ($app['app_id'] ?? '');
|
||||||
|
if ($appId === '') {
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
$override = $accessOverrides[$appId] ?? null;
|
||||||
|
if (is_array($override) && array_key_exists('required_groups', $override)) {
|
||||||
|
$app['required_groups'] = array_values(array_map('strval', (array) ($override['required_groups'] ?? [])));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}, $mergedApps);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -195,6 +195,37 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-access-card {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-access-groups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-group-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-group-item input {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-group-main strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
#admin-apps-app .aa-message {
|
#admin-apps-app .aa-message {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ $service = new AdminAppsService($projectRoot);
|
|||||||
$messages = [];
|
$messages = [];
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$section = trim((string) ($_GET['section'] ?? $_POST['active_section'] ?? 'overview'));
|
$section = trim((string) ($_GET['section'] ?? $_POST['active_section'] ?? 'overview'));
|
||||||
$allowedSections = ['overview', 'widgets', 'integrations'];
|
$allowedSections = ['overview', 'installation', 'access', 'widgets', 'integrations'];
|
||||||
if (!in_array($section, $allowedSections, true)) {
|
if (!in_array($section, $allowedSections, true)) {
|
||||||
$section = 'overview';
|
$section = 'overview';
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
]);
|
]);
|
||||||
$messages[] = 'Gitea-Deploy-Status-Konfiguration gespeichert.';
|
$messages[] = 'Gitea-Deploy-Status-Konfiguration gespeichert.';
|
||||||
$section = 'integrations';
|
$section = 'integrations';
|
||||||
|
} elseif ($action === 'save-app-access') {
|
||||||
|
$service->saveAppAccess(
|
||||||
|
(string) ($_POST['app_id'] ?? ''),
|
||||||
|
array_values(array_map('strval', (array) ($_POST['required_groups'] ?? [])))
|
||||||
|
);
|
||||||
|
$messages[] = 'LDAP-Gruppen fuer die App wurden gespeichert.';
|
||||||
|
$section = 'access';
|
||||||
}
|
}
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$errors[] = $exception->getMessage();
|
$errors[] = $exception->getMessage();
|
||||||
@@ -90,6 +97,7 @@ $apps = is_array($bootstrap['apps'] ?? null) ? $bootstrap['apps'] : [];
|
|||||||
$widgets = is_array($bootstrap['widgets'] ?? null) ? $bootstrap['widgets'] : [];
|
$widgets = is_array($bootstrap['widgets'] ?? null) ? $bootstrap['widgets'] : [];
|
||||||
$stats = is_array($bootstrap['stats'] ?? null) ? $bootstrap['stats'] : [];
|
$stats = is_array($bootstrap['stats'] ?? null) ? $bootstrap['stats'] : [];
|
||||||
$meta = is_array($bootstrap['meta'] ?? null) ? $bootstrap['meta'] : [];
|
$meta = is_array($bootstrap['meta'] ?? null) ? $bootstrap['meta'] : [];
|
||||||
|
$availableGroups = is_array($meta['available_groups'] ?? null) ? $meta['available_groups'] : [];
|
||||||
$gitea = is_array($bootstrap['gitea'] ?? null) ? $bootstrap['gitea'] : [];
|
$gitea = is_array($bootstrap['gitea'] ?? null) ? $bootstrap['gitea'] : [];
|
||||||
$giteaConfig = is_array($gitea['config'] ?? null) ? $gitea['config'] : [];
|
$giteaConfig = is_array($gitea['config'] ?? null) ? $gitea['config'] : [];
|
||||||
$giteaDiagnostics = is_array($gitea['diagnostics'] ?? null) ? $gitea['diagnostics'] : [];
|
$giteaDiagnostics = is_array($gitea['diagnostics'] ?? null) ? $gitea['diagnostics'] : [];
|
||||||
@@ -99,15 +107,27 @@ $sectionUrl = static fn (string $targetSection): string => '/admin/apps/?section
|
|||||||
$sectionMeta = [
|
$sectionMeta = [
|
||||||
'overview' => [
|
'overview' => [
|
||||||
'label' => 'Apps',
|
'label' => 'Apps',
|
||||||
'title' => 'App-Bestand und Scope',
|
'title' => 'Apps und Bereitstellung',
|
||||||
'description' => 'Installierbare Fach-Apps, Systemtools und Core-Eintraege der Desktop-Shell im aktuellen Registry-Stand.',
|
'description' => 'Apps sind allgemeine Anwendungen. Sie koennen als Menue-App, Tray-App, Widget-Quelle oder mit optionalem Desktop-Icon auftreten.',
|
||||||
'nav' => 'Registry und Freigaben pruefen.',
|
'nav' => 'Bestand und Installationsweg pruefen.',
|
||||||
|
],
|
||||||
|
'installation' => [
|
||||||
|
'label' => 'Installation',
|
||||||
|
'title' => 'Installationswege fuer Apps',
|
||||||
|
'description' => 'Installierbare Fach-Apps liegen unter custom/apps/. Upload- und Importwege werden hier als naechster Ausbauschritt vorbereitet.',
|
||||||
|
'nav' => 'ZIP-, Verzeichnis- und Server-Workflow beschreiben.',
|
||||||
|
],
|
||||||
|
'access' => [
|
||||||
|
'label' => 'Berechtigungen',
|
||||||
|
'title' => 'LDAP-Gruppen pro App',
|
||||||
|
'description' => 'Admins steuern hier, welche LDAP-Gruppen eine App benutzen duerfen. Leere Auswahl bedeutet: fuer alle sichtbaren Benutzer.',
|
||||||
|
'nav' => 'LDAP-Gruppen verwalten.',
|
||||||
],
|
],
|
||||||
'widgets' => [
|
'widgets' => [
|
||||||
'label' => 'Widgets',
|
'label' => 'Widgets',
|
||||||
'title' => 'Widget-Bereich und Quellen',
|
'title' => 'Widgets und Quellen',
|
||||||
'description' => 'Widget-Inventar aus globaler Registry und Modul-Manifesten mit Quellen, Launch-App und Sichtbarkeit.',
|
'description' => 'Widgets sind kleine Desktop-Bereiche ohne klassische Fensterfunktionen. Sie koennen eigenstaendig oder Teil einer App sein.',
|
||||||
'nav' => 'Widget-Quellen und Startpunkte pruefen.',
|
'nav' => 'Widget-Quellen und Bedeutung pruefen.',
|
||||||
],
|
],
|
||||||
'integrations' => [
|
'integrations' => [
|
||||||
'label' => 'Integrationen',
|
'label' => 'Integrationen',
|
||||||
@@ -230,6 +250,7 @@ ob_start();
|
|||||||
<th>Titel</th>
|
<th>Titel</th>
|
||||||
<th>Scope</th>
|
<th>Scope</th>
|
||||||
<th>Typ</th>
|
<th>Typ</th>
|
||||||
|
<th>LDAP-Gruppen</th>
|
||||||
<th>Route</th>
|
<th>Route</th>
|
||||||
<th>Flags</th>
|
<th>Flags</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -243,6 +264,7 @@ ob_start();
|
|||||||
</td>
|
</td>
|
||||||
<td><?= $h((string) ($app['app_scope'] ?? '')) ?></td>
|
<td><?= $h((string) ($app['app_scope'] ?? '')) ?></td>
|
||||||
<td><?= $h(!empty($app['installable']) ? 'Custom App' : 'System') ?></td>
|
<td><?= $h(!empty($app['installable']) ? 'Custom App' : 'System') ?></td>
|
||||||
|
<td><?= $h((string) ($app['required_groups_display'] ?? '')) ?></td>
|
||||||
<td><code><?= $h((string) ($app['entry_route'] ?? '')) ?></code></td>
|
<td><code><?= $h((string) ($app['entry_route'] ?? '')) ?></code></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="aa-flag-list">
|
<div class="aa-flag-list">
|
||||||
@@ -259,6 +281,108 @@ ob_start();
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="window-app-card aa-card">
|
||||||
|
<div class="aa-section-head">
|
||||||
|
<div>
|
||||||
|
<p class="window-app-kicker aa-kicker">Installation</p>
|
||||||
|
<h3 class="aa-section-title">Aktueller Installationsweg</h3>
|
||||||
|
<p class="aa-copy">Neue Apps koennen aktuell als Modul-Verzeichnis unter <code>custom/apps/</code> abgelegt werden. Ein eigener Upload-Bereich fuer ZIP-Dateien oder Verzeichnisse ist noch offen und wird als naechster Ausbauschritt vorbereitet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($section === 'installation'): ?>
|
||||||
|
<section class="aa-grid aa-grid--stats">
|
||||||
|
<article class="window-app-card aa-card aa-stat-card">
|
||||||
|
<span>Installierbare Apps</span>
|
||||||
|
<strong><?= (int) ($stats['installable_apps'] ?? 0) ?></strong>
|
||||||
|
</article>
|
||||||
|
<article class="window-app-card aa-card aa-stat-card">
|
||||||
|
<span>Systemtools</span>
|
||||||
|
<strong><?= (int) ($stats['system_tools'] ?? 0) ?></strong>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="window-app-card aa-card">
|
||||||
|
<div class="aa-section-head">
|
||||||
|
<div>
|
||||||
|
<p class="window-app-kicker aa-kicker">Installationswege</p>
|
||||||
|
<h3 class="aa-section-title">Aktueller und geplanter Ablauf</h3>
|
||||||
|
<p class="aa-copy">Installierbare Apps liegen im Projekt unter <code>custom/apps/</code>. Der Desktop erkennt neue Module automatisch ueber deren Desktop-Metadaten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aa-grid">
|
||||||
|
<article class="window-app-card aa-card aa-inline-card">
|
||||||
|
<strong>1. Manuell ueber den Server</strong>
|
||||||
|
<p>Ein Modul-Verzeichnis wird nach <code>custom/apps/<app>/</code> kopiert. Das ist der aktuell gueltige produktive Weg.</p>
|
||||||
|
</article>
|
||||||
|
<article class="window-app-card aa-card aa-inline-card">
|
||||||
|
<strong>2. Verzeichnis-Upload</strong>
|
||||||
|
<p>Ein Browser-Upload fuer ein vorbereitetes Modul-Verzeichnis ist fachlich vorgesehen, aber noch nicht implementiert.</p>
|
||||||
|
</article>
|
||||||
|
<article class="window-app-card aa-card aa-inline-card">
|
||||||
|
<strong>3. ZIP-Upload</strong>
|
||||||
|
<p>Ein ZIP-Import mit Entpacken und Validierung ist vorgesehen, benoetigt aber noch einen dedizierten Installations-Workflow.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($section === 'access'): ?>
|
||||||
|
<section class="window-app-card aa-card">
|
||||||
|
<div class="aa-section-head">
|
||||||
|
<div>
|
||||||
|
<p class="window-app-kicker aa-kicker">Access Control</p>
|
||||||
|
<h3 class="aa-section-title">LDAP-Gruppen pro App</h3>
|
||||||
|
<p class="aa-copy">Gespeichert wird in <?= $h((string) ($meta['app_access_config_path'] ?? '')) ?>. Die kurzen Gruppennamen entsprechen den LDAP-/Auth-Gruppen im Desktop.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aa-grid">
|
||||||
|
<?php foreach ($apps as $app): ?>
|
||||||
|
<form class="window-app-card aa-card aa-access-card" method="post" action="/admin/apps/">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $h($csrfToken) ?>">
|
||||||
|
<input type="hidden" name="action" value="save-app-access">
|
||||||
|
<input type="hidden" name="active_section" value="access">
|
||||||
|
<input type="hidden" name="app_id" value="<?= $h((string) ($app['app_id'] ?? '')) ?>">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong><?= $h((string) ($app['title'] ?? '')) ?></strong>
|
||||||
|
<p class="aa-table-copy"><?= $h((string) ($app['summary'] ?? '')) ?></p>
|
||||||
|
<p class="aa-table-copy">Aktuell: <?= $h((string) ($app['required_groups_display'] ?? '')) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aa-access-groups">
|
||||||
|
<?php if ($availableGroups === []): ?>
|
||||||
|
<p class="aa-copy">Keine LDAP-Gruppen aus der Registration-Konfiguration gefunden.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($availableGroups as $group): ?>
|
||||||
|
<label class="window-app-item aa-group-item">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="required_groups[]"
|
||||||
|
value="<?= $h((string) ($group['id'] ?? '')) ?>"
|
||||||
|
<?= in_array((string) ($group['id'] ?? ''), array_values(array_map('strval', (array) ($app['required_groups'] ?? []))), true) ? 'checked' : '' ?>
|
||||||
|
>
|
||||||
|
<span class="window-app-item-main aa-group-main">
|
||||||
|
<strong><?= $h((string) ($group['label'] ?? '')) ?></strong>
|
||||||
|
<span class="aa-meta"><?= $h((string) ($group['id'] ?? '')) ?></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aa-actions">
|
||||||
|
<button class="window-app-button aa-primary" type="submit">Berechtigungen speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($section === 'widgets'): ?>
|
<?php if ($section === 'widgets'): ?>
|
||||||
@@ -267,7 +391,7 @@ ob_start();
|
|||||||
<div>
|
<div>
|
||||||
<p class="window-app-kicker aa-kicker">Widget Registry</p>
|
<p class="window-app-kicker aa-kicker">Widget Registry</p>
|
||||||
<h3 class="aa-section-title">Alle Widgets</h3>
|
<h3 class="aa-section-title">Alle Widgets</h3>
|
||||||
<p class="aa-copy">Globale Widgets und modulbasierte Widgets werden hier gemeinsam sichtbar. Modul-Widgets kommen bevorzugt aus `module.json`.</p>
|
<p class="aa-copy">Globale Widgets und modulbasierte Widgets werden hier gemeinsam sichtbar. Fachlich sind Widgets kleine Desktop-Elemente; technisch werden sie aktuell noch uebergangsweise im rechten Infobereich gerendert.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||||
|
|
||||||
use App\AppPaths;
|
use App\AppPaths;
|
||||||
|
use App\AppAccessConfig;
|
||||||
use App\ConfigLoader;
|
use App\ConfigLoader;
|
||||||
use App\KeycloakAuth;
|
use App\KeycloakAuth;
|
||||||
use App\LdapProvisioner;
|
use App\LdapProvisioner;
|
||||||
@@ -30,7 +31,11 @@ if ($method === 'OPTIONS') {
|
|||||||
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
|
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
|
||||||
$auth = new KeycloakAuth($keycloakConfig);
|
$auth = new KeycloakAuth($keycloakConfig);
|
||||||
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($projectRoot));
|
$moduleRegistry = new ModuleRegistry(AppPaths::customAppsRoot($projectRoot));
|
||||||
$registry = new AppRegistry($projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
$registry = new AppRegistry(
|
||||||
|
$projectRoot . '/config/apps.php',
|
||||||
|
$moduleRegistry->desktopApps(),
|
||||||
|
AppAccessConfig::load($projectRoot)
|
||||||
|
);
|
||||||
$widgetRegistry = new WidgetRegistry($projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
$widgetRegistry = new WidgetRegistry($projectRoot . '/config/widgets.php', $moduleRegistry->widgets());
|
||||||
$service = new UserSelfManagementService($projectRoot);
|
$service = new UserSelfManagementService($projectRoot);
|
||||||
$registrationConfig = ConfigLoader::load($projectRoot, 'registration');
|
$registrationConfig = ConfigLoader::load($projectRoot, 'registration');
|
||||||
@@ -103,9 +108,25 @@ function requiresReload(array $before, array $after): bool
|
|||||||
$afterSkin = (string) ($after['desktop']['active_skin'] ?? '');
|
$afterSkin = (string) ($after['desktop']['active_skin'] ?? '');
|
||||||
$beforeApps = array_values(array_map('strval', (array) ($before['apps']['enabled_ids'] ?? [])));
|
$beforeApps = array_values(array_map('strval', (array) ($before['apps']['enabled_ids'] ?? [])));
|
||||||
$afterApps = array_values(array_map('strval', (array) ($after['apps']['enabled_ids'] ?? [])));
|
$afterApps = array_values(array_map('strval', (array) ($after['apps']['enabled_ids'] ?? [])));
|
||||||
|
$beforeDesktopIcons = array_values(array_map('strval', (array) ($before['apps']['desktop_icon_ids'] ?? [])));
|
||||||
|
$afterDesktopIcons = array_values(array_map('strval', (array) ($after['apps']['desktop_icon_ids'] ?? [])));
|
||||||
|
$beforeTray = array_values(array_map('strval', (array) ($before['tray']['active_ids'] ?? [])));
|
||||||
|
$afterTray = array_values(array_map('strval', (array) ($after['tray']['active_ids'] ?? [])));
|
||||||
|
$beforeWidgets = array_values(array_map('strval', (array) ($before['widgets']['active_ids'] ?? [])));
|
||||||
|
$afterWidgets = array_values(array_map('strval', (array) ($after['widgets']['active_ids'] ?? [])));
|
||||||
|
|
||||||
sort($beforeApps);
|
sort($beforeApps);
|
||||||
sort($afterApps);
|
sort($afterApps);
|
||||||
|
sort($beforeDesktopIcons);
|
||||||
|
sort($afterDesktopIcons);
|
||||||
|
sort($beforeTray);
|
||||||
|
sort($afterTray);
|
||||||
|
sort($beforeWidgets);
|
||||||
|
sort($afterWidgets);
|
||||||
|
|
||||||
return $beforeSkin !== $afterSkin || $beforeApps !== $afterApps;
|
return $beforeSkin !== $afterSkin
|
||||||
|
|| $beforeApps !== $afterApps
|
||||||
|
|| $beforeDesktopIcons !== $afterDesktopIcons
|
||||||
|
|| $beforeTray !== $afterTray
|
||||||
|
|| $beforeWidgets !== $afterWidgets;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,29 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#user-self-management-app .usm-item--app {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-self-management-app .usm-item-toggle {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-self-management-app .usm-inline-check {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-self-management-app .usm-inline-check input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#user-self-management-app .usm-item-main {
|
#user-self-management-app .usm-item-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
const SECTIONS = [
|
const SECTIONS = [
|
||||||
{ id: 'desktop', label: 'Desktop Type' },
|
{ id: 'desktop', label: 'Desktop Type' },
|
||||||
{ id: 'profile', label: 'Benutzerdaten' },
|
{ id: 'profile', label: 'Benutzerdaten' },
|
||||||
{ id: 'apps', label: 'App-Installation' },
|
{ id: 'apps', label: 'Apps' },
|
||||||
{ id: 'widgets', label: 'Infobereich' },
|
{ id: 'tray', label: 'Tray-Apps' },
|
||||||
|
{ id: 'widgets', label: 'Widgets' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const escapeHtml = (value) => String(value)
|
const escapeHtml = (value) => String(value)
|
||||||
@@ -32,10 +33,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sectionId === 'apps') {
|
if (sectionId === 'apps') {
|
||||||
return 'Bestimmt, welche Desktop-Apps fuer diesen Benutzer sichtbar bleiben.';
|
return 'Aktiviert Anwendungen fuer das Startmenue und optional als Desktop-Icon.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Steuert, welche Elemente im rechten Infobereich des Desktops aktiv sind.';
|
if (sectionId === 'tray') {
|
||||||
|
return 'Steuert Schnellaktionen und Visualisierungen neben der Uhr, sofern Zugriff besteht.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Kleine Desktop-Bereiche ohne Fensterfunktionen, die separat aktiviert werden koennen.';
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSkinChoices = (state) => state.bootstrap.options.skins.map((skin) => {
|
const renderSkinChoices = (state) => state.bootstrap.options.skins.map((skin) => {
|
||||||
@@ -68,20 +73,52 @@
|
|||||||
|
|
||||||
const renderAppItems = (state) => state.bootstrap.options.apps.map((app) => {
|
const renderAppItems = (state) => state.bootstrap.options.apps.map((app) => {
|
||||||
const checked = state.draft.apps.enabled_ids.includes(app.app_id);
|
const checked = state.draft.apps.enabled_ids.includes(app.app_id);
|
||||||
|
const desktopIconChecked = state.draft.apps.desktop_icon_ids.includes(app.app_id);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="window-app-item usm-item usm-item--app">
|
||||||
|
<label class="usm-item-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-app-id="${escapeHtml(app.app_id)}"
|
||||||
|
${checked ? 'checked' : ''}
|
||||||
|
${app.required ? 'disabled' : ''}
|
||||||
|
>
|
||||||
|
<span class="window-app-item-main usm-item-main">
|
||||||
|
<strong>${escapeHtml(app.title)}</strong>
|
||||||
|
<p>${escapeHtml(app.summary || '')}</p>
|
||||||
|
<span class="usm-item-meta">${app.required ? 'Pflicht-App im Menue' : 'Im Menue sichtbar'}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
${app.can_show_on_desktop ? `
|
||||||
|
<label class="usm-inline-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-app-desktop-icon-id="${escapeHtml(app.app_id)}"
|
||||||
|
${desktopIconChecked ? 'checked' : ''}
|
||||||
|
${checked ? '' : 'disabled'}
|
||||||
|
>
|
||||||
|
<span>Desktop-Icon anzeigen</span>
|
||||||
|
</label>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const renderTrayItems = (state) => state.bootstrap.options.tray_apps.map((app) => {
|
||||||
|
const checked = state.draft.tray.active_ids.includes(app.app_id);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<label class="window-app-item usm-item">
|
<label class="window-app-item usm-item">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
data-action="toggle-app"
|
data-tray-app-id="${escapeHtml(app.app_id)}"
|
||||||
data-app-id="${escapeHtml(app.app_id)}"
|
|
||||||
${checked ? 'checked' : ''}
|
${checked ? 'checked' : ''}
|
||||||
${app.required ? 'disabled' : ''}
|
|
||||||
>
|
>
|
||||||
<span class="window-app-item-main usm-item-main">
|
<span class="window-app-item-main usm-item-main">
|
||||||
<strong>${escapeHtml(app.title)}</strong>
|
<strong>${escapeHtml(app.title)}</strong>
|
||||||
<p>${escapeHtml(app.summary || '')}</p>
|
<p>${escapeHtml(app.summary || '')}</p>
|
||||||
<span class="usm-item-meta">${app.required ? 'Pflicht-App' : 'Optional sichtbar'}</span>
|
<span class="usm-item-meta">Im Tray neben der Uhr</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
@@ -101,7 +138,7 @@
|
|||||||
<span class="window-app-item-main usm-item-main">
|
<span class="window-app-item-main usm-item-main">
|
||||||
<strong>${escapeHtml(widget.title)}</strong>
|
<strong>${escapeHtml(widget.title)}</strong>
|
||||||
<p>${escapeHtml(widget.summary || '')}</p>
|
<p>${escapeHtml(widget.summary || '')}</p>
|
||||||
<span class="usm-item-meta">${widget.default_enabled ? 'Standardbereich' : 'Optionaler Bereich'}</span>
|
<span class="usm-item-meta">${widget.default_enabled ? 'Standard-Widget' : 'Optionales Widget'}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
@@ -166,8 +203,8 @@
|
|||||||
return `
|
return `
|
||||||
<section class="window-app-card usm-card">
|
<section class="window-app-card usm-card">
|
||||||
<p class="window-app-kicker usm-kicker">App-Auswahl</p>
|
<p class="window-app-kicker usm-kicker">App-Auswahl</p>
|
||||||
<h2>Sichtbare Desktop-Apps</h2>
|
<h2>Apps im Menue und auf dem Desktop</h2>
|
||||||
<p class="window-app-copy usm-copy">Aenderungen an der App-Auswahl werden nach dem Speichern mit einem Desktop-Reload sauber uebernommen.</p>
|
<p class="window-app-copy usm-copy">Eine aktivierte App erscheint standardmaessig im Menue. Fuer geeignete Apps kann zusaetzlich ein Desktop-Icon aktiviert werden.</p>
|
||||||
<div class="window-app-list usm-list">
|
<div class="window-app-list usm-list">
|
||||||
${renderAppItems(state)}
|
${renderAppItems(state)}
|
||||||
</div>
|
</div>
|
||||||
@@ -175,11 +212,24 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.activeSection === 'tray') {
|
||||||
|
return `
|
||||||
|
<section class="window-app-card usm-card">
|
||||||
|
<p class="window-app-kicker usm-kicker">Tray-Apps</p>
|
||||||
|
<h2>Bereich neben der Uhr</h2>
|
||||||
|
<p class="window-app-copy usm-copy">Tray-Apps sind Schnellzugriffe oder Statusanzeigen im Bereich neben der Uhr. Sie koennen eigenstaendig oder zusammen mit der Haupt-App genutzt werden.</p>
|
||||||
|
<div class="window-app-list usm-list">
|
||||||
|
${renderTrayItems(state)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="window-app-card usm-card">
|
<section class="window-app-card usm-card">
|
||||||
<p class="window-app-kicker usm-kicker">Infobereich</p>
|
<p class="window-app-kicker usm-kicker">Widgets</p>
|
||||||
<h2>Rechter Desktopbereich</h2>
|
<h2>Kleine Desktop-Bereiche</h2>
|
||||||
<p class="window-app-copy usm-copy">Diese Inhalte liegen aktuell im rechten Infobereich des Desktops und werden hier benutzerbezogen ein- oder ausgeblendet.</p>
|
<p class="window-app-copy usm-copy">Widgets sind kleine Desktop-Bereiche ohne klassische Fensterfunktionen. Aktivierung und Positionierung werden schrittweise ausgebaut.</p>
|
||||||
<div class="window-app-list usm-list">
|
<div class="window-app-list usm-list">
|
||||||
${renderWidgetItems(state)}
|
${renderWidgetItems(state)}
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +255,7 @@
|
|||||||
<div class="window-app-brand usm-brand">
|
<div class="window-app-brand usm-brand">
|
||||||
<p class="window-app-kicker usm-kicker">Setup</p>
|
<p class="window-app-kicker usm-kicker">Setup</p>
|
||||||
<h1>User Self Management</h1>
|
<h1>User Self Management</h1>
|
||||||
<p class="window-app-copy">Ein gemeinsamer Setup-Bereich fuer Desktop-Typ, Benutzerdaten, Apps und den rechten Infobereich.</p>
|
<p class="window-app-copy">Ein gemeinsamer Setup-Bereich fuer Desktop-Typ, Benutzerdaten, Apps, Tray-Apps und Widgets.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-app-nav-list usm-nav-list">
|
<div class="window-app-nav-list usm-nav-list">
|
||||||
${SECTIONS.map((section) => `
|
${SECTIONS.map((section) => `
|
||||||
@@ -299,6 +349,10 @@
|
|||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
enabled_ids: state.draft.apps.enabled_ids,
|
enabled_ids: state.draft.apps.enabled_ids,
|
||||||
|
desktop_icon_ids: state.draft.apps.desktop_icon_ids,
|
||||||
|
},
|
||||||
|
tray: {
|
||||||
|
active_ids: state.draft.tray.active_ids,
|
||||||
},
|
},
|
||||||
widgets: {
|
widgets: {
|
||||||
active_ids: state.draft.widgets.active_ids,
|
active_ids: state.draft.widgets.active_ids,
|
||||||
@@ -306,7 +360,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const requiresReload = (before, after) => before.desktop.active_skin !== after.desktop.active_skin
|
const requiresReload = (before, after) => before.desktop.active_skin !== after.desktop.active_skin
|
||||||
|| !sameSelection(before.apps.enabled_ids, after.apps.enabled_ids);
|
|| !sameSelection(before.apps.enabled_ids, after.apps.enabled_ids)
|
||||||
|
|| !sameSelection(before.apps.desktop_icon_ids, after.apps.desktop_icon_ids)
|
||||||
|
|| !sameSelection(before.tray.active_ids, after.tray.active_ids)
|
||||||
|
|| !sameSelection(before.widgets.active_ids, after.widgets.active_ids);
|
||||||
|
|
||||||
const attachInteractions = (root, state, options) => {
|
const attachInteractions = (root, state, options) => {
|
||||||
root.addEventListener('click', async (event) => {
|
root.addEventListener('click', async (event) => {
|
||||||
@@ -405,6 +462,29 @@
|
|||||||
target.dataset.appId,
|
target.dataset.appId,
|
||||||
target.checked
|
target.checked
|
||||||
);
|
);
|
||||||
|
if (!target.checked) {
|
||||||
|
state.draft.apps.desktop_icon_ids = state.draft.apps.desktop_icon_ids.filter((appId) => appId !== target.dataset.appId);
|
||||||
|
}
|
||||||
|
renderApp(root, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.dataset.appDesktopIconId) {
|
||||||
|
state.draft.apps.desktop_icon_ids = syncSelectionWithCheckedState(
|
||||||
|
state.draft.apps.desktop_icon_ids,
|
||||||
|
target.dataset.appDesktopIconId,
|
||||||
|
target.checked
|
||||||
|
);
|
||||||
|
renderApp(root, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.dataset.trayAppId) {
|
||||||
|
state.draft.tray.active_ids = syncSelectionWithCheckedState(
|
||||||
|
state.draft.tray.active_ids,
|
||||||
|
target.dataset.trayAppId,
|
||||||
|
target.checked
|
||||||
|
);
|
||||||
renderApp(root, state);
|
renderApp(root, state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user