From 5f4336d824fa2380352af0ce7f84e44fe435aece Mon Sep 17 00:00:00 2001 From: Charles Date: Mon, 8 Dec 2025 16:27:55 +0100 Subject: [PATCH] Refactor for monolog in user controller --- src/Controller/SecurityController.php | 7 +- src/Controller/UserController.php | 396 +++++++++++++-------- src/Service/EmailService.php | 23 +- src/Service/LoggerService.php | 157 ++++++-- src/Service/OrganizationsService.php | 54 ++- src/Service/UserOrganizationAppService.php | 38 +- src/Service/UserOrganizationService.php | 13 +- src/Service/UserService.php | 90 +++-- 8 files changed, 542 insertions(+), 236 deletions(-) diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index f51f040..9ea51a6 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -98,7 +98,7 @@ class SecurityController extends AbstractController } #[Route('/password_reset/{id}', name: 'password_reset', methods: ['POST'])] - public function password_reset(int $id): Response + public function password_reset(int $id, Request $request): Response { $user = $this->userRepository->find($id); if (!$user) { @@ -127,7 +127,10 @@ class SecurityController extends AbstractController $uo->setIsActive(true); $this->entityManager->persist($uo); $this->entityManager->flush(); - $data = ['user' => $user, 'organization' => $uo->getOrganization()]; + $data = ['user' => $user, + 'organization' => $uo->getOrganization(), + 'ip' => $request->getClientIp(), + ]; $this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED"); } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 40f52c4..a7e6e03 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -22,18 +22,14 @@ 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; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; #[Route(path: '/user', name: 'user_')] class UserController extends AbstractController @@ -76,6 +72,7 @@ class UserController extends AbstractController // Vérification des droits d'accès supplémentaires if (!$this->userService->hasAccessTo($actingUser)) { + $this->loggerService->logAccessDenied($actingUser->getId()); throw $this->createAccessDeniedException(self::ACCESS_DENIED); } @@ -104,6 +101,10 @@ class UserController extends AbstractController ]); if (!$uoList) { + $this->loggerService->logEntityNotFound('UsersOrganization', [ + 'user_id' => $user->getId(), + 'organization_id' => $orgId], + $actingUser->getId()); throw $this->createNotFoundException(self::NOT_FOUND); } @@ -117,6 +118,13 @@ class UserController extends AbstractController 'users' => $user, 'isActive' => true, ]); + if (!$uoList) { + $this->loggerService->logEntityNotFound('UsersOrganization', [ + 'user_id' => $user->getId(), + 'organization_id' => $orgId], + $actingUser->getId()); + throw $this->createNotFoundException(self::NOT_FOUND); + } } // Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées @@ -127,6 +135,13 @@ class UserController extends AbstractController 'userOrganization' => $uoList, 'isActive' => true, ]); + if (!$uoa) { + $this->loggerService->logEntityNotFound('UsersOrganizationApplication', [ + 'user_id' => $user->getId(), + 'organization_id' => $orgId], + $actingUser->getId()); + throw $this->createNotFoundException(self::NOT_FOUND); + } // Group UOA by app and ensure every app has a group $data['uoas'] = $this->userOrganizationAppService @@ -171,17 +186,11 @@ class UserController extends AbstractController { $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), - ]); + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); throw $this->createNotFoundException(self::NOT_FOUND); } $form = $this->createForm(UserForm::class, $user); @@ -190,64 +199,70 @@ class UserController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { // Handle user edit $picture = $form->get('pictureUrl')->getData(); - $this->userService->formatNewUserData($user, $picture); + $this->userService->formatUserData($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')); + $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->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')]); + $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User information edited'); + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin( + $user->getId(), + null, + $actingUser->getId(), + "Super Admin accessed user edit page", + ); + } + return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $orgId]); } - } else { - $this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier()); - return $this->redirectToRoute('user_show', ['id' => $user->getId()]); + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); + throw $this->createNotFoundException(self::NOT_FOUND); } + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin( + $user->getId(), + null, + $actingUser->getId(), + "Super Admin accessed user edit page", + ); + } + $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()); + throw $this->createAccessDeniedException(self::ACCESS_DENIED); } 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(), - ]); + // 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_ADMIN'); - try { $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if (!$this->userService->hasAccessTo($actingUser)) { + $this->loggerService->logAccessDenied($actingUser->getId()); throw $this->createAccessDeniedException(self::ACCESS_DENIED); } @@ -256,15 +271,12 @@ class UserController extends AbstractController $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 ($orgId) { + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); + throw $this->createNotFoundException(self::NOT_FOUND); + } } if ($form->isSubmitted() && $form->isValid()) { @@ -276,7 +288,6 @@ class UserController extends AbstractController $existingUser, $org, $actingUser, - $request->getClientIp() ); if ($this->isGranted('ROLE_SUPER_ADMIN')) { @@ -284,20 +295,17 @@ class UserController extends AbstractController $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 + // Case : User exists but NO organization context -> throw error on email field. 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( @@ -314,26 +322,24 @@ class UserController extends AbstractController } $picture = $form->get('pictureUrl')->getData(); - $this->userService->createNewUser($user, $actingUser, $picture, $request->getClientIp()); + $this->userService->createNewUser($user, $actingUser, $picture); 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 + // Case : Organization provided and user doesn't already exist if ($org) { $this->userService->linkUserToOrganization( $user, $org, $actingUser, - $request->getClientIp() ); if ($this->isGranted('ROLE_SUPER_ADMIN')) { @@ -341,8 +347,7 @@ class UserController extends AbstractController $user->getId(), $org->getId(), $actingUser->getId(), - $request->getClientIp(), - "Super Admin linked user to organization", + "Super Admin linked user to organization during creation", ); } @@ -359,11 +364,9 @@ class UserController extends AbstractController ]); } catch (\Exception $e) { - $this->loggerService->logError("Error on user creation route: " . $e->getMessage(), [ - 'ip' => $request->getClientIp(), - ]); + $this->errorLogger->critical($e->getMessage()); - if ($orgId ?? null) { + if ($orgId) { return $this->redirectToRoute('organization_show', ['id' => $orgId]); } @@ -378,32 +381,19 @@ class UserController extends AbstractController $this->denyAccessUnlessGranted('ROLE_ADMIN'); $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - $ip = $request->getClientIp(); $status = $request->get('status'); try { // Access control if (!$this->userService->hasAccessTo($actingUser, true)) { - $this->securityLogger->warning('Access denied on user status change', [ - 'target_user_id' => $id, - 'acting_user_id' => $actingUser?->getId(), - 'acting_identifier' => $actingUser?->getId(), - 'requested_status' => $status, - 'ip' => $ip, - ]); - + $this->loggerService->logAccessDenied($actingUser->getId()); throw $this->createAccessDeniedException(self::ACCESS_DENIED); } // Load target user $user = $this->userRepository->find($id); if (!$user) { - $this->securityLogger->warning('User not found for status change', [ - 'target_user_id' => $id, - 'acting_user_id' => $actingUser->getId(), - 'requested_status' => $status, - 'ip' => $ip, - ]); + $this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getId()); throw $this->createNotFoundException(self::NOT_FOUND); } @@ -416,31 +406,18 @@ class UserController extends AbstractController if ($this->userService->isUserConnected($user->getUserIdentifier())) { $this->userService->revokeUserTokens($user->getUserIdentifier()); - - $this->securityLogger->info('User tokens revoked due to deactivation', [ - 'target_user_id' => $user->getId(), - 'target_identifier' => $user->getId(), - 'acting_user_id' => $actingUser->getId(), - 'ip' => $ip, - ]); } $user->setModifiedAt(new \DateTimeImmutable('now')); + $this->entityManager->persist($user); $this->entityManager->flush(); - - $this->userManagementLogger->notice('User deactivated', [ - 'target_user_id' => $user->getId(), - 'target_identifier' => $user->getId(), - 'acting_user_id' => $actingUser->getId(), - 'ip' => $ip, - ]); + $this->loggerService->logUserAction($user->getId(), $actingUser->getId(), 'User deactivated'); if ($this->isGranted('ROLE_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), null, $actingUser->getId(), - $ip, 'Super admin deactivated user' ); } @@ -454,21 +431,16 @@ class UserController extends AbstractController 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'); - $this->userManagementLogger->notice('User activated', [ - 'target_user_id' => $user->getId(), - 'target_identifier' => $user->getId(), - 'acting_user_id' => $actingUser->getId(), - 'ip' => $ip, - ]); if ($this->isGranted('ROLE_SUPER_ADMIN')) { $this->loggerService->logSuperAdmin( $user->getId(), null, $actingUser->getId(), - $ip, 'Super admin activated user' ); } @@ -479,25 +451,16 @@ class UserController extends AbstractController } // Invalid status - $this->errorLogger->warning('Invalid status passed to activeStatus', [ - 'target_user_id' => $user->getId(), - 'acting_user_id' => $actingUser->getId(), + $this->loggerService->logError('Invalid status provided for activeStatus', [ 'requested_status' => $status, - 'ip' => $ip, + 'target_user_id' => $id, ]); return new JsonResponse(['error' => 'Invalid status'], Response::HTTP_BAD_REQUEST); } catch (\Throwable $e) { // Application-level error logging → error.log (via error channel) - $this->errorLogger->error('Error in activeStatus', [ - 'exception_message' => $e->getMessage(), - 'exception_class' => get_class($e), - 'target_user_id' => $id, - 'acting_user_id' => $actingUser?->getId(), - 'requested_status' => $status, - 'ip' => $ip, - ]); + $this->errorLogger->critical($e->getMessage()); // Preserve 403/404 semantics, 500 for everything else if ($e instanceof NotFoundHttpException || $e instanceof AccessDeniedException) { @@ -508,34 +471,152 @@ class UserController extends AbstractController } } -//TODO : MONOLOG + remove picture from bucket + #[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) { + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getId()); + throw $this->createNotFoundException(self::NOT_FOUND); + } + $user = $this->userRepository->find($id); + if (!$user) { + $this->loggerService->logEntityNotFound('User', ['id' => $id], $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); + } + +//TODO :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"); + $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); - return new Response('', Response::HTTP_NO_CONTENT); //204 + $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()); + + throw $this->createNotFoundException(self::NOT_FOUND); + } + + // Soft delete the user + $user->setIsActive(false); + $user->setIsDeleted(true); + $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->userService->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(), + null, + $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(), + ]); + // No rethrow here: deletion succeeded; only notifications failed + } + + return new Response('', Response::HTTP_NO_CONTENT); // 204 + + } 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 + } + + return new Response('', Response::HTTP_INTERNAL_SERVER_ERROR); + } } - //TODO : MONOLOG #[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])] public function applicationRole(int $id, Request $request): Response { @@ -543,19 +624,26 @@ class UserController extends AbstractController $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); if ($this->userService->hasAccessTo($actingUser, true)) { - $uo = $this->userOrganizationService->getByIdOrFail($id); + $uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id); + if (!$uo) { + $this->loggerService->logEntityNotFound('UsersOrganization', ['id' => $id], $actingUser->getId()); + 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()); 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'); + $this->loggerService->logEntityNotFound('Role', ['name' => 'USER'], $actingUser->getId()); + 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 { @@ -651,6 +739,7 @@ class UserController extends AbstractController #[Route(path: '/', name: 'index', methods: ['GET'])] public function index(): Response { + $this->isGranted('ROLE_SUPER_ADMIN'); $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]); @@ -658,6 +747,9 @@ class UserController extends AbstractController 'users' => $totalUsers ]); } + + //shouldn't be reached normally + $this->loggerService->logAccessDenied($actingUser->getId()); throw $this->createAccessDeniedException(self::ACCESS_DENIED); } @@ -811,16 +903,21 @@ class UserController extends AbstractController $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); } $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()); @@ -832,7 +929,12 @@ class UserController extends AbstractController $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()); + $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); } } @@ -846,20 +948,35 @@ class UserController extends AbstractController $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->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId); + $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()); @@ -867,7 +984,8 @@ class UserController extends AbstractController $uo->setIsActive(true); $this->entityManager->persist($uo); $this->entityManager->flush(); - $this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId); + $this->loggerService->logUserAction($user->getId(), null, "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'); } diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php index 61d750e..f4f1b3e 100644 --- a/src/Service/EmailService.php +++ b/src/Service/EmailService.php @@ -4,6 +4,7 @@ namespace App\Service; use App\Entity\Organizations; use App\Entity\User; +use App\Service\LoggerService; use Psr\Log\LoggerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; @@ -15,7 +16,7 @@ class EmailService public function __construct( private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, - private UrlGeneratorInterface $urlGenerator + private UrlGeneratorInterface $urlGenerator, private readonly LoggerService $loggerService ) {} public function sendPasswordSetupEmail(User $user, string $token): void @@ -43,7 +44,9 @@ class EmailService ]); try { + $orgId = $this->getOrgFromToken($token); $this->mailer->send($email); + $this->loggerService->logEmailSent($user->getId(), $orgId, 'Password setup email sent.'); } catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) { $this->logger->error('Failed to send password setup email: ' . $e->getMessage()); } @@ -69,10 +72,26 @@ class EmailService ]); try{ + $orgId = $org->getId(); + $this->loggerService->logEmailSent($existingUser->getId(), $orgId, 'Existing user notification email sent.'); $this->mailer->send($email); } catch (TransportExceptionInterface $e) { $this->logger->error('Failed to send existing user notification email: ' . $e->getMessage()); } } -} \ No newline at end of file + + private function getOrgFromToken(string $token): ?int + { + if (str_starts_with($token, 'o')) { + $parts = explode('@', $token); + if (count($parts) === 2) { + $orgPart = substr($parts[0], 1); // Remove the leading 'o' + if (is_numeric($orgPart)) { + return (int)$orgPart; + } + } + } + return null; + } +} diff --git a/src/Service/LoggerService.php b/src/Service/LoggerService.php index 433ea71..30ed48b 100644 --- a/src/Service/LoggerService.php +++ b/src/Service/LoggerService.php @@ -4,106 +4,99 @@ namespace App\Service; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; readonly class LoggerService { public function __construct( - private LoggerInterface $userManagementLogger, - private LoggerInterface $organizationManagementLogger, - private LoggerInterface $accessControlLogger, - private LoggerInterface $emailNotificationLogger, - private LoggerInterface $adminActionsLogger, - private LoggerInterface $securityLogger, - private LoggerInterface $errorLogger, + private LoggerInterface $userManagementLogger, + private LoggerInterface $organizationManagementLogger, + private LoggerInterface $accessControlLogger, + private LoggerInterface $emailNotificationLogger, + private LoggerInterface $adminActionsLogger, + private LoggerInterface $securityLogger, + private LoggerInterface $errorLogger, + private RequestStack $requestStack, ) {} + + // User Management Logs - public function logUserCreated(int $userId, int $actingUserId, ?string $ip): void + public function logUserCreated(int $userId, int $actingUserId): void { $this->userManagementLogger->notice("New user created: $userId", [ 'target_user_id' => $userId, 'acting_user_id' => $actingUserId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } - public function logUserEdited(int $userId, int $actingUserId, ?int $orgId, ?string $ip): void - { - $this->userManagementLogger->notice('User information edited', [ - 'target_user_id' => $userId, - 'acting_user_id' => $actingUserId, - 'organization_id' => $orgId, - 'ip' => $ip, - 'timestamp' => $this->now(), - ]); - } - // Organization Management Logs - public function logUserOrganizationLinkCreated(int $userId, int $orgId, int $actingUserId, ?int $uoId, ?string $ip): void + public function logUserOrganizationLinkCreated(int $userId, int $orgId, int $actingUserId, ?int $uoId): void { $this->organizationManagementLogger->notice('User-Organization link created', [ 'target_user_id' => $userId, 'organization_id' => $orgId, 'acting_user_id' => $actingUserId, 'uo_id' => $uoId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } - public function logExistingUserAddedToOrg(int $userId, int $orgId, int $actingUserId, int $uoId, ?string $ip): void + public function logExistingUserAddedToOrg(int $userId, int $orgId, int $actingUserId, int $uoId): void { $this->organizationManagementLogger->notice('Existing user added to organization', [ 'target_user_id' => $userId, 'organization_id' => $orgId, 'acting_user_id' => $actingUserId, 'uo_id' => $uoId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } // Email Notification Logs - public function logPasswordSetupEmailSent(int $userId, ?int $orgId, ?string $ip): void + public function logEmailSent(int $userId, ?int $orgId, string $message): void { - $this->emailNotificationLogger->notice("Password setup email sent to $userId", [ + $this->emailNotificationLogger->notice($message, [ 'target_user_id' => $userId, 'organization_id' => $orgId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } - public function logExistingUserNotificationSent(int $userId, int $orgId, ?string $ip): void + public function logExistingUserNotificationSent(int $userId, int $orgId): void { $this->emailNotificationLogger->notice("Existing user notification email sent to $userId", [ 'target_user_id' => $userId, 'organization_id' => $orgId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } - public function logAdminNotified(int $adminUserId, int $targetUserId, int $orgId, int $actingUserId, ?string $ip): void + public function logAdminNotified(int $adminUserId, int $targetUserId, int $orgId, int $actingUserId): void { $this->emailNotificationLogger->notice('Organization admin notified', [ 'admin_user_id' => $adminUserId, 'target_user_id' => $targetUserId, 'organization_id' => $orgId, 'acting_user_id' => $actingUserId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } - public function logSuperAdmin(int $userId, ?int $orgId = null, int $actingUserId, ?string $ip, string $message): void + public function logSuperAdmin(int $userId, ?int $orgId = null, int $actingUserId, string $message): void { $this->adminActionsLogger->notice($message, [ 'target_user_id' => $userId, 'organization_id' => $orgId, 'acting_user_id' => $actingUserId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), ]); } @@ -113,6 +106,7 @@ readonly class LoggerService { $this->errorLogger->error($message, array_merge($context, [ 'timestamp' => $this->now(), + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', ])); } @@ -120,17 +114,18 @@ readonly class LoggerService { $this->errorLogger->critical($message, array_merge($context, [ 'timestamp' => $this->now(), + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', ])); } // Security Logs - public function logAccessDenied(int $targetId, ?int $actingUserId, ?string $ip): void + public function logAccessDenied(?int $actingUserId): void { $this->securityLogger->warning('Access denied', [ - 'target_id' => $targetId, 'acting_user_id' => $actingUserId, - 'ip' => $ip, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', 'timestamp' => $this->now(), + 'page_accessed' => $_SERVER['REQUEST_URI'] ?? 'unknown', ]); } @@ -139,4 +134,94 @@ readonly class LoggerService { return (new \DateTimeImmutable('now'))->format(DATE_ATOM); } + + + public function logUserAction(int $targetId, int $actingUserId, string $message): void + { + $this->userManagementLogger->notice($message, [ + 'target_user_id'=> $targetId, + 'acting_user_id'=> $actingUserId, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } + + public function logAdminAction(int $targetId, int $actingUserId, int $organizationId, string $message): void + { + $this->adminActionsLogger->notice($message, [ + 'target_id' => $targetId, + 'acting_user_id'=> $actingUserId, + 'organization_id'=> $organizationId, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } + + public function logEntityNotFound(string $entityType, array $criteria, ?int $actingUserId): void + { + $this->errorLogger->warning('Entity not found', [ + 'entity_type' => $entityType, + 'criteria' => $criteria, + 'acting_user_id' => $actingUserId, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } + + public function logAWSAction(string $action, array $details): void + { + $this->securityLogger->info("AWS action performed: $action", array_merge($details, [ + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ])); + } + + public function logTokenRevocation(string $message, array $array): void + { + $this->securityLogger->notice($message, array_merge($array, [ + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ])); + } + + public function logUOALinkDeactivated(int $uoaId, int $appId, int $roleId): void + { + $this->securityLogger->notice('UOA link deactivated', [ + 'uoa_id' => $uoaId, + 'app_id' => $appId, + 'role_id' => $roleId, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } + + public function logOrganizationInformation(int $organizationId, int $actingUserId, string $message): void + { + $this->organizationManagementLogger->info($message, [ + 'organization_id' => $organizationId, + 'acting_user_id' => $actingUserId, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } + + public function logRoleEntityAssignment(int $userId, int $organizationId, int $roleId, int $actingUserId, string $message): void + { + $this->accessControlLogger->info($message, [ + 'target_user_id' => $userId, + 'organization_id' => $organizationId, + 'role_id' => $roleId, + 'acting_user_id' => $actingUserId, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } + public function logRoleAssignment(string $message, array $context): void + { + $this->accessControlLogger->info($message, [ + 'context' => $context, + 'ip' => $this->requestStack->getCurrentRequest()?->getClientIp() ?? 'unknown', + 'timestamp' => $this->now(), + ]); + } } diff --git a/src/Service/OrganizationsService.php b/src/Service/OrganizationsService.php index 9384809..142ab2e 100644 --- a/src/Service/OrganizationsService.php +++ b/src/Service/OrganizationsService.php @@ -8,7 +8,9 @@ use App\Entity\Roles; use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; use App\Repository\UsersOrganizationsRepository; +use App\Service\LoggerService; use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\Exception\FileException; class OrganizationsService @@ -16,10 +18,11 @@ class OrganizationsService private string $logoDirectory; public function __construct( - string $logoDirectory, private readonly AwsService $awsService, - private readonly EntityManagerInterface $entityManager, + string $logoDirectory, private readonly AwsService $awsService, + private readonly EntityManagerInterface $entityManager, private readonly UsersOrganizationsRepository $uoRepository, - private readonly NotificationService $notificationService + private readonly NotificationService $notificationService, + private readonly LoggerInterface $emailNotificationLogger, private readonly LoggerService $loggerService, ) { $this->logoDirectory = $logoDirectory; @@ -33,8 +36,18 @@ class OrganizationsService try { $this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/'); + $this->loggerService->logAWSAction('Upload organization logo', [ + 'organization_id' => $organization->getId(), + 'filename' => $customFilename, + 'bucket' => $_ENV['S3_PORTAL_BUCKET'], + ]); $organization->setLogoUrl('logo/' . $customFilename); } catch (FileException $e) { + $this->loggerService->logError('Failed to upload organization logo to S3', [ + 'organization_id' => $organization->getId(), + 'error' => $e->getMessage(), + 'bucket' => $_ENV['S3_PORTAL_BUCKET'], + ]); throw new FileException('Failed to upload logo to S3: ' . $e->getMessage()); } } @@ -87,6 +100,13 @@ class OrganizationsService $data['organization'] ); } + $this->emailNotificationLogger->info('Organization admins notified of new user accept', [ + 'admin_user_id' => $adminUO->getUsers()->getId(), + 'organization_id' => $data['organization']->getId(), + 'ip' => $data['ip'] ?? null, + 'user_accepted' => $newUser->getId(), + 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM), + ]); break; case 'USER_INVITED': if ($uoa) { @@ -97,6 +117,13 @@ class OrganizationsService $data['organization'] ); } + $this->emailNotificationLogger->info('Organization admins notified of new user invited', [ + 'admin_user_id' => $adminUO->getUsers()->getId(), + 'organization_id' => $data['organization']->getId(), + 'ip' => $data['ip'] ?? null, + 'user_accepted' => $invitedUser->getId(), + 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM), + ]); break; case 'USER_DEACTIVATED': if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) { @@ -107,6 +134,13 @@ class OrganizationsService $data['organization'] ); } + $this->emailNotificationLogger->info('Organization admins notified of user deactivated', [ + 'admin_user_id' => $adminUO->getUsers()->getId(), + 'organization_id' => $data['organization']->getId(), + 'ip' => $data['ip'] ?? null, + 'user_accepted' => $removedUser->getId(), + 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM), + ]); break; case 'USER_DELETED': if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) { @@ -117,6 +151,13 @@ class OrganizationsService $data['organization'] ); } + $this->emailNotificationLogger->info('Organization admins notified of user deleted', [ + 'admin_user_id' => $adminUO->getUsers()->getId(), + 'organization_id' => $data['organization']->getId(), + 'ip' => $data['ip'] ?? null, + 'user_accepted' => $removedUser->getId(), + 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM), + ]); break; case 'USER_ACTIVATED': if ($uoa && $adminUO->getUsers()->getId() !== $data['user']->getId() ) { @@ -127,6 +168,13 @@ class OrganizationsService $data['organization'] ); } + $this->emailNotificationLogger->info('Organization admins notified of user activated', [ + 'admin_user_id' => $adminUO->getUsers()->getId(), + 'organization_id' => $data['organization']->getId(), + 'ip' => $data['ip'] ?? null, + 'user_accepted' => $activatedUser->getId(), + 'timestamp' => (new \DateTimeImmutable('now'))->format(DATE_ATOM), + ]); break; } diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php index 67c9613..bf2615f 100644 --- a/src/Service/UserOrganizationAppService.php +++ b/src/Service/UserOrganizationAppService.php @@ -8,13 +8,15 @@ use App\Entity\User; use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; use App\Service\ActionService; +use App\Service\LoggerService; use App\Service\UserService; use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Bundle\SecurityBundle\Security; class UserOrganizationAppService { - public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService) + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService, private readonly LoggerInterface $logger, private readonly LoggerService $loggerService) { } @@ -79,10 +81,20 @@ class UserOrganizationAppService $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); } foreach ($uoas as $uoa) { - $uoa->setIsActive(false); - $this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(), - $userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName()); - $this->entityManager->persist($uoa); + try{ + $uoa->setIsActive(false); + $this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(), + $userOrganization->getOrganization(), "App: " . $uoa->getApplication()->getName() . ", Role: " . $uoa->getRole()->getName()); + $this->entityManager->persist($uoa); + $this->loggerService->logUOALinkDeactivated($uoa->getId(), $uoa->getApplication()->getId(), $uoa->getRole()->getId()); + }catch (\Exception $exception){ + $this->loggerService->logCritical("Error deactivating UOA link", [ + 'uoa_id' => $uoa->getId(), + 'app_id' => $uoa->getApplication()->getId(), + 'role_id' => $uoa->getRole()->getId(), + 'exception_message' => $exception->getMessage(), + ]); + } } } @@ -128,6 +140,11 @@ class UserOrganizationAppService if (!$uoa->isActive()) { $uoa->setIsActive(true); $this->entityManager->persist($uoa); + $this->loggerService->logOrganizationInformation( + $uo->getOrganization()->getId(), + $actingUser->getId(), + "Re-activated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()} with UOA ID {$uoa->getId()}'" + ); $this->actionService->createAction( "Re-activate user role for application", $actingUser, @@ -148,7 +165,11 @@ class UserOrganizationAppService if ($uoa->isActive()) { $uoa->setIsActive(false); $this->entityManager->persist($uoa); - + $this->loggerService->logOrganizationInformation( + $uo->getOrganization()->getId(), + $actingUser->getId(), + "Deactivated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$uoa->getId()}'" + ); $this->actionService->createAction( "Deactivate user role for application", $actingUser, @@ -185,6 +206,11 @@ class UserOrganizationAppService $this->ensureAdminRoleForSuperAdmin($newUoa); } $this->entityManager->persist($newUoa); + $this->loggerService->logOrganizationInformation( + $uo->getOrganization()->getId(), + $actingUser->getId(), + "Created new role '{$role->getName()}' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$newUoa->getId()}'" + ); $this->actionService->createAction("New user role for application", $actingUser, $uo->getOrganization(), diff --git a/src/Service/UserOrganizationService.php b/src/Service/UserOrganizationService.php index 8a323da..afd2d42 100644 --- a/src/Service/UserOrganizationService.php +++ b/src/Service/UserOrganizationService.php @@ -7,6 +7,7 @@ use App\Entity\Organizations; use App\Entity\User; use App\Entity\UsersOrganizations; use App\Service\ActionService; +use App\Service\LoggerService; use \App\Service\UserOrganizationAppService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -19,7 +20,7 @@ readonly class UserOrganizationService { public function __construct( - private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, + private userOrganizationAppService $userOrganizationAppService, private EntityManagerInterface $entityManager, private ActionService $actionService, private LoggerService $loggerService, ) { } @@ -43,20 +44,14 @@ readonly class UserOrganizationService //deactivate all UO links foreach ($uos as $uo) { $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo); + $this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getId(), + 'Uo link deactivated'); $uo->setIsActive(false); $this->entityManager->persist($uo); $this->actionService->createAction("Deactivate UO link", $actingUser, $uo->getOrganization(), $uo->getOrganization()->getName() ); } } - public function getByIdOrFail(int $id): UsersOrganizations - { - $uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id); - if (!$uo) { - throw new NotFoundHttpException("UserOrganization not found"); - } - return $uo; - } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 228b8b2..9f2810c 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -95,6 +95,9 @@ class UserService */ public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool { + if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { + return true; + } if (!$skipSelfCheck && $user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) { return true; } @@ -106,9 +109,6 @@ class UserService } } } - if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { - return true; - } return false; } @@ -151,6 +151,7 @@ class UserService { $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]); if (!$user) { + $this->loggerService->logEntityNotFound('User', ['user_identifier' => $userIdentifier], null); throw new EntityNotFoundException(self::NOT_FOUND); } return $user; @@ -182,18 +183,20 @@ class UserService return ['none' => $group]; } -//TODO: reset function public function handleProfilePicture(User $user, $picture): void { // Get file extension $extension = $picture->guessExtension(); - // Create custom filename: userNameUserSurname_ddmmyyhhmmss + // Create custom filename: userNameUserSurname_dmyHis $customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension; -// $customFilename = $user->getName() . $user->getSurname() . "." .$extension; try { $this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/'); - + $this->loggerService->logAWSAction( + 'Profile picture uploaded to S3',[ + 'user_id' => $user->getId(), + 'filename' => $customFilename, + ]); $user->setPictureUrl('profile/' . $customFilename); } catch (FileException $e) { // Handle upload error @@ -249,12 +252,18 @@ class UserService if ($roleFormatted === 'ROLE_SUPER_ADMIN' && !in_array('ROLE_ADMIN', $user->getRoles(), true)) { $user->setRoles(array_merge($user->getRoles(), ['ROLE_ADMIN'])); } + $this->loggerService->logRoleAssignment( + 'Role assigned to user', + [ + 'user_id' => $user->getId(), + 'role' => $roleFormatted, + ] + ); } else { // Remove the role if present and not used elsewhere if (in_array($roleFormatted, $user->getRoles(), true)) { $uos = $this->entityManager->getRepository(UsersOrganizations::class) ->findBy(['users' => $user, 'isActive' => true]); - $hasRole = false; foreach ($uos as $uo) { $uoa = $this->entityManager->getRepository(UserOrganizatonApp::class) @@ -264,7 +273,6 @@ class UserService 'role' => $this->entityManager->getRepository(Roles::class) ->findOneBy(['name' => $role]), ]); - if ($uoa) { $hasRole = true; break; @@ -300,9 +308,26 @@ class UserService 'userIdentifier' => $userIdentifier, 'revoked' => false ]); - foreach ($tokens as $token) { - $token->revoke(); + try{ + $token->revoke(); + $this->loggerService->logTokenRevocation( + 'Access token revoked for user', + [ + 'user_identifier' => $userIdentifier, + 'token_id' => $token->getIdentifier(), + ] + ); + }catch (\Exception $e){ + $this->loggerService->logError( + 'Error revoking access token: ' . $e->getMessage(), + [ + 'user_identifier' => $userIdentifier, + 'token_id' => $token->getIdentifier(), + ] + ); + } + } } @@ -469,7 +494,7 @@ class UserService * @param User $user * @return void */ - public function formatNewUserData(User $user, $picture, bool $setPassword = false): void + public function formatUserData(User $user, $picture, bool $setPassword = false): void { // capitalize name and surname $user->setName(ucfirst(strtolower($user->getName()))); @@ -496,7 +521,6 @@ class UserService User $existingUser, Organizations $org, User $actingUser, - ?string $ip ): int { try { $uoId = $this->handleExistingUser($existingUser, $org); @@ -506,17 +530,14 @@ class UserService $org->getId(), $actingUser->getId(), $uoId, - $ip ); - $this->actionService->createAction( "Add existing user to organization", $actingUser, $org, "Added user {$existingUser->getUserIdentifier()} to {$org->getName()}" ); - - $this->sendExistingUserNotifications($existingUser, $org, $actingUser, $ip); + $this->sendExistingUserNotifications($existingUser, $org, $actingUser); return $uoId; } catch (\Exception $e) { @@ -524,7 +545,6 @@ class UserService 'target_user_id' => $existingUser->getId(), 'organization_id' => $org->getId(), 'acting_user_id' => $actingUser->getId(), - 'ip' => $ip, ]); throw $e; } @@ -533,14 +553,14 @@ class UserService /** * Create a brand-new user */ - public function createNewUser(User $user, User $actingUser, $picture, ?string $ip): void + public function createNewUser(User $user, User $actingUser, $picture): void { try { - $this->formatNewUserData($user, $picture, true); + $this->formatUserData($user, $picture, true); $this->entityManager->persist($user); $this->entityManager->flush(); - $this->loggerService->logUserCreated($user->getId(), $actingUser->getId(), $ip); + $this->loggerService->logUserCreated($user->getId(), $actingUser->getId()); $token = $this->generatePasswordToken($user); $this->emailService->sendPasswordSetupEmail($user, $token); $this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier()); @@ -548,7 +568,6 @@ class UserService $this->loggerService->logError('Error creating new user: ' . $e->getMessage(), [ 'target_user_email' => $user->getEmail(), 'acting_user_id' => $actingUser->getId(), - 'ip' => $ip, ]); throw $e; } @@ -561,7 +580,6 @@ class UserService User $user, Organizations $org, User $actingUser, - ?string $ip ): UsersOrganizations { try { $uo = new UsersOrganizations(); @@ -578,7 +596,7 @@ class UserService $org->getId(), $actingUser->getId(), $uo->getId(), - $ip + ); $this->actionService->createAction( @@ -588,7 +606,7 @@ class UserService "Added {$user->getUserIdentifier()} to {$org->getName()}" ); - $this->sendNewUserNotifications($user, $org, $actingUser, $ip); + $this->sendNewUserNotifications($user, $org, $actingUser); return $uo; } catch (\Exception $e) { @@ -596,51 +614,47 @@ class UserService 'target_user_id' => $user->getId(), 'organization_id' => $org->getId(), 'acting_user_id' => $actingUser->getId(), - 'ip' => $ip, ]); throw $e; } } // Private helpers for email notifications - private function sendExistingUserNotifications(User $user, Organizations $org, User $actingUser, ?string $ip): void + private function sendExistingUserNotifications(User $user, Organizations $org, User $actingUser): void { try { $token = $this->generatePasswordToken($user, $org->getId()); $this->emailService->sendExistingUserNotificationEmail($user, $org, $token); - $this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId(), $ip); + $this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId()); } catch (\Exception $e) { $this->loggerService->logError("Error sending existing user notification: " . $e->getMessage(), [ 'target_user_id' => $user->getId(), 'organization_id' => $org->getId(), - 'ip' => $ip, ]); } - - $this->notifyOrgAdmins($user, $org, $actingUser, $ip); + $this->notifyOrgAdmins($user, $org, $actingUser,); } - private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser, ?string $ip): void + private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser): void { try { $token = $this->generatePasswordToken($user, $org->getId()); $this->emailService->sendPasswordSetupEmail($user, $token); - $this->loggerService->logPasswordSetupEmailSent($user->getId(), $org->getId(), $ip); } catch (\Exception $e) { $this->loggerService->logError("Error sending password setup email: " . $e->getMessage(), [ 'target_user_id' => $user->getId(), 'organization_id' => $org->getId(), - 'ip' => $ip, ]); } - $this->notifyOrgAdmins($user, $org, $actingUser, $ip); + $this->notifyOrgAdmins($user, $org, $actingUser); } - private function notifyOrgAdmins(User $user, Organizations $org, User $actingUser, ?string $ip): void + private function notifyOrgAdmins(User $user, Organizations $org, User $actingUser): void { try { - $data = ['user' => $user, 'organization' => $org]; + $data = ['user' => $user, + 'organization' => $org]; $adminsUos = $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED'); foreach ($adminsUos as $adminUo) { @@ -649,14 +663,12 @@ class UserService $user->getId(), $org->getId(), $actingUser->getId(), - $ip ); } } catch (\Exception $e) { $this->loggerService->logError("Error notifying organization admins: " . $e->getMessage(), [ 'target_user_id' => $user->getId(), 'organization_id' => $org->getId(), - 'ip' => $ip, ]); } }