874 lines
40 KiB
PHP
874 lines
40 KiB
PHP
<?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\AccessTokenService;
|
||
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 Psr\Log\LoggerInterface;
|
||
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\AccessDeniedHttpException;
|
||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||
use Symfony\Component\Routing\Attribute\Route;
|
||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||
|
||
#[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 LoggerInterface $securityLogger,
|
||
private readonly LoggerService $loggerService,
|
||
private readonly EmailService $emailService,
|
||
private readonly AwsService $awsService,
|
||
private readonly OrganizationsService $organizationsService,
|
||
private readonly AppsRepository $appsRepository,
|
||
private readonly RolesRepository $rolesRepository, private readonly AccessTokenService $accessTokenService,
|
||
)
|
||
{
|
||
}
|
||
//TODO: Move mailing/notification logic to event listeners/subscribers for better separation of concerns and to avoid bloating the controller with non-controller logic. Keep in mind the potential for circular dependencies and design accordingly (e.g. using interfaces or decoupled events).
|
||
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||
public function index(): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||
return $this->render('user/index.html.twig', [
|
||
'users' => $totalUsers
|
||
]);
|
||
}
|
||
|
||
#[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());
|
||
|
||
// Chargement de l'utilisateur cible à afficher
|
||
$user = $this->userRepository->find($id);
|
||
if (!$user) {
|
||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||
$this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
//if hasAccessTo is false, turn to true and denie access
|
||
if (!$this->userService->hasAccessTo($user)) {
|
||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||
$this->addFlash('danger', "Vous n'avez pas accès à cette information.");
|
||
throw new AccessDeniedHttpException (self::ACCESS_DENIED);
|
||
}
|
||
try {
|
||
// Paramètre optionnel de contexte organisationnel
|
||
$orgId = $request->query->get('organizationId');
|
||
if ($orgId) {
|
||
// TODO: afficher les projets de l'organisation
|
||
} else {
|
||
// Afficher tous les projets de l'utilisateur
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->loggerService->logError('error while loading user information', [
|
||
'target_user_id' => $id,
|
||
'acting_user_id' => $actingUser->getId(),
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
$this->addFlash('danger', 'Une erreur est survenue lors du chargement des informations utilisateur.');
|
||
$referer = $request->headers->get('referer');
|
||
return $this->redirect($referer ?? $this->generateUrl('app_index'));
|
||
}
|
||
return $this->render('user/show.html.twig', [
|
||
'user' => $user,
|
||
'organizationId' => $orgId ?? null,
|
||
]);
|
||
}
|
||
|
||
#[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());
|
||
$user = $this->userRepository->find($id);
|
||
if (!$user) {
|
||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||
$this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
try {
|
||
if ($this->userService->hasAccessTo($user)) {
|
||
|
||
$form = $this->createForm(UserForm::class, $user);
|
||
$form->handleRequest($request);
|
||
if ($form->isSubmitted() && $form->isValid()) {
|
||
|
||
// Handle user edit
|
||
$picture = $form->get('pictureUrl')->getData();;
|
||
$this->userService->formatUserData($user, $picture);
|
||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||
$this->entityManager->persist($user);
|
||
$this->entityManager->flush();
|
||
|
||
//log and action
|
||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||
$orgId = $request->get('organizationId');
|
||
if ($orgId) {
|
||
$org = $this->organizationRepository->find($orgId);
|
||
if ($org) {
|
||
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited');
|
||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$user->getId(),
|
||
$actingUser->getId(),
|
||
"Super Admin accessed user edit page",
|
||
);
|
||
}
|
||
$this->addFlash('success', 'Information modifié avec success.');
|
||
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]);
|
||
}
|
||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||
$this->addFlash('danger', "L'organisation n'existe pas.");
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$user->getId(),
|
||
$actingUser->getId(),
|
||
"Super Admin accessed user edit page",
|
||
);
|
||
}
|
||
$this->addFlash('success', 'Information modifié avec success.');
|
||
$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')
|
||
]);
|
||
}
|
||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||
$this->addFlash('danger', "Accès non autorisé.");
|
||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||
} catch (\Exception $e) {
|
||
$this->addFlash('danger', 'Une erreur est survenue lors de la modification des informations utilisateur.');
|
||
$this->errorLogger->critical($e->getMessage());
|
||
}
|
||
// Default deny access. shouldn't reach here normally.
|
||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||
|
||
}
|
||
|
||
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||
public function new(Request $request): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||
try {
|
||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||
|
||
$user = new User();
|
||
$form = $this->createForm(UserForm::class, $user);
|
||
$form->handleRequest($request);
|
||
|
||
$orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
|
||
if ($orgId) {
|
||
$org = $this->organizationRepository->find($orgId);
|
||
if (!$org) {
|
||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||
$this->addFlash('danger', "L'organisation n'existe pas.");
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
|
||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||
$this->addFlash('danger', "Accès non autorisé.");
|
||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||
}
|
||
} else{
|
||
$this->loggerService->logAccessDenied($actingUser->getId());
|
||
$this->addFlash('danger', "Accès non autorisé.");
|
||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||
}
|
||
|
||
if ($form->isSubmitted() && $form->isValid()) {
|
||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||
|
||
// Case : User exists -> link him to given organization if not already linked, else error message
|
||
if ($existingUser && $org) {
|
||
$this->userService->addExistingUserToOrganization(
|
||
$existingUser,
|
||
$org,
|
||
);
|
||
|
||
if ($this->isGranted('ROLE_ADMIN')) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$existingUser->getId(),
|
||
$actingUser->getId(),
|
||
"Super Admin linked user to organization",
|
||
$org->getId(),
|
||
);
|
||
}
|
||
$this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
|
||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||
}
|
||
|
||
// Case : user doesn't already exist
|
||
|
||
$picture = $form->get('pictureUrl')->getData();
|
||
$this->userService->createNewUser($user, $actingUser, $picture);
|
||
|
||
$this->userService->linkUserToOrganization(
|
||
$user,
|
||
$org,
|
||
);
|
||
$this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
|
||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||
}
|
||
|
||
return $this->render('user/new.html.twig', [
|
||
'user' => $user,
|
||
'form' => $form->createView(),
|
||
'organizationId' => $orgId,
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
$this->errorLogger->critical($e->getMessage());
|
||
|
||
if ($orgId) {
|
||
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
|
||
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||
}
|
||
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
|
||
return $this->redirectToRoute('user_index');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Endpoint to activate/deactivate a user (soft delete)
|
||
* If deactivating, also deactivate all org links and revoke tokens
|
||
*/
|
||
#[Route('/activeStatus/{id}', name: 'active_status', methods: ['POST'])]
|
||
public function activeStatus(int $id, Request $request): JsonResponse
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||
|
||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||
$status = $request->request->get('status');
|
||
try {
|
||
$user = $this->userRepository->find($id);
|
||
if (!$user) {
|
||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
|
||
if ($status === 'deactivate') {
|
||
$user->setIsActive(false);
|
||
|
||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||
|
||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||
$this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
|
||
}
|
||
|
||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||
$this->entityManager->persist($user);
|
||
$this->entityManager->flush();
|
||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated');
|
||
|
||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$user->getId(),
|
||
$actingUser->getId(),
|
||
'Super admin deactivated user'
|
||
);
|
||
}
|
||
|
||
$this->actionService->createAction('Deactivate user', $actingUser, null, $user->getUserIdentifier());
|
||
|
||
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||
}
|
||
|
||
// Activate
|
||
if ($status === 'activate') {
|
||
$user->setIsActive(true);
|
||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||
$this->entityManager->persist($user);
|
||
$this->entityManager->flush();
|
||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User activated');
|
||
|
||
|
||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$user->getId(),
|
||
$actingUser->getId(),
|
||
'Super admin activated user'
|
||
);
|
||
}
|
||
|
||
$this->actionService->createAction('Activate user', $actingUser, null, $user->getUserIdentifier());
|
||
|
||
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
||
}
|
||
|
||
// Invalid status
|
||
$this->loggerService->logError('Invalid status provided for activeStatus', [
|
||
'requested_status' => $status,
|
||
'target_user_id' => $id,
|
||
]);
|
||
|
||
return new JsonResponse(['error' => 'Status invalide'], Response::HTTP_BAD_REQUEST);
|
||
|
||
} catch (\Throwable $e) {
|
||
// Application-level error logging → error.log (via error channel)
|
||
$this->errorLogger->critical($e->getMessage());
|
||
|
||
// Preserve 403/404 semantics, 500 for everything else
|
||
if ($e instanceof NotFoundHttpException || $e instanceof AccessDeniedException) {
|
||
throw $e;
|
||
}
|
||
|
||
return new JsonResponse(['error' => 'Une erreur est survenue'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
|
||
public function activateStatusOrganization(int $id, Request $request): JsonResponse
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||
try {
|
||
$user = $this->userRepository->find($id);
|
||
if (!$user) {
|
||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
if ($this->userService->isAdminOfUser($user)) {
|
||
$orgId = $request->get('organizationId');
|
||
$org = $this->organizationRepository->find($orgId);
|
||
if (!$org) {
|
||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
|
||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||
'organization' => $org]);
|
||
if (!$uo) {
|
||
$this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $user->getId(),
|
||
'organization_id' => $org->getId()], $actingUser->getId());
|
||
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->loggerService->logOrganizationInformation($org->getId(), $actingUser->getId(), "UO link deactivated with uo id : {$uo->getId()}");
|
||
$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->loggerService->logOrganizationInformation($orgId, $actingUser->getId(), "UO link activated with uo id : {$uo->getId()}");
|
||
$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);
|
||
}
|
||
//invalid status
|
||
$this->loggerService->logError('Invalid status provided for activateStatusOrganization', [
|
||
'requested_status' => $status,
|
||
'target_user_id' => $id,
|
||
'organization_id' => $orgId,
|
||
]);
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
} catch (\Exception $exception) {
|
||
$this->loggerService->logCritical($exception->getMessage());
|
||
}
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
|
||
#[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());
|
||
|
||
try {
|
||
$user = $this->userRepository->find($id);
|
||
if (!$user) {
|
||
// Security/audit log for missing user
|
||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId());
|
||
$this->addFlash('danger', "L'utilisateur demandé n'existe pas.");
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
|
||
// Soft delete the user
|
||
|
||
$user->setIsActive(false);
|
||
$user->setIsDeleted(true);
|
||
$this->userService->deleteProfilePicture($user);
|
||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||
// Deactivate all org links
|
||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||
$this->loggerService->logOrganizationInformation($user->getId(), $actingUser->getId(), 'All user organization links deactivated');
|
||
|
||
// Revoke tokens if connected
|
||
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||
$this->accessTokenService->revokeUserTokens($user->getUserIdentifier());
|
||
}
|
||
|
||
$this->entityManager->flush();
|
||
|
||
// User management log
|
||
$this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deleted');
|
||
|
||
// Super admin log (standardized style)
|
||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$user->getId(),
|
||
$actingUser->getId(),
|
||
'Super admin deleted user'
|
||
);
|
||
}
|
||
|
||
$this->actionService->createAction('Delete user', $actingUser, null, $user->getUserIdentifier());
|
||
|
||
// Notify organization admins (user may belong to multiple organizations)
|
||
try {
|
||
$data = [
|
||
'user' => $user,
|
||
'organization' => null,
|
||
];
|
||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_DELETED');
|
||
|
||
|
||
} catch (\Throwable $e) {
|
||
$this->loggerService->logCritical($e->getMessage(), [
|
||
'target_user_id' => $id,
|
||
'acting_user_id' => $actingUser?->getId(),
|
||
]);
|
||
}
|
||
$this->addFlash('success', 'Utilisateur supprimé avec succès.');
|
||
return $this->redirectToRoute('user_index');
|
||
|
||
} catch (\Exception $e) {
|
||
// Route-level error logging → error.log
|
||
$this->loggerService->logCritical('error while deleting user', [
|
||
'target_user_id' => $id,
|
||
'acting_user_id' => $actingUser?->getId(),
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
if ($e instanceof NotFoundHttpException) {
|
||
throw $e; // keep 404 semantics
|
||
}
|
||
$this->addFlash('danger', 'Erreur lors de la suppression de l\'utilisateur\.');
|
||
return $this->redirectToRoute('user_index');
|
||
}
|
||
}
|
||
|
||
#[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->entityManager->getRepository(UsersOrganizations::class)->find($id);
|
||
if (!$uo) {
|
||
$this->loggerService->logEntityNotFound('UsersOrganization', ['id' => $id], $actingUser->getId());
|
||
$this->addFlash('danger', "La liaison utilisateur-organisation n'existe pas.");
|
||
throw new NotFoundHttpException("UserOrganization not found");
|
||
}
|
||
$application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId'));
|
||
if (!$application) {
|
||
$this->loggerService->logEntityNotFound('Application', ['id' => $request->get('appId')], $actingUser->getId());
|
||
$this->addFlash('danger', "L'application demandée n'existe pas.");
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
|
||
$selectedRolesIds = $request->get('roles', []);
|
||
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
||
if (!$roleUser) {
|
||
$this->loggerService->logEntityNotFound('Role', ['name' => 'USER'], $actingUser->getId());
|
||
$this->addFlash('danger', "Le role de l'utilisateur n'existe pas.");
|
||
throw $this->createNotFoundException('User role not found');
|
||
}
|
||
|
||
if (!empty($selectedRolesIds)) {
|
||
// Si le role User n'est pas sélectionné, on désactive tous les liens (affiché comme 'accès' dans l'UI)
|
||
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();
|
||
$this->addFlash('success', 'Rôles mis à jour avec succès.');
|
||
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) {
|
||
return [
|
||
'id' => $user->getId(),
|
||
'pictureUrl' => $user->getPictureUrl(),
|
||
'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,
|
||
]);
|
||
}
|
||
|
||
/*
|
||
* 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_USER")) {
|
||
$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();
|
||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||
return [
|
||
'pictureUrl' => $user->getPictureUrl(),
|
||
'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_USER")) {
|
||
$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();
|
||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||
return [
|
||
'pictureUrl' => $user->getPictureUrl(),
|
||
'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_USER")) {
|
||
$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) {
|
||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId());
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
$user = $this->userRepository->find($userId);
|
||
if (!$user) {
|
||
$this->loggerService->logEntityNotFound('User', ['id' => $user->getId()], $actingUser->getId());
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
$token = $this->userService->generatePasswordToken($user, $org->getId());
|
||
if ($user->getLastConnection() !== null) {
|
||
$this->userService->sendExistingUserNotifications($user, $org, $actingUser);
|
||
} else {
|
||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||
'organization' => $org,
|
||
'statut' => "INVITED"]);
|
||
if (!$uo) {
|
||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
||
'user_id' => $user->getId(),
|
||
'organization_id' => $orgId], $actingUser->getId());
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||
try {
|
||
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
|
||
$this->emailService->sendPasswordSetupEmail($user, $token);
|
||
$this->loggerService->logEmailSent($userId, $org->getId(), 'Invitation Resent');
|
||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
|
||
} catch (\Exception $e) {
|
||
$this->loggerService->logCritical('Error while resending invitation', [
|
||
'target_user_id' => $user->getId(),
|
||
'organization_id' => $orgId,
|
||
'acting_user_id' => $actingUser->getId(),
|
||
'error' => $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) {
|
||
$this->loggerService->logEntityNotFound('Token or UserId missing in accept invitation', [
|
||
'token' => $token,
|
||
'user_id' => $userId
|
||
],
|
||
null);
|
||
throw $this->createNotFoundException('Invalid invitation link.');
|
||
}
|
||
$user = $this->userRepository->find($userId);
|
||
if (!$user) {
|
||
$this->loggerService->logEntityNotFound('User not found in accept invitation', [
|
||
'user_id' => $userId
|
||
], null);
|
||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||
}
|
||
if (!$this->userService->isPasswordTokenValid($user, $token)) {
|
||
$this->loggerService->logError('Token or UserId mismatch in accept invitation', [
|
||
'token' => $token,
|
||
'user_id' => $userId
|
||
]);
|
||
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->loggerService->logEntityNotFound('UsersOrganization not found or not in INVITED status in accept invitation', [
|
||
'user_id' => $user->getId(),
|
||
'organization_id' => $orgId
|
||
], null);
|
||
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->loggerService->logUserAction($user->getId(), $user->getId(), "User accepted invitation for organization id : {$orgId}");
|
||
$this->loggerService->logOrganizationInformation($orgId, $user->getId(), "User accepted invitation with uo id : {$uo->getId()}");
|
||
}
|
||
return $this->render('security/login.html.twig', ['error'=> null, 'last_username' => $user->getEmail()]);
|
||
}
|
||
}
|
||
|