Easy_solution/src/Controller/UserController.php

836 lines
37 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\LoggerService;
use App\Service\OrganizationsService;
use App\Service\UserOrganizationAppService;
use App\Service\UserOrganizationService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use mysql_xdevapi\Exception;
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\HttpKernel\Exception\NotFoundHttpException;
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 $userManagementLogger,
private readonly LoggerInterface $organizationManagementLogger,
private readonly LoggerInterface $errorLogger,
private readonly LoggerService $loggerService,
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->errorLogger->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,
]);
}
#[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
public function edit(int $id, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
try{
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->userService->hasAccessTo($actingUser)) {
$user = $this->userRepository->find($id);
if (!$user) {
$this->userManagementLogger->notice('User not found for edit', [
'target_user_id' => $user->getId(),
'acting_user_id' => $actingUser->getId(),
'ip' => $request->getClientIp(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
throw $this->createNotFoundException(self::NOT_FOUND);
}
$form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request);
$this->userManagementLogger->notice('Format test', [
'target_user_id' => $user->getId(),
'acting_user_id' => $actingUser->getId(),
'ip' => $request->getClientIp(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
if ($form->isSubmitted() && $form->isValid()) {
// Handle user edit
$picture = $form->get('pictureUrl')->getData();
$this->userService->formatNewUserData($user, $picture);
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user);
$this->entityManager->flush();
//log and action
$this->userManagementLogger->notice('User information edited', [
'target_user_id' => $user->getId(),
'acting_user_id' => $actingUser->getId(),
'organization_id' => $request->get('organizationId'),
'ip' => $request->getClientIp(),
'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM),
]);
if ($request->get('organizationId')) {
$org = $this->organizationRepository->find($request->get('organizationId'));
if ($org) {
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
$this->organizationManagementLogger->info('User edited within organization context', [
'target_user_id' => $user->getId(),
'organization_id' => $org->getId(),
'acting_user' => $actingUser->getUserIdentifier(),
'ip' => $request->getClientIp(),
]);
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $request->get('organizationId')]);
}
} else {
$this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
'organizationId' => $request->get('organizationId')
]);
}
}catch (\Exception $e){
$this->errorLogger->critical($e->getMessage());
}
$this->SecurityLogger->warning('Access denied on user edit', [
'target_user_id' => $id,
'acting_user' => $actingUser?->getId(),
'ip' => $request->getClientIp(),
]);
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)) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
$user = new User();
$form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request);
$orgId = $request->get('organizationId');
$org = $orgId ? $this->organizationRepository->find($orgId) : null;
if (!$org && $orgId) {
$this->loggerService->logCritical('Organization not found for user creation', [
'organization_id' => $orgId,
'acting_user_id' => $actingUser->getId(),
'ip' => $request->getClientIp(),
]);
throw $this->createNotFoundException('Organization not found');
}
if ($form->isSubmitted() && $form->isValid()) {
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
// Case : User exists + has organization context
if ($existingUser && $org) {
$this->userService->addExistingUserToOrganization(
$existingUser,
$org,
$actingUser,
$request->getClientIp()
);
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin(
$existingUser->getId(),
$org->getId(),
$actingUser->getId(),
$request->getClientIp(),
"Super Admin linked user to organization",
);
}
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
}
// Case : User exists but NO organization context → error
if ($existingUser) {
$this->loggerService->logError('Attempt to create user with existing email without organization', [
'target_user_email' => $user->getid(),
'acting_user_id' => $actingUser->getId(),
'ip' => $request->getClientIp(),
]);
$form->get('email')->addError(
new \Symfony\Component\Form\FormError(
'This email is already in use. Add the user to an organization instead.'
)
);
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
'organizationId' => $orgId,
]);
}
$picture = $form->get('pictureUrl')->getData();
$this->userService->createNewUser($user, $actingUser, $picture, $request->getClientIp());
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin(
$user->getId(),
null,
$actingUser->getId(),
$request->getClientIp(),
"Super Admin created new user",
);
}
// Link to organization if provided
if ($org) {
$this->userService->linkUserToOrganization(
$user,
$org,
$actingUser,
$request->getClientIp()
);
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$this->loggerService->logSuperAdmin(
$user->getId(),
$org->getId(),
$actingUser->getId(),
$request->getClientIp(),
"Super Admin linked user to organization",
);
}
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
}
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
'organizationId' => $orgId,
]);
} catch (\Exception $e) {
$this->loggerService->logError("Error on user creation route: " . $e->getMessage(), [
'ip' => $request->getClientIp(),
]);
if ($orgId ?? null) {
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
}
return $this->redirectToRoute('user_index');
}
}
#[Route('/activeStatus/{id}', name: 'active_status', methods: ['GET', 'POST'])]
public function activeStatus(int $id, Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
try{
if ($this->userService->hasAccessTo($actingUser, true)) {
$user = $this->userRepository->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$status = $request->get('status');
if ($status === 'deactivate') {
$user->setIsActive(false);
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
$this->userService->revokeUserTokens($user->getUserIdentifier());
}
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->logger->notice("User deactivated " . $user->getUserIdentifier());
$this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
}
if ($status === 'activate') {
$user->setIsActive(true);
$user->setModifiedAt(new \DateTimeImmutable('now'));
$this->logger->notice("User activated " . $user->getUserIdentifier());
$this->actionService->createAction("Activate user", $actingUser, null, $user->getUserIdentifier());
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
}
}
}catch (\Exception $e){
$this->logger->error($e->getMessage());
}
throw $this->createNotFoundException(self::NOT_FOUND);
}
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
public function activateStatusOrganization(int $id, Request $request): JsonResponse{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
try {
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]);
if (!$uo) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$status = $request->get('status');
if ($status === 'deactivate') {
$uo->setIsActive(false);
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
$this->entityManager->persist($uo);
$this->entityManager->flush();
$data = ['user' => $user,
'organization' => $org];
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
$this->logger->notice("User Organizaton deactivated " . $user->getUserIdentifier());
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
}
if($status === "activate"){
$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 new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
}
}
}catch (\Exception $exception){
$this->logger->error($exception->getMessage());
}
throw $this->createNotFoundException(self::NOT_FOUND);
}
//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($actingUser, $user);
$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()];
$token = $this->userService->generatePasswordToken($user, $org->getId());
$this->emailService->sendPasswordSetupEmail($user, $token);
$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);
if ($orgId) {
$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('security/login.html.twig');
}
}