1
0
Files
breadcrumb-the-shire/core/Service/Settings/AdminSettingsService.php
fs 0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00

419 lines
26 KiB
PHP

<?php
namespace MintyPHP\Service\Settings;
use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Repository\Auth\RememberTokenRepository;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Tenant\TenantService;
class AdminSettingsService
{
public function __construct(
private readonly SettingsDefaultsGateway $settingsDefaultsGateway,
private readonly SettingsAppGateway $settingsAppGateway,
private readonly SettingsApiPolicyGateway $settingsApiPolicyGateway,
private readonly SettingsUserLifecycleGateway $settingsUserLifecycleGateway,
private readonly SettingsSessionGateway $settingsSessionGateway,
private readonly SettingsSystemAuditGateway $settingsSystemAuditGateway,
private readonly SettingsFrontendTelemetryGateway $settingsFrontendTelemetryGateway,
private readonly SettingsMicrosoftGateway $settingsMicrosoftGateway,
private readonly SettingsSmtpGateway $settingsSmtpGateway,
private readonly SettingsLoginPersistenceGateway $settingsLoginPersistenceGateway,
private readonly SettingsMetadataGateway $settingsMetadataGateway,
private readonly SettingCacheService $settingCacheService,
private readonly TenantService $tenantService,
private readonly RoleService $roleService,
private readonly DepartmentService $departmentService,
private readonly RememberTokenRepository $rememberTokenRepository,
private readonly ApiTokenRepository $apiTokenRepository,
private readonly AuditRecorderInterface $systemAuditService
) {
}
public function buildPageData(): array
{
$tenants = $this->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->settingsDefaultsGateway->getDefaultTenantId(),
'default_role_id' => $this->settingsDefaultsGateway->getDefaultRoleId(),
'default_department_id' => $this->settingsDefaultsGateway->getDefaultDepartmentId(),
'app_title' => $this->settingsMetadataGateway->getValue(SettingKeys::APP_TITLE_KEY),
'app_locale' => $this->settingsMetadataGateway->getValue(SettingKeys::APP_LOCALE_KEY),
'app_theme' => $this->settingsMetadataGateway->getValue(SettingKeys::APP_THEME_KEY),
'app_theme_user' => $this->settingsAppGateway->isUserThemeAllowed(),
'app_registration' => $this->settingsAppGateway->isRegistrationEnabled(),
'app_primary_color' => $this->settingsMetadataGateway->getValue(SettingKeys::APP_PRIMARY_COLOR_KEY),
'api_token_default_ttl_days' => $this->settingsApiPolicyGateway->getApiTokenDefaultTtlDays(),
'api_token_max_ttl_days' => $this->settingsApiPolicyGateway->getApiTokenMaxTtlDays(),
'user_inactivity_deactivate_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeactivateDays(),
'user_inactivity_delete_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeleteDays(),
'session_idle_timeout_minutes' => $this->settingsSessionGateway->getSessionIdleTimeoutMinutes(),
'session_absolute_timeout_hours' => $this->settingsSessionGateway->getSessionAbsoluteTimeoutHours(),
'microsoft_auto_remember_default' => $this->settingsLoginPersistenceGateway->isMicrosoftAutoRememberDefault(),
'remember_token_lifetime_days' => $this->settingsLoginPersistenceGateway->getRememberTokenLifetimeDays(),
'api_cors_allowed_origins' => $this->settingsApiPolicyGateway->getApiCorsAllowedOriginsText(),
'system_audit_enabled' => $this->settingsSystemAuditGateway->isSystemAuditEnabled(),
'system_audit_retention_days' => $this->settingsSystemAuditGateway->getSystemAuditRetentionDays(),
'frontend_telemetry_enabled' => $this->settingsFrontendTelemetryGateway->isFrontendTelemetryEnabled(),
'frontend_telemetry_sample_rate' => $this->settingsFrontendTelemetryGateway->getFrontendTelemetrySampleRate(),
'frontend_telemetry_allowed_events' => $this->settingsFrontendTelemetryGateway->getFrontendTelemetryAllowedEvents(),
'microsoft_shared_client_id' => $this->settingsMetadataGateway->getValue(SettingKeys::MICROSOFT_SHARED_CLIENT_ID_KEY),
'microsoft_authority' => $this->settingsMicrosoftGateway->getMicrosoftAuthority(),
'smtp_host' => $this->settingsMetadataGateway->getValue(SettingKeys::SMTP_HOST_KEY),
'smtp_port' => $this->settingsMetadataGateway->getValue(SettingKeys::SMTP_PORT_KEY),
'smtp_user' => $this->settingsMetadataGateway->getValue(SettingKeys::SMTP_USER_KEY),
'smtp_secure' => $this->settingsSmtpGateway->getSmtpSecure(),
'smtp_from' => $this->settingsMetadataGateway->getValue(SettingKeys::SMTP_FROM_KEY),
'smtp_from_name' => $this->settingsMetadataGateway->getValue(SettingKeys::SMTP_FROM_NAME_KEY),
],
'settings' => [
'default_tenant' => $this->settingsMetadataGateway->get(SettingKeys::DEFAULT_TENANT_KEY),
'default_role' => $this->settingsMetadataGateway->get(SettingKeys::DEFAULT_ROLE_KEY),
'default_department' => $this->settingsMetadataGateway->get(SettingKeys::DEFAULT_DEPARTMENT_KEY),
'app_title' => $this->settingsMetadataGateway->get(SettingKeys::APP_TITLE_KEY),
'app_locale' => $this->settingsMetadataGateway->get(SettingKeys::APP_LOCALE_KEY),
'app_theme' => $this->settingsMetadataGateway->get(SettingKeys::APP_THEME_KEY),
'app_theme_user' => $this->settingsMetadataGateway->get(SettingKeys::APP_THEME_USER_KEY),
'app_registration' => $this->settingsMetadataGateway->get(SettingKeys::APP_REGISTRATION_KEY),
'app_primary_color' => $this->settingsMetadataGateway->get(SettingKeys::APP_PRIMARY_COLOR_KEY),
'api_token_default_ttl_days' => $this->settingsMetadataGateway->get(SettingKeys::API_TOKEN_DEFAULT_TTL_DAYS_KEY),
'api_token_max_ttl_days' => $this->settingsMetadataGateway->get(SettingKeys::API_TOKEN_MAX_TTL_DAYS_KEY),
'user_inactivity_deactivate_days' => $this->settingsMetadataGateway->get(SettingKeys::USER_INACTIVITY_DEACTIVATE_DAYS_KEY),
'user_inactivity_delete_days' => $this->settingsMetadataGateway->get(SettingKeys::USER_INACTIVITY_DELETE_DAYS_KEY),
'session_idle_timeout_minutes' => $this->settingsMetadataGateway->get(SettingKeys::SESSION_IDLE_TIMEOUT_MINUTES_KEY),
'session_absolute_timeout_hours' => $this->settingsMetadataGateway->get(SettingKeys::SESSION_ABSOLUTE_TIMEOUT_HOURS_KEY),
'microsoft_auto_remember_default' => $this->settingsMetadataGateway->get(SettingKeys::MICROSOFT_AUTO_REMEMBER_DEFAULT_KEY),
'remember_token_lifetime_days' => $this->settingsMetadataGateway->get(SettingKeys::REMEMBER_TOKEN_LIFETIME_DAYS_KEY),
'api_cors_allowed_origins' => $this->settingsMetadataGateway->get(SettingKeys::API_CORS_ALLOWED_ORIGINS_KEY),
'system_audit_enabled' => $this->settingsMetadataGateway->get(SettingKeys::SYSTEM_AUDIT_ENABLED_KEY),
'system_audit_retention_days' => $this->settingsMetadataGateway->get(SettingKeys::SYSTEM_AUDIT_RETENTION_DAYS_KEY),
'frontend_telemetry_enabled' => $this->settingsMetadataGateway->get(SettingKeys::FRONTEND_TELEMETRY_ENABLED_KEY),
'frontend_telemetry_sample_rate' => $this->settingsMetadataGateway->get(SettingKeys::FRONTEND_TELEMETRY_SAMPLE_RATE_KEY),
'frontend_telemetry_allowed_events' => $this->settingsMetadataGateway->get(SettingKeys::FRONTEND_TELEMETRY_ALLOWED_EVENTS_KEY),
'microsoft_shared_client_id' => $this->settingsMetadataGateway->get(SettingKeys::MICROSOFT_SHARED_CLIENT_ID_KEY),
'microsoft_shared_client_secret_enc' => $this->settingsMetadataGateway->get(SettingKeys::MICROSOFT_SHARED_CLIENT_SECRET_ENC_KEY),
'microsoft_authority' => $this->settingsMetadataGateway->get(SettingKeys::MICROSOFT_AUTHORITY_KEY),
'smtp_host' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_HOST_KEY),
'smtp_port' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_PORT_KEY),
'smtp_user' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_USER_KEY),
'smtp_password' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_PASSWORD_KEY),
'smtp_secure' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_SECURE_KEY),
'smtp_from' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_FROM_KEY),
'smtp_from_name' => $this->settingsMetadataGateway->get(SettingKeys::SMTP_FROM_NAME_KEY),
],
'active_login_tokens' => $this->rememberTokenRepository->countActive(),
'active_api_tokens' => $this->apiTokenRepository->countActive(),
];
}
public function updateFromAdmin(array $post): array
{
$input = $this->sanitizeAdminInput($post);
$beforeAudit = $this->captureAuditSnapshot();
$errors = [];
$this->applyDefaultsAndAppSettings($input);
array_push($errors, ...$this->applyApiAndLifecycleSettings($input));
array_push($errors, ...$this->applySessionAndLoginSettings($input));
array_push($errors, ...$this->applyAuditAndTelemetrySettings($input));
array_push($errors, ...$this->applyAppPrimaryColor($input));
$this->applySmtpSettings($input);
array_push($errors, ...$this->applyMicrosoftSettings($input));
$this->updateSettingsCache($input);
$afterAudit = $this->captureAuditSnapshot();
$this->recordAudit($errors, $beforeAudit, $afterAudit);
return ['errors' => $errors];
}
private function sanitizeAdminInput(array $post): array
{
return array_merge(
$this->sanitizeAppAndPolicyInput($post),
$this->sanitizeIntegrationInput($post),
);
}
private function sanitizeAppAndPolicyInput(array $post): array
{
return [
'default_tenant_id' => (int) ($post['default_tenant_id'] ?? 0),
'default_role_id' => (int) ($post['default_role_id'] ?? 0),
'default_department_id' => (int) ($post['default_department_id'] ?? 0),
'app_title' => trim((string) ($post['app_title'] ?? '')),
'app_locale' => trim((string) ($post['app_locale'] ?? '')),
'app_theme' => trim((string) ($post['app_theme'] ?? '')),
'app_theme_user' => isset($post['app_theme_user']),
'app_registration' => isset($post['app_registration']),
'app_primary_color' => $this->normalizePrimaryColor($post['app_primary_color'] ?? ''),
'api_token_default_ttl_days' => (int) ($post['api_token_default_ttl_days'] ?? 0),
'api_token_max_ttl_days' => (int) ($post['api_token_max_ttl_days'] ?? 0),
'api_cors_allowed_origins' => $this->normalizeScalarText($post['api_cors_allowed_origins'] ?? ''),
'user_inactivity_deactivate_days' => (int) ($post['user_inactivity_deactivate_days'] ?? 0),
'user_inactivity_delete_days' => (int) ($post['user_inactivity_delete_days'] ?? 0),
'session_idle_timeout_minutes' => (int) ($post['session_idle_timeout_minutes'] ?? 0),
'session_absolute_timeout_hours' => (int) ($post['session_absolute_timeout_hours'] ?? 0),
'microsoft_auto_remember_default' => isset($post['microsoft_auto_remember_default']),
'remember_token_lifetime_days' => (int) ($post['remember_token_lifetime_days'] ?? 0),
];
}
private function sanitizeIntegrationInput(array $post): array
{
return [
'system_audit_enabled' => isset($post['system_audit_enabled']),
'system_audit_retention_days' => (int) ($post['system_audit_retention_days'] ?? 0),
'frontend_telemetry_enabled' => isset($post['frontend_telemetry_enabled']),
'frontend_telemetry_sample_rate' => $this->normalizeScalarText($post['frontend_telemetry_sample_rate'] ?? ''),
'frontend_telemetry_allowed_events' => $post['frontend_telemetry_allowed_events'] ?? [],
'smtp_host' => trim((string) ($post['smtp_host'] ?? '')),
'smtp_port' => (int) ($post['smtp_port'] ?? 0),
'smtp_user' => trim((string) ($post['smtp_user'] ?? '')),
'smtp_password' => (string) ($post['smtp_password'] ?? ''),
'smtp_secure' => trim((string) ($post['smtp_secure'] ?? '')),
'smtp_from' => trim((string) ($post['smtp_from'] ?? '')),
'smtp_from_name' => trim((string) ($post['smtp_from_name'] ?? '')),
'microsoft_shared_client_id' => trim((string) ($post['microsoft_shared_client_id'] ?? '')),
'microsoft_shared_client_secret' => (string) ($post['microsoft_shared_client_secret'] ?? ''),
'microsoft_authority' => trim((string) ($post['microsoft_authority'] ?? '')),
];
}
private function captureAuditSnapshot(): array
{
return [
'default_tenant_id' => $this->settingsDefaultsGateway->getDefaultTenantId(),
'default_role_id' => $this->settingsDefaultsGateway->getDefaultRoleId(),
'default_department_id' => $this->settingsDefaultsGateway->getDefaultDepartmentId(),
'app_locale' => $this->settingsAppGateway->getAppLocale(),
'app_theme' => $this->settingsAppGateway->getAppTheme(),
'app_theme_user' => $this->settingsAppGateway->isUserThemeAllowed(),
'app_registration' => $this->settingsAppGateway->isRegistrationEnabled(),
'app_primary_color' => $this->settingsAppGateway->getAppPrimaryColor(),
'api_token_default_ttl_days' => $this->settingsApiPolicyGateway->getApiTokenDefaultTtlDays(),
'api_token_max_ttl_days' => $this->settingsApiPolicyGateway->getApiTokenMaxTtlDays(),
'user_inactivity_deactivate_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeactivateDays(),
'user_inactivity_delete_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeleteDays(),
'session_idle_timeout_minutes' => $this->settingsSessionGateway->getSessionIdleTimeoutMinutes(),
'session_absolute_timeout_hours' => $this->settingsSessionGateway->getSessionAbsoluteTimeoutHours(),
'microsoft_auto_remember_default' => $this->settingsLoginPersistenceGateway->isMicrosoftAutoRememberDefault(),
'remember_token_lifetime_days' => $this->settingsLoginPersistenceGateway->getRememberTokenLifetimeDays(),
'system_audit_enabled' => $this->settingsSystemAuditGateway->isSystemAuditEnabled(),
'system_audit_retention_days' => $this->settingsSystemAuditGateway->getSystemAuditRetentionDays(),
'frontend_telemetry_enabled' => $this->settingsFrontendTelemetryGateway->isFrontendTelemetryEnabled(),
'frontend_telemetry_sample_rate' => $this->settingsFrontendTelemetryGateway->getFrontendTelemetrySampleRate(),
'frontend_telemetry_allowed_events' => $this->settingsFrontendTelemetryGateway->getFrontendTelemetryAllowedEvents(),
];
}
private function applyDefaultsAndAppSettings(array $input): void
{
$this->settingsDefaultsGateway->setDefaultTenantId($input['default_tenant_id'] > 0 ? $input['default_tenant_id'] : null);
$this->settingsDefaultsGateway->setDefaultRoleId($input['default_role_id'] > 0 ? $input['default_role_id'] : null);
$this->settingsDefaultsGateway->setDefaultDepartmentId($input['default_department_id'] > 0 ? $input['default_department_id'] : null);
$this->settingsAppGateway->setAppTitle($input['app_title']);
$this->settingsAppGateway->setAppLocale($input['app_locale']);
$this->settingsAppGateway->setAppTheme($input['app_theme']);
$this->settingsAppGateway->setUserThemeAllowed($input['app_theme_user']);
$this->settingsAppGateway->setRegistrationEnabled($input['app_registration']);
}
/** @return list<array{message: string, key: string}> */
private function applyApiAndLifecycleSettings(array $input): array
{
$errors = [];
if (!$this->settingsApiPolicyGateway->setApiTokenMaxTtlDays($input['api_token_max_ttl_days'] > 0 ? $input['api_token_max_ttl_days'] : null)) {
$errors[] = ['message' => 'API token max lifetime is invalid', 'key' => 'api_token_max_ttl_invalid'];
}
if (!$this->settingsApiPolicyGateway->setApiTokenDefaultTtlDays($input['api_token_default_ttl_days'] > 0 ? $input['api_token_default_ttl_days'] : null)) {
$errors[] = ['message' => 'API token default lifetime is invalid', 'key' => 'api_token_default_ttl_invalid'];
}
if (!$this->settingsApiPolicyGateway->setApiCorsAllowedOrigins($input['api_cors_allowed_origins'])) {
$errors[] = ['message' => 'CORS allowed origins are invalid', 'key' => 'api_cors_allowed_origins_invalid'];
}
if (!$this->settingsUserLifecycleGateway->setUserInactivityDeactivateDays($input['user_inactivity_deactivate_days'])) {
$errors[] = ['message' => 'User inactivity deactivation period is invalid', 'key' => 'user_inactivity_deactivate_days_invalid'];
}
if ($input['user_inactivity_deactivate_days'] > 0) {
if (!$this->settingsUserLifecycleGateway->setUserInactivityDeleteDays($input['user_inactivity_delete_days'])) {
$errors[] = ['message' => 'User inactivity deletion period is invalid', 'key' => 'user_inactivity_delete_days_invalid'];
}
} else {
if ($input['user_inactivity_delete_days'] > 0) {
$errors[] = ['message' => 'Auto-delete is ignored while auto-deactivate is disabled', 'key' => 'user_inactivity_delete_requires_deactivate'];
}
$this->settingsUserLifecycleGateway->setUserInactivityDeleteDays(0);
}
return $errors;
}
/** @return list<array{message: string, key: string}> */
private function applySessionAndLoginSettings(array $input): array
{
$errors = [];
if (!$this->settingsSessionGateway->setSessionIdleTimeoutMinutes($input['session_idle_timeout_minutes'] > 0 ? $input['session_idle_timeout_minutes'] : null)) {
$errors[] = ['message' => 'Session idle timeout is invalid', 'key' => 'session_idle_timeout_invalid'];
}
if (!$this->settingsSessionGateway->setSessionAbsoluteTimeoutHours($input['session_absolute_timeout_hours'] > 0 ? $input['session_absolute_timeout_hours'] : null)) {
$errors[] = ['message' => 'Session absolute timeout is invalid', 'key' => 'session_absolute_timeout_invalid'];
}
$this->settingsLoginPersistenceGateway->setMicrosoftAutoRememberDefault($input['microsoft_auto_remember_default']);
if (!$this->settingsLoginPersistenceGateway->setRememberTokenLifetimeDays($input['remember_token_lifetime_days'] > 0 ? $input['remember_token_lifetime_days'] : null)) {
$errors[] = ['message' => 'Remember token lifetime is invalid', 'key' => 'remember_token_lifetime_days_invalid'];
}
return $errors;
}
/** @return list<array{message: string, key: string}> */
private function applyAuditAndTelemetrySettings(array $input): array
{
$errors = [];
if (!$this->settingsSystemAuditGateway->setSystemAuditEnabled($input['system_audit_enabled'])) {
$errors[] = ['message' => 'System audit setting is invalid', 'key' => 'system_audit_enabled_invalid'];
}
if (!$this->settingsSystemAuditGateway->setSystemAuditRetentionDays($input['system_audit_retention_days'] > 0 ? $input['system_audit_retention_days'] : null)) {
$errors[] = ['message' => 'System audit retention is invalid', 'key' => 'system_audit_retention_invalid'];
}
if (!$this->settingsFrontendTelemetryGateway->setFrontendTelemetryEnabled($input['frontend_telemetry_enabled'])) {
$errors[] = ['message' => 'Frontend telemetry enabled setting is invalid', 'key' => 'frontend_telemetry_enabled_invalid'];
}
$rateValue = null;
if ($input['frontend_telemetry_sample_rate'] !== '') {
$rateValue = is_numeric($input['frontend_telemetry_sample_rate'])
? (float) $input['frontend_telemetry_sample_rate']
: NAN;
}
if (!$this->settingsFrontendTelemetryGateway->setFrontendTelemetrySampleRate($rateValue)) {
$errors[] = ['message' => 'Frontend telemetry sample rate is invalid', 'key' => 'frontend_telemetry_sample_rate_invalid'];
}
if (!$this->settingsFrontendTelemetryGateway->setFrontendTelemetryAllowedEvents($input['frontend_telemetry_allowed_events'])) {
$errors[] = ['message' => 'Frontend telemetry allowed events are invalid', 'key' => 'frontend_telemetry_allowed_events_invalid'];
}
return $errors;
}
/** @return list<array{message: string, key: string}> */
private function applyAppPrimaryColor(array &$input): array
{
if (!$this->settingsAppGateway->setAppPrimaryColor($input['app_primary_color'])) {
$input['app_primary_color'] = '';
return [['message' => 'Primary color is invalid', 'key' => 'app_primary_invalid']];
}
return [];
}
private function applySmtpSettings(array $input): void
{
$this->settingsSmtpGateway->setSmtpHost($input['smtp_host']);
$this->settingsSmtpGateway->setSmtpPort($input['smtp_port'] > 0 ? $input['smtp_port'] : null);
$this->settingsSmtpGateway->setSmtpUser($input['smtp_user']);
if (trim($input['smtp_password']) !== '') {
$this->settingsSmtpGateway->setSmtpPassword($input['smtp_password']);
}
$this->settingsSmtpGateway->setSmtpSecure($input['smtp_secure']);
$this->settingsSmtpGateway->setSmtpFrom($input['smtp_from']);
$this->settingsSmtpGateway->setSmtpFromName($input['smtp_from_name']);
}
/** @return list<array{message: string, key: string}> */
private function applyMicrosoftSettings(array $input): array
{
$errors = [];
if (!$this->settingsMicrosoftGateway->setMicrosoftSharedClientId($input['microsoft_shared_client_id'])) {
$errors[] = ['message' => 'Microsoft client ID is invalid', 'key' => 'microsoft_client_id_invalid'];
}
if (!$this->settingsMicrosoftGateway->setMicrosoftAuthority($input['microsoft_authority'])) {
$errors[] = ['message' => 'Microsoft authority is invalid', 'key' => 'microsoft_authority_invalid'];
}
if (trim($input['microsoft_shared_client_secret']) !== '') {
if (!$this->settingsMicrosoftGateway->setMicrosoftSharedClientSecret($input['microsoft_shared_client_secret'])) {
$errors[] = ['message' => 'Microsoft client secret could not be encrypted', 'key' => 'microsoft_client_secret_invalid'];
}
}
return $errors;
}
private function updateSettingsCache(array $input): void
{
$this->settingCacheService->update([
'app_title' => $input['app_title'] !== '' ? $input['app_title'] : null,
'app_locale' => $input['app_locale'] !== '' ? $input['app_locale'] : null,
'app_theme' => $input['app_theme'] !== '' ? $input['app_theme'] : null,
'app_theme_user' => $input['app_theme_user'] ? '1' : '0',
'app_registration' => $input['app_registration'] ? '1' : '0',
'app_primary_color' => $input['app_primary_color'] !== '' ? $input['app_primary_color'] : null,
'api_token_default_ttl_days' => (string) $this->settingsApiPolicyGateway->getApiTokenDefaultTtlDays(),
'api_token_max_ttl_days' => (string) $this->settingsApiPolicyGateway->getApiTokenMaxTtlDays(),
'api_cors_allowed_origins' => $this->settingsMetadataGateway->getValue(SettingKeys::API_CORS_ALLOWED_ORIGINS_KEY),
'frontend_telemetry_enabled' => $this->settingsFrontendTelemetryGateway->isFrontendTelemetryEnabled() ? '1' : '0',
'frontend_telemetry_sample_rate' => (string) $this->settingsFrontendTelemetryGateway->getFrontendTelemetrySampleRate(),
'frontend_telemetry_allowed_events' => implode(',', $this->settingsFrontendTelemetryGateway->getFrontendTelemetryAllowedEvents()),
]);
}
/** @param list<array{message: string, key: string}> $errors */
private function recordAudit(array $errors, array $beforeAudit, array $afterAudit): void
{
$outcome = $errors === [] ? 'success' : 'failed';
$errorKeys = [];
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],
]);
}
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;
}
}