add user management
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\ApplicationRoles;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -11,6 +14,7 @@ use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
@@ -18,13 +22,13 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
name: 'mto:agent:user:create',
|
||||
description: 'Creates a new application user'
|
||||
)]
|
||||
class CreateUserCommand extends Command
|
||||
final class CreateUserCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private UserPasswordHasherInterface $passwordHasher
|
||||
)
|
||||
{
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly UserRepository $users,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -33,76 +37,81 @@ class CreateUserCommand extends Command
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
// =============================
|
||||
// Email
|
||||
// =============================
|
||||
$emailQuestion = new Question('E-Mail: ');
|
||||
$emailQuestion->setValidator(function ($value) {
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$emailQuestion->setValidator(function (mixed $value): string {
|
||||
$email = strtolower(trim((string) $value));
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \RuntimeException('Invalid email address.');
|
||||
}
|
||||
return strtolower(trim($value));
|
||||
|
||||
return $email;
|
||||
});
|
||||
|
||||
$email = $helper->ask($input, $output, $emailQuestion);
|
||||
|
||||
// Prüfen ob User existiert
|
||||
$existingUser = $this->em
|
||||
->getRepository(User::class)
|
||||
->findOneBy(['email' => $email]);
|
||||
|
||||
if ($existingUser) {
|
||||
if ($this->users->findOneByNormalizedEmail($email) instanceof User) {
|
||||
$output->writeln('<error>User already exists.</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Passwort
|
||||
// =============================
|
||||
$passwordQuestion = new Question('Password: ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$passwordQuestion->setHiddenFallback(false);
|
||||
|
||||
$plainPassword = $helper->ask($input, $output, $passwordQuestion);
|
||||
$plainPassword = (string) $helper->ask($input, $output, $passwordQuestion);
|
||||
|
||||
if (strlen($plainPassword) < 8) {
|
||||
if (strlen(trim($plainPassword)) < 8) {
|
||||
$output->writeln('<error>Password must be at least 8 characters.</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$passwordRepeatQuestion = new Question('Repeat password: ');
|
||||
$passwordRepeatQuestion->setHidden(true);
|
||||
$passwordRepeatQuestion->setHiddenFallback(false);
|
||||
|
||||
$passwordRepeat = (string) $helper->ask($input, $output, $passwordRepeatQuestion);
|
||||
|
||||
if ($plainPassword !== $passwordRepeat) {
|
||||
$output->writeln('<error>Passwords do not match.</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Rolle auswählen
|
||||
// =============================
|
||||
$roleQuestion = new ChoiceQuestion(
|
||||
'Select role:',
|
||||
[
|
||||
'ROLE_SUPER_ADMIN',
|
||||
'ROLE_KNOWLEDGE_ADMIN',
|
||||
'ROLE_EDITOR',
|
||||
'ROLE_ADMIN_AREA',
|
||||
'ROLE_CHAT_USER',
|
||||
],
|
||||
0
|
||||
'Select role(s), comma-separated if needed:',
|
||||
ApplicationRoles::assignableRoleNames(),
|
||||
'0'
|
||||
);
|
||||
$roleQuestion->setMultiselect(true);
|
||||
|
||||
$role = $helper->ask($input, $output, $roleQuestion);
|
||||
$roles = $helper->ask($input, $output, $roleQuestion);
|
||||
$roles = is_array($roles) ? array_values(array_unique(array_map('strval', $roles))) : [];
|
||||
|
||||
if ($roles === []) {
|
||||
$output->writeln('<error>At least one role is required.</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$activeQuestion = new ConfirmationQuestion('Activate user? [Y/n] ', true);
|
||||
$isActive = (bool) $helper->ask($input, $output, $activeQuestion);
|
||||
|
||||
// =============================
|
||||
// User erzeugen
|
||||
// =============================
|
||||
$user = new User();
|
||||
$user->setEmail($email);
|
||||
$user->setRoles([$role]);
|
||||
|
||||
$hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword);
|
||||
$user->setPassword($hashedPassword);
|
||||
$user->setRoles($roles);
|
||||
$user->setIsActive($isActive);
|
||||
$user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword));
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$output->writeln('<info>User created successfully.</info>');
|
||||
$output->writeln('Email: ' . $email);
|
||||
$output->writeln('Role: ' . $role);
|
||||
$output->writeln('Roles: ' . implode(', ', $roles));
|
||||
$output->writeln('Active: ' . ($isActive ? 'yes' : 'no'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
176
src/Controller/Admin/UserController.php
Normal file
176
src/Controller/Admin/UserController.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Service\Admin\UserAdminService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
#[Route('/admin/users')]
|
||||
final class UserController extends AbstractController
|
||||
{
|
||||
#[Route('', name: 'admin_users_index', methods: ['GET'])]
|
||||
public function index(Request $request, UserAdminService $users): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
||||
$query = trim((string) $request->query->get('q', ''));
|
||||
$status = (string) $request->query->get('status', 'all');
|
||||
$role = (string) $request->query->get('role', 'all');
|
||||
$list = $users->listUsers($query, $status, $role);
|
||||
|
||||
return $this->render('admin/user/index.html.twig', [
|
||||
'users' => $list['users'],
|
||||
'total' => $list['total'],
|
||||
'filtered_total' => $list['filtered_total'],
|
||||
'active_total' => $list['active_total'],
|
||||
'inactive_total' => $list['inactive_total'],
|
||||
'role_choices' => $users->roleChoices(),
|
||||
'filters' => [
|
||||
'q' => $query,
|
||||
'status' => in_array($status, ['all', 'active', 'inactive'], true) ? $status : 'all',
|
||||
'role' => array_key_exists($role, $users->roleChoices()) ? $role : 'all',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/create', name: 'admin_users_create', methods: ['GET', 'POST'])]
|
||||
public function create(Request $request, UserAdminService $users): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
||||
if (!$request->isMethod('POST')) {
|
||||
return $this->render('admin/user/create.html.twig', [
|
||||
'role_choices' => $users->roleChoices(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$this->isCsrfTokenValid('admin_user_create', (string) $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||
|
||||
return $this->redirectToRoute('admin_users_create');
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $users->create(
|
||||
(string) $request->request->get('email', ''),
|
||||
(string) $request->request->get('password', ''),
|
||||
(string) $request->request->get('password_repeat', ''),
|
||||
$this->extractRoles($request),
|
||||
$request->request->has('is_active'),
|
||||
);
|
||||
|
||||
$this->addFlash('success', 'Benutzer wurde erstellt: ' . $user->getEmail());
|
||||
|
||||
return $this->redirectToRoute('admin_users_index');
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('danger', $e->getMessage());
|
||||
|
||||
return $this->redirectToRoute('admin_users_create');
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/{id}/edit', name: 'admin_users_edit', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['GET', 'POST'])]
|
||||
public function edit(string $id, Request $request, UserAdminService $users): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
||||
try {
|
||||
$user = $users->requireUser($id);
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('danger', $e->getMessage());
|
||||
|
||||
return $this->redirectToRoute('admin_users_index');
|
||||
}
|
||||
|
||||
if (!$request->isMethod('POST')) {
|
||||
return $this->render('admin/user/edit.html.twig', [
|
||||
'managed_user' => $user,
|
||||
'role_choices' => $users->roleChoices(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$this->isCsrfTokenValid('admin_user_edit_' . $id, (string) $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||
|
||||
return $this->redirectToRoute('admin_users_edit', ['id' => $id]);
|
||||
}
|
||||
|
||||
try {
|
||||
$updatedUser = $users->update(
|
||||
$id,
|
||||
(string) $request->request->get('email', ''),
|
||||
(string) $request->request->get('password', ''),
|
||||
(string) $request->request->get('password_repeat', ''),
|
||||
$this->extractRoles($request),
|
||||
$request->request->has('is_active'),
|
||||
$this->requireCurrentUser(),
|
||||
);
|
||||
|
||||
$this->addFlash('success', 'Benutzer wurde aktualisiert: ' . $updatedUser->getEmail());
|
||||
|
||||
return $this->redirectToRoute('admin_users_index');
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('danger', $e->getMessage());
|
||||
|
||||
return $this->redirectToRoute('admin_users_edit', ['id' => $id]);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/{id}/toggle-active', name: 'admin_users_toggle_active', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['POST'])]
|
||||
public function toggleActive(string $id, Request $request, UserAdminService $users): RedirectResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
|
||||
if (!$this->isCsrfTokenValid('admin_user_toggle_active_' . $id, (string) $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
|
||||
|
||||
return $this->redirectToRoute('admin_users_index');
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $users->toggleActive($id, $this->requireCurrentUser());
|
||||
$this->addFlash('success', sprintf(
|
||||
'Benutzer %s wurde %s.',
|
||||
$user->getEmail(),
|
||||
$user->isActive() ? 'aktiviert' : 'deaktiviert',
|
||||
));
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('danger', $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_users_index');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function extractRoles(Request $request): array
|
||||
{
|
||||
$roles = $request->request->all('roles');
|
||||
|
||||
if (!is_array($roles)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_map(static fn (mixed $role): string => (string) $role, $roles));
|
||||
}
|
||||
|
||||
private function requireCurrentUser(): User
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof UserInterface || !$user instanceof User) {
|
||||
throw $this->createAccessDeniedException('Kein gültiger Benutzer angemeldet.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\ApplicationRoles;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -16,28 +17,42 @@ class UserRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return User[] Returns an array of User objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('u.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
/**
|
||||
* @return list<User>
|
||||
*/
|
||||
public function findForAdminList(): array
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->orderBy('u.email', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
// public function findOneBySomeField($value): ?User
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
public function findOneByNormalizedEmail(string $email): ?User
|
||||
{
|
||||
$normalizedEmail = strtolower(trim($email));
|
||||
|
||||
if ($normalizedEmail === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findOneBy(['email' => $normalizedEmail]);
|
||||
}
|
||||
|
||||
public function countActiveSuperAdmins(?User $exclude = null): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->findBy(['isActive' => true]) as $user) {
|
||||
if ($exclude instanceof User && (string) $user->getId() === (string) $exclude->getId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array(ApplicationRoles::ROLE_SUPER_ADMIN, $user->getRoles(), true)) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
28
src/Security/ActiveUserChecker.php
Normal file
28
src/Security/ActiveUserChecker.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Exception\DisabledException;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Blocks login for users that were deactivated in the admin area.
|
||||
*/
|
||||
final class ActiveUserChecker implements UserCheckerInterface
|
||||
{
|
||||
public function checkPreAuth(UserInterface $user): void
|
||||
{
|
||||
if ($user instanceof User && !$user->isActive()) {
|
||||
throw new DisabledException('Dieser Benutzer ist deaktiviert.');
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPostAuth(UserInterface $user): void
|
||||
{
|
||||
// No post-auth checks required.
|
||||
}
|
||||
}
|
||||
64
src/Security/ActiveUserSessionSubscriber.php
Normal file
64
src/Security/ActiveUserSessionSubscriber.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
/**
|
||||
* Invalidates already-authenticated sessions when an admin deactivates a user.
|
||||
*/
|
||||
final readonly class ActiveUserSessionSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TokenStorageInterface $tokenStorage,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['onKernelRequest', 8],
|
||||
];
|
||||
}
|
||||
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
if (!$event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->tokenStorage->getToken();
|
||||
|
||||
if ($token === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User || $user->isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
$request = $event->getRequest();
|
||||
if ($request->hasSession()) {
|
||||
$request->getSession()->invalidate();
|
||||
}
|
||||
|
||||
$route = str_starts_with($request->getPathInfo(), '/admin') ? 'admin_login' : 'chat_login';
|
||||
$event->setResponse(new RedirectResponse($this->urlGenerator->generate($route)));
|
||||
}
|
||||
}
|
||||
45
src/Security/ApplicationRoles.php
Normal file
45
src/Security/ApplicationRoles.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
/**
|
||||
* Central list of application roles that may be assigned by administrators.
|
||||
*/
|
||||
final class ApplicationRoles
|
||||
{
|
||||
public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
|
||||
public const ROLE_KNOWLEDGE_ADMIN = 'ROLE_KNOWLEDGE_ADMIN';
|
||||
public const ROLE_EDITOR = 'ROLE_EDITOR';
|
||||
public const ROLE_ADMIN_AREA = 'ROLE_ADMIN_AREA';
|
||||
public const ROLE_CHAT_USER = 'ROLE_CHAT_USER';
|
||||
public const ROLE_USER = 'ROLE_USER';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function assignableChoices(): array
|
||||
{
|
||||
return [
|
||||
self::ROLE_SUPER_ADMIN => 'Super Admin',
|
||||
self::ROLE_KNOWLEDGE_ADMIN => 'Knowledge Admin',
|
||||
self::ROLE_EDITOR => 'Editor',
|
||||
self::ROLE_ADMIN_AREA => 'Adminbereich',
|
||||
self::ROLE_CHAT_USER => 'Chat User',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function assignableRoleNames(): array
|
||||
{
|
||||
return array_keys(self::assignableChoices());
|
||||
}
|
||||
|
||||
public static function label(string $role): string
|
||||
{
|
||||
return self::assignableChoices()[$role] ?? $role;
|
||||
}
|
||||
}
|
||||
279
src/Service/Admin/UserAdminService.php
Normal file
279
src/Service/Admin/UserAdminService.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Admin;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\ApplicationRoles;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* Encapsulates user-management rules for the admin UI.
|
||||
*/
|
||||
final readonly class UserAdminService
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $users,
|
||||
private EntityManagerInterface $em,
|
||||
private UserPasswordHasherInterface $passwordHasher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{users: list<User>, 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<string, string>
|
||||
*/
|
||||
public function roleChoices(): array
|
||||
{
|
||||
return ApplicationRoles::assignableChoices();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $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<string> $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<string> $roles
|
||||
* @return list<string>
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
private function storedAssignableRoles(User $user): array
|
||||
{
|
||||
return array_values(array_intersect(ApplicationRoles::assignableRoleNames(), $user->getRoles()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user