Easy_solution/src/Controller/UserController.php

783 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Controller;
use App\Entity\Apps;
use App\Entity\Roles;
use App\Entity\User;
use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations;
use App\Form\UserForm;
use App\Repository\AppsRepository;
use App\Repository\OrganizationsRepository;
use App\Repository\RolesRepository;
use App\Repository\UserRepository;
use App\Repository\UsersOrganizationsRepository;
use App\Service\ActionService;
use App\Service\AwsService;
use App\Service\EmailService;
use App\Service\OrganizationsService;
use App\Service\UserOrganizationAppService;
use App\Service\UserOrganizationService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/user', name: 'user_')]
class UserController extends AbstractController
{
private const NOT_FOUND = 'Entity not found';
private const ACCESS_DENIED = 'Access denied';
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService,
private readonly ActionService $actionService,
private readonly UserOrganizationAppService $userOrganizationAppService,
private readonly UserOrganizationService $userOrganizationService,
private readonly UserRepository $userRepository,
private readonly UsersOrganizationsRepository $uoRepository,
private readonly OrganizationsRepository $organizationRepository,
private readonly LoggerInterface $logger,
private readonly EmailService $emailService,
private readonly AwsService $awsService,
private readonly OrganizationsService $organizationsService,
private readonly AppsRepository $appsRepository,
private readonly RolesRepository $rolesRepository,
)
{
}
#[Route('/view/{id}', name: 'show', methods: ['GET'])]
public function view(int $id, Request $request): Response
{
// Accès : uniquement utilisateur authentifié
$this->denyAccessUnlessGranted('ROLE_USER');
// Utilisateur courant (acting user) via UserService
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
// Vérification des droits d'accès supplémentaires
if (!$this->userService->hasAccessTo($actingUser)) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
// Chargement de l'utilisateur cible à afficher
$user = $this->userRepository->find($id);
try {
// Paramètre optionnel de contexte organisationnel
$orgId = $request->query->get('organizationId');
// Liste de toutes les applications (pour créer des groupes même si vides)
$apps = $this->appsRepository->findAll();
// Initialisations pour la résolution des UsersOrganizations (UO)
$singleUo = null;
$uoActive = null;
// get uo or uoS based on orgId
if ($orgId) {
// Contexte organisation précis : récupérer l'organisation et les liens UO
$organization = $this->organizationRepository->findBy(['id' => $orgId]);
$uoList = $this->uoRepository->findBy([
'users' => $user,
'organization' => $organization,
'isActive' => true,
]);
if (!$uoList) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
// Si contexte org donné, on retient la première UO (singleUo)
$singleUo = $uoList[0];
$data["singleUo"] = $singleUo;
$uoActive = $singleUo->isActive();
} else {
// Pas de contexte org : récupérer toutes les UO actives de l'utilisateur
$uoList = $this->uoRepository->findBy([
'users' => $user,
'isActive' => true,
]);
}
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
// Load user-organization-app roles (can be empty)
$uoa = $this->entityManager
->getRepository(UserOrganizatonApp::class)
->findBy([
'userOrganization' => $uoList,
'isActive' => true,
]);
// Group UOA by app and ensure every app has a group
$data['uoas'] = $this->userOrganizationAppService
->groupUserOrganizationAppsByApplication(
$uoa,
$apps,
$singleUo ? $singleUo->getId() : null
);
//Build roles based on user permissions.
//Admin can't see or edit a super admin user
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$data['rolesArray'] = $this->rolesRepository->findAll();
} elseif (!$orgId) {
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true);
} else {
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser);
}
// -------------------------------------------------------------------
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $organization, $this->isGranted('ROLE_ADMIN'));
} catch (\Exception $e) {
// En cas d'erreur, désactiver l'édition et logger l'exception
$canEdit = false;
$this->logger->error($e->getMessage());
}
return $this->render('user/show.html.twig', [
'user' => $user,
'organizationId' => $orgId ?? null,
'uoActive' => $uoActive ?? null,
'apps' => $apps ?? [],
'data' => $data ?? [],
'canEdit' => $canEdit ?? false,
]);
}
//TODO : MONOLOG
#[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
public function edit(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser)) {
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Handle file upload
$picture = $form->get('pictureUrl')->getData();
if ($picture) {
$this->userService->handleProfilePicture($user, $picture);
}
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user);
$this->entityManager->flush();
if ($request->get('organizationId')) {
$org = $this->organizationRepository->find($request->get('organizationId'));
if ($org) {
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
}
} else {
$this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
}
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $request->get('organizationId')]);
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
'organizationId' => $request->get('organizationId')
]);
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
try {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser)) {
$user = new User();
$form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request);
$orgId = $request->get('organizationId');
if ($form->isSubmitted() && $form->isValid()) {
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
if ($existingUser && $orgId) {
$org = $this->organizationRepository->find($orgId);
$uo = new UsersOrganizations();
$uo->setUsers($existingUser);
$uo->setOrganization($org);
$uo->setStatut("INVITED");
$uo->setIsActive(false);
$uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo);
$this->entityManager->flush();
$this->actionService->createAction("Create new user", $existingUser, $org, "Added user to organization" . $existingUser->getUserIdentifier() . " for organization " . $org->getName());
$this->logger->notice("User added to organization " . $org->getName());
$this->emailService->sendExistingUserNotificationEmail($existingUser, $org);
$this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier());
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
}
// capitalize name and surname
$user->setName(ucfirst(strtolower($user->getName())));
$user->setSurname(ucfirst(strtolower($user->getSurname())));
// Handle file upload
$picture = $form->get('pictureUrl')->getData();
if ($picture) {
$this->userService->handleProfilePicture($user, $picture);
}
//FOR TEST PURPOSES, SETTING A DEFAULT RANDOM PASSWORD
$user->setPassword($this->userService->generateRandomPassword());
if ($orgId) {
$org = $this->organizationRepository->find($orgId);
if ($org) {
$uo = new UsersOrganizations();
$uo->setUsers($user);
$uo->setOrganization($org);
$uo->setStatut("INVITED");
$uo->setIsActive(false);
$uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo);
$this->actionService->createAction("Create new user", $user, $org, "Added user to organization" . $user->getUserIdentifier() . " for organization " . $org->getName());
$this->logger->notice("User added to organization " . $org->getName());
$this->emailService->sendPasswordSetupEmail($user, $orgId);
$this->logger->notice("Password setup email sent to " . $user->getUserIdentifier());
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
}
}
$this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier());
$this->logger->notice("User created " . $user->getUserIdentifier());
$this->entityManager->persist($user);
$this->entityManager->flush();
if ($orgId) {
return $this->redirectToRoute('organization_show', ['organizationId' => $orgId]);
}
return $this->redirectToRoute('user_index');
}
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
'organizationId' => $orgId
]);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
if ($orgId) {
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
}
return $this->redirectToRoute('user_index');
}
}
//TODO : MONOLOG
#[Route('/deactivate/{id}', name: 'deactivate', methods: ['GET', 'POST'])]
public function deactivate(int $id): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user->setIsActive(false);
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->userOrganizationService->deactivateAllUserOrganizationLinks($user, $actingUser);
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
$this->userService->revokeUserTokens($user->getUserIdentifier());
}
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
return $this->redirectToRoute('user_index');
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
//TODO : MONOLOG
#[Route('/activate/{id}', name: 'activate', methods: ['GET', 'POST'])]
public function activate(int $id): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user->setIsActive(true);
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->actionService->createAction("Activate user", $actingUser, null, $user->getUserIdentifier());
return $this->redirectToRoute('user_index');
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
//TODO : MONOLOG
#[Route('/organization/deactivate/{id}', name: 'deactivate_organization', methods: ['GET', 'POST'])]
public function deactivateUserInOrganization(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
$orgId = $request->get('organizationId');
$org = $this->organizationRepository->find($orgId);
if (!$org) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo = $this->uoRepository->findOneBy(['users' => $user,
'organization' => $org,
'isActive' => true]);
if (!$uo) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo->setIsActive(false);
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
$data = ['user' => $user,
'organization' => $org];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
$this->entityManager->persist($uo);
$this->entityManager->flush();
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
return new Response('', Response::HTTP_NO_CONTENT); //204
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
//TODO : MONOLOG
#[Route('/organization/activate/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
public function activateUserInOrganization(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
$orgId = $request->get('organizationId');
$org = $this->organizationRepository->find($orgId);
if (!$org) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo = $this->uoRepository->findOneBy(['users' => $user,
'organization' => $org,
'isActive' => false]);
if (!$uo) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo->setIsActive(true);
$this->entityManager->persist($uo);
$this->entityManager->flush();
$this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
$data = ['user' => $user,
'organization' => $org];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACTIVATED");
return $this->redirectToRoute('user_index');
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
//TODO : MONOLOG + remove picture from bucket
#[Route('/delete/{id}', name: 'delete', methods: ['GET', 'POST'])]
public function delete(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user->setIsActive(false);
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->userOrganizationService->deactivateAllUserOrganizationLinks($user, $actingUser);
$user->setIsDeleted(true);
if ($this->userService->isUserConnected($user)) {
$this->userService->revokeUserTokens($user->getUserIdentifier());
}
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier());
$data = ['user' => $user,
'organization' => null];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DELETED");
return new Response('', Response::HTTP_NO_CONTENT); //204
}
//TODO : MONOLOG
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
public function applicationRole(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted("ROLE_ADMIN");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
$uo = $this->userOrganizationService->getByIdOrFail($id);
$application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId'));
if (!$application) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$selectedRolesIds = $request->get('roles', []);
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
if (!$roleUser) {
throw $this->createNotFoundException('Default role not found');
}
if (!empty($selectedRolesIds)) {
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) {
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
} else {
$this->userOrganizationAppService->syncRolesForUserOrganizationApp(
$uo,
$application,
$selectedRolesIds,
$actingUser
);
}
} else {
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
}
$user = $uo->getUsers();
return $this->redirectToRoute('user_show', [
'user' => $user,
'id' => $user->getId(),
'organizationId' => $uo->getOrganization()->getId()
]);
}
throw $this->createAccessDeniedException();
}
/*
* AJAX endpoint for user listing with pagination
* Get all the users that aren´t deleted and are active
*/
#[Route(path: '/data', name: 'data', methods: ['GET'])]
public function data(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted("ROLE_ADMIN");
$page = max(1, (int)$request->query->get('page', 1));
$size = max(1, (int)$request->query->get('size', 10));
// Get filter parameters
$filters = $request->query->all('filter', []);
$repo = $this->userRepository;
// Base query
$qb = $repo->createQueryBuilder('u')
->where('u.isDeleted = :del')->setParameter('del', false);
// Apply filters
if (!empty($filters['name'])) {
$qb->andWhere('u.surname LIKE :name')
->setParameter('name', '%' . $filters['name'] . '%');
}
if (!empty($filters['prenom'])) {
$qb->andWhere('u.name LIKE :prenom')
->setParameter('prenom', '%' . $filters['prenom'] . '%');
}
if (!empty($filters['email'])) {
$qb->andWhere('u.email LIKE :email')
->setParameter('email', '%' . $filters['email'] . '%');
}
$countQb = clone $qb;
$total = (int)$countQb->select('COUNT(u.id)')->getQuery()->getSingleScalarResult();
// Pagination
$offset = ($page - 1) * $size;
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
// Map to array
$data = array_map(function (User $user) {
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
return [
'id' => $user->getId(),
'pictureUrl' => $picture,
'name' => $user->getSurname(),
'prenom' => $user->getName(),
'email' => $user->getEmail(),
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
'statut' => $user->isActive(),
];
}, $rows);
$lastPage = (int)ceil($total / $size);
return $this->json([
'data' => $data,
'last_page' => $lastPage,
'total' => $total,
]);
}
#[Route(path: '/', name: 'index', methods: ['GET'])]
public function index(): Response
{
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
return $this->render('user/index.html.twig', [
'users' => $totalUsers
]);
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
/*
* AJAX endpoint for new users listing
* Get the 5 most recently created users for an organization
*/
#[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])]
public function dataNew(Request $request): JsonResponse
{
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$orgId = $request->query->get('orgId');
$uos = $this->uoRepository->findBy(['organization' => $orgId, 'statut' => ["ACCEPTED", "INVITED"]],
orderBy: ['createdAt' => 'DESC'], limit: 5);
// Map to array (keep isConnected)
$data = array_map(function (UsersOrganizations $uo) {
$user = $uo->getUsers();
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
$initials = $user->getName()[0] . $user->getSurname()[0];
return [
'pictureUrl' => $picture,
'email' => $user->getEmail(),
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
'initials' => strtoupper($initials),
];
}, $uos);
return $this->json([
'data' => $data,
]);
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
/*
* AJAX endpoint for admin users listing
* Get all admin users for an organization
*/
#[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])]
public function dataAdmin(Request $request): JsonResponse
{
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$orgId = $request->query->get('orgId');
$uos = $this->uoRepository->findBy(['organization' => $orgId]);
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
$users = [];
foreach ($uos as $uo) {
if ($this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin])) {
$users[] = $uo;
}
}
// Map to array (keep isConnected)
$data = array_map(function (UsersOrganizations $uo) {
$user = $uo->getUsers();
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
$initials = $user->getName()[0] . $user->getSurname()[0];
return [
'pictureUrl' => $picture,
'email' => $user->getEmail(),
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
'initials' => strtoupper($initials),
];
}, $users);
return $this->json([
'data' => $data,
]);
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
/*
* AJAX endpoint for All users in an organization
*/
#[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])]
public function dataUserOrganization(Request $request): JsonResponse
{
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
$orgId = $request->query->get('orgId');
$page = max(1, (int)$request->query->get('page', 1));
$size = max(1, (int)$request->query->get('size', 10));
$filters = $request->query->all('filter') ?? [];
$repo = $this->uoRepository;
// Base query
$qb = $repo->createQueryBuilder('uo')
->join('uo.users', 'u')
->where('uo.organization = :orgId')
->setParameter('orgId', $orgId);
// Apply filters
if (!empty($filters['name'])) {
$qb->andWhere('u.surname LIKE :name')
->setParameter('name', '%' . $filters['name'] . '%');
}
if (!empty($filters['prenom'])) {
$qb->andWhere('u.name LIKE :prenom')
->setParameter('prenom', '%' . $filters['prenom'] . '%');
}
if (!empty($filters['email'])) {
$qb->andWhere('u.email LIKE :email')
->setParameter('email', '%' . $filters['email'] . '%');
}
$countQb = clone $qb;
$total = (int)$countQb->select('COUNT(uo.id)')->getQuery()->getSingleScalarResult();
$qb->orderBy('uo.isActive', 'DESC')
->addOrderBy('CASE WHEN uo.statut = :invited THEN 0 ELSE 1 END', 'ASC')
->setParameter('invited', 'INVITED');
$offset = ($page - 1) * $size;
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
$data = $this->userService->formatStatutForOrganizations($rows);
$lastPage = (int)ceil($total / $size);
return $this->json([
'data' => $data,
'last_page' => $lastPage,
'total' => $total,
]);
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
#[Route(path: '/organization/resend-invitation/{userId}', name: 'resend_invitation', methods: ['POST'])]
public function resendInvitation(int $userId, Request $request): JsonResponse
{
$this->denyAccessUnlessGranted("ROLE_ADMIN");
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser, true)) {
$orgId = $request->get('organizationId');
$org = $this->organizationRepository->find($orgId);
if (!$org) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user = $this->userRepository->find($userId);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo = $this->uoRepository->findOneBy(['users' => $user,
'organization' => $org,
'statut' => "INVITED"]);
if (!$uo) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$uo->setModifiedAt(new \DateTimeImmutable());
try {
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
$this->emailService->sendPasswordSetupEmail($user, $orgId);
$this->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName());
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
} catch (\Exception $e) {
$this->logger->error("Error resending invitation email to user " . $user->getUserIdentifier() . " for organization " . $org->getName() . ": " . $e->getMessage());
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
#[Route(path: '/accept-invitation', name: 'accept', methods: ['GET'])]
public function acceptInvitation(Request $request): Response
{
$token = $request->get('token');
$userId = $request->get('id');
if (!$token || !$userId) {
throw $this->createNotFoundException('Invalid invitation link.');
}
$user = $this->userRepository->find($userId);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
if (!$this->userService->isPasswordTokenValid($user, $token)) {
throw $this->createNotFoundException('Invalid or expired invitation token.');
}
$orgId = $this->userService->getOrgFromToken($token);
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
if (!$uo || $uo->getStatut() !== 'INVITED') {
$this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId);
throw $this->createNotFoundException('No pending invitation found for this user and organization.');
}
$uo->setModifiedAt(new \DateTimeImmutable());
$uo->setStatut("ACCEPTED");
$uo->setIsActive(true);
$this->entityManager->persist($uo);
$this->entityManager->flush();
$this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId);
return $this->render('user/show.html.twig', ['user' => $user, 'orgId' => $orgId]);
}
}