2026-02-04 23:31:53 +01:00
|
|
|
<?php
|
|
|
|
|
|
2026-02-11 19:28:12 +01:00
|
|
|
namespace MintyPHP\Service\Access;
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
use MintyPHP\Http\SessionStoreInterface;
|
2026-03-05 08:26:51 +01:00
|
|
|
use MintyPHP\Repository\Access\PermissionRepositoryInterface;
|
|
|
|
|
use MintyPHP\Repository\Access\RolePermissionRepositoryInterface;
|
|
|
|
|
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-03-13 21:58:51 +01:00
|
|
|
/**
|
|
|
|
|
* Central RBAC permission service — resolves and caches permission keys for users.
|
|
|
|
|
*
|
|
|
|
|
* Permission resolution: User → Roles (via user_roles) → Permissions (via role_permissions).
|
|
|
|
|
* The resolved key list is cached in two tiers:
|
|
|
|
|
* - API requests (stateless): in-memory array, lives for one request cycle.
|
|
|
|
|
* - Web requests: session store, survives across page loads until refresh.
|
|
|
|
|
* A forced refresh can be triggered by passing $refresh = true to getUserPermissions().
|
|
|
|
|
*
|
|
|
|
|
* All permission key constants are defined here as the single source of truth,
|
|
|
|
|
* referenced by actions, services, policies, and templates.
|
|
|
|
|
*/
|
2026-02-04 23:31:53 +01:00
|
|
|
class PermissionService
|
|
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
private array $apiPermissionCache = [];
|
|
|
|
|
|
|
|
|
|
public function __construct(
|
2026-03-05 08:26:51 +01:00
|
|
|
private readonly PermissionRepositoryInterface $permissionRepository,
|
|
|
|
|
private readonly RolePermissionRepositoryInterface $rolePermissionRepository,
|
|
|
|
|
private readonly UserRoleRepositoryInterface $userRoleRepository,
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
private readonly AuditRecorderInterface $systemAuditService,
|
2026-03-06 00:44:52 +01:00
|
|
|
private readonly SessionStoreInterface $sessionStore
|
2026-02-23 12:58:19 +01:00
|
|
|
) {
|
|
|
|
|
}
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
2026-02-04 23:31:53 +01:00
|
|
|
public const USERS_CREATE = 'users.create';
|
|
|
|
|
public const USERS_DELETE = 'users.delete';
|
|
|
|
|
public const USERS_VIEW = 'users.view';
|
2026-02-11 19:28:12 +01:00
|
|
|
public const USERS_VIEW_META = 'users.view_meta';
|
|
|
|
|
public const USERS_VIEW_AUDIT = 'users.view_audit';
|
2026-02-04 23:31:53 +01:00
|
|
|
public const USERS_UPDATE = 'users.update';
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
public const USERS_ACCESS_PDF = 'users.access_pdf';
|
2026-02-04 23:31:53 +01:00
|
|
|
public const USERS_SELF_UPDATE = 'users.self_update';
|
|
|
|
|
public const USERS_UPDATE_ASSIGNMENTS = 'users.update_assignments';
|
|
|
|
|
public const TENANTS_VIEW = 'tenants.view';
|
|
|
|
|
public const TENANTS_CREATE = 'tenants.create';
|
|
|
|
|
public const TENANTS_UPDATE = 'tenants.update';
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
public const TENANTS_SSO_MANAGE = 'tenants.sso_manage';
|
2026-02-04 23:31:53 +01:00
|
|
|
public const TENANTS_DELETE = 'tenants.delete';
|
|
|
|
|
public const DEPARTMENTS_VIEW = 'departments.view';
|
|
|
|
|
public const DEPARTMENTS_CREATE = 'departments.create';
|
|
|
|
|
public const DEPARTMENTS_UPDATE = 'departments.update';
|
|
|
|
|
public const DEPARTMENTS_DELETE = 'departments.delete';
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
public const DEPARTMENTS_IMPORT = 'departments.import';
|
2026-02-04 23:31:53 +01:00
|
|
|
public const ROLES_VIEW = 'roles.view';
|
|
|
|
|
public const ROLES_CREATE = 'roles.create';
|
|
|
|
|
public const ROLES_UPDATE = 'roles.update';
|
|
|
|
|
public const ROLES_DELETE = 'roles.delete';
|
2026-03-13 21:11:48 +01:00
|
|
|
public const ROLES_ASSIGN_ALL = 'roles.assign_all';
|
2026-02-04 23:31:53 +01:00
|
|
|
public const PERMISSIONS_VIEW = 'permissions.view';
|
|
|
|
|
public const PERMISSIONS_CREATE = 'permissions.create';
|
|
|
|
|
public const PERMISSIONS_UPDATE = 'permissions.update';
|
|
|
|
|
public const PERMISSIONS_DELETE = 'permissions.delete';
|
|
|
|
|
public const SETTINGS_VIEW = 'settings.view';
|
|
|
|
|
public const SETTINGS_UPDATE = 'settings.update';
|
2026-02-24 08:49:40 +01:00
|
|
|
public const TENANT_SCOPE_GLOBAL = 'tenant.scope.global';
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
public const IMPORTS_VIEW = 'imports.view';
|
|
|
|
|
public const JOBS_VIEW = 'jobs.view';
|
|
|
|
|
public const JOBS_MANAGE = 'jobs.manage';
|
|
|
|
|
public const JOBS_RUN_NOW = 'jobs.run_now';
|
|
|
|
|
public const USERS_IMPORT = 'users.import';
|
|
|
|
|
public const USERS_IMPORT_ASSIGNMENTS = 'users.import_assignments';
|
|
|
|
|
public const CUSTOM_FIELDS_MANAGE = 'custom_fields.manage';
|
|
|
|
|
public const CUSTOM_FIELDS_EDIT_VALUES = 'custom_fields.edit_values';
|
|
|
|
|
public const API_DOCS_VIEW = 'api_docs.view';
|
|
|
|
|
public const DOCS_VIEW = 'docs.view';
|
2026-02-04 23:31:53 +01:00
|
|
|
public const MAIL_LOG_VIEW = 'mail_log.view';
|
|
|
|
|
public const STATS_VIEW = 'stats.view';
|
feat: add read-only System Info admin page with health checks and module inventory
New page at /admin/system-info with three tabs:
- Overview: PHP version, SAPI, environment, 6 health checks (DB, schema,
storage, RBAC baseline, admin role, scheduler heartbeat)
- Modules: table of active modules with version, dependencies, permission count
- Permissions: active/inactive counts and per-source breakdown
Gated behind new system_info.view permission assigned to Admin role.
No mutations — purely diagnostic/observability.
Includes SystemHealthService, SystemInfoService, SystemHealthRepository
with interface, DI registration, i18n keys (de+en), idempotent DB update
script, and 17 new PHPUnit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 15:08:02 +01:00
|
|
|
public const SYSTEM_INFO_VIEW = 'system_info.view';
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
public const API_TOKENS_MANAGE = 'api_tokens.manage';
|
2026-02-04 23:31:53 +01:00
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function userHas(int $userId, string $permissionKey): bool
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
|
|
|
|
$permissionKey = trim((string) $permissionKey);
|
|
|
|
|
if ($userId <= 0 || $permissionKey === '') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-23 12:58:19 +01:00
|
|
|
$keys = $this->getUserPermissions($userId);
|
2026-02-04 23:31:53 +01:00
|
|
|
return in_array($permissionKey, $keys, true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function getUserPermissions(int $userId, bool $refresh = false): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
|
|
|
|
if ($userId <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// Two-tier cache: API requests (stateless) use an in-memory array per-request;
|
|
|
|
|
// web requests use the session store so permissions survive across the page lifecycle.
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($this->isApiRequest()) {
|
|
|
|
|
if (!$refresh && isset($this->apiPermissionCache[$userId])) {
|
|
|
|
|
return $this->apiPermissionCache[$userId];
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$roleIds = $this->userRoleRepository->listRoleIdsByUserId($userId);
|
|
|
|
|
$keys = $this->rolePermissionRepository->listPermissionKeysByRoleIds($roleIds);
|
|
|
|
|
$this->apiPermissionCache[$userId] = $keys;
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
return $keys;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
$cache = $this->sessionStore->get('permissions');
|
2026-02-04 23:31:53 +01:00
|
|
|
if ($refresh) {
|
|
|
|
|
$cache = null;
|
|
|
|
|
}
|
|
|
|
|
if (is_array($cache) && (int) ($cache['user_id'] ?? 0) === $userId) {
|
|
|
|
|
$keys = $cache['keys'] ?? [];
|
|
|
|
|
if (is_array($keys)) {
|
|
|
|
|
return $keys;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 12:58:19 +01:00
|
|
|
$roleIds = $this->userRoleRepository->listRoleIdsByUserId($userId);
|
|
|
|
|
$keys = $this->rolePermissionRepository->listPermissionKeysByRoleIds($roleIds);
|
2026-03-06 00:44:52 +01:00
|
|
|
$this->sessionStore->set('permissions', [
|
2026-02-04 23:31:53 +01:00
|
|
|
'user_id' => $userId,
|
|
|
|
|
'keys' => $keys,
|
2026-03-06 00:44:52 +01:00
|
|
|
]);
|
2026-02-04 23:31:53 +01:00
|
|
|
return $keys;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function getCachedPermissions(int $userId): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
|
|
|
|
if ($userId <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($this->isApiRequest()) {
|
|
|
|
|
return $this->apiPermissionCache[$userId] ?? [];
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
$cache = $this->sessionStore->get('permissions');
|
2026-02-04 23:31:53 +01:00
|
|
|
if (is_array($cache) && (int) ($cache['user_id'] ?? 0) === $userId) {
|
|
|
|
|
$keys = $cache['keys'] ?? [];
|
|
|
|
|
return is_array($keys) ? $keys : [];
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function clearUserCache(int $userId): void
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($this->isApiRequest()) {
|
|
|
|
|
unset($this->apiPermissionCache[$userId]);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
$cache = $this->sessionStore->get('permissions');
|
2026-02-04 23:31:53 +01:00
|
|
|
if (is_array($cache) && (int) ($cache['user_id'] ?? 0) === $userId) {
|
2026-03-06 00:44:52 +01:00
|
|
|
$this->sessionStore->remove('permissions');
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 00:44:52 +01:00
|
|
|
// MINTY_API_REQUEST is defined in the API entry point to distinguish context at runtime.
|
2026-02-23 12:58:19 +01:00
|
|
|
private function isApiRequest(): bool
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
return defined('MINTY_API_REQUEST');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function list(): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
return $this->permissionRepository->list();
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function listActive(): array
|
2026-02-11 19:28:12 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
return $this->permissionRepository->listActive();
|
2026-02-11 19:28:12 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function listPaged(array $options): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
return $this->permissionRepository->listPaged($options);
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function find(int $id): ?array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
return $this->permissionRepository->find($id);
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function findByKey(string $key): ?array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
return $this->permissionRepository->findByKey($key);
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function createFromAdmin(array $input): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
$form = $this->sanitizeBase($input);
|
|
|
|
|
$errors = $this->validateBase($form);
|
2026-02-04 23:31:53 +01:00
|
|
|
if ($errors) {
|
|
|
|
|
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
|
|
|
|
}
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($this->permissionRepository->findByKey($form['key'])) {
|
2026-02-04 23:31:53 +01:00
|
|
|
return ['ok' => false, 'errors' => [t('Permission key already exists')], 'form' => $form];
|
|
|
|
|
}
|
2026-02-23 12:58:19 +01:00
|
|
|
$createdId = $this->permissionRepository->create($form);
|
2026-02-04 23:31:53 +01:00
|
|
|
if (!$createdId) {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Permission can not be created')], 'form' => $form];
|
|
|
|
|
}
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->systemAuditService->record('admin.permissions.create', 'success', [
|
|
|
|
|
'target_type' => 'permission',
|
|
|
|
|
'target_id' => (int) $createdId,
|
|
|
|
|
'metadata' => [
|
|
|
|
|
'key' => $form['key'],
|
|
|
|
|
'active' => (int) ($form['active'] ?? 0),
|
|
|
|
|
'is_system' => (int) ($form['is_system'] ?? 0),
|
|
|
|
|
],
|
|
|
|
|
]);
|
2026-02-04 23:31:53 +01:00
|
|
|
return ['ok' => true, 'form' => $form, 'id' => (int) $createdId];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function updateFromAdmin(int $id, array $input): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
$form = $this->sanitizeBase($input);
|
|
|
|
|
$errors = $this->validateBase($form);
|
2026-02-04 23:31:53 +01:00
|
|
|
if ($errors) {
|
|
|
|
|
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
|
|
|
|
}
|
2026-02-23 12:58:19 +01:00
|
|
|
$existing = $this->permissionRepository->findByKey($form['key']);
|
2026-02-04 23:31:53 +01:00
|
|
|
if ($existing && (int) ($existing['id'] ?? 0) !== $id) {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Permission key already exists')], 'form' => $form];
|
|
|
|
|
}
|
2026-03-04 15:56:58 +01:00
|
|
|
$before = $this->permissionRepository->find($id) ?? [];
|
2026-02-23 12:58:19 +01:00
|
|
|
$updated = $this->permissionRepository->update($id, $form);
|
2026-02-04 23:31:53 +01:00
|
|
|
if (!$updated) {
|
|
|
|
|
return ['ok' => false, 'errors' => [t('Permission can not be updated')], 'form' => $form];
|
|
|
|
|
}
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->systemAuditService->record('admin.permissions.update', 'success', [
|
|
|
|
|
'target_type' => 'permission',
|
|
|
|
|
'target_id' => $id,
|
|
|
|
|
'before' => [
|
|
|
|
|
'active' => $before['active'] ?? null,
|
|
|
|
|
],
|
|
|
|
|
'after' => [
|
|
|
|
|
'active' => $form['active'] ?? null,
|
|
|
|
|
],
|
|
|
|
|
'metadata' => ['key' => $form['key']],
|
|
|
|
|
]);
|
2026-02-04 23:31:53 +01:00
|
|
|
return ['ok' => true, 'form' => $form];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function deleteById(int $id): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
$permission = $this->permissionRepository->find($id);
|
2026-02-04 23:31:53 +01:00
|
|
|
if (!$permission) {
|
|
|
|
|
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
|
|
|
|
}
|
2026-02-11 19:28:12 +01:00
|
|
|
if ((int) ($permission['is_system'] ?? 0) === 1) {
|
|
|
|
|
return ['ok' => false, 'status' => 403, 'error' => 'system_permission_protected'];
|
|
|
|
|
}
|
2026-02-23 12:58:19 +01:00
|
|
|
$deleted = $this->permissionRepository->delete($id);
|
2026-02-04 23:31:53 +01:00
|
|
|
if (!$deleted) {
|
|
|
|
|
return ['ok' => false, 'status' => 500, 'error' => 'delete_failed'];
|
|
|
|
|
}
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->systemAuditService->record('admin.permissions.delete', 'success', [
|
|
|
|
|
'target_type' => 'permission',
|
|
|
|
|
'target_id' => $id,
|
|
|
|
|
'metadata' => ['key' => (string) ($permission['key'] ?? '')],
|
|
|
|
|
]);
|
2026-02-04 23:31:53 +01:00
|
|
|
return ['ok' => true, 'permission' => $permission];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function sanitizeBase(array $input): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'key' => trim((string) ($input['key'] ?? '')),
|
|
|
|
|
'description' => trim((string) ($input['description'] ?? '')),
|
2026-02-23 12:58:19 +01:00
|
|
|
'active' => $this->normalizeActive($input['active'] ?? 1),
|
|
|
|
|
'is_system' => $this->normalizeFlag($input['is_system'] ?? 0),
|
2026-02-04 23:31:53 +01:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function validateBase(array $form): array
|
2026-02-04 23:31:53 +01:00
|
|
|
{
|
|
|
|
|
$errors = [];
|
|
|
|
|
if ($form['key'] === '') {
|
|
|
|
|
$errors[] = t('Permission key cannot be empty');
|
|
|
|
|
} elseif (!preg_match('/^[a-z0-9._-]+$/i', $form['key'])) {
|
|
|
|
|
$errors[] = t('Permission key is invalid');
|
|
|
|
|
}
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
2026-02-11 19:28:12 +01:00
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function normalizeActive($value): int
|
2026-02-11 19:28:12 +01:00
|
|
|
{
|
|
|
|
|
$value = strtolower(trim((string) $value));
|
|
|
|
|
if (in_array($value, ['0', 'false', 'inactive'], true)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function normalizeFlag($value): int
|
2026-02-11 19:28:12 +01:00
|
|
|
{
|
|
|
|
|
if (is_bool($value)) {
|
|
|
|
|
return $value ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
$value = strtolower(trim((string) $value));
|
|
|
|
|
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-02-04 23:31:53 +01:00
|
|
|
}
|