Files
breadcrumb-the-shire/lib/Service/Settings/AdminSettingsService.php
2026-03-04 15:56:58 +01:00

315 lines
18 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\Org\DepartmentService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Tenant\TenantService;
class AdminSettingsService
{
public function __construct(
private readonly SettingGateway $settingGateway,
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 SystemAuditService $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->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;
}
}