all(); Guard::requireLogin(); $request = requestInput(); $returnTarget = requestResolveReturnTarget(); $closeTarget = requestResolveReturnTarget('admin/users'); $userAccountService = app(\MintyPHP\Service\User\UserAccountService::class); $userAssignmentService = app(\MintyPHP\Service\User\UserAssignmentService::class); $userTenantRepository = app(\MintyPHP\Repository\Tenant\UserTenantRepository::class); $userRoleRepository = app(\MintyPHP\Repository\Access\UserRoleRepository::class); $userDepartmentRepository = app(\MintyPHP\Repository\Org\UserDepartmentRepository::class); $userPasswordPolicyService = app(\MintyPHP\Service\User\UserPasswordPolicyService::class); $userCustomFieldValueService = app(UserCustomFieldValueService::class); $authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); $rolePermissionRepository = app(\MintyPHP\Repository\Access\RolePermissionRepository::class); $authService = app(\MintyPHP\Service\Auth\AuthService::class); $apiTokenService = app(\MintyPHP\Service\Auth\ApiTokenService::class); $passwordResetRepository = app(PasswordResetRepository::class); $rememberTokenRepository = app(RememberTokenRepository::class); $tenantService = app(\MintyPHP\Service\Tenant\TenantService::class); $roleService = app(\MintyPHP\Service\Access\RoleService::class); $departmentService = app(\MintyPHP\Service\Org\DepartmentService::class); $passwordMinLength = $userPasswordPolicyService->minLength(); $passwordHints = $userPasswordPolicyService->hints(); $currentUserId = (int) ($session['user']['id'] ?? 0); $uuid = trim((string) ($id ?? '')); $editTarget = requestPathWithReturnTarget("admin/users/edit/{$uuid}", $returnTarget); // Resolve the user BEFORE the aggregator so the authorize-context can carry // the real target_user_id (matches today's order: lookup -> CONTEXT authorize // -> Router::redirect on forbidden). The aggregator receives a trivial finder // that returns the already-loaded model so its not-found branch fires only // when findByUuid() returned null. $user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null; $userId = (int) ($user['id'] ?? 0); $context = actionEditContext([ 'finder' => static fn (string $_id): mixed => $user, 'rawId' => $uuid, 'notFoundFlashKey' => 'User not found', 'notFoundRedirectPath' => $closeTarget, 'notFoundFlashScopeKey' => 'user_not_found', 'abilityKey' => UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, 'context' => [ 'actor_user_id' => $currentUserId, 'target_user_id' => $userId, ], 'viewAuthFlags' => [ 'can_view_users', 'can_view_page', 'can_edit_user', 'can_edit_assignments', 'can_manage_tenants', 'can_edit_custom_field_values', 'can_manage_api_tokens', 'can_view_user_meta', 'can_view_user_audit', 'can_access_pdf', 'can_delete_user', 'can_view_security_artifacts', 'can_view_permissions_table', 'is_own_account', ], 'forbiddenStrategy' => 'redirect', 'tenantScopeFlagKey' => 'can_manage_tenants', ]); $user = is_array($context['model']) ? $context['model'] : $user; $capabilities = $context['capabilities']; $tenantScope = $context['tenantScope']; $viewAuth = $context['viewAuth']; $canViewUsers = (bool) ($capabilities['can_view_users'] ?? false); $canViewPage = (bool) ($capabilities['can_view_page'] ?? false); $canEditUser = (bool) ($capabilities['can_edit_user'] ?? false); $canEditAssignments = (bool) ($capabilities['can_edit_assignments'] ?? false); $canManageTenants = (bool) ($capabilities['can_manage_tenants'] ?? false); $canEditCustomFieldValues = (bool) ($capabilities['can_edit_custom_field_values'] ?? false); $canManageApiTokens = (bool) ($capabilities['can_manage_api_tokens'] ?? false); $canViewUserMeta = (bool) ($capabilities['can_view_user_meta'] ?? false); $canViewUserAudit = (bool) ($capabilities['can_view_user_audit'] ?? false); $canAccessPdf = (bool) ($capabilities['can_access_pdf'] ?? false); $canDeleteUser = (bool) ($capabilities['can_delete_user'] ?? false); $canViewSecurityArtifacts = (bool) ($capabilities['can_view_security_artifacts'] ?? false); $canViewPermissionsTable = (bool) ($capabilities['can_view_permissions_table'] ?? false); $isOwnAccount = (bool) ($capabilities['is_own_account'] ?? false); // $canManageAllTenants = Tupel-Diskriminator (true wenn Aggregator scope='all' // liefert via tenantScopeFlagKey='can_manage_tenants'-Override). $canManageTenants // bleibt als Capability-Flag erhalten — semantisch aequivalent, aber unterschiedliche // Quelle. Filter-Branches unten nutzen $canManageAllTenants als Tupel-Diskriminator. $canManageAllTenants = $tenantScope['scope'] === 'all'; if ($canViewUserMeta || $canViewUserAudit) { app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($user, ['created_by', 'modified_by', 'active_changed_by']); } $errorBag = formErrors(); $errors = []; $form = $user; $scopeGateway = app(\MintyPHP\Service\Tenant\TenantScopeService::class); $strictTenantScope = $scopeGateway->isStrict(); $tenants = $tenantService->list(); if (!$canManageAllTenants) { if (count($tenantScope['ids']) > 0) { $tenants = array_values(array_filter($tenants, static function (array $tenant) use ($tenantScope): bool { $tenantId = (int) ($tenant['id'] ?? 0); return $tenantId > 0 && in_array($tenantId, $tenantScope['ids'], true); })); } elseif ($strictTenantScope) { $tenants = []; } } $selectedTenantIds = $userTenantRepository->listTenantIdsByUserId($userId); if (!$canManageAllTenants && count($tenantScope['ids']) > 0) { $selectedTenantIds = array_values(array_intersect($selectedTenantIds, $tenantScope['ids'])); } $primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0); if (!$primaryTenantId && count($selectedTenantIds) === 1) { $primaryTenantId = (int) $selectedTenantIds[0]; $user['primary_tenant_id'] = $primaryTenantId; } $roles = $roleService->listActive(); $assignableRoleService = app(\MintyPHP\Service\Access\AssignableRoleService::class); $assignableRoleIds = $assignableRoleService->listAssignableRoleIdsForActor($currentUserId); $assignableRoleIdMap = array_fill_keys($assignableRoleIds, true); $roles = array_values(array_filter($roles, static fn (array $r): bool => isset($assignableRoleIdMap[(int) ($r['id'] ?? 0)]))); $selectedRoleIds = $userRoleRepository->listRoleIdsByUserId($userId); $visibleSelectedRoleIds = array_values(array_intersect($selectedRoleIds, $assignableRoleIds)); $permissionRows = $canViewPermissionsTable ? $rolePermissionRepository->listPermissionsWithRolesByRoleIds($selectedRoleIds) : []; $selectedDepartmentIds = $userDepartmentRepository->listDepartmentIdsByUserId($userId); $departmentOptionsByTenant = $departmentService->groupActiveByTenantIds($selectedTenantIds); $customFieldDefinitionsByTenant = $userCustomFieldValueService->buildDefinitionsByTenant($selectedTenantIds); $customFieldDefinitionIds = []; foreach ($customFieldDefinitionsByTenant as $tenantDefinitions) { foreach ($tenantDefinitions as $definition) { $definitionId = (int) ($definition['id'] ?? 0); if ($definitionId > 0) { $customFieldDefinitionIds[] = $definitionId; } } } $customFieldValueMap = $userCustomFieldValueService->buildUserValueMap($userId, $customFieldDefinitionIds); $customFieldPostedValues = [ 'custom_field_values' => [], 'custom_field_values_multi' => [], ]; $passwordResets = []; $rememberTokens = []; $apiTokens = []; if ($canViewSecurityArtifacts) { $passwordResets = $passwordResetRepository->listByUserId($userId, 25); $rememberTokens = $rememberTokenRepository->listByUserId($userId, 25); $apiTokens = $apiTokenService->listForUser($userId); } $canManageApiTokens = $canManageApiTokens && $canViewSecurityArtifacts; $showApiTokens = $canViewSecurityArtifacts; if ($request->isMethod('POST') && !actionRequireCsrf($editTarget, $editTarget, 'csrf_expired')) { return; } if ($request->isMethod('POST')) { $post = $request->bodyAll(); $submitDecision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT, [ 'actor_user_id' => $currentUserId, 'target_user_id' => $userId, 'input' => $post, ]); if (!$submitDecision->isAllowed()) { Router::redirect('error/forbidden'); return; } $submitCapabilities = $submitDecision->attribute('capabilities', []); if (!is_array($submitCapabilities) || !isset($submitCapabilities['can_edit_user']) || !$submitCapabilities['can_edit_user']) { Router::redirect('error/forbidden'); return; } $canEditUser = (bool) $submitCapabilities['can_edit_user']; $canEditAssignments = (bool) ($submitCapabilities['can_edit_assignments'] ?? $canEditAssignments); $canEditCustomFieldValues = (bool) ($submitCapabilities['can_edit_custom_field_values'] ?? $canEditCustomFieldValues); $result = $userAccountService->updateFromAdmin($userId, $post, $currentUserId); $form = $result['form'] ?? $form; $errorBag->merge($result['errors'] ?? []); $tenantIds = $userAssignmentService->normalizeIdInput($post['tenant_ids'] ?? []); $existingTenantIds = $userTenantRepository->listTenantIdsByUserId($userId); $tenantIdsForSync = $tenantIds; if ($canEditAssignments && !$canManageAllTenants && count($tenantScope['ids']) > 0) { $tenantIds = array_values(array_intersect($tenantIds, $tenantScope['ids'])); $tenantIdsForSync = $scopeGateway->mergeTenantIdsPreservingOutOfScope( $tenantIds, $existingTenantIds, $tenantScope['ids'] ); } elseif ($canEditAssignments && !$canManageAllTenants && $strictTenantScope) { $tenantIds = []; $tenantIdsForSync = $existingTenantIds; } if (!$canEditAssignments) { $tenantIds = $userTenantRepository->listTenantIdsByUserId($userId); if (!$canManageAllTenants && count($tenantScope['ids']) > 0) { $tenantIds = array_values(array_intersect($tenantIds, $tenantScope['ids'])); } $tenantIdsForSync = $tenantIds; } $selectedTenantIds = $tenantIds; $primaryTenantId = (int) ($post['primary_tenant_id'] ?? 0); $selectedRoleIds = $userAssignmentService->normalizeIdInput($post['role_ids'] ?? []); $selectedDepartmentIds = $userAssignmentService->normalizeIdInput($post['department_ids'] ?? []); $departmentOptionsByTenant = $departmentService->groupActiveByTenantIds($selectedTenantIds); $customFieldDefinitionsByTenant = $userCustomFieldValueService->buildDefinitionsByTenant($selectedTenantIds); $customFieldDefinitionIds = []; foreach ($customFieldDefinitionsByTenant as $tenantDefinitions) { foreach ($tenantDefinitions as $definition) { $definitionId = (int) ($definition['id'] ?? 0); if ($definitionId > 0) { $customFieldDefinitionIds[] = $definitionId; } } } $customFieldValueMap = $userCustomFieldValueService->buildUserValueMap($userId, $customFieldDefinitionIds); $customFieldPostedValues = [ 'custom_field_values' => is_array($post['custom_field_values'] ?? null) ? $post['custom_field_values'] : [], 'custom_field_values_multi' => is_array($post['custom_field_values_multi'] ?? null) ? $post['custom_field_values_multi'] : [], ]; if ($result['ok'] ?? false) { if ($canEditAssignments) { $assignmentsBefore = $userAssignmentService->buildAssignmentsForUser($userId); $assignmentsSynced = $userAssignmentService->syncAllAssignments( $userId, $tenantIdsForSync, $selectedRoleIds, $selectedDepartmentIds, $currentUserId ); if (!$assignmentsSynced) { $errorBag->addGlobal(t('User assignments can not be updated')); } else { $assignmentsAfter = $userAssignmentService->buildAssignmentsForUser($userId); $userAccountService->dispatchAssignmentChangedIfNeeded( $userId, $assignmentsBefore, $assignmentsAfter, $currentUserId ); } if ($currentUserId === $userId && !$errorBag->hasAny()) { $authService->loadTenantDataIntoSession($userId); } } $customFieldSyncResult = $userCustomFieldValueService->syncForUser( $userId, $selectedTenantIds, $post, $canEditCustomFieldValues ); if (!($customFieldSyncResult['ok'] ?? false)) { $errorBag->merge($customFieldSyncResult['errors'] ?? []); $customFieldValueMap = $userCustomFieldValueService->buildUserValueMap($userId, $customFieldDefinitionIds); } if ($currentUserId === $userId && isset($form['theme'])) { $themes = appThemes(); $themeValue = strtolower(trim((string) ($form['theme'] ?? ''))); $userSession = $sessionStore->get('user', []); if (is_array($userSession)) { $userSession['theme'] = isset($themes[$themeValue]) ? $themeValue : appDefaultTheme(); $sessionStore->set('user', $userSession); } } if ($currentUserId === $userId && isset($form['locale'])) { $userSession = $sessionStore->get('user', []); if (is_array($userSession)) { $userSession['locale'] = $form['locale']; $sessionStore->set('user', $userSession); } I18n::$locale = $form['locale']; } if (!$errorBag->hasAny()) { $action = (string) ($post['action'] ?? 'save'); if ($action === 'save_close') { Flash::success(t('User updated'), $closeTarget, 'user_updated'); Router::redirect($closeTarget); } else { Flash::success(t('User updated'), $editTarget, 'user_updated'); Router::redirect($editTarget); } } } } $validationSummaryErrors = $errorBag->toArray(); $errors = $errorBag->toFlatList(); $titleText = $isOwnAccount ? t('My account') : ($canEditUser ? t('Edit user') : t('View user')); Buffer::set('title', $titleText); if (!($isOwnAccount && !$canViewUsers)) { $breadcrumbs = [ ['label' => t('Home'), 'path' => 'admin'], ['label' => t('Users'), 'path' => 'admin/users'], ['label' => $titleText], ]; }