, total: int, filtered_total: int, active_total: int, inactive_total: int} */ public function listUsers(string $query = '', string $status = 'all', string $role = 'all'): array { $allUsers = $this->users->findForAdminList(); $normalizedQuery = strtolower(trim($query)); $normalizedStatus = in_array($status, ['all', 'active', 'inactive'], true) ? $status : 'all'; $normalizedRole = in_array($role, ApplicationRoles::assignableRoleNames(), true) ? $role : 'all'; $activeTotal = 0; $inactiveTotal = 0; foreach ($allUsers as $user) { if ($user->isActive()) { ++$activeTotal; } else { ++$inactiveTotal; } } $filtered = array_values(array_filter($allUsers, static function (User $user) use ($normalizedQuery, $normalizedStatus, $normalizedRole): bool { if ($normalizedQuery !== '' && !str_contains(strtolower($user->getEmail()), $normalizedQuery)) { return false; } if ($normalizedStatus === 'active' && !$user->isActive()) { return false; } if ($normalizedStatus === 'inactive' && $user->isActive()) { return false; } if ($normalizedRole !== 'all' && !in_array($normalizedRole, $user->getRoles(), true)) { return false; } return true; })); return [ 'users' => $filtered, 'total' => count($allUsers), 'filtered_total' => count($filtered), 'active_total' => $activeTotal, 'inactive_total' => $inactiveTotal, ]; } /** * @return array */ public function roleChoices(): array { return ApplicationRoles::assignableChoices(); } /** * @param list $roles */ public function create(string $email, string $plainPassword, string $passwordRepeat, array $roles, bool $isActive): User { $email = $this->normalizeEmail($email); $roles = $this->normalizeRoles($roles); $this->assertValidEmail($email); $this->assertEmailIsAvailable($email); $this->assertValidPassword($plainPassword, $passwordRepeat, true); $user = new User(); $user->setEmail($email); $user->setRoles($roles); $user->setIsActive($isActive); $user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword)); $this->em->persist($user); $this->em->flush(); return $user; } /** * @param list $roles */ public function update( string $id, string $email, string $plainPassword, string $passwordRepeat, array $roles, bool $isActive, User $currentUser, ): User { $user = $this->requireUser($id); $email = $this->normalizeEmail($email); $roles = $this->normalizeRoles($roles); $this->assertValidEmail($email); $this->assertEmailIsAvailable($email, $user); $this->assertValidPassword($plainPassword, $passwordRepeat, false); $this->assertCanApplySecurityState($user, $currentUser, $roles, $isActive); $user->setEmail($email); $user->setRoles($roles); $user->setIsActive($isActive); if (trim($plainPassword) !== '') { $user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword)); } $this->em->flush(); return $user; } public function toggleActive(string $id, User $currentUser): User { $user = $this->requireUser($id); $nextActiveState = !$user->isActive(); $this->assertCanApplySecurityState($user, $currentUser, $this->storedAssignableRoles($user), $nextActiveState); $user->setIsActive($nextActiveState); $this->em->flush(); return $user; } public function requireUser(string $id): User { if (!Uuid::isValid($id)) { throw new \RuntimeException('Benutzer wurde nicht gefunden.'); } $user = $this->users->find(Uuid::fromString($id)); if (!$user instanceof User) { throw new \RuntimeException('Benutzer wurde nicht gefunden.'); } return $user; } private function normalizeEmail(string $email): string { return strtolower(trim($email)); } private function assertValidEmail(string $email): void { if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) { throw new \RuntimeException('Bitte eine gültige E-Mail-Adresse eingeben.'); } } private function assertEmailIsAvailable(string $email, ?User $currentUser = null): void { $existingUser = $this->users->findOneByNormalizedEmail($email); if (!$existingUser instanceof User) { return; } if ($currentUser instanceof User && (string) $existingUser->getId() === (string) $currentUser->getId()) { return; } throw new \RuntimeException('Diese E-Mail-Adresse wird bereits verwendet.'); } private function assertValidPassword(string $plainPassword, string $passwordRepeat, bool $required): void { if (!$required && $plainPassword === '' && $passwordRepeat === '') { return; } if (trim($plainPassword) === '') { throw new \RuntimeException('Bitte ein Passwort eingeben.'); } if (strlen($plainPassword) < 8) { throw new \RuntimeException('Das Passwort muss mindestens 8 Zeichen lang sein.'); } if ($plainPassword !== $passwordRepeat) { throw new \RuntimeException('Passwort und Wiederholung stimmen nicht überein.'); } } /** * @param list $roles * @return list */ private function normalizeRoles(array $roles): array { $roles = array_values(array_unique(array_map(static fn (mixed $role): string => trim((string) $role), $roles))); $roles = array_values(array_filter($roles, static fn (string $role): bool => $role !== '' && $role !== ApplicationRoles::ROLE_USER)); $allowedRoles = ApplicationRoles::assignableRoleNames(); $unknownRoles = array_values(array_diff($roles, $allowedRoles)); if ($unknownRoles !== []) { throw new \RuntimeException('Unbekannte Rolle: ' . implode(', ', $unknownRoles)); } $orderedRoles = []; foreach ($allowedRoles as $allowedRole) { if (in_array($allowedRole, $roles, true)) { $orderedRoles[] = $allowedRole; } } if ($orderedRoles === []) { throw new \RuntimeException('Bitte mindestens eine Rolle auswählen.'); } return $orderedRoles; } /** * @return list */ private function storedAssignableRoles(User $user): array { return array_values(array_intersect(ApplicationRoles::assignableRoleNames(), $user->getRoles())); } /** * @param list $newRoles */ private function assertCanApplySecurityState(User $targetUser, User $currentUser, array $newRoles, bool $newActiveState): void { $isSelf = (string) $targetUser->getId() === (string) $currentUser->getId(); $currentlySuperAdmin = in_array(ApplicationRoles::ROLE_SUPER_ADMIN, $targetUser->getRoles(), true); $willBeSuperAdmin = in_array(ApplicationRoles::ROLE_SUPER_ADMIN, $newRoles, true); if ($isSelf && !$newActiveState) { throw new \RuntimeException('Du kannst deinen eigenen Benutzer nicht deaktivieren.'); } if ($isSelf && $currentlySuperAdmin && !$willBeSuperAdmin) { throw new \RuntimeException('Du kannst dir die Super-Admin-Rolle nicht selbst entziehen.'); } $removesActiveSuperAdmin = $targetUser->isActive() && $currentlySuperAdmin && (!$newActiveState || !$willBeSuperAdmin); if ($removesActiveSuperAdmin && $this->users->countActiveSuperAdmins($targetUser) < 1) { throw new \RuntimeException('Der letzte aktive Super-Admin darf nicht deaktiviert oder demotet werden.'); } } }