tenantService->list(); $roles = $this->roleService->listActive(); $departments = $this->departmentService->list(); $sortByDescription = static fn (array $left, array $right): int => strcmp((string) ($left['description'] ?? ''), (string) ($right['description'] ?? '')); usort($tenants, $sortByDescription); usort($roles, $sortByDescription); usort($departments, $sortByDescription); return [ 'tenants' => $tenants, 'roles' => $roles, 'departments' => $departments, 'values' => [ 'default_tenant_id' => $this->settingGateway->getDefaultTenantId(), 'default_role_id' => $this->settingGateway->getDefaultRoleId(), 'default_department_id' => $this->settingGateway->getDefaultDepartmentId(), 'app_title' => $this->settingGateway->getValue(SettingService::APP_TITLE_KEY), 'app_locale' => $this->settingGateway->getValue(SettingService::APP_LOCALE_KEY), 'app_theme' => $this->settingGateway->getValue(SettingService::APP_THEME_KEY), 'app_theme_user' => $this->settingGateway->isUserThemeAllowed(), 'app_registration' => $this->settingGateway->isRegistrationEnabled(), 'app_primary_color' => $this->settingGateway->getValue(SettingService::APP_PRIMARY_COLOR_KEY), 'api_token_default_ttl_days' => $this->settingGateway->getApiTokenDefaultTtlDays(), 'api_token_max_ttl_days' => $this->settingGateway->getApiTokenMaxTtlDays(), 'user_inactivity_deactivate_days' => $this->settingGateway->getUserInactivityDeactivateDays(), 'user_inactivity_delete_days' => $this->settingGateway->getUserInactivityDeleteDays(), 'api_cors_allowed_origins' => $this->settingGateway->getApiCorsAllowedOriginsText(), 'system_audit_enabled' => $this->settingGateway->isSystemAuditEnabled(), 'system_audit_retention_days' => $this->settingGateway->getSystemAuditRetentionDays(), 'microsoft_shared_client_id' => $this->settingGateway->getValue(SettingService::MICROSOFT_SHARED_CLIENT_ID_KEY), 'microsoft_authority' => $this->settingGateway->getMicrosoftAuthority(), 'smtp_host' => $this->settingGateway->getValue(SettingService::SMTP_HOST_KEY), 'smtp_port' => $this->settingGateway->getValue(SettingService::SMTP_PORT_KEY), 'smtp_user' => $this->settingGateway->getValue(SettingService::SMTP_USER_KEY), 'smtp_secure' => $this->settingGateway->getSmtpSecure(), 'smtp_from' => $this->settingGateway->getValue(SettingService::SMTP_FROM_KEY), 'smtp_from_name' => $this->settingGateway->getValue(SettingService::SMTP_FROM_NAME_KEY), ], 'settings' => [ 'default_tenant' => $this->settingGateway->get(SettingService::DEFAULT_TENANT_KEY), 'default_role' => $this->settingGateway->get(SettingService::DEFAULT_ROLE_KEY), 'default_department' => $this->settingGateway->get(SettingService::DEFAULT_DEPARTMENT_KEY), 'app_title' => $this->settingGateway->get(SettingService::APP_TITLE_KEY), 'app_locale' => $this->settingGateway->get(SettingService::APP_LOCALE_KEY), 'app_theme' => $this->settingGateway->get(SettingService::APP_THEME_KEY), 'app_theme_user' => $this->settingGateway->get(SettingService::APP_THEME_USER_KEY), 'app_registration' => $this->settingGateway->get(SettingService::APP_REGISTRATION_KEY), 'app_primary_color' => $this->settingGateway->get(SettingService::APP_PRIMARY_COLOR_KEY), 'api_token_default_ttl_days' => $this->settingGateway->get(SettingService::API_TOKEN_DEFAULT_TTL_DAYS_KEY), 'api_token_max_ttl_days' => $this->settingGateway->get(SettingService::API_TOKEN_MAX_TTL_DAYS_KEY), 'user_inactivity_deactivate_days' => $this->settingGateway->get(SettingService::USER_INACTIVITY_DEACTIVATE_DAYS_KEY), 'user_inactivity_delete_days' => $this->settingGateway->get(SettingService::USER_INACTIVITY_DELETE_DAYS_KEY), 'api_cors_allowed_origins' => $this->settingGateway->get(SettingService::API_CORS_ALLOWED_ORIGINS_KEY), 'system_audit_enabled' => $this->settingGateway->get(SettingService::SYSTEM_AUDIT_ENABLED_KEY), 'system_audit_retention_days' => $this->settingGateway->get(SettingService::SYSTEM_AUDIT_RETENTION_DAYS_KEY), 'microsoft_shared_client_id' => $this->settingGateway->get(SettingService::MICROSOFT_SHARED_CLIENT_ID_KEY), 'microsoft_shared_client_secret_enc' => $this->settingGateway->get(SettingService::MICROSOFT_SHARED_CLIENT_SECRET_ENC_KEY), 'microsoft_authority' => $this->settingGateway->get(SettingService::MICROSOFT_AUTHORITY_KEY), 'smtp_host' => $this->settingGateway->get(SettingService::SMTP_HOST_KEY), 'smtp_port' => $this->settingGateway->get(SettingService::SMTP_PORT_KEY), 'smtp_user' => $this->settingGateway->get(SettingService::SMTP_USER_KEY), 'smtp_password' => $this->settingGateway->get(SettingService::SMTP_PASSWORD_KEY), 'smtp_secure' => $this->settingGateway->get(SettingService::SMTP_SECURE_KEY), 'smtp_from' => $this->settingGateway->get(SettingService::SMTP_FROM_KEY), 'smtp_from_name' => $this->settingGateway->get(SettingService::SMTP_FROM_NAME_KEY), ], 'active_login_tokens' => $this->rememberTokenRepository->countActive(), 'active_api_tokens' => $this->apiTokenRepository->countActive(), ]; } public function updateFromAdmin(array $post): array { $defaultTenantId = (int) ($post['default_tenant_id'] ?? 0); $defaultRoleId = (int) ($post['default_role_id'] ?? 0); $defaultDepartmentId = (int) ($post['default_department_id'] ?? 0); $appTitle = trim((string) ($post['app_title'] ?? '')); $appLocale = trim((string) ($post['app_locale'] ?? '')); $appTheme = trim((string) ($post['app_theme'] ?? '')); $appThemeUser = isset($post['app_theme_user']); $appRegistration = isset($post['app_registration']); $apiTokenDefaultTtlDays = (int) ($post['api_token_default_ttl_days'] ?? 0); $apiTokenMaxTtlDays = (int) ($post['api_token_max_ttl_days'] ?? 0); $userInactivityDeactivateDays = (int) ($post['user_inactivity_deactivate_days'] ?? 0); $userInactivityDeleteDays = (int) ($post['user_inactivity_delete_days'] ?? 0); $apiCorsAllowedOrigins = $this->normalizeScalarText($post['api_cors_allowed_origins'] ?? ''); $systemAuditEnabled = isset($post['system_audit_enabled']); $systemAuditRetentionDays = (int) ($post['system_audit_retention_days'] ?? 0); $appPrimaryColor = $this->normalizePrimaryColor($post['app_primary_color'] ?? ''); $smtpHost = trim((string) ($post['smtp_host'] ?? '')); $smtpPort = (int) ($post['smtp_port'] ?? 0); $smtpUser = trim((string) ($post['smtp_user'] ?? '')); $smtpPassword = (string) ($post['smtp_password'] ?? ''); $smtpSecure = trim((string) ($post['smtp_secure'] ?? '')); $smtpFrom = trim((string) ($post['smtp_from'] ?? '')); $smtpFromName = trim((string) ($post['smtp_from_name'] ?? '')); $microsoftSharedClientId = trim((string) ($post['microsoft_shared_client_id'] ?? '')); $microsoftSharedClientSecret = (string) ($post['microsoft_shared_client_secret'] ?? ''); $microsoftAuthority = trim((string) ($post['microsoft_authority'] ?? '')); $errors = []; $beforeAudit = [ 'default_tenant_id' => $this->settingGateway->getDefaultTenantId(), 'default_role_id' => $this->settingGateway->getDefaultRoleId(), 'default_department_id' => $this->settingGateway->getDefaultDepartmentId(), 'app_locale' => $this->settingGateway->getAppLocale(), 'app_theme' => $this->settingGateway->getAppTheme(), 'app_theme_user' => $this->settingGateway->isUserThemeAllowed(), 'app_registration' => $this->settingGateway->isRegistrationEnabled(), 'app_primary_color' => $this->settingGateway->getAppPrimaryColor(), 'api_token_default_ttl_days' => $this->settingGateway->getApiTokenDefaultTtlDays(), 'api_token_max_ttl_days' => $this->settingGateway->getApiTokenMaxTtlDays(), 'user_inactivity_deactivate_days' => $this->settingGateway->getUserInactivityDeactivateDays(), 'user_inactivity_delete_days' => $this->settingGateway->getUserInactivityDeleteDays(), 'system_audit_enabled' => $this->settingGateway->isSystemAuditEnabled(), 'system_audit_retention_days' => $this->settingGateway->getSystemAuditRetentionDays(), ]; $this->settingGateway->setDefaultTenantId($defaultTenantId > 0 ? $defaultTenantId : null); $this->settingGateway->setDefaultRoleId($defaultRoleId > 0 ? $defaultRoleId : null); $this->settingGateway->setDefaultDepartmentId($defaultDepartmentId > 0 ? $defaultDepartmentId : null); $this->settingGateway->setAppTitle($appTitle); $this->settingGateway->setAppLocale($appLocale); $this->settingGateway->setAppTheme($appTheme); $this->settingGateway->setUserThemeAllowed($appThemeUser); $this->settingGateway->setRegistrationEnabled($appRegistration); $apiTokenMaxTtlOk = $this->settingGateway->setApiTokenMaxTtlDays($apiTokenMaxTtlDays > 0 ? $apiTokenMaxTtlDays : null); if (!$apiTokenMaxTtlOk) { $errors[] = ['message' => 'API token max lifetime is invalid', 'key' => 'api_token_max_ttl_invalid']; } $apiTokenTtlOk = $this->settingGateway->setApiTokenDefaultTtlDays($apiTokenDefaultTtlDays > 0 ? $apiTokenDefaultTtlDays : null); if (!$apiTokenTtlOk) { $errors[] = ['message' => 'API token default lifetime is invalid', 'key' => 'api_token_default_ttl_invalid']; } $userDeactivateOk = $this->settingGateway->setUserInactivityDeactivateDays($userInactivityDeactivateDays); if (!$userDeactivateOk) { $errors[] = [ 'message' => 'User inactivity deactivation period is invalid', 'key' => 'user_inactivity_deactivate_days_invalid', ]; } if ($userInactivityDeactivateDays > 0) { $userDeleteOk = $this->settingGateway->setUserInactivityDeleteDays($userInactivityDeleteDays); if (!$userDeleteOk) { $errors[] = [ 'message' => 'User inactivity deletion period is invalid', 'key' => 'user_inactivity_delete_days_invalid', ]; } } else { if ($userInactivityDeleteDays > 0) { $errors[] = [ 'message' => 'Auto-delete is ignored while auto-deactivate is disabled', 'key' => 'user_inactivity_delete_requires_deactivate', ]; } $this->settingGateway->setUserInactivityDeleteDays(0); } $apiCorsOriginsOk = $this->settingGateway->setApiCorsAllowedOrigins($apiCorsAllowedOrigins); if (!$apiCorsOriginsOk) { $errors[] = ['message' => 'CORS allowed origins are invalid', 'key' => 'api_cors_allowed_origins_invalid']; } $systemAuditEnabledOk = $this->settingGateway->setSystemAuditEnabled($systemAuditEnabled); if (!$systemAuditEnabledOk) { $errors[] = ['message' => 'System audit setting is invalid', 'key' => 'system_audit_enabled_invalid']; } $systemAuditRetentionOk = $this->settingGateway->setSystemAuditRetentionDays( $systemAuditRetentionDays > 0 ? $systemAuditRetentionDays : null ); if (!$systemAuditRetentionOk) { $errors[] = ['message' => 'System audit retention is invalid', 'key' => 'system_audit_retention_invalid']; } $primaryOk = $this->settingGateway->setAppPrimaryColor($appPrimaryColor); if (!$primaryOk) { $appPrimaryColor = ''; $errors[] = ['message' => 'Primary color is invalid', 'key' => 'app_primary_invalid']; } $this->settingGateway->setSmtpHost($smtpHost); $this->settingGateway->setSmtpPort($smtpPort > 0 ? $smtpPort : null); $this->settingGateway->setSmtpUser($smtpUser); if (trim($smtpPassword) !== '') { $this->settingGateway->setSmtpPassword($smtpPassword); } $this->settingGateway->setSmtpSecure($smtpSecure); $this->settingGateway->setSmtpFrom($smtpFrom); $this->settingGateway->setSmtpFromName($smtpFromName); $msClientIdOk = $this->settingGateway->setMicrosoftSharedClientId($microsoftSharedClientId); if (!$msClientIdOk) { $errors[] = ['message' => 'Microsoft client ID is invalid', 'key' => 'microsoft_client_id_invalid']; } $msAuthorityOk = $this->settingGateway->setMicrosoftAuthority($microsoftAuthority); if (!$msAuthorityOk) { $errors[] = ['message' => 'Microsoft authority is invalid', 'key' => 'microsoft_authority_invalid']; } if (trim($microsoftSharedClientSecret) !== '') { try { $this->settingGateway->setMicrosoftSharedClientSecret($microsoftSharedClientSecret); } catch (\Throwable) { $errors[] = [ 'message' => 'Microsoft client secret could not be encrypted', 'key' => 'microsoft_client_secret_invalid', ]; } } $this->settingCacheService->update([ 'app_title' => $appTitle !== '' ? $appTitle : null, 'app_locale' => $appLocale !== '' ? $appLocale : null, 'app_theme' => $appTheme !== '' ? $appTheme : null, 'app_theme_user' => $appThemeUser ? '1' : '0', 'app_registration' => $appRegistration ? '1' : '0', 'app_primary_color' => $appPrimaryColor !== '' ? $appPrimaryColor : null, 'api_token_default_ttl_days' => (string) $this->settingGateway->getApiTokenDefaultTtlDays(), 'api_token_max_ttl_days' => (string) $this->settingGateway->getApiTokenMaxTtlDays(), 'api_cors_allowed_origins' => $this->settingGateway->getValue(SettingService::API_CORS_ALLOWED_ORIGINS_KEY), ]); $afterAudit = [ 'default_tenant_id' => $this->settingGateway->getDefaultTenantId(), 'default_role_id' => $this->settingGateway->getDefaultRoleId(), 'default_department_id' => $this->settingGateway->getDefaultDepartmentId(), 'app_locale' => $this->settingGateway->getAppLocale(), 'app_theme' => $this->settingGateway->getAppTheme(), 'app_theme_user' => $this->settingGateway->isUserThemeAllowed(), 'app_registration' => $this->settingGateway->isRegistrationEnabled(), 'app_primary_color' => $this->settingGateway->getAppPrimaryColor(), 'api_token_default_ttl_days' => $this->settingGateway->getApiTokenDefaultTtlDays(), 'api_token_max_ttl_days' => $this->settingGateway->getApiTokenMaxTtlDays(), 'user_inactivity_deactivate_days' => $this->settingGateway->getUserInactivityDeactivateDays(), 'user_inactivity_delete_days' => $this->settingGateway->getUserInactivityDeleteDays(), 'system_audit_enabled' => $this->settingGateway->isSystemAuditEnabled(), 'system_audit_retention_days' => $this->settingGateway->getSystemAuditRetentionDays(), ]; $outcome = $errors === [] ? 'success' : 'failed'; $errorKeys = []; if ($errors !== []) { foreach ($errors as $error) { $key = trim((string) $error['key']); if ($key !== '') { $errorKeys[] = $key; } } } $this->systemAuditService->record('admin.settings.update', $outcome, [ 'before' => $beforeAudit, 'after' => $afterAudit, 'error_code' => $errors === [] ? null : 'validation_error', 'metadata' => $errors === [] ? [] : ['error_keys' => $errorKeys], ]); return ['errors' => $errors]; } private function normalizeScalarText(mixed $value): string { if (is_array($value)) { return ''; } return trim((string) $value); } private function normalizePrimaryColor(mixed $value): string { $color = $this->normalizeScalarText($value); if (in_array(strtolower($color), ['null', 'undefined'], true)) { return ''; } return $color; } }