diff --git a/lib/Repository/Audit/ApiAuditLogRepository.php b/lib/Repository/Audit/ApiAuditLogRepository.php index 13d8e02..e26a681 100644 --- a/lib/Repository/Audit/ApiAuditLogRepository.php +++ b/lib/Repository/Audit/ApiAuditLogRepository.php @@ -4,6 +4,7 @@ namespace MintyPHP\Repository\Audit; use MintyPHP\DB; use MintyPHP\Repository\Support\RepoQuery; +use MintyPHP\Repository\Support\RepositoryArrayHelper; /** Records API request/response audit entries with status codes, timing, and error details. */ class ApiAuditLogRepository implements ApiAuditLogRepositoryInterface @@ -178,7 +179,7 @@ class ApiAuditLogRepository implements ApiAuditLogRepositoryInterface $methods = []; if (is_array($methodRows)) { foreach ($methodRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $method = strtoupper(trim((string) ($flat['method'] ?? ''))); if ($method === '') { continue; @@ -208,7 +209,7 @@ class ApiAuditLogRepository implements ApiAuditLogRepositoryInterface $users = []; if (is_array($userRows)) { foreach ($userRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['user_id'] ?? 0); if ($id <= 0) { continue; @@ -241,7 +242,7 @@ class ApiAuditLogRepository implements ApiAuditLogRepositoryInterface $tenants = []; if (is_array($tenantRows)) { foreach ($tenantRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['tenant_id'] ?? 0); if ($id <= 0) { continue; @@ -334,24 +335,4 @@ class ApiAuditLogRepository implements ApiAuditLogRepositoryInterface return $log; } - /** - * @return array - */ - private static function flattenRow(mixed $row): array - { - if (!is_array($row)) { - return []; - } - - $flat = []; - foreach ($row as $key => $value) { - if (is_array($value)) { - $flat = array_merge($flat, $value); - } elseif (is_string($key)) { - $flat[$key] = $value; - } - } - - return $flat; - } } diff --git a/lib/Repository/Audit/ImportAuditRunRepository.php b/lib/Repository/Audit/ImportAuditRunRepository.php index 930826b..ff0e9fa 100644 --- a/lib/Repository/Audit/ImportAuditRunRepository.php +++ b/lib/Repository/Audit/ImportAuditRunRepository.php @@ -5,6 +5,7 @@ namespace MintyPHP\Repository\Audit; use MintyPHP\DB; use MintyPHP\Domain\Taxonomy\ImportAuditStatus; use MintyPHP\Repository\Support\RepoQuery; +use MintyPHP\Repository\Support\RepositoryArrayHelper; /** Tracks CSV import runs including source file, row counts, status, and completion metadata. */ class ImportAuditRunRepository implements ImportAuditRunRepositoryInterface @@ -202,7 +203,7 @@ class ImportAuditRunRepository implements ImportAuditRunRepositoryInterface $users = []; if (is_array($userRows)) { foreach ($userRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['user_id'] ?? 0); if ($id <= 0) { continue; @@ -298,24 +299,4 @@ class ImportAuditRunRepository implements ImportAuditRunRepositoryInterface return $item; } - /** - * @return array - */ - private static function flattenRow(mixed $row): array - { - if (!is_array($row)) { - return []; - } - - $flat = []; - foreach ($row as $key => $value) { - if (is_array($value)) { - $flat = array_merge($flat, $value); - } elseif (is_string($key)) { - $flat[$key] = $value; - } - } - - return $flat; - } } diff --git a/lib/Repository/Audit/SystemAuditLogRepository.php b/lib/Repository/Audit/SystemAuditLogRepository.php index 147577e..443e046 100644 --- a/lib/Repository/Audit/SystemAuditLogRepository.php +++ b/lib/Repository/Audit/SystemAuditLogRepository.php @@ -6,6 +6,7 @@ use MintyPHP\DB; use MintyPHP\Domain\Taxonomy\SystemAuditChannel; use MintyPHP\Domain\Taxonomy\SystemAuditOutcome; use MintyPHP\Repository\Support\RepoQuery; +use MintyPHP\Repository\Support\RepositoryArrayHelper; /** Persists system audit events with actor, target, outcome, channel, and hashed request metadata. */ class SystemAuditLogRepository implements SystemAuditLogRepositoryInterface @@ -205,7 +206,7 @@ class SystemAuditLogRepository implements SystemAuditLogRepositoryInterface $eventTypes = []; if (is_array($eventRows)) { foreach ($eventRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $eventType = strtolower(trim((string) ($flat['event_type'] ?? ''))); if ($eventType === '' || strlen($eventType) > 64) { continue; @@ -238,7 +239,7 @@ class SystemAuditLogRepository implements SystemAuditLogRepositoryInterface $actors = []; if (is_array($actorRows)) { foreach ($actorRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['actor_user_id'] ?? 0); if ($id <= 0) { continue; @@ -335,24 +336,4 @@ class SystemAuditLogRepository implements SystemAuditLogRepositoryInterface return $log; } - /** - * @return array - */ - private static function flattenRow(mixed $row): array - { - if (!is_array($row)) { - return []; - } - - $flat = []; - foreach ($row as $key => $value) { - if (is_array($value)) { - $flat = array_merge($flat, $value); - } elseif (is_string($key)) { - $flat[$key] = $value; - } - } - - return $flat; - } } diff --git a/lib/Repository/Audit/UserLifecycleAuditRepository.php b/lib/Repository/Audit/UserLifecycleAuditRepository.php index 9eacfea..2e758f7 100644 --- a/lib/Repository/Audit/UserLifecycleAuditRepository.php +++ b/lib/Repository/Audit/UserLifecycleAuditRepository.php @@ -7,6 +7,7 @@ use MintyPHP\Domain\Taxonomy\UserLifecycleAction; use MintyPHP\Domain\Taxonomy\UserLifecycleStatus; use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType; use MintyPHP\Repository\Support\RepoQuery; +use MintyPHP\Repository\Support\RepositoryArrayHelper; /** Records user lifecycle transitions (deactivation, deletion, restore) with reason codes and snapshots. */ class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterface @@ -204,7 +205,7 @@ class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterf $actions = []; if (is_array($actionRows)) { foreach ($actionRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $action = strtolower(trim((string) ($flat['action'] ?? ''))); if (UserLifecycleAction::tryNormalize($action) === null) { continue; @@ -230,7 +231,7 @@ class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterf $statuses = []; if (is_array($statusRows)) { foreach ($statusRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $status = strtolower(trim((string) ($flat['status'] ?? ''))); if (UserLifecycleStatus::tryNormalize($status) === null) { continue; @@ -256,7 +257,7 @@ class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterf $triggerTypes = []; if (is_array($triggerRows)) { foreach ($triggerRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $triggerType = strtolower(trim((string) ($flat['trigger_type'] ?? ''))); if (UserLifecycleTriggerType::tryNormalize($triggerType) === null) { continue; @@ -286,7 +287,7 @@ class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterf $actors = []; if (is_array($actorRows)) { foreach ($actorRows as $row) { - $flat = self::flattenRow($row); + $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['actor_user_id'] ?? 0); if ($id <= 0) { continue; @@ -445,24 +446,4 @@ class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterf return $item; } - /** - * @return array - */ - private static function flattenRow(mixed $row): array - { - if (!is_array($row)) { - return []; - } - - $flat = []; - foreach ($row as $key => $value) { - if (is_array($value)) { - $flat = array_merge($flat, $value); - } elseif (is_string($key)) { - $flat[$key] = $value; - } - } - - return $flat; - } } diff --git a/lib/Repository/Support/RepositoryArrayHelper.php b/lib/Repository/Support/RepositoryArrayHelper.php index 5b4899a..ef608c3 100644 --- a/lib/Repository/Support/RepositoryArrayHelper.php +++ b/lib/Repository/Support/RepositoryArrayHelper.php @@ -70,4 +70,27 @@ final class RepositoryArrayHelper return array_values(array_filter($ids, static fn (int $id): bool => $id > 0)); } + + /** + * Flatten a MintyPHP DB row that can contain nested table-key arrays. + * + * @return array + */ + public static function flattenRow(mixed $row): array + { + if (!is_array($row)) { + return []; + } + + $flat = []; + foreach ($row as $key => $value) { + if (is_array($value)) { + $flat = array_merge($flat, $value); + } elseif (is_string($key)) { + $flat[$key] = $value; + } + } + + return $flat; + } } diff --git a/lib/Service/Access/AuthorizationPolicyContextTrait.php b/lib/Service/Access/AuthorizationPolicyContextTrait.php new file mode 100644 index 0000000..c62998f --- /dev/null +++ b/lib/Service/Access/AuthorizationPolicyContextTrait.php @@ -0,0 +1,22 @@ + 0 && $this->permissionService->userHas($userId, $permissionKey); + } + + protected function capabilitiesFromDecision(AuthorizationDecision $decision): array + { + $capabilities = $decision->attribute('capabilities', []); + return is_array($capabilities) ? $capabilities : []; + } +} diff --git a/lib/Service/Access/DepartmentAuthorizationPolicy.php b/lib/Service/Access/DepartmentAuthorizationPolicy.php index b88e658..52ae92d 100644 --- a/lib/Service/Access/DepartmentAuthorizationPolicy.php +++ b/lib/Service/Access/DepartmentAuthorizationPolicy.php @@ -6,6 +6,8 @@ use MintyPHP\Service\Tenant\TenantScopeService; class DepartmentAuthorizationPolicy implements AuthorizationPolicyInterface { + use AuthorizationPolicyContextTrait; + public const ABILITY_ADMIN_DEPARTMENTS_VIEW = 'admin.departments.view'; public const ABILITY_ADMIN_DEPARTMENTS_CREATE = 'admin.departments.create'; public const ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT = 'admin.departments.edit.context'; @@ -138,24 +140,8 @@ class DepartmentAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow(); } - private function actorUserId(array $context): int - { - return (int) ($context['actor_user_id'] ?? 0); - } - private function targetDepartmentId(array $context): int { return (int) ($context['target_department_id'] ?? 0); } - - private function hasPermission(int $userId, string $permissionKey): bool - { - return $userId > 0 && $this->permissionService->userHas($userId, $permissionKey); - } - - private function capabilitiesFromDecision(AuthorizationDecision $decision): array - { - $capabilities = $decision->attribute('capabilities', []); - return is_array($capabilities) ? $capabilities : []; - } } diff --git a/lib/Service/Access/PermissionAuthorizationPolicy.php b/lib/Service/Access/PermissionAuthorizationPolicy.php index 8a495a7..e1fb3b5 100644 --- a/lib/Service/Access/PermissionAuthorizationPolicy.php +++ b/lib/Service/Access/PermissionAuthorizationPolicy.php @@ -4,6 +4,8 @@ namespace MintyPHP\Service\Access; class PermissionAuthorizationPolicy implements AuthorizationPolicyInterface { + use AuthorizationPolicyContextTrait; + public const ABILITY_ADMIN_PERMISSIONS_VIEW = 'admin.permissions.view'; public const ABILITY_ADMIN_PERMISSIONS_CREATE = 'admin.permissions.create'; public const ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT = 'admin.permissions.edit.context'; @@ -111,19 +113,4 @@ class PermissionAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow(); } - private function actorUserId(array $context): int - { - return (int) ($context['actor_user_id'] ?? 0); - } - - private function hasPermission(int $userId, string $permissionKey): bool - { - return $userId > 0 && $this->permissionService->userHas($userId, $permissionKey); - } - - private function capabilitiesFromDecision(AuthorizationDecision $decision): array - { - $capabilities = $decision->attribute('capabilities', []); - return is_array($capabilities) ? $capabilities : []; - } } diff --git a/lib/Service/Access/RoleAuthorizationPolicy.php b/lib/Service/Access/RoleAuthorizationPolicy.php index e976664..57cda0c 100644 --- a/lib/Service/Access/RoleAuthorizationPolicy.php +++ b/lib/Service/Access/RoleAuthorizationPolicy.php @@ -4,6 +4,8 @@ namespace MintyPHP\Service\Access; class RoleAuthorizationPolicy implements AuthorizationPolicyInterface { + use AuthorizationPolicyContextTrait; + public const ABILITY_ADMIN_ROLES_VIEW = 'admin.roles.view'; public const ABILITY_ADMIN_ROLES_CREATE = 'admin.roles.create'; public const ABILITY_ADMIN_ROLES_EDIT_CONTEXT = 'admin.roles.edit.context'; @@ -109,19 +111,4 @@ class RoleAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow(); } - private function actorUserId(array $context): int - { - return (int) ($context['actor_user_id'] ?? 0); - } - - private function hasPermission(int $userId, string $permissionKey): bool - { - return $userId > 0 && $this->permissionService->userHas($userId, $permissionKey); - } - - private function capabilitiesFromDecision(AuthorizationDecision $decision): array - { - $capabilities = $decision->attribute('capabilities', []); - return is_array($capabilities) ? $capabilities : []; - } } diff --git a/lib/Service/Access/SettingsAuthorizationPolicy.php b/lib/Service/Access/SettingsAuthorizationPolicy.php index 1e8a633..edad0e5 100644 --- a/lib/Service/Access/SettingsAuthorizationPolicy.php +++ b/lib/Service/Access/SettingsAuthorizationPolicy.php @@ -4,6 +4,8 @@ namespace MintyPHP\Service\Access; class SettingsAuthorizationPolicy implements AuthorizationPolicyInterface { + use AuthorizationPolicyContextTrait; + public const ABILITY_ADMIN_SETTINGS_VIEW = 'admin.settings.view'; public const ABILITY_ADMIN_SETTINGS_UPDATE = 'admin.settings.update'; public const ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE = 'admin.settings.branding.update'; @@ -63,13 +65,4 @@ class SettingsAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow(); } - private function actorUserId(array $context): int - { - return (int) ($context['actor_user_id'] ?? 0); - } - - private function hasPermission(int $userId, string $permissionKey): bool - { - return $userId > 0 && $this->permissionService->userHas($userId, $permissionKey); - } } diff --git a/lib/Service/Access/TenantAuthorizationPolicy.php b/lib/Service/Access/TenantAuthorizationPolicy.php index 7b5fef6..98d4b5d 100644 --- a/lib/Service/Access/TenantAuthorizationPolicy.php +++ b/lib/Service/Access/TenantAuthorizationPolicy.php @@ -6,6 +6,8 @@ use MintyPHP\Service\Tenant\TenantScopeService; class TenantAuthorizationPolicy implements AuthorizationPolicyInterface { + use AuthorizationPolicyContextTrait; + public const ABILITY_ADMIN_TENANTS_VIEW = 'admin.tenants.view'; public const ABILITY_ADMIN_TENANTS_CREATE = 'admin.tenants.create'; public const ABILITY_ADMIN_TENANTS_EDIT_CONTEXT = 'admin.tenants.edit.context'; @@ -211,24 +213,8 @@ class TenantAuthorizationPolicy implements AuthorizationPolicyInterface return false; } - private function actorUserId(array $context): int - { - return (int) ($context['actor_user_id'] ?? 0); - } - private function targetTenantId(array $context): int { return (int) ($context['target_tenant_id'] ?? 0); } - - private function hasPermission(int $userId, string $permissionKey): bool - { - return $userId > 0 && $this->permissionService->userHas($userId, $permissionKey); - } - - private function capabilitiesFromDecision(AuthorizationDecision $decision): array - { - $capabilities = $decision->attribute('capabilities', []); - return is_array($capabilities) ? $capabilities : []; - } } diff --git a/lib/Service/Access/UserAuthorizationPolicy.php b/lib/Service/Access/UserAuthorizationPolicy.php index 71529ef..6fa3df3 100644 --- a/lib/Service/Access/UserAuthorizationPolicy.php +++ b/lib/Service/Access/UserAuthorizationPolicy.php @@ -6,6 +6,8 @@ use MintyPHP\Service\Tenant\TenantScopeService; class UserAuthorizationPolicy implements AuthorizationPolicyInterface { + use AuthorizationPolicyContextTrait; + public const ABILITY_ADMIN_USERS_VIEW = 'admin.users.view'; public const ABILITY_ADMIN_USERS_CREATE = 'admin.users.create'; public const ABILITY_ADMIN_USERS_EDIT_CONTEXT = 'admin.users.edit.context'; @@ -350,16 +352,6 @@ class UserAuthorizationPolicy implements AuthorizationPolicyInterface return array_values($tenantIds); } - private function actorUserId(array $context): int - { - return (int) ($context['actor_user_id'] ?? 0); - } - - private function hasPermission(int $userId, string $permissionKey): bool - { - return $userId > 0 && $this->permissionService->userHas($userId, $permissionKey); - } - private function canEditUserForTarget(int $actorUserId, int $targetUserId): bool { if ($actorUserId <= 0 || $targetUserId <= 0) { @@ -425,12 +417,6 @@ class UserAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow(['capabilities' => $capabilities]); } - private function capabilitiesFromDecision(AuthorizationDecision $decision): array - { - $capabilities = $decision->attribute('capabilities', []); - return is_array($capabilities) ? $capabilities : []; - } - private function authorizeAdminUserWithPermission( string $permissionKey, array $context, diff --git a/lib/Support/helpers/grid.php b/lib/Support/helpers/grid.php index 9fe4230..f5f288a 100644 --- a/lib/Support/helpers/grid.php +++ b/lib/Support/helpers/grid.php @@ -631,52 +631,6 @@ function gridParseFilters(array $query, array $schema): array return $parsed; } -/** - * Normalize common limit/offset query params. - * - * @return array{0:int,1:int} - */ -function gridQueryLimitOffset(array $query, int $defaultLimit = 10, int $maxLimit = 100): array -{ - $limit = (int) ($query['limit'] ?? $defaultLimit); - if ($limit < 1) { - $limit = $defaultLimit; - } elseif ($limit > $maxLimit) { - $limit = $maxLimit; - } - - $offset = (int) ($query['offset'] ?? 0); - if ($offset < 0) { - $offset = 0; - } - - return [$limit, $offset]; -} - -/** - * Normalize order/dir query params against an allow-list. - * - * @return array{0:string,1:string} - */ -function gridQueryOrderDir(array $query, array $allowedOrder, string $defaultOrder, string $defaultDir = 'asc'): array -{ - if (!in_array($defaultDir, ['asc', 'desc'], true)) { - $defaultDir = 'asc'; - } - - $order = trim((string) ($query['order'] ?? $defaultOrder)); - if (!in_array($order, $allowedOrder, true)) { - $order = $defaultOrder; - } - - $dir = strtolower(trim((string) ($query['dir'] ?? $defaultDir))); - if (!in_array($dir, ['asc', 'desc'], true)) { - $dir = $defaultDir; - } - - return [$order, $dir]; -} - /** * Read a trimmed string from query params. */ @@ -916,19 +870,3 @@ function gridNormalizeLabelList(mixed $value, string $separator = '||'): array $items = array_values(array_filter($items, static fn (string $item): bool => $item !== '')); return $items; } - -/** - * Normalize mixed values to a unique list of positive integer IDs. - * - * @return int[] - */ -function gridNormalizePositiveIntList(mixed $value): array -{ - if (!is_array($value)) { - return []; - } - - $ids = array_map('intval', $value); - $ids = array_values(array_filter($ids, static fn (int $id): bool => $id > 0)); - return array_values(array_unique($ids)); -} diff --git a/tests/Architecture/AuthzUiActionContractTest.php b/tests/Architecture/AuthzUiActionContractTest.php index 0a31a0b..56a234e 100644 --- a/tests/Architecture/AuthzUiActionContractTest.php +++ b/tests/Architecture/AuthzUiActionContractTest.php @@ -10,28 +10,6 @@ class AuthzUiActionContractTest extends TestCase use AuthzUiContractSupport; use ProjectFileAssertionSupport; - public function testHardCutActionsProvideViewAuthPageCapabilities(): void - { - $actions = [ - 'pages/admin/users/index().php', - 'pages/admin/users/create().php', - 'pages/admin/tenants/edit($id).php', - 'pages/admin/departments/edit($id).php', - 'pages/admin/stats/index().php', - 'pages/admin/api-audit/index().php', - 'pages/admin/system-audit/index().php', - 'pages/admin/import-audit/index().php', - 'pages/admin/scheduled-jobs/index().php', - 'pages/admin/user-lifecycle-audit/index().php', - 'pages/admin/user-lifecycle-audit/view($id).php', - ]; - - foreach ($actions as $action) { - $content = $this->readProjectFile($action); - $this->assertStringContainsString("\$viewAuth['page']", $content, $action); - } - } - #[DataProvider('adminPageAuthWiringProvider')] public function testAdminPageAuthKeysAreWiredFromActions( string $templateFile, diff --git a/tests/Architecture/ListTemplateContractTest.php b/tests/Architecture/ListTemplateContractTest.php index e0359e5..8669eab 100644 --- a/tests/Architecture/ListTemplateContractTest.php +++ b/tests/Architecture/ListTemplateContractTest.php @@ -21,9 +21,6 @@ class ListTemplateContractTest extends TestCase ); $this->assertStringContainsString('initStandardListPage(', $moduleContent, $file); $this->assertStringNotContainsString('gridFiltersFromSchema(', $content, $file); - $this->assertStringNotContainsString('data-toolbar-toggle', $content, $file); - $this->assertStringNotContainsString('data-filter-overflow', $content, $file); - $this->assertStringNotContainsString('data-filter-toggle', $content, $file); $this->assertSame(0, preg_match('/