Self management
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App;
|
||||
|
||||
use Desktop\AppRegistry;
|
||||
use Desktop\AppIconResolver;
|
||||
use Desktop\AppVisibility;
|
||||
use Desktop\DesktopState;
|
||||
use Desktop\SkinResolver;
|
||||
use Desktop\WidgetRegistry;
|
||||
@@ -28,16 +29,30 @@ final class App
|
||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php');
|
||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
|
||||
$skins = SkinResolver::all();
|
||||
$activeSkin = SkinResolver::resolve($_GET['skin'] ?? null);
|
||||
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
||||
$authUser = $isAuthenticated && is_array($_SESSION['desktop_auth']['user'] ?? null)
|
||||
? $_SESSION['desktop_auth']['user']
|
||||
: [];
|
||||
$authGroups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
|
||||
$visibleApps = $this->filterAppsForGroups($registry->all(), $authGroups);
|
||||
$apps = AppIconResolver::decorate($visibleApps, $activeSkin);
|
||||
$visibleApps = AppVisibility::filterByGroups($registry->all(), $authGroups);
|
||||
$userSelfManagement = new UserSelfManagementService($this->projectRoot);
|
||||
$resolvedSkin = $userSelfManagement->resolveActiveSkin(
|
||||
isset($_GET['skin']) ? (string) $_GET['skin'] : null,
|
||||
$authUser,
|
||||
$visibleApps,
|
||||
$widgetRegistry->all(),
|
||||
$skins
|
||||
);
|
||||
$userSettings = $resolvedSkin['settings'];
|
||||
$activeSkin = (string) $resolvedSkin['skin'];
|
||||
$enabledApps = AppVisibility::filterByEnabledIds(
|
||||
$visibleApps,
|
||||
array_values(array_map('strval', (array) ($userSettings['apps']['enabled_ids'] ?? []))),
|
||||
[UserSelfManagementService::APP_ID]
|
||||
);
|
||||
$apps = AppIconResolver::decorate($enabledApps, $activeSkin);
|
||||
$windows = (new WindowManager($registry))->defaultWindows();
|
||||
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
||||
$activeWidgetIds = array_values(array_map('strval', (array) ($userSettings['widgets']['active_ids'] ?? DesktopState::defaultWidgetIds())));
|
||||
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
||||
$storageScope = $isAuthenticated
|
||||
? strtolower((string) ($authUser['username'] ?? $authUser['sub'] ?? 'user'))
|
||||
@@ -56,6 +71,8 @@ final class App
|
||||
'wallpaper' => SkinResolver::wallpaper($activeSkin),
|
||||
'storage_scope' => preg_replace('/[^a-z0-9._-]+/i', '-', $storageScope) ?: 'guest',
|
||||
'workspace' => DesktopState::defaultWorkspace(),
|
||||
'settings_api' => '/api/user-self-management/index.php',
|
||||
'user_preferences' => $userSettings,
|
||||
'login' => [
|
||||
'authenticated' => $isAuthenticated,
|
||||
'mode' => $isAuthenticated ? 'authenticated' : 'login-required',
|
||||
@@ -85,30 +102,4 @@ final class App
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $apps
|
||||
* @param array<int, string> $groups
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function filterAppsForGroups(array $apps, array $groups): array
|
||||
{
|
||||
$normalizedGroups = array_map(static fn (string $group): string => strtolower(trim($group, '/')), $groups);
|
||||
|
||||
return array_values(array_filter($apps, static function (array $app) use ($normalizedGroups): bool {
|
||||
$requiredGroups = array_values(array_map('strval', (array) ($app['required_groups'] ?? [])));
|
||||
|
||||
if ($requiredGroups === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($requiredGroups as $group) {
|
||||
if (in_array(strtolower(trim($group, '/')), $normalizedGroups, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
314
src/App/UserSelfManagementService.php
Normal file
314
src/App/UserSelfManagementService.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Desktop\SkinResolver;
|
||||
|
||||
final class UserSelfManagementService
|
||||
{
|
||||
public const APP_ID = 'user-self-management';
|
||||
|
||||
private const PROFILE_FIELDS = [
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'birthdate',
|
||||
'title',
|
||||
'department',
|
||||
'city',
|
||||
'website',
|
||||
'notes',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $projectRoot,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $authUser
|
||||
* @param array<int, array<string, mixed>> $visibleApps
|
||||
* @param array<int, array<string, mixed>> $widgets
|
||||
* @param array<int, string> $availableSkins
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function load(array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
||||
{
|
||||
return $this->normalizeSettings(
|
||||
$this->store($this->storageScope($authUser))->read(),
|
||||
$authUser,
|
||||
$visibleApps,
|
||||
$widgets,
|
||||
$availableSkins
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $patch
|
||||
* @param array<string, mixed> $authUser
|
||||
* @param array<int, array<string, mixed>> $visibleApps
|
||||
* @param array<int, array<string, mixed>> $widgets
|
||||
* @param array<int, string> $availableSkins
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function save(array $patch, array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
||||
{
|
||||
$current = $this->load($authUser, $visibleApps, $widgets, $availableSkins);
|
||||
$merged = $this->mergeRecursive($current, $patch);
|
||||
$normalized = $this->normalizeSettings($merged, $authUser, $visibleApps, $widgets, $availableSkins);
|
||||
$normalized['updated_at'] = gmdate(DATE_ATOM);
|
||||
|
||||
$this->store($this->storageScope($authUser))->write($normalized);
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $authUser
|
||||
* @param array<int, array<string, mixed>> $visibleApps
|
||||
* @param array<int, array<string, mixed>> $widgets
|
||||
* @param array<int, string> $availableSkins
|
||||
* @return array{skin: string, settings: array<string, mixed>}
|
||||
*/
|
||||
public function resolveActiveSkin(?string $requestedSkin, array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
||||
{
|
||||
$settings = $this->load($authUser, $visibleApps, $widgets, $availableSkins);
|
||||
$storedSkin = (string) (($settings['desktop']['active_skin'] ?? '') ?: SkinResolver::DEFAULT_SKIN);
|
||||
$resolvedSkin = SkinResolver::resolve($requestedSkin !== null && $requestedSkin !== '' ? $requestedSkin : $storedSkin);
|
||||
|
||||
if ($resolvedSkin !== $storedSkin) {
|
||||
$settings = $this->save(
|
||||
['desktop' => ['active_skin' => $resolvedSkin]],
|
||||
$authUser,
|
||||
$visibleApps,
|
||||
$widgets,
|
||||
$availableSkins
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'skin' => $resolvedSkin,
|
||||
'settings' => $settings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $authUser
|
||||
* @param array<int, array<string, mixed>> $visibleApps
|
||||
* @param array<int, array<string, mixed>> $widgets
|
||||
* @param array<int, string> $availableSkins
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildBootstrapPayload(array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
||||
{
|
||||
$settings = $this->load($authUser, $visibleApps, $widgets, $availableSkins);
|
||||
$enabledAppIds = array_values(array_map('strval', (array) ($settings['apps']['enabled_ids'] ?? [])));
|
||||
$activeWidgetIds = array_values(array_map('strval', (array) ($settings['widgets']['active_ids'] ?? [])));
|
||||
|
||||
return [
|
||||
'preferences' => $settings,
|
||||
'options' => [
|
||||
'skins' => array_map(
|
||||
static fn (string $skin): array => [
|
||||
'skin_id' => $skin,
|
||||
'label' => (string) (SkinResolver::profile($skin)['label'] ?? ucfirst($skin)),
|
||||
],
|
||||
$availableSkins
|
||||
),
|
||||
'apps' => array_map(
|
||||
static fn (array $app): array => [
|
||||
'app_id' => (string) ($app['app_id'] ?? ''),
|
||||
'title' => (string) ($app['title'] ?? ''),
|
||||
'summary' => (string) ($app['summary'] ?? ''),
|
||||
'module_name' => (string) ($app['module_name'] ?? ''),
|
||||
'required' => in_array((string) ($app['app_id'] ?? ''), [self::APP_ID], true),
|
||||
'enabled' => in_array((string) ($app['app_id'] ?? ''), $enabledAppIds, true),
|
||||
],
|
||||
$visibleApps
|
||||
),
|
||||
'widgets' => array_map(
|
||||
static fn (array $widget): array => [
|
||||
'widget_id' => (string) ($widget['widget_id'] ?? ''),
|
||||
'title' => (string) ($widget['title'] ?? ''),
|
||||
'summary' => (string) ($widget['summary'] ?? ''),
|
||||
'default_enabled' => (bool) ($widget['default_enabled'] ?? false),
|
||||
'enabled' => in_array((string) ($widget['widget_id'] ?? ''), $activeWidgetIds, true),
|
||||
],
|
||||
$widgets
|
||||
),
|
||||
],
|
||||
'meta' => [
|
||||
'storage_scope' => $this->storageScope($authUser),
|
||||
'authenticated' => ($authUser['sub'] ?? '') !== '' || ($authUser['username'] ?? '') !== '',
|
||||
'sync_mode' => 'local-profile',
|
||||
'sync_targets' => [
|
||||
'ldap' => 'planned',
|
||||
'keycloak' => 'planned',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $authUser
|
||||
*/
|
||||
public function storageScope(array $authUser): string
|
||||
{
|
||||
$rawScope = (string) ($authUser['username'] ?? $authUser['sub'] ?? 'guest');
|
||||
$sanitized = preg_replace('/[^a-z0-9._-]+/i', '-', strtolower($rawScope));
|
||||
|
||||
return $sanitized !== null && $sanitized !== '' ? $sanitized : 'guest';
|
||||
}
|
||||
|
||||
private function store(string $scope): JsonStore
|
||||
{
|
||||
return new JsonStore($this->projectRoot . '/data/user-self-management/users/' . $scope . '.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @param array<string, mixed> $authUser
|
||||
* @param array<int, array<string, mixed>> $visibleApps
|
||||
* @param array<int, array<string, mixed>> $widgets
|
||||
* @param array<int, string> $availableSkins
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function normalizeSettings(array $settings, array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
||||
{
|
||||
$visibleAppIds = array_values(array_filter(array_map(
|
||||
static fn (array $app): string => (string) ($app['app_id'] ?? ''),
|
||||
$visibleApps
|
||||
)));
|
||||
$defaultWidgetIds = array_values(array_filter(array_map(
|
||||
static fn (array $widget): ?string => ($widget['default_enabled'] ?? false) ? (string) ($widget['widget_id'] ?? '') : null,
|
||||
$widgets
|
||||
)));
|
||||
$allWidgetIds = array_values(array_filter(array_map(
|
||||
static fn (array $widget): string => (string) ($widget['widget_id'] ?? ''),
|
||||
$widgets
|
||||
)));
|
||||
$enabledAppIds = $this->sanitizeSelection(
|
||||
$settings['apps']['enabled_ids'] ?? $visibleAppIds,
|
||||
$visibleAppIds,
|
||||
[self::APP_ID]
|
||||
);
|
||||
$activeWidgetIds = $this->sanitizeSelection(
|
||||
$settings['widgets']['active_ids'] ?? $defaultWidgetIds,
|
||||
$allWidgetIds
|
||||
);
|
||||
$profile = [];
|
||||
|
||||
foreach (self::PROFILE_FIELDS as $field) {
|
||||
$fallback = match ($field) {
|
||||
'name' => (string) ($authUser['name'] ?? ''),
|
||||
'email' => (string) ($authUser['email'] ?? ''),
|
||||
default => '',
|
||||
};
|
||||
|
||||
$profile[$field] = $this->limitString(
|
||||
(string) (($settings['profile'][$field] ?? '') ?: $fallback),
|
||||
$field === 'notes' ? 2000 : 255
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'schema_version' => 1,
|
||||
'updated_at' => (string) ($settings['updated_at'] ?? ''),
|
||||
'profile' => $profile,
|
||||
'desktop' => [
|
||||
'active_skin' => $this->sanitizeSkin(
|
||||
(string) ($settings['desktop']['active_skin'] ?? SkinResolver::DEFAULT_SKIN),
|
||||
$availableSkins
|
||||
),
|
||||
],
|
||||
'apps' => [
|
||||
'enabled_ids' => $enabledAppIds,
|
||||
],
|
||||
'widgets' => [
|
||||
'active_ids' => $activeWidgetIds,
|
||||
],
|
||||
'integration' => [
|
||||
'ldap_write_status' => 'planned',
|
||||
'keycloak_write_status' => 'planned',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $selected
|
||||
* @param array<int, string> $allowedIds
|
||||
* @param array<int, string> $mandatoryIds
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function sanitizeSelection(mixed $selected, array $allowedIds, array $mandatoryIds = []): array
|
||||
{
|
||||
$allowedLookup = array_fill_keys($allowedIds, true);
|
||||
$result = [];
|
||||
|
||||
foreach ((array) $selected as $id) {
|
||||
$stringId = (string) $id;
|
||||
|
||||
if ($stringId !== '' && isset($allowedLookup[$stringId])) {
|
||||
$result[$stringId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($mandatoryIds as $mandatoryId) {
|
||||
if ($mandatoryId !== '' && isset($allowedLookup[$mandatoryId])) {
|
||||
$result[$mandatoryId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $availableSkins
|
||||
*/
|
||||
private function sanitizeSkin(string $skin, array $availableSkins): string
|
||||
{
|
||||
$resolved = SkinResolver::resolve($skin);
|
||||
|
||||
return in_array($resolved, $availableSkins, true) ? $resolved : SkinResolver::DEFAULT_SKIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $base
|
||||
* @param array<string, mixed> $patch
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function mergeRecursive(array $base, array $patch): array
|
||||
{
|
||||
foreach ($patch as $key => $value) {
|
||||
if (is_array($value) && isset($base[$key]) && is_array($base[$key]) && !$this->isList($value)) {
|
||||
/** @var array<string, mixed> $baseItem */
|
||||
$baseItem = $base[$key];
|
||||
/** @var array<string, mixed> $patchItem */
|
||||
$patchItem = $value;
|
||||
$base[$key] = $this->mergeRecursive($baseItem, $patchItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
$base[$key] = $value;
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $value
|
||||
*/
|
||||
private function isList(array $value): bool
|
||||
{
|
||||
return array_keys($value) === range(0, count($value) - 1);
|
||||
}
|
||||
|
||||
private function limitString(string $value, int $maxLength): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
|
||||
return mb_substr($trimmed, 0, $maxLength);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ final class AppIconResolver
|
||||
'public-dashboard' => '/assets/desktop/skin-icons/windows/public-dashboard.ico',
|
||||
'workspace-manager' => '/assets/desktop/skin-icons/windows/workspace-manager.ico',
|
||||
'admin-apps' => '/assets/desktop/skin-icons/windows/admin-apps.ico',
|
||||
'user-self-management' => '/assets/desktop/skin-icons/windows/admin-apps.ico',
|
||||
'desktop-login' => '/assets/desktop/skin-icons/windows/desktop-login.ico',
|
||||
'mining-checker' => '/assets/desktop/skin-icons/windows/admin-apps.ico',
|
||||
],
|
||||
@@ -48,6 +49,7 @@ final class AppIconResolver
|
||||
'public-dashboard' => '/assets/desktop/skin-icons/apple/public-dashboard.svg',
|
||||
'workspace-manager' => '/assets/desktop/skin-icons/apple/workspace-manager.svg',
|
||||
'admin-apps' => '/assets/desktop/skin-icons/apple/admin-apps.svg',
|
||||
'user-self-management' => '/assets/desktop/skin-icons/apple/admin-apps.svg',
|
||||
'desktop-login' => '/assets/desktop/skin-icons/apple/desktop-login.svg',
|
||||
'mining-checker' => '/assets/desktop/skin-icons/apple/admin-apps.svg',
|
||||
],
|
||||
@@ -56,6 +58,7 @@ final class AppIconResolver
|
||||
'public-dashboard' => '/assets/desktop/skin-icons/linux/public-dashboard.svg',
|
||||
'workspace-manager' => '/assets/desktop/skin-icons/linux/workspace-manager.svg',
|
||||
'admin-apps' => '/assets/desktop/skin-icons/linux/admin-apps.svg',
|
||||
'user-self-management' => '/assets/desktop/skin-icons/linux/admin-apps.svg',
|
||||
'desktop-login' => '/assets/desktop/skin-icons/linux/desktop-login.svg',
|
||||
'mining-checker' => '/assets/desktop/skin-icons/linux/admin-apps.svg',
|
||||
],
|
||||
|
||||
59
src/Desktop/AppVisibility.php
Normal file
59
src/Desktop/AppVisibility.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Desktop;
|
||||
|
||||
final class AppVisibility
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $apps
|
||||
* @param array<int, string> $groups
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function filterByGroups(array $apps, array $groups): array
|
||||
{
|
||||
$normalizedGroups = array_map(
|
||||
static fn (string $group): string => strtolower(trim($group, '/')),
|
||||
$groups
|
||||
);
|
||||
|
||||
return array_values(array_filter($apps, static function (array $app) use ($normalizedGroups): bool {
|
||||
$requiredGroups = array_values(array_map('strval', (array) ($app['required_groups'] ?? [])));
|
||||
|
||||
if ($requiredGroups === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($requiredGroups as $group) {
|
||||
if (in_array(strtolower(trim($group, '/')), $normalizedGroups, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $apps
|
||||
* @param array<int, string> $enabledAppIds
|
||||
* @param array<int, string> $mandatoryAppIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function filterByEnabledIds(array $apps, array $enabledAppIds, array $mandatoryAppIds = []): array
|
||||
{
|
||||
$lookup = array_fill_keys($enabledAppIds, true);
|
||||
$mandatoryLookup = array_fill_keys($mandatoryAppIds, true);
|
||||
|
||||
return array_values(array_filter($apps, static function (array $app) use ($lookup, $mandatoryLookup): bool {
|
||||
$appId = (string) ($app['app_id'] ?? '');
|
||||
|
||||
if ($appId === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset($lookup[$appId]) || isset($mandatoryLookup[$appId]);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ final class DesktopState
|
||||
public static function quickActions(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'action_id' => 'open-user-self-management',
|
||||
'label' => 'Setup',
|
||||
'description' => 'Oeffnet die persoenlichen Desktop-Einstellungen.',
|
||||
],
|
||||
[
|
||||
'action_id' => 'open-public-dashboard',
|
||||
'label' => 'Public Dashboard',
|
||||
@@ -55,11 +60,6 @@ final class DesktopState
|
||||
'label' => 'Widgets umschalten',
|
||||
'description' => 'Blendet alle registrierten Widgets ein oder aus.',
|
||||
],
|
||||
[
|
||||
'action_id' => 'cycle-skin',
|
||||
'label' => 'Skin wechseln',
|
||||
'description' => 'Wechselt zyklisch zwischen Windows, Apple und Linux.',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user