Rename the top-level lib/ directory to core/ so the project root immediately communicates which code is core platform and which lives in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the Composer PSR-4 path mapping moves from lib/ to core/. Module-internal lib/ directories (modules/*/lib/) are untouched. Updated across the full stack: - composer.json PSR-4 mapping - bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php) - tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer) - 26 architecture contract tests - enforcement-policy, guard-catalog, quality-gates - all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/) - bin/qa-extended.sh search paths - .claude/settings.local.json permission paths Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner → Executor → Code Review (4 findings fixed) → Security Review → Acceptance Test → Finalizer) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
186 lines
6.1 KiB
PHP
186 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Service\Access;
|
|
|
|
use MintyPHP\Repository\Access\RoleRepositoryInterface;
|
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
|
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
|
|
|
class RoleService
|
|
{
|
|
public function __construct(
|
|
private readonly RoleRepositoryInterface $roleRepository,
|
|
private readonly DirectorySettingsGateway $settingsGateway,
|
|
private readonly AuditRecorderInterface $systemAuditService
|
|
) {
|
|
}
|
|
|
|
public function list(): array
|
|
{
|
|
return $this->roleRepository->list();
|
|
}
|
|
|
|
public function listActive(): array
|
|
{
|
|
return $this->roleRepository->listActive();
|
|
}
|
|
|
|
public function listPaged(array $options): array
|
|
{
|
|
return $this->roleRepository->listPaged($options);
|
|
}
|
|
|
|
public function findByUuid(string $uuid): ?array
|
|
{
|
|
return $this->roleRepository->findByUuid($uuid);
|
|
}
|
|
|
|
public function findById(int $id): ?array
|
|
{
|
|
return $this->roleRepository->find($id);
|
|
}
|
|
|
|
public function createFromAdmin(array $input, int $currentUserId = 0): array
|
|
{
|
|
$form = $this->sanitizeBase($input);
|
|
$errors = $this->validateBase($form);
|
|
$warnings = $this->warningsForCode($form, 0);
|
|
|
|
if ($errors) {
|
|
return ['ok' => false, 'errors' => $errors, 'warnings' => $warnings, 'form' => $form];
|
|
}
|
|
|
|
$createdId = $this->roleRepository->create([
|
|
'description' => $form['description'],
|
|
'code' => $form['code'] ?: null,
|
|
'active' => $form['active'],
|
|
'created_by' => $currentUserId > 0 ? $currentUserId : null,
|
|
]);
|
|
|
|
if (!$createdId) {
|
|
return ['ok' => false, 'errors' => [t('Role can not be created')], 'form' => $form];
|
|
}
|
|
|
|
$createdRole = $this->roleRepository->find((int) $createdId);
|
|
$uuid = $createdRole['uuid'] ?? null;
|
|
if (!empty($input['is_default'])) {
|
|
$this->settingsGateway->setDefaultRoleId((int) $createdId);
|
|
}
|
|
|
|
$this->systemAuditService->record('admin.roles.create', 'success', [
|
|
'actor_user_id' => $currentUserId > 0 ? $currentUserId : null,
|
|
'target_type' => 'role',
|
|
'target_id' => (int) $createdId,
|
|
'target_uuid' => is_string($uuid) ? $uuid : '',
|
|
'metadata' => ['active' => $form['active']],
|
|
]);
|
|
|
|
return ['ok' => true, 'form' => $form, 'warnings' => $warnings, 'uuid' => $uuid, 'id' => (int) $createdId];
|
|
}
|
|
|
|
public function updateFromAdmin(int $roleId, array $input, int $currentUserId = 0): array
|
|
{
|
|
$form = $this->sanitizeBase($input);
|
|
$errors = $this->validateBase($form);
|
|
$warnings = $this->warningsForCode($form, $roleId);
|
|
|
|
if ($errors) {
|
|
return ['ok' => false, 'errors' => $errors, 'warnings' => $warnings, 'form' => $form];
|
|
}
|
|
|
|
$existingBefore = $this->roleRepository->find($roleId) ?? [];
|
|
|
|
$updated = $this->roleRepository->update($roleId, [
|
|
'description' => $form['description'],
|
|
'code' => $form['code'] ?: null,
|
|
'active' => $form['active'],
|
|
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
|
|
]);
|
|
|
|
if (!$updated) {
|
|
return ['ok' => false, 'errors' => [t('Role can not be updated')], 'form' => $form];
|
|
}
|
|
|
|
$this->systemAuditService->record('admin.roles.update', 'success', [
|
|
'actor_user_id' => $currentUserId > 0 ? $currentUserId : null,
|
|
'target_type' => 'role',
|
|
'target_id' => $roleId,
|
|
'target_uuid' => (string) ($existingBefore['uuid'] ?? ''),
|
|
'before' => ['active' => $existingBefore['active'] ?? null],
|
|
'after' => ['active' => $form['active']],
|
|
]);
|
|
|
|
return ['ok' => true, 'form' => $form, 'warnings' => $warnings];
|
|
}
|
|
|
|
public function deleteByUuid(string $uuid): array
|
|
{
|
|
$uuid = trim($uuid);
|
|
if ($uuid === '') {
|
|
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
|
}
|
|
|
|
$role = $this->roleRepository->findByUuid($uuid);
|
|
if (!$role || !isset($role['id'])) {
|
|
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
|
}
|
|
|
|
if (($role['description'] ?? '') === 'Admin') {
|
|
return ['ok' => false, 'status' => 403, 'error' => 'admin_role_protected'];
|
|
}
|
|
|
|
$deleted = $this->roleRepository->delete((int) $role['id']);
|
|
if (!$deleted) {
|
|
return ['ok' => false, 'status' => 500, 'error' => 'delete_failed'];
|
|
}
|
|
|
|
$this->systemAuditService->record('admin.roles.delete', 'success', [
|
|
'target_type' => 'role',
|
|
'target_id' => (int) $role['id'],
|
|
'target_uuid' => (string) ($role['uuid'] ?? ''),
|
|
]);
|
|
|
|
return ['ok' => true, 'role' => $role];
|
|
}
|
|
|
|
private function sanitizeBase(array $input): array
|
|
{
|
|
return [
|
|
'description' => trim((string) ($input['description'] ?? '')),
|
|
'code' => trim((string) ($input['code'] ?? '')),
|
|
'active' => $this->normalizeActive($input['active'] ?? 1),
|
|
];
|
|
}
|
|
|
|
private function validateBase(array $form): array
|
|
{
|
|
$errors = [];
|
|
if ($form['description'] === '') {
|
|
$errors[] = t('Description cannot be empty');
|
|
}
|
|
return $errors;
|
|
}
|
|
|
|
// Duplicate codes are a warning, not a hard error — codes are optional identifiers,
|
|
// not unique keys, so duplicates are allowed but surfaced to the user.
|
|
private function warningsForCode(array $form, int $excludeId): array
|
|
{
|
|
$warnings = [];
|
|
$code = $form['code'] ?? '';
|
|
if ($code !== '' && $this->roleRepository->existsByCode($code, $excludeId)) {
|
|
$warnings[] = t('Role code already exists');
|
|
}
|
|
return $warnings;
|
|
}
|
|
|
|
private function normalizeActive($value): int
|
|
{
|
|
$value = strtolower(trim((string) $value));
|
|
if (in_array($value, ['0', 'false', 'inactive'], true)) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
}
|