agent foundation

This commit is contained in:
2026-03-06 00:44:52 +01:00
parent 9819cba733
commit 9a08f96c11
199 changed files with 8522 additions and 1880 deletions

View File

@@ -2,11 +2,12 @@
namespace MintyPHP\Service\Import;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\ImportAuditService;
use MintyPHP\Service\Import\Profile\ImportProfileInterface;
use MintyPHP\Service\Settings\SettingGateway;
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
use MintyPHP\Service\User\UserScopeGateway;
class ImportService
@@ -28,8 +29,9 @@ class ImportService
private readonly ImportStateStoreService $importStateStoreService,
private readonly array $profiles,
private readonly PermissionGateway $permissionGateway,
private readonly SettingGateway $settingGateway,
private readonly UserScopeGateway $userScopeGateway
private readonly SettingsDefaultsGateway $settingsDefaultsGateway,
private readonly UserScopeGateway $userScopeGateway,
private readonly SessionStoreInterface $sessionStore
) {
}
@@ -139,6 +141,7 @@ class ImportService
if (!$state) {
return ['ok' => false, 'code' => 'invalid_file_type'];
}
// Prevent cross-user token reuse — the state token is session-scoped but user_id is verified explicitly.
if ((int) ($state['user_id'] ?? 0) !== $currentUserId) {
return ['ok' => false, 'code' => 'invalid_file_type'];
}
@@ -154,6 +157,8 @@ class ImportService
}
$mappedTargets = $mappingResult['mapped_targets'] ?? [];
// Tenant/role/department column mapping requires a separate permission — a user who can import
// users generally may not be allowed to also assign them to tenants.
if (
$profile->requiresAssignmentPermission()
&& $this->usesAssignmentTargets($mappedTargets)
@@ -203,7 +208,7 @@ class ImportService
$mappingResult['mapped_targets'] ?? [],
isset($state['source_filename']) ? (string) $state['source_filename'] : null,
$currentUserId,
isset($_SESSION['current_tenant']['id']) ? (int) $_SESSION['current_tenant']['id'] : null
$this->currentTenantId()
);
$result = ['ok' => false, 'code' => 'unexpected_error', 'processed' => 0, 'created' => 0, 'skipped' => 0, 'failed' => 1];
@@ -301,6 +306,7 @@ class ImportService
$normalized = $validation['normalized'];
$identifier = $profile->rowIdentifier($normalized);
// Detect duplicates within the file itself (e.g. same email appearing twice).
$duplicateKey = $profile->inFileDuplicateKey($normalized);
if ($duplicateKey !== null && $duplicateKey !== '') {
if (isset($seenRowKeys[$duplicateKey])) {
@@ -496,14 +502,28 @@ class ImportService
return [
'current_user_id' => $currentUserId,
'is_global_admin' => $isGlobalAdmin,
// Empty allowed_tenant_ids signals "no restriction" to profile validators for global admins.
'allowed_tenant_ids' => $isGlobalAdmin ? [] : $this->userScopeGateway->getUserTenantIds($currentUserId),
'default_tenant_id' => $this->settingGateway->getDefaultTenantId(),
'default_role_id' => $this->settingGateway->getDefaultRoleId(),
'default_department_id' => $this->settingGateway->getDefaultDepartmentId(),
'default_tenant_id' => $this->settingsDefaultsGateway->getDefaultTenantId(),
'default_role_id' => $this->settingsDefaultsGateway->getDefaultRoleId(),
'default_department_id' => $this->settingsDefaultsGateway->getDefaultDepartmentId(),
'can_import_assignments' => $this->canImportAssignments($currentUserId),
];
}
private function currentTenantId(): ?int
{
$tenant = $this->sessionStore->get('current_tenant', []);
if (!is_array($tenant)) {
return null;
}
$tenantId = (int) ($tenant['id'] ?? 0);
return $tenantId > 0 ? $tenantId : null;
}
// Fallback to uniqid if random_bytes is unavailable — not cryptographically ideal but only
// used for session-scoped state tokens, not for security-critical values.
private function newToken(): string
{
try {
@@ -530,6 +550,7 @@ class ImportService
/**
* @param array<int, array{row_number:int,identifier:string,code:string,message:string}> $errors
*/
// Cap the error list to avoid oversized responses — error_counts still reflects the full count.
private function pushError(array &$errors, int $lineNumber, string $identifier, string $code, ?string $message): void
{
if (count($errors) >= self::MAX_ERROR_ROWS) {