0 && $this->scopeGateway->hasGlobalAccess($tenantUserId)) { unset($options['tenantUserId']); } } return $this->userListQueryRepository->listPaged($options); } public function findByUuid(string $uuid): ?array { return $this->userReadRepository->findByUuid($uuid); } public function findById(int $id): ?array { return $this->userReadRepository->find($id); } public function findByEmail(string $email): ?array { return $this->userReadRepository->findByEmail($email); } public function setLocale(int $userId, string $locale): bool { return $this->userWriteRepository->setLocale($userId, $locale); } public function setTheme(int $userId, string $theme): bool { $theme = $this->normalizeTheme($theme); return $this->userWriteRepository->setTheme($userId, $theme); } public function deleteByUuid(string $uuid, int $currentUserId = 0): array { $uuid = trim($uuid); if ($uuid === '') { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $user = $this->userReadRepository->findByUuid($uuid); if (!$user || !isset($user['id'])) { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $userId = (int) $user['id']; if ($currentUserId && $currentUserId === $userId) { return [ 'ok' => false, 'status' => 400, 'error' => 'self_delete', 'message' => t('You cannot delete your own account'), ]; } $deleted = $this->userWriteRepository->delete($userId); if (!$deleted) { return ['ok' => false, 'status' => 500, 'error' => 'delete_failed']; } $this->systemAuditService->record('admin.users.delete', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'user', 'target_id' => $userId, 'target_uuid' => (string) ($user['uuid'] ?? ''), ]); return ['ok' => true, 'user' => $user]; } public function deleteByUuids(array $uuids, int $currentUserId = 0): array { $uuids = array_values(array_filter(array_map('trim', $uuids))); if (!$uuids) { return ['ok' => false, 'error' => 'no_selection']; } if ($currentUserId > 0) { $uuids = $this->filterUuidsByTenantScope($uuids, $currentUserId); if (!$uuids) { return ['ok' => false, 'error' => 'permission_denied']; } } if ($currentUserId > 0) { $currentUser = $this->userReadRepository->find($currentUserId); $currentUuid = $currentUser['uuid'] ?? ''; if ($currentUuid !== '') { $uuids = array_values(array_filter($uuids, static fn ($uuid) => $uuid !== $currentUuid)); } if (!$uuids) { return ['ok' => false, 'error' => 'self_delete']; } } $deleted = $this->userWriteRepository->deleteByUuids($uuids); if (!$deleted) { return ['ok' => false, 'error' => 'delete_failed']; } $this->systemAuditService->record('admin.users.bulk_update', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'user', 'metadata' => [ 'action' => 'delete', 'count' => count($uuids), 'target_uuids' => $uuids, ], ]); return ['ok' => true, 'count' => count($uuids)]; } public function createFromAdmin(array $input, int $currentUserId = 0): array { $form = $this->sanitizeBase($input); $form['totp_secret'] = trim((string) ($input['totp_secret'] ?? '')); $form['theme'] = $this->normalizeTheme($input['theme'] ?? null); $form['locale'] = $this->normalizeLocale($input['locale'] ?? null); $primaryTenantId = (int) ($input['primary_tenant_id'] ?? 0); if (array_key_exists('active', $input)) { $form['active'] = isset($input['active']) ? 1 : 0; } else { $form['active'] = 1; } $password = (string) ($input['password'] ?? ''); $password2 = (string) ($input['password2'] ?? ''); $errors = $this->validateBase($form); $errors = array_merge($errors, $this->userPasswordService->validatePassword($password, $password2, true, $form['email'])); $tenantIds = $input['tenant_ids'] ?? []; if (!is_array($tenantIds)) { $tenantIds = [$tenantIds]; } $tenantIds = $this->normalizeTenantIds($tenantIds); if (!$tenantIds) { $defaultTenantId = $this->settingsGateway->getDefaultTenantId(); if ($defaultTenantId) { $tenantIds = [$defaultTenantId]; } } [$primaryTenantId, $primaryErrors] = $this->normalizePrimaryTenant($primaryTenantId, $tenantIds); $errors = array_merge($errors, $primaryErrors); if ($errors) { $form['primary_tenant_id'] = $primaryTenantId; return ['ok' => false, 'errors' => $errors, 'form' => $form]; } $activeChangedAt = gmdate('Y-m-d H:i:s'); $transactionStarted = false; try { $this->databaseSessionRepository->beginTransaction(); $transactionStarted = true; $created = $this->userWriteRepository->create([ 'first_name' => $form['first_name'], 'last_name' => $form['last_name'], 'email' => $form['email'], 'profile_description' => $form['profile_description'] !== '' ? $form['profile_description'] : null, 'job_title' => $form['job_title'] !== '' ? $form['job_title'] : null, 'phone' => $form['phone'] !== '' ? $form['phone'] : null, 'mobile' => $form['mobile'] !== '' ? $form['mobile'] : null, 'short_dial' => $form['short_dial'] !== '' ? $form['short_dial'] : null, 'address' => $form['address'] !== '' ? $form['address'] : null, 'postal_code' => $form['postal_code'] !== '' ? $form['postal_code'] : null, 'city' => $form['city'] !== '' ? $form['city'] : null, 'country' => $form['country'] !== '' ? $form['country'] : null, 'region' => $form['region'] !== '' ? $form['region'] : null, 'hire_date' => $form['hire_date'] !== '' ? $form['hire_date'] : null, 'password' => $password, 'locale' => $form['locale'], 'totp_secret' => $form['totp_secret'], 'theme' => $form['theme'], 'primary_tenant_id' => $primaryTenantId > 0 ? $primaryTenantId : null, 'active' => $form['active'], 'created_by' => $currentUserId > 0 ? $currentUserId : null, 'active_changed_at' => $activeChangedAt, 'active_changed_by' => $currentUserId > 0 ? $currentUserId : null, ]); if (!$created) { $this->rollbackQuietly(); return ['ok' => false, 'errors' => [t('User can not be registered')], 'form' => $form]; } $createdUser = $this->userReadRepository->findByEmail($form['email']); $uuid = $createdUser['uuid'] ?? null; $userId = (int) ($createdUser['id'] ?? 0); if ($userId <= 0) { $this->rollbackQuietly(); return ['ok' => false, 'errors' => [t('User can not be registered')], 'form' => $form]; } if ($tenantIds && !$this->userAssignmentService->syncTenants($userId, $tenantIds)) { $this->rollbackQuietly(); return ['ok' => false, 'errors' => [t('User can not be registered')], 'form' => $form]; } $roleIds = $this->userAssignmentService->normalizeIdInput($input['role_ids'] ?? []); if (!$roleIds) { $defaultRoleId = $this->settingsGateway->getDefaultRoleId(); if ($defaultRoleId) { $roleIds = [$defaultRoleId]; } } if ($roleIds && !$this->userAssignmentService->syncRoles($userId, $roleIds)) { $this->rollbackQuietly(); return ['ok' => false, 'errors' => [t('User can not be registered')], 'form' => $form]; } $departmentIds = $this->userAssignmentService->normalizeIdInput($input['department_ids'] ?? []); if (!$departmentIds) { $defaultDepartmentId = $this->settingsGateway->getDefaultDepartmentId(); if ($defaultDepartmentId) { $departmentIds = [$defaultDepartmentId]; } } if ($departmentIds && !$this->userAssignmentService->syncDepartments($userId, $departmentIds)) { $this->rollbackQuietly(); return ['ok' => false, 'errors' => [t('User can not be registered')], 'form' => $form]; } $this->databaseSessionRepository->commitTransaction(); $transactionStarted = false; } catch (\Throwable $exception) { if ($transactionStarted) { $this->rollbackQuietly(); } return ['ok' => false, 'errors' => [t('User can not be registered')], 'form' => $form]; } $this->systemAuditService->record('admin.users.create', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'user', 'target_id' => $userId, 'target_uuid' => is_string($uuid) ? $uuid : '', 'metadata' => [ 'assigned_tenant_ids' => $tenantIds, 'assigned_role_ids' => $roleIds, 'assigned_department_ids' => $departmentIds, ], ]); return ['ok' => true, 'form' => $form, 'uuid' => $uuid]; } public function updateFromAdmin(int $userId, array $input, int $currentUserId = 0): array { $form = $this->sanitizeBase($input); $form['totp_secret'] = trim((string) ($input['totp_secret'] ?? '')); $form['locale'] = $this->normalizeLocale($input['locale'] ?? null); $themeProvided = array_key_exists('theme', $input); $primaryTenantId = (int) ($input['primary_tenant_id'] ?? 0); $tenantIdsProvided = array_key_exists('tenant_ids', $input); $primaryProvided = array_key_exists('primary_tenant_id', $input); $existing = $this->userReadRepository->find($userId) ?? []; if ($themeProvided) { $form['theme'] = $this->normalizeTheme($input['theme'] ?? null); } else { $form['theme'] = $this->normalizeTheme($existing['theme'] ?? null); } if ($tenantIdsProvided) { $tenantIds = $input['tenant_ids'] ?? []; if (!is_array($tenantIds)) { $tenantIds = [$tenantIds]; } $tenantIds = $this->normalizeTenantIds($tenantIds); } else { $tenantIds = $this->userAssignmentService->buildAssignmentsForUser($userId)['tenants'] ?? []; $tenantIds = array_values(array_map(static fn (array $tenant): int => (int) ($tenant['id'] ?? 0), $tenantIds)); $tenantIds = array_values(array_filter($tenantIds, static fn (int $id): bool => $id > 0)); } if (array_key_exists('active', $input)) { $form['active'] = isset($input['active']) ? 1 : 0; } else { $form['active'] = (int) ($existing['active'] ?? 1); } $password = (string) ($input['password'] ?? ''); $password2 = (string) ($input['password2'] ?? ''); $errors = $this->validateBase($form, $userId); if ($userId === $currentUserId && !$form['active']) { $errors[] = t('You cannot deactivate your own account'); } $errors = array_merge($errors, $this->userPasswordService->validatePassword($password, $password2, false, $form['email'])); if ($tenantIds && ($tenantIdsProvided || $primaryProvided)) { [$primaryTenantId, $primaryErrors] = $this->normalizePrimaryTenant($primaryTenantId, $tenantIds); $errors = array_merge($errors, $primaryErrors); } if ($errors) { if ($tenantIdsProvided || $primaryProvided) { $form['primary_tenant_id'] = $primaryTenantId; } return ['ok' => false, 'errors' => $errors, 'form' => $form]; } if ($tenantIdsProvided || $primaryProvided) { $form['primary_tenant_id'] = $primaryTenantId > 0 ? $primaryTenantId : null; } $updateData = [ 'first_name' => $form['first_name'], 'last_name' => $form['last_name'], 'email' => $form['email'], 'profile_description' => $form['profile_description'] !== '' ? $form['profile_description'] : null, 'job_title' => $form['job_title'] !== '' ? $form['job_title'] : null, 'phone' => $form['phone'] !== '' ? $form['phone'] : null, 'mobile' => $form['mobile'] !== '' ? $form['mobile'] : null, 'short_dial' => $form['short_dial'] !== '' ? $form['short_dial'] : null, 'address' => $form['address'] !== '' ? $form['address'] : null, 'postal_code' => $form['postal_code'] !== '' ? $form['postal_code'] : null, 'city' => $form['city'] !== '' ? $form['city'] : null, 'country' => $form['country'] !== '' ? $form['country'] : null, 'region' => $form['region'] !== '' ? $form['region'] : null, 'hire_date' => $form['hire_date'] !== '' ? $form['hire_date'] : null, 'totp_secret' => $form['totp_secret'], 'locale' => $form['locale'], 'active' => $form['active'], 'password' => $password, 'modified_by' => $currentUserId > 0 ? $currentUserId : null, ]; if ($themeProvided) { $updateData['theme'] = $form['theme']; } if ($tenantIdsProvided || $primaryProvided) { $updateData['primary_tenant_id'] = $form['primary_tenant_id'] ?? null; } $activeChanged = (int) ($existing['active'] ?? 1) !== (int) $form['active']; if ($activeChanged) { $updateData['active_changed_at'] = gmdate('Y-m-d H:i:s'); $updateData['active_changed_by'] = $currentUserId > 0 ? $currentUserId : null; } $updated = $this->userWriteRepository->update($userId, $updateData); if (!$updated) { return ['ok' => false, 'errors' => [t('User can not be updated')], 'form' => $form]; } if ($activeChanged) { $this->userAssignmentService->bumpAuthzVersion($userId); } $this->systemAuditService->record('admin.users.update', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'user', 'target_id' => $userId, 'target_uuid' => (string) ($existing['uuid'] ?? ''), 'before' => [ 'active' => $existing['active'] ?? null, 'locale' => $existing['locale'] ?? null, 'theme' => $existing['theme'] ?? null, 'primary_tenant_id' => $existing['primary_tenant_id'] ?? null, ], 'after' => [ 'active' => $form['active'], 'locale' => $form['locale'], 'theme' => $form['theme'], 'primary_tenant_id' => $form['primary_tenant_id'] ?? ($existing['primary_tenant_id'] ?? null), ], ]); return ['ok' => true, 'form' => $form]; } public function setActiveByUuid(string $uuid, bool $active, int $currentUserId = 0): array { $uuid = trim($uuid); if ($uuid === '') { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $user = $this->userReadRepository->findByUuid($uuid); if (!$user || !isset($user['id'])) { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $userId = (int) $user['id']; if (!$active && $currentUserId && $currentUserId === $userId) { return [ 'ok' => false, 'status' => 400, 'error' => 'self_deactivate', 'message' => t('You cannot deactivate your own account'), ]; } $updated = $this->userWriteRepository->setActive($userId, $active, $currentUserId > 0 ? $currentUserId : null); if (!$updated) { return ['ok' => false, 'status' => 500, 'error' => 'update_failed']; } $this->userAssignmentService->bumpAuthzVersion($userId); $this->systemAuditService->record($active ? 'admin.users.activate' : 'admin.users.deactivate', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'user', 'target_id' => $userId, 'target_uuid' => (string) ($user['uuid'] ?? ''), 'before' => ['active' => $user['active'] ?? null], 'after' => ['active' => $active ? 1 : 0], ]); return ['ok' => true, 'user' => $user]; } public function setActiveByUuids(array $uuids, bool $active, int $currentUserId = 0): array { $uuids = array_values(array_filter(array_map('trim', $uuids))); if (!$uuids) { return ['ok' => false, 'error' => 'no_selection']; } if ($currentUserId > 0) { $uuids = $this->filterUuidsByTenantScope($uuids, $currentUserId); if (!$uuids) { return ['ok' => false, 'error' => 'permission_denied']; } } $updated = $this->userWriteRepository->setActiveByUuids($uuids, $active, $currentUserId > 0 ? $currentUserId : null); if (!$updated) { return ['ok' => false, 'error' => 'update_failed']; } $userIds = []; foreach ($uuids as $uuid) { $user = $this->userReadRepository->findByUuid((string) $uuid); $userId = (int) ($user['id'] ?? 0); if ($userId > 0) { $userIds[] = $userId; } } if ($userIds) { $this->userWriteRepository->bumpAuthzVersionByUserIds($userIds); } $this->systemAuditService->record('admin.users.bulk_update', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'user', 'metadata' => [ 'action' => $active ? 'activate' : 'deactivate', 'count' => count($uuids), 'target_uuids' => $uuids, ], ]); return ['ok' => true, 'count' => count($uuids)]; } public function register(array $input): array { $form = $this->sanitizeBase($input); $password = (string) ($input['password'] ?? ''); $password2 = (string) ($input['password2'] ?? ''); $errors = $this->validateBase($form); $errors = array_merge($errors, $this->userPasswordService->validatePassword($password, $password2, true, $form['email'])); if ($errors) { return ['ok' => false, 'error' => $errors[0]]; } $defaultTheme = $this->settingsGateway->getAppTheme(); $defaultTenantId = $this->settingsGateway->getDefaultTenantId(); $activeChangedAt = gmdate('Y-m-d H:i:s'); $transactionStarted = false; try { $this->databaseSessionRepository->beginTransaction(); $transactionStarted = true; $created = $this->userWriteRepository->create([ 'first_name' => $form['first_name'], 'last_name' => $form['last_name'], 'email' => $form['email'], 'password' => $password, 'locale' => I18n::$locale, 'totp_secret' => '', 'theme' => $this->normalizeTheme($defaultTheme ?? 'light'), 'primary_tenant_id' => $defaultTenantId ?: null, 'active' => 1, 'created_by' => null, 'active_changed_at' => $activeChangedAt, 'active_changed_by' => null, ]); if (!$created) { $this->rollbackQuietly(); return ['ok' => false, 'error' => t('User can not be registered')]; } $createdUser = $this->userReadRepository->findByEmail($form['email']); $userId = (int) ($createdUser['id'] ?? 0); if ($userId <= 0) { $this->rollbackQuietly(); return ['ok' => false, 'error' => t('User can not be registered')]; } if ($defaultTenantId && !$this->userAssignmentService->syncTenants($userId, [$defaultTenantId])) { $this->rollbackQuietly(); return ['ok' => false, 'error' => t('User can not be registered')]; } $defaultRoleId = $this->settingsGateway->getDefaultRoleId(); if ($defaultRoleId && !$this->userAssignmentService->syncRoles($userId, [$defaultRoleId])) { $this->rollbackQuietly(); return ['ok' => false, 'error' => t('User can not be registered')]; } $defaultDepartmentId = $this->settingsGateway->getDefaultDepartmentId(); if ($defaultDepartmentId && !$this->userAssignmentService->syncDepartments($userId, [$defaultDepartmentId])) { $this->rollbackQuietly(); return ['ok' => false, 'error' => t('User can not be registered')]; } $this->databaseSessionRepository->commitTransaction(); $transactionStarted = false; } catch (\Throwable $exception) { if ($transactionStarted) { $this->rollbackQuietly(); } return ['ok' => false, 'error' => t('User can not be registered')]; } return ['ok' => true]; } /** * Self-service profile update (subset of fields, no admin-only changes). * * @param int $userId * @param array $input * @return array{ok: bool, errors?: list, form?: array} */ public function updateSelfProfile(int $userId, array $input): array { $existing = $this->userReadRepository->find($userId); if (!$existing) { return ['ok' => false, 'status' => 404, 'error' => 'not_found']; } $allowedFields = [ 'first_name', 'last_name', 'profile_description', 'job_title', 'phone', 'mobile', 'short_dial', 'address', 'postal_code', 'city', 'country', 'region', ]; $form = []; foreach ($allowedFields as $field) { $form[$field] = array_key_exists($field, $input) ? trim((string) $input[$field]) : ($existing[$field] ?? ''); } $errors = []; if ($form['first_name'] === '') { $errors[] = t('First name cannot be empty'); } if ($form['last_name'] === '') { $errors[] = t('Last name cannot be empty'); } $locale = array_key_exists('locale', $input) ? $this->normalizeLocale($input['locale']) : ($existing['locale'] ?? ''); $theme = array_key_exists('theme', $input) ? $this->normalizeTheme($input['theme']) : ($existing['theme'] ?? 'light'); // Tenant switch $currentTenantId = null; if (array_key_exists('current_tenant_uuid', $input)) { $tenantUuid = trim((string) ($input['current_tenant_uuid'] ?? '')); if ($tenantUuid !== '') { $tenant = $this->directoryGateway->findTenantByUuid($tenantUuid); if (!$tenant || !isset($tenant['id'])) { $errors[] = t('Tenant not found'); } else { $userTenantIds = $this->userAssignmentService->buildAssignmentsForUser($userId)['tenants'] ?? []; $userTenantIds = array_map(static fn (array $t): int => (int) ($t['id'] ?? 0), $userTenantIds); $tenantId = (int) $tenant['id']; if (!in_array($tenantId, $userTenantIds, true)) { $errors[] = t('No access to this tenant'); } else { $currentTenantId = $tenantId; } } } } if ($errors) { return ['ok' => false, 'errors' => $errors, 'form' => $form]; } $updateData = [ 'first_name' => $form['first_name'], 'last_name' => $form['last_name'], 'profile_description' => $form['profile_description'] !== '' ? $form['profile_description'] : null, 'job_title' => $form['job_title'] !== '' ? $form['job_title'] : null, 'phone' => $form['phone'] !== '' ? $form['phone'] : null, 'mobile' => $form['mobile'] !== '' ? $form['mobile'] : null, 'short_dial' => $form['short_dial'] !== '' ? $form['short_dial'] : null, 'address' => $form['address'] !== '' ? $form['address'] : null, 'postal_code' => $form['postal_code'] !== '' ? $form['postal_code'] : null, 'city' => $form['city'] !== '' ? $form['city'] : null, 'country' => $form['country'] !== '' ? $form['country'] : null, 'region' => $form['region'] !== '' ? $form['region'] : null, 'locale' => $locale, 'theme' => $theme, 'modified_by' => $userId, ]; $updated = $this->userWriteRepository->update($userId, $updateData); if (!$updated) { return ['ok' => false, 'errors' => [t('Profile can not be updated')]]; } if ($currentTenantId !== null) { $this->userWriteRepository->setCurrentTenant($userId, $currentTenantId); } return ['ok' => true, 'form' => $form]; } private function filterUuidsByTenantScope(array $uuids, int $currentUserId): array { $allowed = []; foreach ($uuids as $uuid) { $user = $this->userReadRepository->findByUuid((string) $uuid); if (!$user || !isset($user['id'])) { continue; } $userId = (int) $user['id']; if ($userId === $currentUserId) { $allowed[] = (string) $uuid; continue; } if ($this->scopeGateway->canAccess('users', $userId, $currentUserId)) { $allowed[] = (string) $uuid; } } return array_values(array_unique($allowed)); } private function sanitizeBase(array $input): array { return [ 'first_name' => trim((string) ($input['first_name'] ?? '')), 'last_name' => trim((string) ($input['last_name'] ?? '')), 'email' => trim((string) ($input['email'] ?? '')), 'profile_description' => trim((string) ($input['profile_description'] ?? '')), 'job_title' => trim((string) ($input['job_title'] ?? '')), 'phone' => trim((string) ($input['phone'] ?? '')), 'mobile' => trim((string) ($input['mobile'] ?? '')), 'short_dial' => trim((string) ($input['short_dial'] ?? '')), 'address' => trim((string) ($input['address'] ?? '')), 'postal_code' => trim((string) ($input['postal_code'] ?? '')), 'city' => trim((string) ($input['city'] ?? '')), 'country' => trim((string) ($input['country'] ?? '')), 'region' => trim((string) ($input['region'] ?? '')), 'hire_date' => trim((string) ($input['hire_date'] ?? '')), ]; } private function rollbackQuietly(): void { try { $this->databaseSessionRepository->rollbackTransaction(); } catch (\Throwable) { // Swallow — the original exception is more important. } } private function normalizeTheme($value): string { $theme = strtolower(trim((string) $value)); $themes = function_exists('appThemes') ? appThemes() : ['light' => 'Light', 'dark' => 'Dark']; if ($theme === '' || !isset($themes[$theme])) { return function_exists('appDefaultTheme') ? appDefaultTheme() : 'light'; } return $theme; } private function normalizeLocale($value): string { $locale = strtolower(trim((string) $value)); $available = defined('APP_LOCALES') ? APP_LOCALES : [I18n::$defaultLocale]; if ($locale === '' || !in_array($locale, $available, true)) { return I18n::$locale ?? I18n::$defaultLocale; } return $locale; } private function validateBase(array $form, ?int $excludeId = null): array { $errors = []; if ($form['first_name'] === '') { $errors[] = t('First name cannot be empty'); } if ($form['last_name'] === '') { $errors[] = t('Last name cannot be empty'); } if ($form['email'] === '') { $errors[] = t('Email cannot be empty'); } elseif (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) { $errors[] = t('Email is not valid'); } else { $existing = $this->userReadRepository->findByEmail($form['email']); if ($existing && (!isset($existing['id']) || (int) $existing['id'] !== (int) $excludeId)) { $errors[] = t('Email is already taken'); } } return $errors; } private function normalizePrimaryTenant(int $primaryTenantId, array $tenantIds): array { if (!$tenantIds) { return [0, []]; } if ($primaryTenantId > 0 && !in_array($primaryTenantId, $tenantIds, true)) { return [$primaryTenantId, [t('Primary tenant must be one of the assigned tenants')]]; } if ($primaryTenantId === 0 && count($tenantIds) > 1) { return [0, [t('Please select a primary tenant')]]; } // Single tenant assigned → auto-select it as primary to avoid requiring the user to pick. if ($primaryTenantId === 0 && count($tenantIds) === 1) { return [(int) $tenantIds[0], []]; } return [$primaryTenantId, []]; } private function normalizeTenantIds(array $tenantIds): array { return $this->userAssignmentService->normalizeTenantIds($tenantIds); } }