instances added god may help
This commit is contained in:
@@ -6,7 +6,7 @@ use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use MintyPHP\Service\Branding\BrandingLogoService;
|
||||
use MintyPHP\Service\Branding\BrandingServicesFactory;
|
||||
use Throwable;
|
||||
use ZipArchive;
|
||||
|
||||
@@ -168,12 +168,13 @@ class UserAccessPdfService
|
||||
$path = '';
|
||||
$mime = '';
|
||||
|
||||
$logoService = (new BrandingServicesFactory())->createBrandingLogoService();
|
||||
// Prefer tenant/app branding logo first.
|
||||
if (class_exists(BrandingLogoService::class) && BrandingLogoService::hasLogo()) {
|
||||
$logoPath = BrandingLogoService::findLogoPath(128);
|
||||
if ($logoService->hasLogo()) {
|
||||
$logoPath = $logoService->findLogoPath(128);
|
||||
if ($logoPath && is_file($logoPath)) {
|
||||
$path = $logoPath;
|
||||
$mime = BrandingLogoService::detectMime($logoPath);
|
||||
$mime = $logoService->detectMime($logoPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ namespace MintyPHP\Service\User;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Repository\Tenant\TenantRepository;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Service\Auth\TenantSsoService;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
|
||||
class UserAccessTemplateService
|
||||
{
|
||||
@@ -63,6 +62,7 @@ class UserAccessTemplateService
|
||||
|
||||
private static function resolveTenantLoginUrl(array $user, string $locale): string
|
||||
{
|
||||
$tenantSsoService = (new AuthServicesFactory())->createTenantSsoService();
|
||||
$tenantIds = [];
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
$currentTenantId = (int) ($user['current_tenant_id'] ?? 0);
|
||||
@@ -75,18 +75,21 @@ class UserAccessTemplateService
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if (!$tenantIds && $userId > 0) {
|
||||
$tenantIds = array_values(array_map('intval', UserTenantRepository::listTenantIdsByUserId($userId)));
|
||||
$tenantIds = array_values(array_map(
|
||||
'intval',
|
||||
(new UserServicesFactory())->createUserTenantRepository()->listTenantIdsByUserId($userId)
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tenant = TenantRepository::find($tenantId);
|
||||
$tenant = (new TenantRepository())->find($tenantId);
|
||||
if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
|
||||
continue;
|
||||
}
|
||||
$tenantSlug = TenantSsoService::tenantLoginSlug($tenant);
|
||||
$tenantSlug = $tenantSsoService->tenantLoginSlug($tenant);
|
||||
if ($tenantSlug !== '') {
|
||||
return appUrl(Request::withLocale('login?tenant=' . rawurlencode($tenantSlug), $locale));
|
||||
}
|
||||
|
||||
550
lib/Service/User/UserAccountService.php
Normal file
550
lib/Service/User/UserAccountService.php
Normal file
@@ -0,0 +1,550 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Repository\User\UserListQueryRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
|
||||
class UserAccountService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserReadRepository $userReadRepository,
|
||||
private readonly UserWriteRepository $userWriteRepository,
|
||||
private readonly UserListQueryRepository $userListQueryRepository,
|
||||
private readonly UserAssignmentService $userAssignmentService,
|
||||
private readonly UserPasswordService $userPasswordService,
|
||||
private readonly UserSettingsGateway $settingsGateway,
|
||||
private readonly UserScopeGateway $scopeGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function listPaged(array $options): array
|
||||
{
|
||||
if (!empty($options['tenantUserId'])) {
|
||||
$tenantUserId = (int) $options['tenantUserId'];
|
||||
if ($tenantUserId > 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'];
|
||||
}
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
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');
|
||||
$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) {
|
||||
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 && $tenantIds) {
|
||||
$this->userAssignmentService->syncTenants($userId, $tenantIds);
|
||||
}
|
||||
|
||||
$roleIds = $this->userAssignmentService->normalizeIdInput($input['role_ids'] ?? []);
|
||||
if (!$roleIds) {
|
||||
$defaultRoleId = $this->settingsGateway->getDefaultRoleId();
|
||||
if ($defaultRoleId) {
|
||||
$roleIds = [$defaultRoleId];
|
||||
}
|
||||
}
|
||||
if ($userId > 0 && $roleIds) {
|
||||
$this->userAssignmentService->syncRoles($userId, $roleIds);
|
||||
}
|
||||
|
||||
$departmentIds = $this->userAssignmentService->normalizeIdInput($input['department_ids'] ?? []);
|
||||
if (!$departmentIds) {
|
||||
$defaultDepartmentId = $this->settingsGateway->getDefaultDepartmentId();
|
||||
if ($defaultDepartmentId) {
|
||||
$departmentIds = [$defaultDepartmentId];
|
||||
}
|
||||
}
|
||||
if ($userId > 0 && $departmentIds) {
|
||||
$this->userAssignmentService->syncDepartments($userId, $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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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');
|
||||
$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) {
|
||||
return ['ok' => false, 'error' => t('User can not be registered')];
|
||||
}
|
||||
|
||||
$createdUser = $this->userReadRepository->findByEmail($form['email']);
|
||||
$userId = (int) ($createdUser['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
if ($defaultTenantId) {
|
||||
$this->userAssignmentService->syncTenants($userId, [$defaultTenantId]);
|
||||
}
|
||||
$defaultRoleId = $this->settingsGateway->getDefaultRoleId();
|
||||
if ($defaultRoleId) {
|
||||
$this->userAssignmentService->syncRoles($userId, [$defaultRoleId]);
|
||||
}
|
||||
$defaultDepartmentId = $this->settingsGateway->getDefaultDepartmentId();
|
||||
if ($defaultDepartmentId) {
|
||||
$this->userAssignmentService->syncDepartments($userId, [$defaultDepartmentId]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
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 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')]];
|
||||
}
|
||||
if ($primaryTenantId === 0 && count($tenantIds) === 1) {
|
||||
return [(int) $tenantIds[0], []];
|
||||
}
|
||||
return [$primaryTenantId, []];
|
||||
}
|
||||
|
||||
private function normalizeTenantIds(array $tenantIds): array
|
||||
{
|
||||
return $this->userAssignmentService->normalizeTenantIds($tenantIds);
|
||||
}
|
||||
}
|
||||
274
lib/Service/User/UserAssignmentService.php
Normal file
274
lib/Service/User/UserAssignmentService.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Repository\Access\UserRoleRepository;
|
||||
use MintyPHP\Repository\Org\UserDepartmentRepository;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
|
||||
class UserAssignmentService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserWriteRepository $userWriteRepository,
|
||||
private readonly UserTenantRepository $userTenantRepository,
|
||||
private readonly UserRoleRepository $userRoleRepository,
|
||||
private readonly UserDepartmentRepository $userDepartmentRepository,
|
||||
private readonly UserDirectoryGateway $directoryGateway,
|
||||
private readonly UserPermissionGateway $permissionGateway
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildAssignmentsForUser(int $userId): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return [
|
||||
'tenants' => [],
|
||||
'departments' => [],
|
||||
'roles' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$tenantIds = $this->userTenantRepository->listTenantIdsByUserId($userId);
|
||||
$departmentIds = $this->userDepartmentRepository->listDepartmentIdsByUserId($userId);
|
||||
$roleIds = $this->userRoleRepository->listRoleIdsByUserId($userId);
|
||||
|
||||
$tenantsById = [];
|
||||
foreach ($this->directoryGateway->listTenantsByIds($tenantIds) as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tenantsById[$tenantId] = [
|
||||
'id' => $tenantId,
|
||||
'uuid' => (string) ($tenant['uuid'] ?? ''),
|
||||
'description' => (string) ($tenant['description'] ?? ''),
|
||||
'status' => (string) ($tenant['status'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$departmentsById = [];
|
||||
foreach ($this->directoryGateway->listDepartmentsByIds($departmentIds, true) as $department) {
|
||||
$departmentId = (int) ($department['id'] ?? 0);
|
||||
if ($departmentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$departmentTenantId = (int) ($department['tenant_id'] ?? 0);
|
||||
$departmentsById[$departmentId] = [
|
||||
'id' => $departmentId,
|
||||
'uuid' => (string) ($department['uuid'] ?? ''),
|
||||
'description' => (string) ($department['description'] ?? ''),
|
||||
'active' => (bool) ($department['active'] ?? 0),
|
||||
'tenant_id' => $departmentTenantId,
|
||||
'tenant_uuid' => (string) (($tenantsById[$departmentTenantId]['uuid'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
$rolesById = [];
|
||||
foreach ($this->directoryGateway->listRolesByIds($roleIds) as $role) {
|
||||
$roleId = (int) ($role['id'] ?? 0);
|
||||
if ($roleId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$rolesById[$roleId] = [
|
||||
'id' => $roleId,
|
||||
'uuid' => (string) ($role['uuid'] ?? ''),
|
||||
'description' => (string) ($role['description'] ?? ''),
|
||||
'active' => (bool) ($role['active'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$tenants = [];
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
if (isset($tenantsById[$tenantId])) {
|
||||
$tenants[] = $tenantsById[$tenantId];
|
||||
}
|
||||
}
|
||||
|
||||
$departments = [];
|
||||
foreach ($departmentIds as $departmentId) {
|
||||
if (isset($departmentsById[$departmentId])) {
|
||||
$departments[] = $departmentsById[$departmentId];
|
||||
}
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
foreach ($roleIds as $roleId) {
|
||||
if (isset($rolesById[$roleId])) {
|
||||
$roles[] = $rolesById[$roleId];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'tenants' => $tenants,
|
||||
'departments' => $departments,
|
||||
'roles' => $roles,
|
||||
];
|
||||
}
|
||||
|
||||
public function findTenantSummaryInAssignments(array $assignments, int $tenantId): ?array
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (($assignments['tenants'] ?? []) as $tenant) {
|
||||
$currentTenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($currentTenantId !== $tenantId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'uuid' => (string) ($tenant['uuid'] ?? ''),
|
||||
'description' => (string) ($tenant['description'] ?? ''),
|
||||
'status' => (string) ($tenant['status'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function mapAssignmentsToPublic(array $assignments): array
|
||||
{
|
||||
return [
|
||||
'tenants' => array_values(array_map(
|
||||
static fn (array $tenant): array => [
|
||||
'uuid' => (string) ($tenant['uuid'] ?? ''),
|
||||
'description' => (string) ($tenant['description'] ?? ''),
|
||||
'status' => (string) ($tenant['status'] ?? ''),
|
||||
],
|
||||
$assignments['tenants'] ?? []
|
||||
)),
|
||||
'departments' => array_values(array_map(
|
||||
static fn (array $department): array => [
|
||||
'uuid' => (string) ($department['uuid'] ?? ''),
|
||||
'description' => (string) ($department['description'] ?? ''),
|
||||
'active' => (bool) ($department['active'] ?? 0),
|
||||
'tenant_uuid' => (string) ($department['tenant_uuid'] ?? ''),
|
||||
],
|
||||
$assignments['departments'] ?? []
|
||||
)),
|
||||
'roles' => array_values(array_map(
|
||||
static fn (array $role): array => [
|
||||
'uuid' => (string) ($role['uuid'] ?? ''),
|
||||
'description' => (string) ($role['description'] ?? ''),
|
||||
'active' => (bool) ($role['active'] ?? 0),
|
||||
],
|
||||
$assignments['roles'] ?? []
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
public function syncTenants(int $userId, array $tenantIds, bool $bumpAuthz = true): bool
|
||||
{
|
||||
$ids = $this->normalizeTenantIds($tenantIds);
|
||||
$result = $this->userTenantRepository->replaceForUser($userId, $ids);
|
||||
if ($result && $bumpAuthz) {
|
||||
$this->bumpAuthzVersion($userId);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function syncRoles(int $userId, array $roleIds, bool $bumpAuthz = true): bool
|
||||
{
|
||||
$ids = array_values(array_unique(array_map('intval', $roleIds)));
|
||||
$ids = array_values(array_filter($ids, static fn ($id) => $id > 0));
|
||||
$validIds = $this->directoryGateway->listActiveRoleIds();
|
||||
if ($validIds) {
|
||||
$validMap = array_fill_keys($validIds, true);
|
||||
$ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id])));
|
||||
}
|
||||
$result = $this->userRoleRepository->replaceForUser($userId, $ids);
|
||||
$this->permissionGateway->clearUserCache($userId);
|
||||
if ($result && $bumpAuthz) {
|
||||
$this->bumpAuthzVersion($userId);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function syncDepartments(int $userId, array $departmentIds, bool $bumpAuthz = true): bool
|
||||
{
|
||||
$ids = array_values(array_unique(array_map('intval', $departmentIds)));
|
||||
$ids = array_values(array_filter($ids, static fn ($id) => $id > 0));
|
||||
// Important: use the user's assigned tenants directly (no permission bypass),
|
||||
// otherwise privileged users (e.g. admins) would incorrectly allow departments
|
||||
// from tenants that were just removed from their assignments.
|
||||
$userTenantIds = array_values(array_unique(array_map(
|
||||
'intval',
|
||||
$this->userTenantRepository->listTenantIdsByUserId($userId)
|
||||
)));
|
||||
$userTenantIds = array_values(array_filter($userTenantIds, static fn ($id) => $id > 0));
|
||||
if (!$userTenantIds) {
|
||||
$ids = [];
|
||||
} else {
|
||||
$allowedIds = $this->directoryGateway->listActiveDepartmentIdsByTenantIds($userTenantIds);
|
||||
if ($allowedIds) {
|
||||
$allowedMap = array_fill_keys($allowedIds, true);
|
||||
$ids = array_values(array_filter($ids, static fn ($id) => isset($allowedMap[$id])));
|
||||
} else {
|
||||
$ids = [];
|
||||
}
|
||||
}
|
||||
$result = $this->userDepartmentRepository->replaceForUser($userId, $ids);
|
||||
if ($result && $bumpAuthz) {
|
||||
$this->bumpAuthzVersion($userId);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function bumpAuthzVersion(int $userId): void
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return;
|
||||
}
|
||||
$this->userWriteRepository->bumpAuthzVersion($userId);
|
||||
}
|
||||
|
||||
public function normalizeIdInput($value): array
|
||||
{
|
||||
$raw = is_array($value) ? $value : [$value];
|
||||
$flat = [];
|
||||
|
||||
$collect = static function ($item) use (&$collect, &$flat): void {
|
||||
if (is_array($item)) {
|
||||
foreach ($item as $nested) {
|
||||
$collect($nested);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$text = trim((string) $item);
|
||||
if ($text === '') {
|
||||
return;
|
||||
}
|
||||
if (str_contains($text, ',')) {
|
||||
foreach (explode(',', $text) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '') {
|
||||
$flat[] = $part;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
$flat[] = $text;
|
||||
};
|
||||
|
||||
foreach ($raw as $item) {
|
||||
$collect($item);
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique(array_map('intval', $flat)));
|
||||
return array_values(array_filter($ids, static fn ($id) => $id > 0));
|
||||
}
|
||||
|
||||
public function normalizeTenantIds(array $tenantIds): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_map('intval', $tenantIds)));
|
||||
$ids = array_filter($ids, static fn ($id) => $id > 0);
|
||||
$validIds = $this->directoryGateway->listTenantIds();
|
||||
if ($validIds) {
|
||||
$validMap = array_fill_keys($validIds, true);
|
||||
$ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id])));
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
@@ -12,38 +12,38 @@ class UserAvatarService
|
||||
private const SIZES = [64, 128, 256];
|
||||
private const DEFAULT_SIZE = 128;
|
||||
|
||||
public static function isValidUuid(string $uuid): bool
|
||||
public function isValidUuid(string $uuid): bool
|
||||
{
|
||||
return self::imageIsValidUuid($uuid);
|
||||
}
|
||||
|
||||
public static function storageBase(): string
|
||||
public function storageBase(): string
|
||||
{
|
||||
return self::imageStorageBase();
|
||||
}
|
||||
|
||||
public static function userDir(string $uuid): string
|
||||
public function userDir(string $uuid): string
|
||||
{
|
||||
return self::storageBase() . '/users/' . $uuid;
|
||||
return $this->storageBase() . '/users/' . $uuid;
|
||||
}
|
||||
|
||||
public static function findAvatarPath(string $uuid, ?int $size = null): ?string
|
||||
public function findAvatarPath(string $uuid, ?int $size = null): ?string
|
||||
{
|
||||
if (!self::isValidUuid($uuid)) {
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return null;
|
||||
}
|
||||
$dir = self::userDir($uuid);
|
||||
$dir = $this->userDir($uuid);
|
||||
if (!is_dir($dir)) {
|
||||
return null;
|
||||
}
|
||||
if ($size) {
|
||||
$size = self::normalizeSize($size);
|
||||
$variant = self::findVariantPath($dir, $size);
|
||||
$size = $this->normalizeSize($size);
|
||||
$variant = $this->findVariantPath($dir, $size);
|
||||
if ($variant) {
|
||||
return $variant;
|
||||
}
|
||||
}
|
||||
$defaultVariant = self::findVariantPath($dir, self::DEFAULT_SIZE);
|
||||
$defaultVariant = $this->findVariantPath($dir, self::DEFAULT_SIZE);
|
||||
if ($defaultVariant) {
|
||||
return $defaultVariant;
|
||||
}
|
||||
@@ -51,18 +51,18 @@ class UserAvatarService
|
||||
return $original ?: null;
|
||||
}
|
||||
|
||||
public static function hasAvatar(string $uuid): bool
|
||||
public function hasAvatar(string $uuid): bool
|
||||
{
|
||||
$path = self::findAvatarPath($uuid);
|
||||
$path = $this->findAvatarPath($uuid);
|
||||
return $path ? is_file($path) : false;
|
||||
}
|
||||
|
||||
public static function delete(string $uuid): bool
|
||||
public function delete(string $uuid): bool
|
||||
{
|
||||
if (!self::isValidUuid($uuid)) {
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return false;
|
||||
}
|
||||
$dir = self::userDir($uuid);
|
||||
$dir = $this->userDir($uuid);
|
||||
if (!is_dir($dir)) {
|
||||
return true;
|
||||
}
|
||||
@@ -79,9 +79,9 @@ class UserAvatarService
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function saveUpload(string $uuid, array $file): array
|
||||
public function saveUpload(string $uuid, array $file): array
|
||||
{
|
||||
if (!self::isValidUuid($uuid)) {
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return ['ok' => false, 'error' => t('User not found')];
|
||||
}
|
||||
if (empty($file) || !isset($file['tmp_name'])) {
|
||||
@@ -95,7 +95,7 @@ class UserAvatarService
|
||||
}
|
||||
|
||||
$tmpPath = $file['tmp_name'];
|
||||
$mime = self::detectMime($tmpPath);
|
||||
$mime = $this->detectMime($tmpPath);
|
||||
$isSvg = self::imageIsSvgUpload($mime, $tmpPath);
|
||||
$ext = self::imageExtensionForMime($mime, $isSvg);
|
||||
if (!$ext) {
|
||||
@@ -105,12 +105,12 @@ class UserAvatarService
|
||||
return ['ok' => false, 'error' => t('Invalid image file')];
|
||||
}
|
||||
|
||||
$dir = self::userDir($uuid);
|
||||
$dir = $this->userDir($uuid);
|
||||
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
|
||||
return ['ok' => false, 'error' => t('Upload failed')];
|
||||
}
|
||||
|
||||
self::delete($uuid);
|
||||
$this->delete($uuid);
|
||||
$originalPath = $dir . '/original.' . $ext;
|
||||
if (!move_uploaded_file($tmpPath, $originalPath)) {
|
||||
return ['ok' => false, 'error' => t('Upload failed')];
|
||||
@@ -128,9 +128,9 @@ class UserAvatarService
|
||||
return ['ok' => true, 'path' => $originalPath, 'mime' => $mime];
|
||||
}
|
||||
|
||||
public static function saveBinary(string $uuid, string $contents, string $mime = ''): array
|
||||
public function saveBinary(string $uuid, string $contents, string $mime = ''): array
|
||||
{
|
||||
if (!self::isValidUuid($uuid)) {
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return ['ok' => false, 'error' => t('User not found')];
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ class UserAvatarService
|
||||
return ['ok' => false, 'error' => t('Upload failed')];
|
||||
}
|
||||
|
||||
$detectedMime = self::detectMime($tmpPath);
|
||||
$detectedMime = $this->detectMime($tmpPath);
|
||||
if ($detectedMime === 'application/octet-stream') {
|
||||
$headerMime = strtolower(trim(explode(';', $mime, 2)[0]));
|
||||
if (in_array($headerMime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
|
||||
@@ -164,13 +164,13 @@ class UserAvatarService
|
||||
return ['ok' => false, 'error' => t('Invalid image file')];
|
||||
}
|
||||
|
||||
$dir = self::userDir($uuid);
|
||||
$dir = $this->userDir($uuid);
|
||||
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
|
||||
@unlink($tmpPath);
|
||||
return ['ok' => false, 'error' => t('Upload failed')];
|
||||
}
|
||||
|
||||
self::delete($uuid);
|
||||
$this->delete($uuid);
|
||||
$originalPath = $dir . '/original.' . $ext;
|
||||
if (!@rename($tmpPath, $originalPath)) {
|
||||
if (!@copy($tmpPath, $originalPath)) {
|
||||
@@ -192,12 +192,12 @@ class UserAvatarService
|
||||
return ['ok' => true, 'path' => $originalPath, 'mime' => $detectedMime];
|
||||
}
|
||||
|
||||
public static function detectMime(string $path): string
|
||||
public function detectMime(string $path): string
|
||||
{
|
||||
return self::imageDetectMime($path);
|
||||
}
|
||||
|
||||
private static function normalizeSize(int $size): int
|
||||
private function normalizeSize(int $size): int
|
||||
{
|
||||
if (in_array($size, self::SIZES, true)) {
|
||||
return $size;
|
||||
@@ -205,7 +205,7 @@ class UserAvatarService
|
||||
return self::DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
private static function findVariantPath(string $dir, int $size): ?string
|
||||
private function findVariantPath(string $dir, int $size): ?string
|
||||
{
|
||||
$matches = glob($dir . '/avatar-' . $size . '.*');
|
||||
if (!$matches) {
|
||||
|
||||
55
lib/Service/User/UserDirectoryGateway.php
Normal file
55
lib/Service/User/UserDirectoryGateway.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Repository\Access\RoleRepository;
|
||||
use MintyPHP\Repository\Org\DepartmentRepository;
|
||||
use MintyPHP\Repository\Tenant\TenantRepository;
|
||||
|
||||
class UserDirectoryGateway
|
||||
{
|
||||
public function listTenantIds(): array
|
||||
{
|
||||
return (new TenantRepository())->listIds();
|
||||
}
|
||||
|
||||
public function listTenantsByIds(array $tenantIds): array
|
||||
{
|
||||
return (new TenantRepository())->listByIds($tenantIds);
|
||||
}
|
||||
|
||||
public function findTenant(int $tenantId): ?array
|
||||
{
|
||||
return (new TenantRepository())->find($tenantId);
|
||||
}
|
||||
|
||||
public function listActiveTenantIdsByIds(array $tenantIds): array
|
||||
{
|
||||
return (new TenantRepository())->listActiveIdsByIds($tenantIds);
|
||||
}
|
||||
|
||||
public function listActiveRoleIds(): array
|
||||
{
|
||||
return (new RoleRepository())->listActiveIds();
|
||||
}
|
||||
|
||||
public function listRolesByIds(array $roleIds): array
|
||||
{
|
||||
return (new RoleRepository())->listByIds($roleIds);
|
||||
}
|
||||
|
||||
public function listDepartmentsByIds(array $departmentIds, bool $includeInactive = true): array
|
||||
{
|
||||
return (new DepartmentRepository())->listByIds($departmentIds, $includeInactive);
|
||||
}
|
||||
|
||||
public function listActiveDepartmentIdsByTenantIds(array $tenantIds): array
|
||||
{
|
||||
return (new DepartmentRepository())->listActiveIdsByTenantIds($tenantIds);
|
||||
}
|
||||
|
||||
public function listDepartmentsByTenantIds(array $tenantIds): array
|
||||
{
|
||||
return (new DepartmentRepository())->listByTenantIds($tenantIds);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,20 @@ namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Repository\Support\RepoQuery;
|
||||
use MintyPHP\Repository\User\UserRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
|
||||
class UserLifecycleRestoreService
|
||||
{
|
||||
public static function restoreFromLifecycleAudit(int $auditId, int $actorUserId): array
|
||||
public function __construct(
|
||||
private readonly UserReadRepository $userReadRepository,
|
||||
private readonly UserWriteRepository $userWriteRepository,
|
||||
private readonly UserLifecycleAuditService $userLifecycleAuditService
|
||||
) {
|
||||
}
|
||||
|
||||
public function restoreFromLifecycleAudit(int $auditId, int $actorUserId): array
|
||||
{
|
||||
if ($auditId <= 0 || $actorUserId <= 0) {
|
||||
return ['ok' => false, 'error' => 'invalid_request'];
|
||||
@@ -18,7 +26,7 @@ class UserLifecycleRestoreService
|
||||
$db = DB::handle();
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
$event = UserLifecycleAuditService::findDeleteEventForRestore($auditId, true);
|
||||
$event = $this->userLifecycleAuditService->findDeleteEventForRestore($auditId, true);
|
||||
if (!$event) {
|
||||
$db->rollback();
|
||||
return ['ok' => false, 'error' => 'audit_event_not_found'];
|
||||
@@ -28,7 +36,7 @@ class UserLifecycleRestoreService
|
||||
return ['ok' => false, 'error' => 'audit_event_already_restored'];
|
||||
}
|
||||
|
||||
$snapshot = UserLifecycleAuditService::decryptSnapshot($event);
|
||||
$snapshot = $this->userLifecycleAuditService->decryptSnapshot($event);
|
||||
if (!$snapshot) {
|
||||
$db->rollback();
|
||||
return ['ok' => false, 'error' => 'snapshot_unavailable'];
|
||||
@@ -41,35 +49,35 @@ class UserLifecycleRestoreService
|
||||
return ['ok' => false, 'error' => 'snapshot_invalid'];
|
||||
}
|
||||
|
||||
if (UserRepository::findByUuid($uuid)) {
|
||||
if ($this->userReadRepository->findByUuid($uuid)) {
|
||||
$db->rollback();
|
||||
return ['ok' => false, 'error' => 'restore_uuid_exists'];
|
||||
}
|
||||
if (UserRepository::findByEmail($email)) {
|
||||
if ($this->userReadRepository->findByEmail($email)) {
|
||||
$db->rollback();
|
||||
return ['ok' => false, 'error' => 'restore_email_exists'];
|
||||
}
|
||||
|
||||
$createdId = UserRepository::create([
|
||||
$createdId = $this->userWriteRepository->create([
|
||||
'uuid' => $uuid,
|
||||
'first_name' => trim((string) ($snapshot['first_name'] ?? '')),
|
||||
'last_name' => trim((string) ($snapshot['last_name'] ?? '')),
|
||||
'email' => $email,
|
||||
'profile_description' => self::nullableText($snapshot['profile_description'] ?? null),
|
||||
'job_title' => self::nullableText($snapshot['job_title'] ?? null),
|
||||
'phone' => self::nullableText($snapshot['phone'] ?? null),
|
||||
'mobile' => self::nullableText($snapshot['mobile'] ?? null),
|
||||
'short_dial' => self::nullableText($snapshot['short_dial'] ?? null),
|
||||
'address' => self::nullableText($snapshot['address'] ?? null),
|
||||
'postal_code' => self::nullableText($snapshot['postal_code'] ?? null),
|
||||
'city' => self::nullableText($snapshot['city'] ?? null),
|
||||
'country' => self::nullableText($snapshot['country'] ?? null),
|
||||
'region' => self::nullableText($snapshot['region'] ?? null),
|
||||
'hire_date' => self::nullableDate($snapshot['hire_date'] ?? null),
|
||||
'password' => self::randomPassword(),
|
||||
'locale' => self::nullableText($snapshot['locale'] ?? null),
|
||||
'profile_description' => $this->nullableText($snapshot['profile_description'] ?? null),
|
||||
'job_title' => $this->nullableText($snapshot['job_title'] ?? null),
|
||||
'phone' => $this->nullableText($snapshot['phone'] ?? null),
|
||||
'mobile' => $this->nullableText($snapshot['mobile'] ?? null),
|
||||
'short_dial' => $this->nullableText($snapshot['short_dial'] ?? null),
|
||||
'address' => $this->nullableText($snapshot['address'] ?? null),
|
||||
'postal_code' => $this->nullableText($snapshot['postal_code'] ?? null),
|
||||
'city' => $this->nullableText($snapshot['city'] ?? null),
|
||||
'country' => $this->nullableText($snapshot['country'] ?? null),
|
||||
'region' => $this->nullableText($snapshot['region'] ?? null),
|
||||
'hire_date' => $this->nullableDate($snapshot['hire_date'] ?? null),
|
||||
'password' => $this->randomPassword(),
|
||||
'locale' => $this->nullableText($snapshot['locale'] ?? null),
|
||||
'totp_secret' => '',
|
||||
'theme' => self::normalizeTheme($snapshot['theme'] ?? null),
|
||||
'theme' => $this->normalizeTheme($snapshot['theme'] ?? null),
|
||||
'primary_tenant_id' => null,
|
||||
'current_tenant_id' => null,
|
||||
'created_by' => $actorUserId,
|
||||
@@ -83,13 +91,17 @@ class UserLifecycleRestoreService
|
||||
}
|
||||
$restoredUserId = (int) $createdId;
|
||||
|
||||
$marked = UserLifecycleAuditService::markDeleteEventRestored($auditId, $actorUserId, $restoredUserId);
|
||||
$marked = $this->userLifecycleAuditService->markDeleteEventRestored(
|
||||
$auditId,
|
||||
$actorUserId,
|
||||
$restoredUserId
|
||||
);
|
||||
if (!$marked) {
|
||||
$db->rollback();
|
||||
return ['ok' => false, 'error' => 'restore_mark_failed'];
|
||||
}
|
||||
|
||||
UserLifecycleAuditService::logRestore(
|
||||
$this->userLifecycleAuditService->logRestore(
|
||||
RepoQuery::uuidV4(),
|
||||
'manual',
|
||||
[
|
||||
@@ -123,13 +135,13 @@ class UserLifecycleRestoreService
|
||||
}
|
||||
}
|
||||
|
||||
private static function nullableText(mixed $value): ?string
|
||||
private function nullableText(mixed $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
return $value !== '' ? $value : null;
|
||||
}
|
||||
|
||||
private static function nullableDate(mixed $value): ?string
|
||||
private function nullableDate(mixed $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
@@ -141,16 +153,15 @@ class UserLifecycleRestoreService
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function randomPassword(): string
|
||||
private function randomPassword(): string
|
||||
{
|
||||
return bin2hex(random_bytes(24));
|
||||
}
|
||||
|
||||
private static function normalizeTheme(mixed $value): string
|
||||
private function normalizeTheme(mixed $value): string
|
||||
{
|
||||
$theme = strtolower(trim((string) $value));
|
||||
$themes = appThemes();
|
||||
return isset($themes[$theme]) ? $theme : appDefaultTheme();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,29 +3,37 @@
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Repository\User\UserRepository;
|
||||
use MintyPHP\Repository\Support\RepoQuery;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Service\Settings\SettingService;
|
||||
|
||||
class UserLifecycleService
|
||||
{
|
||||
private const LOCK_NAME = 'user_lifecycle_maintenance';
|
||||
private const BATCH_SIZE = 500;
|
||||
|
||||
public static function run(?int $actorUserId = null): array
|
||||
public function __construct(
|
||||
private readonly UserReadRepository $userReadRepository,
|
||||
private readonly UserWriteRepository $userWriteRepository,
|
||||
private readonly UserSettingsGateway $settingsGateway,
|
||||
private readonly UserLifecycleAuditService $userLifecycleAuditService
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(?int $actorUserId = null): array
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
$runUuid = RepoQuery::uuidV4();
|
||||
$triggerType = ($actorUserId ?? 0) > 0 ? 'manual' : 'cron';
|
||||
$deactivateDays = SettingService::getUserInactivityDeactivateDays();
|
||||
$deleteDays = SettingService::getUserInactivityDeleteDays();
|
||||
$deactivateDays = $this->settingsGateway->getUserInactivityDeactivateDays();
|
||||
$deleteDays = $this->settingsGateway->getUserInactivityDeleteDays();
|
||||
if ($deactivateDays <= 0) {
|
||||
$deleteDays = 0;
|
||||
}
|
||||
|
||||
$privilegedUserIds = UserRepository::listPrivilegedUserIdsByPermissionKeys([
|
||||
$privilegedUserIds = $this->userReadRepository->listPrivilegedUserIdsByPermissionKeys([
|
||||
PermissionService::SETTINGS_UPDATE,
|
||||
PermissionService::TENANTS_UPDATE,
|
||||
]);
|
||||
@@ -46,30 +54,30 @@ class UserLifecycleService
|
||||
'duration_ms' => 0,
|
||||
];
|
||||
|
||||
if (!self::acquireLock()) {
|
||||
if (!$this->acquireLock()) {
|
||||
$result['ok'] = false;
|
||||
$result['locked'] = true;
|
||||
$result['error'] = 'lock_not_acquired';
|
||||
$result['duration_ms'] = self::durationMs($startedAt);
|
||||
$result['duration_ms'] = $this->durationMs($startedAt);
|
||||
return $result;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($deactivateDays > 0) {
|
||||
while (true) {
|
||||
$ids = UserRepository::listIdsForAutoDeactivate($deactivateDays, $privilegedUserIds, self::BATCH_SIZE);
|
||||
$ids = $this->userReadRepository->listIdsForAutoDeactivate($deactivateDays, $privilegedUserIds, self::BATCH_SIZE);
|
||||
if (!$ids) {
|
||||
break;
|
||||
}
|
||||
|
||||
$usersById = [];
|
||||
foreach ($ids as $id) {
|
||||
$user = UserRepository::find((int) $id);
|
||||
$user = $this->userReadRepository->find((int) $id);
|
||||
if ($user) {
|
||||
$usersById[(int) $id] = $user;
|
||||
}
|
||||
}
|
||||
$updated = UserRepository::setInactiveByIds(
|
||||
$updated = $this->userWriteRepository->setInactiveByIds(
|
||||
$ids,
|
||||
$actorUserId && $actorUserId > 0 ? $actorUserId : null
|
||||
);
|
||||
@@ -79,12 +87,12 @@ class UserLifecycleService
|
||||
break;
|
||||
}
|
||||
$result['deactivated_count'] += $updated;
|
||||
UserRepository::bumpAuthzVersionByUserIds($ids);
|
||||
$this->userWriteRepository->bumpAuthzVersionByUserIds($ids);
|
||||
foreach ($ids as $id) {
|
||||
if (!isset($usersById[(int) $id])) {
|
||||
continue;
|
||||
}
|
||||
UserLifecycleAuditService::logDeactivate(
|
||||
$this->userLifecycleAuditService->logDeactivate(
|
||||
$runUuid,
|
||||
$triggerType,
|
||||
$result['policy'],
|
||||
@@ -102,18 +110,18 @@ class UserLifecycleService
|
||||
|
||||
if ($deleteDays > 0) {
|
||||
while (true) {
|
||||
$ids = UserRepository::listIdsForAutoDelete($deleteDays, $privilegedUserIds, self::BATCH_SIZE);
|
||||
$ids = $this->userReadRepository->listIdsForAutoDelete($deleteDays, $privilegedUserIds, self::BATCH_SIZE);
|
||||
if (!$ids) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$user = UserRepository::find((int) $id);
|
||||
$user = $this->userReadRepository->find((int) $id);
|
||||
if (!$user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$auditId = UserLifecycleAuditService::logDeleteWithSnapshot(
|
||||
$auditId = $this->userLifecycleAuditService->logDeleteWithSnapshot(
|
||||
$runUuid,
|
||||
$triggerType,
|
||||
$result['policy'],
|
||||
@@ -122,7 +130,7 @@ class UserLifecycleService
|
||||
);
|
||||
if (!$auditId) {
|
||||
$result['skipped_count']++;
|
||||
UserLifecycleAuditService::logDeleteFailure(
|
||||
$this->userLifecycleAuditService->logDeleteFailure(
|
||||
$runUuid,
|
||||
$triggerType,
|
||||
$result['policy'],
|
||||
@@ -133,10 +141,10 @@ class UserLifecycleService
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleted = UserRepository::deleteByIds([(int) $id]);
|
||||
$deleted = $this->userWriteRepository->deleteByIds([(int) $id]);
|
||||
if ($deleted <= 0) {
|
||||
$result['skipped_count']++;
|
||||
UserLifecycleAuditService::updateStatus((int) $auditId, 'failed', 'delete_failed');
|
||||
$this->userLifecycleAuditService->updateStatus((int) $auditId, 'failed', 'delete_failed');
|
||||
continue;
|
||||
}
|
||||
$result['deleted_count'] += $deleted;
|
||||
@@ -150,20 +158,20 @@ class UserLifecycleService
|
||||
$result['ok'] = false;
|
||||
$result['error'] = 'unexpected_error';
|
||||
} finally {
|
||||
self::releaseLock();
|
||||
$result['duration_ms'] = self::durationMs($startedAt);
|
||||
$this->releaseLock();
|
||||
$result['duration_ms'] = $this->durationMs($startedAt);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function acquireLock(): bool
|
||||
private function acquireLock(): bool
|
||||
{
|
||||
$gotLock = DB::selectValue('select GET_LOCK(?, 0) as got_lock', self::LOCK_NAME);
|
||||
return (int) $gotLock === 1;
|
||||
}
|
||||
|
||||
private static function releaseLock(): void
|
||||
private function releaseLock(): void
|
||||
{
|
||||
try {
|
||||
DB::selectValue('select RELEASE_LOCK(?) as released_lock', self::LOCK_NAME);
|
||||
@@ -172,7 +180,7 @@ class UserLifecycleService
|
||||
}
|
||||
}
|
||||
|
||||
private static function durationMs(float $startedAt): int
|
||||
private function durationMs(float $startedAt): int
|
||||
{
|
||||
return (int) max(0, round((microtime(true) - $startedAt) * 1000));
|
||||
}
|
||||
|
||||
74
lib/Service/User/UserPasswordPolicyService.php
Normal file
74
lib/Service/User/UserPasswordPolicyService.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
class UserPasswordPolicyService
|
||||
{
|
||||
private const PASSWORD_MIN_LENGTH = 12;
|
||||
|
||||
public function validate(string $password, string $password2, bool $required, ?string $email): array
|
||||
{
|
||||
$errors = [];
|
||||
if ($required && $password === '') {
|
||||
$errors[] = $this->translate('Password cannot be empty');
|
||||
return $errors;
|
||||
}
|
||||
if ($password !== '' && $password !== $password2) {
|
||||
$errors[] = $this->translate('Passwords must match');
|
||||
}
|
||||
if ($password === '') {
|
||||
return $errors;
|
||||
}
|
||||
if (strlen($password) < self::PASSWORD_MIN_LENGTH) {
|
||||
$errors[] = $this->translate('Password must be at least %d characters', self::PASSWORD_MIN_LENGTH);
|
||||
}
|
||||
if (!preg_match('/[A-Z]/', $password)) {
|
||||
$errors[] = $this->translate('Password must include an uppercase letter');
|
||||
}
|
||||
if (!preg_match('/[a-z]/', $password)) {
|
||||
$errors[] = $this->translate('Password must include a lowercase letter');
|
||||
}
|
||||
if (!preg_match('/\d/', $password)) {
|
||||
$errors[] = $this->translate('Password must include a number');
|
||||
}
|
||||
if (!preg_match('/[^a-zA-Z0-9]/', $password)) {
|
||||
$errors[] = $this->translate('Password must include a symbol');
|
||||
}
|
||||
if ($email !== null) {
|
||||
$email = trim($email);
|
||||
if ($email !== '' && stripos($password, $email) !== false) {
|
||||
$errors[] = $this->translate('Password must not contain your email');
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public function hints(): array
|
||||
{
|
||||
return [
|
||||
['rule' => 'min', 'text' => $this->translate('At least %d characters', self::PASSWORD_MIN_LENGTH)],
|
||||
['rule' => 'upper', 'text' => $this->translate('At least one uppercase letter')],
|
||||
['rule' => 'lower', 'text' => $this->translate('At least one lowercase letter')],
|
||||
['rule' => 'number', 'text' => $this->translate('At least one number')],
|
||||
['rule' => 'symbol', 'text' => $this->translate('At least one symbol')],
|
||||
['rule' => 'email', 'text' => $this->translate('Must not contain your email')],
|
||||
];
|
||||
}
|
||||
|
||||
public function minLength(): int
|
||||
{
|
||||
return self::PASSWORD_MIN_LENGTH;
|
||||
}
|
||||
|
||||
private function translate(string $text, mixed ...$args): string
|
||||
{
|
||||
if (function_exists('t')) {
|
||||
return t($text, ...$args);
|
||||
}
|
||||
if ($args === []) {
|
||||
return $text;
|
||||
}
|
||||
return vsprintf($text, array_map(static fn ($arg) => (string) $arg, $args));
|
||||
}
|
||||
}
|
||||
48
lib/Service/User/UserPasswordService.php
Normal file
48
lib/Service/User/UserPasswordService.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
|
||||
class UserPasswordService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserPasswordPolicyService $passwordPolicyService,
|
||||
private readonly UserWriteRepository $userWriteRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function validatePassword(
|
||||
string $password,
|
||||
string $password2,
|
||||
bool $required,
|
||||
?string $email
|
||||
): array {
|
||||
return $this->passwordPolicyService->validate($password, $password2, $required, $email);
|
||||
}
|
||||
|
||||
public function passwordHints(): array
|
||||
{
|
||||
return $this->passwordPolicyService->hints();
|
||||
}
|
||||
|
||||
public function passwordMinLength(): int
|
||||
{
|
||||
return $this->passwordPolicyService->minLength();
|
||||
}
|
||||
|
||||
public function resetPassword(int $userId, string $password, string $password2): array
|
||||
{
|
||||
$errors = $this->passwordPolicyService->validate($password, $password2, true, '');
|
||||
if ($errors) {
|
||||
return ['ok' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
$updated = $this->userWriteRepository->setPassword($userId, $password);
|
||||
if (!$updated) {
|
||||
return ['ok' => false, 'errors' => [t('Password can not be updated')]];
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
}
|
||||
17
lib/Service/User/UserPermissionGateway.php
Normal file
17
lib/Service/User/UserPermissionGateway.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
|
||||
class UserPermissionGateway
|
||||
{
|
||||
public function __construct(private readonly PermissionGateway $permissionGateway)
|
||||
{
|
||||
}
|
||||
|
||||
public function clearUserCache(int $userId): void
|
||||
{
|
||||
$this->permissionGateway->clearUserCache($userId);
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,31 @@ class UserSavedFilterService
|
||||
private const SEARCH_MAX_LENGTH = 200;
|
||||
private const MAX_PER_USER_CONTEXT = 20;
|
||||
|
||||
public static function listForAddressBook(int $userId): array
|
||||
public function __construct(private readonly UserSavedFilterRepository $userSavedFilterRepository)
|
||||
{
|
||||
return self::listByContext($userId, self::CONTEXT_ADDRESS_BOOK);
|
||||
}
|
||||
|
||||
public static function saveAddressBookFilter(int $userId, string $name, array $rawQuery): array
|
||||
public function listForAddressBook(int $userId): array
|
||||
{
|
||||
return self::saveByContext($userId, self::CONTEXT_ADDRESS_BOOK, $name, $rawQuery);
|
||||
return $this->listByContext($userId, self::CONTEXT_ADDRESS_BOOK);
|
||||
}
|
||||
|
||||
public static function deleteAddressBookFilter(int $userId, string $uuid): bool
|
||||
public function saveAddressBookFilter(int $userId, string $name, array $rawQuery): array
|
||||
{
|
||||
return self::deleteByContext($userId, self::CONTEXT_ADDRESS_BOOK, $uuid);
|
||||
return $this->saveByContext($userId, self::CONTEXT_ADDRESS_BOOK, $name, $rawQuery);
|
||||
}
|
||||
|
||||
public static function listByContext(int $userId, string $context): array
|
||||
public function deleteAddressBookFilter(int $userId, string $uuid): bool
|
||||
{
|
||||
return $this->deleteByContext($userId, self::CONTEXT_ADDRESS_BOOK, $uuid);
|
||||
}
|
||||
|
||||
public function listByContext(int $userId, string $context): array
|
||||
{
|
||||
if ($userId <= 0 || $context === '') {
|
||||
return [];
|
||||
}
|
||||
$rows = UserSavedFilterRepository::listByUserAndContext($userId, $context);
|
||||
$rows = $this->userSavedFilterRepository->listByUserAndContext($userId, $context);
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$name = trim((string) ($row['name'] ?? ''));
|
||||
@@ -40,7 +44,7 @@ class UserSavedFilterService
|
||||
if ($name === '' || $uuid === '') {
|
||||
continue;
|
||||
}
|
||||
$query = self::normalizeQuery(self::decodeQuery((string) ($row['query_json'] ?? '')));
|
||||
$query = $this->normalizeQuery($this->decodeQuery((string) ($row['query_json'] ?? '')));
|
||||
$list[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'uuid' => $uuid,
|
||||
@@ -52,25 +56,25 @@ class UserSavedFilterService
|
||||
return $list;
|
||||
}
|
||||
|
||||
public static function saveByContext(int $userId, string $context, string $name, array $rawQuery): array
|
||||
public function saveByContext(int $userId, string $context, string $name, array $rawQuery): array
|
||||
{
|
||||
$name = self::truncate(trim($name), self::NAME_MAX_LENGTH);
|
||||
$name = $this->truncate(trim($name), self::NAME_MAX_LENGTH);
|
||||
if ($userId <= 0 || $context === '') {
|
||||
return ['ok' => false, 'error' => 'invalid_user_or_context'];
|
||||
}
|
||||
if ($name === '') {
|
||||
return ['ok' => false, 'error' => 'name_required'];
|
||||
}
|
||||
$count = UserSavedFilterRepository::countByUserAndContext($userId, $context);
|
||||
$count = $this->userSavedFilterRepository->countByUserAndContext($userId, $context);
|
||||
if ($count >= self::MAX_PER_USER_CONTEXT) {
|
||||
return ['ok' => false, 'error' => 'max_reached'];
|
||||
}
|
||||
$query = self::normalizeQuery($rawQuery);
|
||||
$query = $this->normalizeQuery($rawQuery);
|
||||
$queryJson = json_encode($query, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($queryJson)) {
|
||||
$queryJson = '{}';
|
||||
}
|
||||
$id = UserSavedFilterRepository::create([
|
||||
$id = $this->userSavedFilterRepository->create([
|
||||
'user_id' => $userId,
|
||||
'context' => $context,
|
||||
'name' => $name,
|
||||
@@ -82,27 +86,27 @@ class UserSavedFilterService
|
||||
return ['ok' => true, 'id' => (int) $id, 'name' => $name, 'query' => $query];
|
||||
}
|
||||
|
||||
public static function deleteByContext(int $userId, string $context, string $uuid): bool
|
||||
public function deleteByContext(int $userId, string $context, string $uuid): bool
|
||||
{
|
||||
if ($userId <= 0 || $context === '' || trim($uuid) === '') {
|
||||
return false;
|
||||
}
|
||||
return UserSavedFilterRepository::deleteByUuidForUserAndContext($uuid, $userId, $context);
|
||||
return $this->userSavedFilterRepository->deleteByUuidForUserAndContext($uuid, $userId, $context);
|
||||
}
|
||||
|
||||
private static function decodeQuery(string $json): array
|
||||
private function decodeQuery(string $json): array
|
||||
{
|
||||
$decoded = json_decode($json, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private static function normalizeQuery(array $rawQuery): array
|
||||
private function normalizeQuery(array $rawQuery): array
|
||||
{
|
||||
$search = self::truncate(trim((string) ($rawQuery['search'] ?? '')), self::SEARCH_MAX_LENGTH);
|
||||
$tenants = self::normalizeUuidList($rawQuery['tenants'] ?? []);
|
||||
$search = $this->truncate(trim((string) ($rawQuery['search'] ?? '')), self::SEARCH_MAX_LENGTH);
|
||||
$tenants = $this->normalizeUuidList($rawQuery['tenants'] ?? []);
|
||||
$departments = RepoQuery::normalizeIdList($rawQuery['departments'] ?? []);
|
||||
$roles = RepoQuery::normalizeIdList($rawQuery['roles'] ?? []);
|
||||
$custom = self::normalizeCustomFieldQuery($rawQuery);
|
||||
$custom = $this->normalizeCustomFieldQuery($rawQuery);
|
||||
|
||||
$query = [];
|
||||
if ($search !== '') {
|
||||
@@ -124,7 +128,7 @@ class UserSavedFilterService
|
||||
return $query;
|
||||
}
|
||||
|
||||
private static function normalizeCustomFieldQuery(array $rawQuery): array
|
||||
private function normalizeCustomFieldQuery(array $rawQuery): array
|
||||
{
|
||||
$normalized = [];
|
||||
foreach ($rawQuery as $rawKey => $rawValue) {
|
||||
@@ -158,9 +162,9 @@ class UserSavedFilterService
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private static function normalizeUuidList($value): array
|
||||
private function normalizeUuidList($value): array
|
||||
{
|
||||
$items = self::normalizeStringList($value);
|
||||
$items = $this->normalizeStringList($value);
|
||||
$list = [];
|
||||
foreach ($items as $item) {
|
||||
$item = strtolower($item);
|
||||
@@ -173,7 +177,7 @@ class UserSavedFilterService
|
||||
return $list;
|
||||
}
|
||||
|
||||
private static function normalizeStringList($value): array
|
||||
private function normalizeStringList($value): array
|
||||
{
|
||||
$raw = is_array($value) ? $value : [$value];
|
||||
$flat = [];
|
||||
@@ -205,7 +209,7 @@ class UserSavedFilterService
|
||||
return array_values(array_unique($flat));
|
||||
}
|
||||
|
||||
private static function truncate(string $value, int $maxLength): string
|
||||
private function truncate(string $value, int $maxLength): string
|
||||
{
|
||||
if ($maxLength <= 0 || $value === '') {
|
||||
return $value;
|
||||
|
||||
37
lib/Service/User/UserScopeGateway.php
Normal file
37
lib/Service/User/UserScopeGateway.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
|
||||
class UserScopeGateway
|
||||
{
|
||||
public function __construct(private readonly ?TenantScopeService $tenantScopeService = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function hasGlobalAccess(int $userId): bool
|
||||
{
|
||||
return $this->tenantScopeService()->hasGlobalAccess($userId);
|
||||
}
|
||||
|
||||
public function canAccess(string $resourceType, int $resourceId, int $userId): bool
|
||||
{
|
||||
return $this->tenantScopeService()->canAccess($resourceType, $resourceId, $userId);
|
||||
}
|
||||
|
||||
public function getUserTenantIds(int $userId): array
|
||||
{
|
||||
return $this->tenantScopeService()->getUserTenantIds($userId);
|
||||
}
|
||||
|
||||
private function tenantScopeService(): TenantScopeService
|
||||
{
|
||||
if ($this->tenantScopeService instanceof TenantScopeService) {
|
||||
return $this->tenantScopeService;
|
||||
}
|
||||
|
||||
return (new TenantServicesFactory())->createTenantScopeService();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
219
lib/Service/User/UserServicesFactory.php
Normal file
219
lib/Service/User/UserServicesFactory.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
use MintyPHP\Repository\Access\UserRoleRepository;
|
||||
use MintyPHP\Repository\Org\UserDepartmentRepository;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Repository\User\UserListQueryRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
use MintyPHP\Repository\User\UserSavedFilterRepository;
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
use MintyPHP\Service\Audit\AuditServicesFactory;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Service\Settings\SettingGateway;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
|
||||
class UserServicesFactory
|
||||
{
|
||||
private ?AccessServicesFactory $accessServicesFactory = null;
|
||||
private ?TenantServicesFactory $tenantServicesFactory = null;
|
||||
private ?UserReadRepository $userReadRepository = null;
|
||||
private ?AuditServicesFactory $auditServicesFactory = null;
|
||||
private ?SettingServicesFactory $settingServicesFactory = null;
|
||||
private ?UserWriteRepository $userWriteRepository = null;
|
||||
private ?UserListQueryRepository $userListQueryRepository = null;
|
||||
private ?UserSavedFilterRepository $userSavedFilterRepository = null;
|
||||
private ?UserTenantRepository $userTenantRepository = null;
|
||||
private ?UserRoleRepository $userRoleRepository = null;
|
||||
private ?UserDepartmentRepository $userDepartmentRepository = null;
|
||||
private ?UserPasswordPolicyService $userPasswordPolicyService = null;
|
||||
private ?UserPasswordService $userPasswordService = null;
|
||||
private ?UserAvatarService $userAvatarService = null;
|
||||
private ?UserSavedFilterService $userSavedFilterService = null;
|
||||
private ?UserSettingsGateway $userSettingsGateway = null;
|
||||
private ?SettingGateway $settingGateway = null;
|
||||
private ?UserScopeGateway $userScopeGateway = null;
|
||||
private ?UserDirectoryGateway $userDirectoryGateway = null;
|
||||
private ?UserPermissionGateway $userPermissionGateway = null;
|
||||
private ?PermissionGateway $permissionGateway = null;
|
||||
private ?UserAssignmentService $userAssignmentService = null;
|
||||
private ?UserTenantContextService $userTenantContextService = null;
|
||||
private ?UserAccountService $userAccountService = null;
|
||||
private ?UserLifecycleService $userLifecycleService = null;
|
||||
private ?UserLifecycleRestoreService $userLifecycleRestoreService = null;
|
||||
private ?UserLifecycleAuditService $userLifecycleAuditService = null;
|
||||
|
||||
public function createUserAccountService(): UserAccountService
|
||||
{
|
||||
return $this->userAccountService ??= new UserAccountService(
|
||||
$this->createUserReadRepository(),
|
||||
$this->createUserWriteRepository(),
|
||||
$this->createUserListQueryRepository(),
|
||||
$this->createUserAssignmentService(),
|
||||
$this->createUserPasswordService(),
|
||||
$this->createUserSettingsGateway(),
|
||||
$this->createUserScopeGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createUserAssignmentService(): UserAssignmentService
|
||||
{
|
||||
return $this->userAssignmentService ??= new UserAssignmentService(
|
||||
$this->createUserWriteRepository(),
|
||||
$this->createUserTenantRepository(),
|
||||
$this->createUserRoleRepository(),
|
||||
$this->createUserDepartmentRepository(),
|
||||
$this->createUserDirectoryGateway(),
|
||||
$this->createUserPermissionGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createUserTenantContextService(): UserTenantContextService
|
||||
{
|
||||
return $this->userTenantContextService ??= new UserTenantContextService(
|
||||
$this->createUserReadRepository(),
|
||||
$this->createUserWriteRepository(),
|
||||
$this->createUserTenantRepository(),
|
||||
$this->createUserScopeGateway(),
|
||||
$this->createUserDirectoryGateway()
|
||||
);
|
||||
}
|
||||
|
||||
public function createUserPasswordService(): UserPasswordService
|
||||
{
|
||||
return $this->userPasswordService ??= new UserPasswordService(
|
||||
$this->createUserPasswordPolicyService(),
|
||||
$this->createUserWriteRepository()
|
||||
);
|
||||
}
|
||||
|
||||
public function createUserAvatarService(): UserAvatarService
|
||||
{
|
||||
return $this->userAvatarService ??= new UserAvatarService();
|
||||
}
|
||||
|
||||
public function createUserSavedFilterService(): UserSavedFilterService
|
||||
{
|
||||
return $this->userSavedFilterService ??= new UserSavedFilterService(
|
||||
$this->createUserSavedFilterRepository()
|
||||
);
|
||||
}
|
||||
|
||||
public function createUserReadRepository(): UserReadRepository
|
||||
{
|
||||
return $this->userReadRepository ??= new UserReadRepository();
|
||||
}
|
||||
|
||||
public function createUserWriteRepository(): UserWriteRepository
|
||||
{
|
||||
return $this->userWriteRepository ??= new UserWriteRepository();
|
||||
}
|
||||
|
||||
public function createUserListQueryRepository(): UserListQueryRepository
|
||||
{
|
||||
return $this->userListQueryRepository ??= new UserListQueryRepository();
|
||||
}
|
||||
|
||||
public function createUserSavedFilterRepository(): UserSavedFilterRepository
|
||||
{
|
||||
return $this->userSavedFilterRepository ??= new UserSavedFilterRepository();
|
||||
}
|
||||
|
||||
public function createUserTenantRepository(): UserTenantRepository
|
||||
{
|
||||
return $this->userTenantRepository ??= new UserTenantRepository();
|
||||
}
|
||||
|
||||
public function createUserRoleRepository(): UserRoleRepository
|
||||
{
|
||||
return $this->userRoleRepository ??= new UserRoleRepository();
|
||||
}
|
||||
|
||||
public function createUserDepartmentRepository(): UserDepartmentRepository
|
||||
{
|
||||
return $this->userDepartmentRepository ??= new UserDepartmentRepository();
|
||||
}
|
||||
|
||||
public function createUserPasswordPolicyService(): UserPasswordPolicyService
|
||||
{
|
||||
return $this->userPasswordPolicyService ??= new UserPasswordPolicyService();
|
||||
}
|
||||
|
||||
public function createUserSettingsGateway(): UserSettingsGateway
|
||||
{
|
||||
return $this->userSettingsGateway ??= new UserSettingsGateway($this->createSettingGateway());
|
||||
}
|
||||
|
||||
public function createUserScopeGateway(): UserScopeGateway
|
||||
{
|
||||
return $this->userScopeGateway ??= new UserScopeGateway($this->tenantServicesFactory()->createTenantScopeService());
|
||||
}
|
||||
|
||||
public function createUserDirectoryGateway(): UserDirectoryGateway
|
||||
{
|
||||
return $this->userDirectoryGateway ??= new UserDirectoryGateway();
|
||||
}
|
||||
|
||||
public function createUserPermissionGateway(): UserPermissionGateway
|
||||
{
|
||||
return $this->userPermissionGateway ??= new UserPermissionGateway($this->createPermissionGateway());
|
||||
}
|
||||
|
||||
private function createPermissionGateway(): PermissionGateway
|
||||
{
|
||||
return $this->permissionGateway ??= $this->accessServicesFactory()->createPermissionGateway();
|
||||
}
|
||||
|
||||
private function accessServicesFactory(): AccessServicesFactory
|
||||
{
|
||||
return $this->accessServicesFactory ??= new AccessServicesFactory();
|
||||
}
|
||||
|
||||
private function createSettingGateway(): SettingGateway
|
||||
{
|
||||
return $this->settingGateway ??= $this->settingServicesFactory()->createSettingGateway();
|
||||
}
|
||||
|
||||
private function settingServicesFactory(): SettingServicesFactory
|
||||
{
|
||||
return $this->settingServicesFactory ??= new SettingServicesFactory();
|
||||
}
|
||||
|
||||
private function tenantServicesFactory(): TenantServicesFactory
|
||||
{
|
||||
return $this->tenantServicesFactory ??= new TenantServicesFactory();
|
||||
}
|
||||
|
||||
public function createUserLifecycleService(): UserLifecycleService
|
||||
{
|
||||
return $this->userLifecycleService ??= new UserLifecycleService(
|
||||
$this->createUserReadRepository(),
|
||||
$this->createUserWriteRepository(),
|
||||
$this->createUserSettingsGateway(),
|
||||
$this->createUserLifecycleAuditService()
|
||||
);
|
||||
}
|
||||
|
||||
public function createUserLifecycleRestoreService(): UserLifecycleRestoreService
|
||||
{
|
||||
return $this->userLifecycleRestoreService ??= new UserLifecycleRestoreService(
|
||||
$this->createUserReadRepository(),
|
||||
$this->createUserWriteRepository(),
|
||||
$this->createUserLifecycleAuditService()
|
||||
);
|
||||
}
|
||||
|
||||
private function createUserLifecycleAuditService(): UserLifecycleAuditService
|
||||
{
|
||||
return $this->userLifecycleAuditService ??= $this->auditServicesFactory()->createUserLifecycleAuditService();
|
||||
}
|
||||
|
||||
private function auditServicesFactory(): AuditServicesFactory
|
||||
{
|
||||
return $this->auditServicesFactory ??= new AuditServicesFactory();
|
||||
}
|
||||
}
|
||||
42
lib/Service/User/UserSettingsGateway.php
Normal file
42
lib/Service/User/UserSettingsGateway.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Service\Settings\SettingGateway;
|
||||
|
||||
class UserSettingsGateway
|
||||
{
|
||||
public function __construct(private readonly SettingGateway $settingGateway)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDefaultTenantId(): ?int
|
||||
{
|
||||
return $this->settingGateway->getDefaultTenantId();
|
||||
}
|
||||
|
||||
public function getDefaultRoleId(): ?int
|
||||
{
|
||||
return $this->settingGateway->getDefaultRoleId();
|
||||
}
|
||||
|
||||
public function getDefaultDepartmentId(): ?int
|
||||
{
|
||||
return $this->settingGateway->getDefaultDepartmentId();
|
||||
}
|
||||
|
||||
public function getAppTheme(): ?string
|
||||
{
|
||||
return $this->settingGateway->getAppTheme();
|
||||
}
|
||||
|
||||
public function getUserInactivityDeactivateDays(): int
|
||||
{
|
||||
return $this->settingGateway->getUserInactivityDeactivateDays();
|
||||
}
|
||||
|
||||
public function getUserInactivityDeleteDays(): int
|
||||
{
|
||||
return $this->settingGateway->getUserInactivityDeleteDays();
|
||||
}
|
||||
}
|
||||
204
lib/Service/User/UserTenantContextService.php
Normal file
204
lib/Service/User/UserTenantContextService.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
use MintyPHP\Repository\User\UserWriteRepository;
|
||||
|
||||
class UserTenantContextService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserReadRepository $userReadRepository,
|
||||
private readonly UserWriteRepository $userWriteRepository,
|
||||
private readonly UserTenantRepository $userTenantRepository,
|
||||
private readonly UserScopeGateway $scopeGateway,
|
||||
private readonly UserDirectoryGateway $directoryGateway
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tenant ID for a user (with fallback to primary tenant)
|
||||
*/
|
||||
public function getCurrentTenantId(int $userId): ?int
|
||||
{
|
||||
$user = $this->userReadRepository->find($userId);
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$activeTenantIds = $this->scopeGateway->getUserTenantIds($userId);
|
||||
if (!$activeTenantIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentTenantId = (int) ($user['current_tenant_id'] ?? 0);
|
||||
if ($currentTenantId > 0 && in_array($currentTenantId, $activeTenantIds, true)) {
|
||||
return $currentTenantId;
|
||||
}
|
||||
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
if ($primaryTenantId > 0 && in_array($primaryTenantId, $activeTenantIds, true)) {
|
||||
return $primaryTenantId;
|
||||
}
|
||||
|
||||
return $activeTenantIds[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tenant data for a user
|
||||
*/
|
||||
public function getCurrentTenant(int $userId): ?array
|
||||
{
|
||||
$currentTenantId = $this->getCurrentTenantId($userId);
|
||||
if (!$currentTenantId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->directoryGateway->findTenant($currentTenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tenants the user has access to
|
||||
*/
|
||||
public function getAvailableTenants(int $userId): array
|
||||
{
|
||||
$tenantIds = $this->scopeGateway->getUserTenantIds($userId);
|
||||
if (!$tenantIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenants = [];
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
$tenant = $this->directoryGateway->findTenant((int) $tenantId);
|
||||
if ($tenant && (($tenant['status'] ?? 'active') === 'active')) {
|
||||
$tenants[] = $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
usort($tenants, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
|
||||
return $tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only explicitly assigned active tenants for a user.
|
||||
* This intentionally ignores global tenant-scope bypass permissions.
|
||||
*/
|
||||
public function getAssignedActiveTenants(int $userId): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenantIds = array_values(array_unique(array_map(
|
||||
'intval',
|
||||
$this->userTenantRepository->listTenantIdsByUserId($userId)
|
||||
)));
|
||||
$tenantIds = array_values(array_filter($tenantIds, static fn ($id) => $id > 0));
|
||||
if (!$tenantIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$activeTenantIds = $this->directoryGateway->listActiveTenantIdsByIds($tenantIds);
|
||||
if (!$activeTenantIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenantsById = [];
|
||||
foreach ($this->directoryGateway->listTenantsByIds($activeTenantIds) as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tenantsById[$tenantId] = $tenant;
|
||||
}
|
||||
|
||||
$tenants = [];
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
if (isset($tenantsById[$tenantId])) {
|
||||
$tenants[] = $tenantsById[$tenantId];
|
||||
}
|
||||
}
|
||||
|
||||
usort($tenants, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
|
||||
return $tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available departments grouped by tenant for a user.
|
||||
* Returns an array of groups: ['tenant' => [...], 'departments' => [...]]
|
||||
*/
|
||||
public function getAvailableDepartmentsByTenant(int $userId): array
|
||||
{
|
||||
$tenants = $this->getAvailableTenants($userId);
|
||||
if (!$tenants) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
foreach ($tenants as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$departments = $this->directoryGateway->listDepartmentsByTenantIds([$tenantId]);
|
||||
usort($departments, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
$departments = array_map(static function (array $department): array {
|
||||
return [
|
||||
'id' => (int) ($department['id'] ?? 0),
|
||||
'uuid' => $department['uuid'] ?? '',
|
||||
'description' => $department['description'] ?? '',
|
||||
];
|
||||
}, $departments);
|
||||
|
||||
$groups[] = [
|
||||
'tenant' => [
|
||||
'id' => (int) ($tenant['id'] ?? 0),
|
||||
'uuid' => $tenant['uuid'] ?? '',
|
||||
'description' => $tenant['description'] ?? '',
|
||||
],
|
||||
'departments' => $departments,
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current tenant for a user (with validation)
|
||||
*/
|
||||
public function setCurrentTenant(int $userId, int $tenantId): array
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return ['ok' => false, 'error' => 'invalid_tenant'];
|
||||
}
|
||||
|
||||
$userTenantIds = $this->userTenantRepository->listTenantIdsByUserId($userId);
|
||||
if (!in_array($tenantId, $userTenantIds, true)) {
|
||||
return ['ok' => false, 'error' => 'tenant_not_assigned'];
|
||||
}
|
||||
|
||||
$tenant = $this->directoryGateway->findTenant($tenantId);
|
||||
if (!$tenant) {
|
||||
return ['ok' => false, 'error' => 'tenant_not_found'];
|
||||
}
|
||||
if (($tenant['status'] ?? 'active') !== 'active') {
|
||||
return ['ok' => false, 'error' => 'tenant_inactive'];
|
||||
}
|
||||
|
||||
$updated = $this->userWriteRepository->setCurrentTenant($userId, $tenantId);
|
||||
if (!$updated) {
|
||||
return ['ok' => false, 'error' => 'update_failed'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'tenant' => $tenant];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user