diff --git a/i18n/default_de.json b/i18n/default_de.json
index 6415623..245062e 100644
--- a/i18n/default_de.json
+++ b/i18n/default_de.json
@@ -33,6 +33,8 @@
"Access emails sent to %d users, %f failed": "Zugangsdaten an %d Benutzer gesendet, %f fehlgeschlagen",
"Access PDF": "Zugangs-PDF",
"Account": "Konto",
+ "Account access": "Kontozugang",
+ "Account access settings": "Kontozugang-Einstellungen",
"Account is inactive": "Konto ist deaktiviert",
"Action": "Aktion",
"Actions": "Aktionen",
@@ -133,6 +135,8 @@
"Attempts": "Versuche",
"Attribute mapping and sync": "Attributzuordnung und Synchronisierung",
"Audit": "Audit",
+ "Audit log": "Audit-Log",
+ "Audit log settings": "Audit-Log-Einstellungen",
"Authority endpoint": "Authority-Endpunkt",
"Authority URL": "Authority-URL",
"Auto-delete is ignored while auto-deactivate is disabled": "Auto-Löschung wird ignoriert, solange Auto-Deaktivierung deaktiviert ist",
@@ -450,6 +454,7 @@
"Fri": "Fr",
"Frontend": "Frontend",
"Frontend telemetry": "Frontend-Telemetrie",
+ "Frontend telemetry collection and event allowlist": "Frontend-Telemetrie-Erfassung und Event-Allowlist",
"General": "Allgemein",
"General settings": "Allgemeine Einstellungen",
"Generate access PDFs for selected users?": "Zugangs-PDFs für ausgewählte Benutzer erstellen?",
@@ -502,6 +507,7 @@
"Inactive tenants": "Inaktive Mandanten",
"Inactive tenants are excluded from user access and tenant-scoped data.": "Inaktive Mandanten werden vom Benutzerzugriff und mandantenspezifischen Daten ausgeschlossen.",
"Inactive users": "Inaktive Benutzer",
+ "Inactivity deactivation and deletion policy": "Richtlinie für Inaktivitäts-Deaktivierung und Löschung",
"Incomplete": "Unvollständig",
"Inherit global default": "Globalen Standard verwenden",
"Internal Server Error": "Interner Serverfehler",
@@ -865,6 +871,7 @@
"Registration controls whether new users can create an account.": "Hier wird gesteuert, ob neue Benutzer ein Konto erstellen dürfen.",
"Registration is currently disabled": "Registrierung ist derzeit deaktiviert.",
"Registration successful! Please check your email for the verification code.": "Registrierung erfolgreich! Bitte prüfe deine E-Mails für den Bestätigungscode.",
+ "Registration, sessions, login persistence": "Registrierung, Sitzungen, Login-Persistenz",
"Remember login after Microsoft sign-in": "Angemeldet bleiben nach Microsoft-Login",
"Remember me": "Angemeldet bleiben",
"Remember token lifetime (days)": "Token-Lebensdauer Angemeldet-bleiben (Tage)",
@@ -934,6 +941,7 @@
"Rows total": "Zeilen gesamt",
"Rules": "Regeln",
"Run history": "Laufhistorie",
+ "Run lifecycle now": "Lebenszyklus jetzt ausführen",
"Run now": "Jetzt ausführen",
"Run preview": "Vorschau ausführen",
"Run user lifecycle now": "Benutzer-Lifecycle jetzt ausführen",
@@ -1138,6 +1146,7 @@
"System audit controls whether security events are logged and how long they are kept.": "Hier wird gesteuert, ob Sicherheitsereignisse protokolliert werden und wie lange sie aufbewahrt werden.",
"System audit entry not found": "System-Protokoll-Eintrag nicht gefunden",
"System audit events (24h)": "System-Protokoll-Ereignisse (24h)",
+ "System audit log enable and retention": "System-Audit-Log aktivieren und Aufbewahrung",
"System audit logs": "System-Protokolle",
"System audit risks (24h)": "System-Protokoll-Risiken (24h)",
"System Info": "Systeminfo",
@@ -1153,7 +1162,9 @@
"Target type": "Zieltyp",
"Target UUID": "Ziel-UUID",
"Tax number": "Steuernummer",
+ "Telemetry": "Telemetrie",
"Telemetry helps detect recurring UI issues and failed requests in production.": "Telemetry hilft, wiederkehrende UI-Probleme und fehlgeschlagene Requests in Produktion zu erkennen.",
+ "Telemetry settings": "Telemetrie-Einstellungen",
"Template": "Vorlage",
"Tenant": "Mandant",
"Tenant #%d": "Mandant #%d",
@@ -1196,6 +1207,7 @@
"These limits define default and maximum lifetimes for newly issued API tokens.": "Diese Grenzwerte definieren Standard- und Maximallaufzeit für neu ausgestellte API-Tokens.",
"This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.",
"This action revokes all active API tokens immediately across all users.": "Diese Aktion widerruft sofort alle aktiven API-Tokens über alle Benutzer hinweg.",
+ "This runs the lifecycle policy immediately and may deactivate or delete users.": "Dies führt die Lebenszyklus-Richtlinie sofort aus und kann Benutzer deaktivieren oder löschen.",
"This scheduled job is no longer registered and cannot be edited. It has been superseded by a newer version.": "Diese geplante Aufgabe ist nicht mehr registriert und kann nicht bearbeitet werden. Sie wurde durch eine neuere Version ersetzt.",
"This setting controls the application name shown across the UI.": "Diese Einstellung steuert den in der gesamten UI angezeigten Anwendungsnamen.",
"This setting controls the default language for users without a personal preference.": "Diese Einstellung steuert die Standardsprache für Benutzer ohne persönliche Präferenz.",
@@ -1277,12 +1289,14 @@
"User ID": "Benutzer-ID",
"User inactivity deactivation period is invalid": "Zeitraum für automatische Deaktivierung ist ungültig",
"User inactivity deletion period is invalid": "Zeitraum für automatische Löschung ist ungültig",
+ "User lifecycle": "Benutzer-Lebenszyklus",
"User lifecycle already running": "Benutzer-Lifecycle läuft bereits",
"User lifecycle completed: %d deactivated, %d deleted": "Benutzer-Lifecycle abgeschlossen: %d deaktiviert, %d gelöscht",
"User lifecycle defines when inactive users are deactivated or deleted.": "Hier wird festgelegt, wann inaktive Benutzer deaktiviert oder gelöscht werden.",
"User lifecycle failed": "Benutzer-Lifecycle fehlgeschlagen",
"User lifecycle logs": "Benutzer-Lifecycle-Protokolle",
"User lifecycle policy": "Benutzer-Lifecycle-Regel",
+ "User lifecycle settings": "Benutzer-Lebenszyklus-Einstellungen",
"User not found": "Benutzer nicht gefunden",
"User override disabled": "Benutzer-Override deaktiviert",
"User override enabled": "Benutzer-Override aktiviert",
diff --git a/i18n/default_en.json b/i18n/default_en.json
index 733ae73..2d487f7 100644
--- a/i18n/default_en.json
+++ b/i18n/default_en.json
@@ -33,6 +33,8 @@
"Access emails sent to %d users, %f failed": "Access emails sent to %d users, %f failed",
"Access PDF": "Access PDF",
"Account": "Account",
+ "Account access": "Account access",
+ "Account access settings": "Account access settings",
"Account is inactive": "Account is inactive",
"Action": "Action",
"Actions": "Actions",
@@ -133,6 +135,8 @@
"Attempts": "Attempts",
"Attribute mapping and sync": "Attribute mapping and sync",
"Audit": "Audit",
+ "Audit log": "Audit log",
+ "Audit log settings": "Audit log settings",
"Authority endpoint": "Authority endpoint",
"Authority URL": "Authority URL",
"Auto-delete is ignored while auto-deactivate is disabled": "Auto-delete is ignored while auto-deactivate is disabled",
@@ -450,6 +454,7 @@
"Fri": "Fri",
"Frontend": "Frontend",
"Frontend telemetry": "Frontend telemetry",
+ "Frontend telemetry collection and event allowlist": "Frontend telemetry collection and event allowlist",
"General": "General",
"General settings": "General settings",
"Generate access PDFs for selected users?": "Generate access PDFs for selected users?",
@@ -502,6 +507,7 @@
"Inactive tenants": "Inactive tenants",
"Inactive tenants are excluded from user access and tenant-scoped data.": "Inactive tenants are excluded from user access and tenant-scoped data.",
"Inactive users": "Inactive users",
+ "Inactivity deactivation and deletion policy": "Inactivity deactivation and deletion policy",
"Incomplete": "Incomplete",
"Inherit global default": "Inherit global default",
"Internal Server Error": "Internal Server Error",
@@ -865,6 +871,7 @@
"Registration controls whether new users can create an account.": "Registration controls whether new users can create an account.",
"Registration is currently disabled": "Registration is currently disabled.",
"Registration successful! Please check your email for the verification code.": "Registration successful! Please check your email for the verification code.",
+ "Registration, sessions, login persistence": "Registration, sessions, login persistence",
"Remember login after Microsoft sign-in": "Remember login after Microsoft sign-in",
"Remember me": "Remember me",
"Remember token lifetime (days)": "Remember token lifetime (days)",
@@ -934,6 +941,7 @@
"Rows total": "Rows total",
"Rules": "Rules",
"Run history": "Run history",
+ "Run lifecycle now": "Run lifecycle now",
"Run now": "Run now",
"Run preview": "Run preview",
"Run user lifecycle now": "Run user lifecycle now",
@@ -1138,6 +1146,7 @@
"System audit controls whether security events are logged and how long they are kept.": "System audit controls whether security events are logged and how long they are kept.",
"System audit entry not found": "System audit entry not found",
"System audit events (24h)": "System audit events (24h)",
+ "System audit log enable and retention": "System audit log enable and retention",
"System audit logs": "System audit logs",
"System audit risks (24h)": "System audit risks (24h)",
"System Info": "System Info",
@@ -1153,7 +1162,9 @@
"Target type": "Target type",
"Target UUID": "Target UUID",
"Tax number": "Tax number",
+ "Telemetry": "Telemetry",
"Telemetry helps detect recurring UI issues and failed requests in production.": "Telemetry helps detect recurring UI issues and failed requests in production.",
+ "Telemetry settings": "Telemetry settings",
"Template": "Template",
"Tenant": "Tenant",
"Tenant #%d": "Tenant #%d",
@@ -1196,6 +1207,7 @@
"These limits define default and maximum lifetimes for newly issued API tokens.": "These limits define default and maximum lifetimes for newly issued API tokens.",
"This action cannot be undone.": "This action cannot be undone.",
"This action revokes all active API tokens immediately across all users.": "This action revokes all active API tokens immediately across all users.",
+ "This runs the lifecycle policy immediately and may deactivate or delete users.": "This runs the lifecycle policy immediately and may deactivate or delete users.",
"This scheduled job is no longer registered and cannot be edited. It has been superseded by a newer version.": "This scheduled job is no longer registered and cannot be edited. It has been superseded by a newer version.",
"This setting controls the application name shown across the UI.": "This setting controls the application name shown across the UI.",
"This setting controls the default language for users without a personal preference.": "This setting controls the default language for users without a personal preference.",
@@ -1277,12 +1289,14 @@
"User ID": "User ID",
"User inactivity deactivation period is invalid": "User inactivity deactivation period is invalid",
"User inactivity deletion period is invalid": "User inactivity deletion period is invalid",
+ "User lifecycle": "User lifecycle",
"User lifecycle already running": "User lifecycle already running",
"User lifecycle completed: %d deactivated, %d deleted": "User lifecycle completed: %d deactivated, %d deleted",
"User lifecycle defines when inactive users are deactivated or deleted.": "User lifecycle defines when inactive users are deactivated or deleted.",
"User lifecycle failed": "User lifecycle failed",
"User lifecycle logs": "User lifecycle logs",
"User lifecycle policy": "User lifecycle policy",
+ "User lifecycle settings": "User lifecycle settings",
"User not found": "User not found",
"User override disabled": "User override disabled",
"User override enabled": "User override enabled",
diff --git a/pages/admin/settings/security().php b/pages/admin/settings/account-access().php
similarity index 82%
rename from pages/admin/settings/security().php
rename to pages/admin/settings/account-access().php
index c6f137a..ce1829b 100644
--- a/pages/admin/settings/security().php
+++ b/pages/admin/settings/account-access().php
@@ -29,7 +29,7 @@ $values = is_array($pageData['values'] ?? null) ? $pageData['values'] : [];
$settings = is_array($pageData['settings'] ?? null) ? $pageData['settings'] : [];
$activeLoginTokens = (int) ($pageData['active_login_tokens'] ?? 0);
-if ($request->isMethod('POST') && !actionRequireCsrf('admin/settings/security', 'admin/settings/security', 'csrf_expired')) {
+if ($request->isMethod('POST') && !actionRequireCsrf('admin/settings/account-access', 'admin/settings/account-access', 'csrf_expired')) {
return;
}
@@ -47,13 +47,6 @@ if ($request->isMethod('POST')) {
'session_absolute_timeout_hours',
'remember_token_lifetime_days',
'microsoft_auto_remember_default',
- 'user_inactivity_deactivate_days',
- 'user_inactivity_delete_days',
- 'system_audit_enabled',
- 'system_audit_retention_days',
- 'frontend_telemetry_enabled',
- 'frontend_telemetry_sample_rate',
- 'frontend_telemetry_allowed_events',
];
$mergedPost = settingsSectionMergePost($values, $request->bodyAll(), $sectionKeys);
@@ -75,21 +68,21 @@ if ($request->isMethod('POST')) {
}
if ($errorBag->hasAny()) {
- flashFormErrors($errorBag, 'admin/settings/security', 'settings_update_error');
- Router::redirect('admin/settings/security');
+ flashFormErrors($errorBag, 'admin/settings/account-access', 'settings_update_error');
+ Router::redirect('admin/settings/account-access');
}
$redirectTarget = ((string) $request->body('action', 'save') === 'save_close')
? 'admin/settings'
- : 'admin/settings/security';
+ : 'admin/settings/account-access';
Flash::success('Settings updated', $redirectTarget, 'settings_updated');
Router::redirect($redirectTarget);
}
-Buffer::set('title', t('Security settings'));
+Buffer::set('title', t('Account access settings'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Settings'), 'path' => 'admin/settings'],
- ['label' => t('Security')],
+ ['label' => t('Account access')],
];
diff --git a/pages/admin/settings/account-access(default).phtml b/pages/admin/settings/account-access(default).phtml
new file mode 100644
index 0000000..45c1133
--- /dev/null
+++ b/pages/admin/settings/account-access(default).phtml
@@ -0,0 +1,181 @@
+
+
+
+
+ t('Account access settings'),
+ 'backHref' => 'admin/settings',
+ 'backTitle' => t('Cancel'),
+ 'actions' => $canUpdateSettings ? [
+ [
+ 'form' => 'settings-account-access-form',
+ 'name' => 'action',
+ 'value' => 'save',
+ 'class' => 'secondary outline',
+ 'label' => t('Save'),
+ ],
+ [
+ 'form' => 'settings-account-access-form',
+ 'name' => 'action',
+ 'value' => 'save_close',
+ 'class' => 'primary',
+ 'label' => t('Save & close'),
+ ],
+ ] : [],
+ ];
+ require templatePath('partials/app-details-titlebar.phtml');
+ ?>
+
+
+
+
+
diff --git a/pages/admin/settings/audit().php b/pages/admin/settings/audit().php
new file mode 100644
index 0000000..168f39e
--- /dev/null
+++ b/pages/admin/settings/audit().php
@@ -0,0 +1,84 @@
+all();
+Guard::requireLogin();
+$request = requestInput();
+
+$currentUserId = (int) ($session['user']['id'] ?? 0);
+$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class);
+$viewDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW, [
+ 'actor_user_id' => $currentUserId,
+]);
+if (!$viewDecision->isAllowed()) {
+ Guard::deny();
+}
+$viewCapabilities = $viewDecision->attribute('capabilities', []);
+$canUpdateSettings = is_array($viewCapabilities) ? (bool) ($viewCapabilities['can_update_settings'] ?? false) : false;
+
+$adminSettingsService = app(AdminSettingsService::class);
+$pageData = $adminSettingsService->buildPageData();
+$values = is_array($pageData['values'] ?? null) ? $pageData['values'] : [];
+$settings = is_array($pageData['settings'] ?? null) ? $pageData['settings'] : [];
+
+if ($request->isMethod('POST') && !actionRequireCsrf('admin/settings/audit', 'admin/settings/audit', 'csrf_expired')) {
+ return;
+}
+
+if ($request->isMethod('POST')) {
+ $updateDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE, [
+ 'actor_user_id' => $currentUserId,
+ ]);
+ if (!$updateDecision->isAllowed()) {
+ Guard::deny();
+ }
+
+ $sectionKeys = [
+ 'system_audit_enabled',
+ 'system_audit_retention_days',
+ ];
+ $mergedPost = settingsSectionMergePost($values, $request->bodyAll(), $sectionKeys);
+
+ $updateResult = $adminSettingsService->updateFromAdmin($mergedPost);
+ $errorBag = formErrors();
+ foreach ((array) ($updateResult['errors'] ?? []) as $error) {
+ if (is_array($error)) {
+ $message = trim((string) ($error['message'] ?? ''));
+ $field = trim((string) ($error['field'] ?? 'input'));
+ if ($message !== '') {
+ $errorBag->add($field !== '' ? $field : 'input', $message);
+ }
+ continue;
+ }
+ $message = trim((string) $error);
+ if ($message !== '') {
+ $errorBag->addGlobal($message);
+ }
+ }
+
+ if ($errorBag->hasAny()) {
+ flashFormErrors($errorBag, 'admin/settings/audit', 'settings_update_error');
+ Router::redirect('admin/settings/audit');
+ }
+
+ $redirectTarget = ((string) $request->body('action', 'save') === 'save_close')
+ ? 'admin/settings'
+ : 'admin/settings/audit';
+ Flash::success('Settings updated', $redirectTarget, 'settings_updated');
+ Router::redirect($redirectTarget);
+}
+
+Buffer::set('title', t('Audit log settings'));
+
+$breadcrumbs = [
+ ['label' => t('Home'), 'path' => 'admin'],
+ ['label' => t('Settings'), 'path' => 'admin/settings'],
+ ['label' => t('Audit log')],
+];
diff --git a/pages/admin/settings/audit(default).phtml b/pages/admin/settings/audit(default).phtml
new file mode 100644
index 0000000..5e76b47
--- /dev/null
+++ b/pages/admin/settings/audit(default).phtml
@@ -0,0 +1,87 @@
+
+
+
+
+ t('Audit log settings'),
+ 'backHref' => 'admin/settings',
+ 'backTitle' => t('Cancel'),
+ 'actions' => $canUpdateSettings ? [
+ [
+ 'form' => 'settings-audit-form',
+ 'name' => 'action',
+ 'value' => 'save',
+ 'class' => 'secondary outline',
+ 'label' => t('Save'),
+ ],
+ [
+ 'form' => 'settings-audit-form',
+ 'name' => 'action',
+ 'value' => 'save_close',
+ 'class' => 'primary',
+ 'label' => t('Save & close'),
+ ],
+ ] : [],
+ ];
+ require templatePath('partials/app-details-titlebar.phtml');
+ ?>
+
+
+
+
+
diff --git a/pages/admin/settings/expire-remember-tokens().php b/pages/admin/settings/expire-remember-tokens().php
index 18c29d3..1439807 100644
--- a/pages/admin/settings/expire-remember-tokens().php
+++ b/pages/admin/settings/expire-remember-tokens().php
@@ -18,15 +18,15 @@ if (!$decision->isAllowed()) {
Guard::deny();
}
-if (!actionRequirePost('admin/settings/security')) {
+if (!actionRequirePost('admin/settings/account-access')) {
return;
}
-if (!actionRequireCsrf('admin/settings/security', 'admin/settings/security', 'settings_tokens_expire')) {
+if (!actionRequireCsrf('admin/settings/account-access', 'admin/settings/account-access', 'settings_tokens_expire')) {
return;
}
$rememberMeService = app(\MintyPHP\Service\Auth\RememberMeService::class);
$count = $rememberMeService->expireAllTokensByAdmin();
-Flash::success(sprintf(t('%d login tokens expired'), $count), 'admin/settings/security', 'login_tokens_expired');
-Router::redirect('admin/settings/security');
+Flash::success(sprintf(t('%d login tokens expired'), $count), 'admin/settings/account-access', 'login_tokens_expired');
+Router::redirect('admin/settings/account-access');
diff --git a/pages/admin/settings/index(default).phtml b/pages/admin/settings/index(default).phtml
index 633e8b5..7ba8139 100644
--- a/pages/admin/settings/index(default).phtml
+++ b/pages/admin/settings/index(default).phtml
@@ -24,12 +24,36 @@
'tooltip' => t('App title, language, user creation defaults'),
]);
appTile([
- 'href' => 'admin/settings/security',
- 'label' => t('Security'),
+ 'href' => 'admin/settings/account-access',
+ 'label' => t('Account access'),
'icon' => 'bi bi-shield-lock',
'iconBg' => '#ede9fe',
'iconColor' => '#7c3aed',
- 'tooltip' => t('Sessions, registration, lifecycle, audit, telemetry'),
+ 'tooltip' => t('Registration, sessions, login persistence'),
+ ]);
+ appTile([
+ 'href' => 'admin/settings/user-lifecycle',
+ 'label' => t('User lifecycle'),
+ 'icon' => 'bi bi-arrow-repeat',
+ 'iconBg' => '#fee2e2',
+ 'iconColor' => '#b91c1c',
+ 'tooltip' => t('Inactivity deactivation and deletion policy'),
+ ]);
+ appTile([
+ 'href' => 'admin/settings/audit',
+ 'label' => t('Audit log'),
+ 'icon' => 'bi bi-journal-text',
+ 'iconBg' => '#fff7ed',
+ 'iconColor' => '#c2410c',
+ 'tooltip' => t('System audit log enable and retention'),
+ ]);
+ appTile([
+ 'href' => 'admin/settings/telemetry',
+ 'label' => t('Telemetry'),
+ 'icon' => 'bi bi-graph-up',
+ 'iconBg' => '#ecfdf5',
+ 'iconColor' => '#047857',
+ 'tooltip' => t('Frontend telemetry collection and event allowlist'),
]);
appTile([
'href' => 'admin/settings/email',
diff --git a/pages/admin/settings/run-user-lifecycle().php b/pages/admin/settings/run-user-lifecycle().php
index f12acb9..3f11a65 100644
--- a/pages/admin/settings/run-user-lifecycle().php
+++ b/pages/admin/settings/run-user-lifecycle().php
@@ -18,11 +18,11 @@ if (!$decision->isAllowed()) {
Guard::deny();
}
-if (!actionRequirePost('admin/settings/security')) {
+if (!actionRequirePost('admin/settings/user-lifecycle')) {
return;
}
-if (!actionRequireCsrf('admin/settings/security', 'admin/settings/security', 'user_lifecycle_run')) {
+if (!actionRequireCsrf('admin/settings/user-lifecycle', 'admin/settings/user-lifecycle', 'user_lifecycle_run')) {
return;
}
@@ -35,8 +35,8 @@ if (!($result['ok'] ?? false)) {
} else {
$errorBag->addGlobal('User lifecycle failed');
}
- flashFormErrors($errorBag, 'admin/settings/security', 'user_lifecycle_run');
- Router::redirect('admin/settings/security');
+ flashFormErrors($errorBag, 'admin/settings/user-lifecycle', 'user_lifecycle_run');
+ Router::redirect('admin/settings/user-lifecycle');
}
Flash::success(
@@ -45,7 +45,7 @@ Flash::success(
(int) ($result['deactivated_count'] ?? 0),
(int) ($result['deleted_count'] ?? 0)
),
- 'admin/settings/security',
+ 'admin/settings/user-lifecycle',
'user_lifecycle_completed'
);
-Router::redirect('admin/settings/security');
+Router::redirect('admin/settings/user-lifecycle');
diff --git a/pages/admin/settings/security(default).phtml b/pages/admin/settings/security(default).phtml
deleted file mode 100644
index fa196d2..0000000
--- a/pages/admin/settings/security(default).phtml
+++ /dev/null
@@ -1,344 +0,0 @@
- strtolower(trim((string) $entry)),
- $frontendTelemetryAllowedEvents
-))));
-if ($frontendTelemetryAllowedEvents === []) {
- $frontendTelemetryAllowedEvents = ['warn_once', 'ajax_error'];
-}
-$appRegistrationDesc = $settings['app_registration']['description'] ?? 'setting.app_registration';
-$sessionIdleTimeoutMinutesDesc = $settings['session_idle_timeout_minutes']['description'] ?? 'setting.session_idle_timeout_minutes';
-$sessionAbsoluteTimeoutHoursDesc = $settings['session_absolute_timeout_hours']['description'] ?? 'setting.session_absolute_timeout_hours';
-$rememberTokenLifetimeDaysDesc = $settings['remember_token_lifetime_days']['description'] ?? 'setting.remember_token_lifetime_days';
-$microsoftAutoRememberDefaultDesc = $settings['microsoft_auto_remember_default']['description'] ?? 'setting.microsoft_auto_remember_default';
-$userInactivityDeactivateDaysDesc = $settings['user_inactivity_deactivate_days']['description'] ?? 'setting.user_inactivity_deactivate_days';
-$userInactivityDeleteDaysDesc = $settings['user_inactivity_delete_days']['description'] ?? 'setting.user_inactivity_delete_days';
-$systemAuditRetentionDaysDesc = $settings['system_audit_retention_days']['description'] ?? 'setting.system_audit_retention_days';
-$activeLoginTokens = (int) ($activeLoginTokens ?? 0);
-$canUpdateSettings = (bool) ($canUpdateSettings ?? false);
-$isReadOnly = !$canUpdateSettings;
-$readonlyAttr = $isReadOnly ? 'readonly' : '';
-$disabledAttr = $isReadOnly ? 'disabled' : '';
-?>
-
-
-
- t('Security settings'),
- 'backHref' => 'admin/settings',
- 'backTitle' => t('Cancel'),
- 'actions' => $canUpdateSettings ? [
- [
- 'form' => 'settings-security-form',
- 'name' => 'action',
- 'value' => 'save',
- 'class' => 'secondary outline',
- 'label' => t('Save'),
- ],
- [
- 'form' => 'settings-security-form',
- 'name' => 'action',
- 'value' => 'save_close',
- 'class' => 'primary',
- 'label' => t('Save & close'),
- ],
- ] : [],
- ];
- require templatePath('partials/app-details-titlebar.phtml');
- ?>
-
-
-
-
-
diff --git a/pages/admin/settings/telemetry().php b/pages/admin/settings/telemetry().php
new file mode 100644
index 0000000..70ca049
--- /dev/null
+++ b/pages/admin/settings/telemetry().php
@@ -0,0 +1,85 @@
+all();
+Guard::requireLogin();
+$request = requestInput();
+
+$currentUserId = (int) ($session['user']['id'] ?? 0);
+$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class);
+$viewDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW, [
+ 'actor_user_id' => $currentUserId,
+]);
+if (!$viewDecision->isAllowed()) {
+ Guard::deny();
+}
+$viewCapabilities = $viewDecision->attribute('capabilities', []);
+$canUpdateSettings = is_array($viewCapabilities) ? (bool) ($viewCapabilities['can_update_settings'] ?? false) : false;
+
+$adminSettingsService = app(AdminSettingsService::class);
+$pageData = $adminSettingsService->buildPageData();
+$values = is_array($pageData['values'] ?? null) ? $pageData['values'] : [];
+$settings = is_array($pageData['settings'] ?? null) ? $pageData['settings'] : [];
+
+if ($request->isMethod('POST') && !actionRequireCsrf('admin/settings/telemetry', 'admin/settings/telemetry', 'csrf_expired')) {
+ return;
+}
+
+if ($request->isMethod('POST')) {
+ $updateDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE, [
+ 'actor_user_id' => $currentUserId,
+ ]);
+ if (!$updateDecision->isAllowed()) {
+ Guard::deny();
+ }
+
+ $sectionKeys = [
+ 'frontend_telemetry_enabled',
+ 'frontend_telemetry_sample_rate',
+ 'frontend_telemetry_allowed_events',
+ ];
+ $mergedPost = settingsSectionMergePost($values, $request->bodyAll(), $sectionKeys);
+
+ $updateResult = $adminSettingsService->updateFromAdmin($mergedPost);
+ $errorBag = formErrors();
+ foreach ((array) ($updateResult['errors'] ?? []) as $error) {
+ if (is_array($error)) {
+ $message = trim((string) ($error['message'] ?? ''));
+ $field = trim((string) ($error['field'] ?? 'input'));
+ if ($message !== '') {
+ $errorBag->add($field !== '' ? $field : 'input', $message);
+ }
+ continue;
+ }
+ $message = trim((string) $error);
+ if ($message !== '') {
+ $errorBag->addGlobal($message);
+ }
+ }
+
+ if ($errorBag->hasAny()) {
+ flashFormErrors($errorBag, 'admin/settings/telemetry', 'settings_update_error');
+ Router::redirect('admin/settings/telemetry');
+ }
+
+ $redirectTarget = ((string) $request->body('action', 'save') === 'save_close')
+ ? 'admin/settings'
+ : 'admin/settings/telemetry';
+ Flash::success('Settings updated', $redirectTarget, 'settings_updated');
+ Router::redirect($redirectTarget);
+}
+
+Buffer::set('title', t('Telemetry settings'));
+
+$breadcrumbs = [
+ ['label' => t('Home'), 'path' => 'admin'],
+ ['label' => t('Settings'), 'path' => 'admin/settings'],
+ ['label' => t('Telemetry')],
+];
diff --git a/pages/admin/settings/telemetry(default).phtml b/pages/admin/settings/telemetry(default).phtml
new file mode 100644
index 0000000..d2062fe
--- /dev/null
+++ b/pages/admin/settings/telemetry(default).phtml
@@ -0,0 +1,136 @@
+ strtolower(trim((string) $entry)),
+ $frontendTelemetryAllowedEvents
+))));
+if ($frontendTelemetryAllowedEvents === []) {
+ $frontendTelemetryAllowedEvents = ['warn_once', 'ajax_error'];
+}
+$canUpdateSettings = (bool) ($canUpdateSettings ?? false);
+$isReadOnly = !$canUpdateSettings;
+$disabledAttr = $isReadOnly ? 'disabled' : '';
+?>
+
+
+
+ t('Telemetry settings'),
+ 'backHref' => 'admin/settings',
+ 'backTitle' => t('Cancel'),
+ 'actions' => $canUpdateSettings ? [
+ [
+ 'form' => 'settings-telemetry-form',
+ 'name' => 'action',
+ 'value' => 'save',
+ 'class' => 'secondary outline',
+ 'label' => t('Save'),
+ ],
+ [
+ 'form' => 'settings-telemetry-form',
+ 'name' => 'action',
+ 'value' => 'save_close',
+ 'class' => 'primary',
+ 'label' => t('Save & close'),
+ ],
+ ] : [],
+ ];
+ require templatePath('partials/app-details-titlebar.phtml');
+ ?>
+
+
+
+
+
diff --git a/pages/admin/settings/user-lifecycle().php b/pages/admin/settings/user-lifecycle().php
new file mode 100644
index 0000000..c3f98ec
--- /dev/null
+++ b/pages/admin/settings/user-lifecycle().php
@@ -0,0 +1,84 @@
+all();
+Guard::requireLogin();
+$request = requestInput();
+
+$currentUserId = (int) ($session['user']['id'] ?? 0);
+$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class);
+$viewDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW, [
+ 'actor_user_id' => $currentUserId,
+]);
+if (!$viewDecision->isAllowed()) {
+ Guard::deny();
+}
+$viewCapabilities = $viewDecision->attribute('capabilities', []);
+$canUpdateSettings = is_array($viewCapabilities) ? (bool) ($viewCapabilities['can_update_settings'] ?? false) : false;
+
+$adminSettingsService = app(AdminSettingsService::class);
+$pageData = $adminSettingsService->buildPageData();
+$values = is_array($pageData['values'] ?? null) ? $pageData['values'] : [];
+$settings = is_array($pageData['settings'] ?? null) ? $pageData['settings'] : [];
+
+if ($request->isMethod('POST') && !actionRequireCsrf('admin/settings/user-lifecycle', 'admin/settings/user-lifecycle', 'csrf_expired')) {
+ return;
+}
+
+if ($request->isMethod('POST')) {
+ $updateDecision = $authorizationService->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE, [
+ 'actor_user_id' => $currentUserId,
+ ]);
+ if (!$updateDecision->isAllowed()) {
+ Guard::deny();
+ }
+
+ $sectionKeys = [
+ 'user_inactivity_deactivate_days',
+ 'user_inactivity_delete_days',
+ ];
+ $mergedPost = settingsSectionMergePost($values, $request->bodyAll(), $sectionKeys);
+
+ $updateResult = $adminSettingsService->updateFromAdmin($mergedPost);
+ $errorBag = formErrors();
+ foreach ((array) ($updateResult['errors'] ?? []) as $error) {
+ if (is_array($error)) {
+ $message = trim((string) ($error['message'] ?? ''));
+ $field = trim((string) ($error['field'] ?? 'input'));
+ if ($message !== '') {
+ $errorBag->add($field !== '' ? $field : 'input', $message);
+ }
+ continue;
+ }
+ $message = trim((string) $error);
+ if ($message !== '') {
+ $errorBag->addGlobal($message);
+ }
+ }
+
+ if ($errorBag->hasAny()) {
+ flashFormErrors($errorBag, 'admin/settings/user-lifecycle', 'settings_update_error');
+ Router::redirect('admin/settings/user-lifecycle');
+ }
+
+ $redirectTarget = ((string) $request->body('action', 'save') === 'save_close')
+ ? 'admin/settings'
+ : 'admin/settings/user-lifecycle';
+ Flash::success('Settings updated', $redirectTarget, 'settings_updated');
+ Router::redirect($redirectTarget);
+}
+
+Buffer::set('title', t('User lifecycle settings'));
+
+$breadcrumbs = [
+ ['label' => t('Home'), 'path' => 'admin'],
+ ['label' => t('Settings'), 'path' => 'admin/settings'],
+ ['label' => t('User lifecycle')],
+];
diff --git a/pages/admin/settings/user-lifecycle(default).phtml b/pages/admin/settings/user-lifecycle(default).phtml
new file mode 100644
index 0000000..79ae05f
--- /dev/null
+++ b/pages/admin/settings/user-lifecycle(default).phtml
@@ -0,0 +1,107 @@
+
+
+
+
+ t('User lifecycle settings'),
+ 'backHref' => 'admin/settings',
+ 'backTitle' => t('Cancel'),
+ 'actions' => $canUpdateSettings ? [
+ [
+ 'form' => 'settings-user-lifecycle-form',
+ 'name' => 'action',
+ 'value' => 'save',
+ 'class' => 'secondary outline',
+ 'label' => t('Save'),
+ ],
+ [
+ 'form' => 'settings-user-lifecycle-form',
+ 'name' => 'action',
+ 'value' => 'save_close',
+ 'class' => 'primary',
+ 'label' => t('Save & close'),
+ ],
+ ] : [],
+ ];
+ require templatePath('partials/app-details-titlebar.phtml');
+ ?>
+
+
+
+
+
diff --git a/tests/Architecture/AuthzAdminSettingsContractTest.php b/tests/Architecture/AuthzAdminSettingsContractTest.php
index 4737345..ffaac51 100644
--- a/tests/Architecture/AuthzAdminSettingsContractTest.php
+++ b/tests/Architecture/AuthzAdminSettingsContractTest.php
@@ -16,7 +16,7 @@ class AuthzAdminSettingsContractTest extends TestCase
$this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW', $indexContent);
// Subpages that expose a settings form: VIEW + UPDATE.
- foreach (['general', 'security', 'email', 'api', 'sso'] as $section) {
+ foreach (['general', 'account-access', 'user-lifecycle', 'audit', 'telemetry', 'email', 'api', 'sso'] as $section) {
$content = $this->readProjectFile("pages/admin/settings/{$section}().php");
$this->assertStringContainsString('AuthorizationService::class', $content, $section);
$this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW', $content, $section);
diff --git a/tests/Architecture/AuthzUiTemplateContractTest.php b/tests/Architecture/AuthzUiTemplateContractTest.php
index ce6efa1..a9c658e 100644
--- a/tests/Architecture/AuthzUiTemplateContractTest.php
+++ b/tests/Architecture/AuthzUiTemplateContractTest.php
@@ -59,7 +59,7 @@ class AuthzUiTemplateContractTest extends TestCase
// Settings was split into a landing hub + per-section subpages.
// The form-carrying subpages must use the server-side $canUpdateSettings
// capability instead of the legacy can() helper.
- foreach (['general', 'security', 'email', 'api', 'sso', 'branding'] as $section) {
+ foreach (['general', 'account-access', 'user-lifecycle', 'audit', 'telemetry', 'email', 'api', 'sso', 'branding'] as $section) {
$templateContent = $this->readProjectFile("pages/admin/settings/{$section}(default).phtml");
$this->assertStringNotContainsString("can('settings.update')", $templateContent, $section);
$this->assertStringContainsString('$canUpdateSettings', $templateContent, $section);
diff --git a/tests/Architecture/DetailActionPolicyContractFiles.php b/tests/Architecture/DetailActionPolicyContractFiles.php
index 3e935d0..a2bfbe4 100644
--- a/tests/Architecture/DetailActionPolicyContractFiles.php
+++ b/tests/Architecture/DetailActionPolicyContractFiles.php
@@ -18,9 +18,11 @@ final class DetailActionPolicyContractFiles
'pages/admin/roles/edit(default).phtml',
'pages/admin/permissions/edit(default).phtml',
// Settings subpages that carry danger actions (and therefore need
- // the data-detail-confirm-message contract) — general/email/sso/branding
- // have no destructive buttons and are deliberately excluded.
- 'pages/admin/settings/security(default).phtml',
+ // the data-detail-confirm-message contract). Subpages without
+ // destructive buttons (general/audit/telemetry/email/sso/branding)
+ // are deliberately excluded.
+ 'pages/admin/settings/account-access(default).phtml',
+ 'pages/admin/settings/user-lifecycle(default).phtml',
'pages/admin/settings/api(default).phtml',
'templates/partials/app-details-titlebar.phtml',
'templates/partials/app-details-aside-actions.phtml',
diff --git a/tests/Architecture/DetailPageContractFiles.php b/tests/Architecture/DetailPageContractFiles.php
index 148e019..242be1f 100644
--- a/tests/Architecture/DetailPageContractFiles.php
+++ b/tests/Architecture/DetailPageContractFiles.php
@@ -17,7 +17,10 @@ final class DetailPageContractFiles
'pages/admin/permissions/_form.phtml',
'pages/admin/scheduled-jobs/edit(default).phtml',
'pages/admin/settings/general(default).phtml',
- 'pages/admin/settings/security(default).phtml',
+ 'pages/admin/settings/account-access(default).phtml',
+ 'pages/admin/settings/user-lifecycle(default).phtml',
+ 'pages/admin/settings/audit(default).phtml',
+ 'pages/admin/settings/telemetry(default).phtml',
'pages/admin/settings/email(default).phtml',
'pages/admin/settings/api(default).phtml',
'pages/admin/settings/sso(default).phtml',