Self management
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user