} */ public function buildProfile(int $currentUserId, string $uuid): array { $uuid = trim($uuid); if ($uuid === '') { return ['status' => 'invalid_uuid']; } $user = $this->userAccountService->findByUuid($uuid); if (!$user || !isset($user['id'])) { return ['status' => 'not_found']; } $targetUserId = (int) $user['id']; if ($targetUserId <= 0) { return ['status' => 'not_found']; } // Users can always view their own profile; others require scope access. if ( $currentUserId !== $targetUserId && !$this->scopeGateway->canAccess('users', $targetUserId, $currentUserId) ) { return ['status' => 'forbidden']; } $viewerTenantIds = toIntIds($this->scopeGateway->getUserTenantIds($currentUserId)); $assignments = $this->userAssignmentService->buildAssignmentsForUser($targetUserId); $departmentMap = []; foreach (($assignments['departments'] ?? []) as $department) { if (empty($department['active'])) { continue; } $tenantId = (int) ($department['tenant_id'] ?? 0); $label = trim((string) ($department['description'] ?? '')); // Only show departments in tenants the viewer also belongs to — prevents leaking org structure. if ($tenantId <= 0 || $label === '' || !in_array($tenantId, $viewerTenantIds, true)) { continue; } $departmentMap[$tenantId] ??= []; $departmentMap[$tenantId][] = $label; } foreach ($departmentMap as $tenantId => $labels) { $labels = array_values(array_unique($labels)); natcasesort($labels); $departmentMap[$tenantId] = array_values($labels); } $primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0); $tenantGroups = []; foreach (($assignments['tenants'] ?? []) as $tenant) { $tenantId = (int) ($tenant['id'] ?? 0); $tenantLabel = trim((string) ($tenant['description'] ?? '')); $status = strtolower(trim((string) ($tenant['status'] ?? ''))); if ( $tenantId <= 0 || $tenantLabel === '' || $status !== 'active' || !in_array($tenantId, $viewerTenantIds, true) ) { continue; } $tenantGroups[] = [ 'label' => $tenantLabel, 'is_primary' => $primaryTenantId > 0 && $tenantId === $primaryTenantId, 'departments' => $departmentMap[$tenantId] ?? [], ]; } usort($tenantGroups, static fn (array $a, array $b): int => strnatcasecmp( (string) $a['label'], (string) $b['label'] )); $roleLabels = []; foreach (($assignments['roles'] ?? []) as $role) { if (empty($role['active'])) { continue; } $label = trim((string) ($role['description'] ?? '')); if ($label !== '') { $roleLabels[] = $label; } } $roleLabels = array_values(array_unique($roleLabels)); natcasesort($roleLabels); $roleLabels = array_values($roleLabels); $avatarUuid = (string) ($user['uuid'] ?? ''); $user['has_avatar'] = $avatarUuid !== '' && $this->avatarService->hasAvatar($avatarUuid); $user['tenant_groups'] = $tenantGroups; $user['role_labels'] = $roleLabels; return [ 'status' => 'ok', 'user' => $user, ]; } /** * Extended profile for admin contexts. Returns the same shape as * buildProfile() plus `$user['admin_extras']` with operational fields * that are inappropriate for non-admin views (email verification state, * last login, active status, created/modified timestamps). * * @api Called from pages/admin/users/view-fragment($id).php via app(...). * * @return array{status: string, user?: array} */ public function buildAdminProfile(int $currentUserId, string $uuid): array { $profile = $this->buildProfile($currentUserId, $uuid); if ($profile['status'] !== 'ok' || !isset($profile['user'])) { return $profile; } $user = $profile['user']; $user['admin_extras'] = [ 'active' => (bool) ($user['active'] ?? false), 'email_verified_at' => (string) ($user['email_verified_at'] ?? ''), 'last_login_at' => (string) ($user['last_login_at'] ?? ''), 'last_login_provider' => (string) ($user['last_login_provider'] ?? ''), 'created' => (string) ($user['created'] ?? ''), 'modified' => (string) ($user['modified'] ?? ''), ]; $profile['user'] = $user; return $profile; } }