From 47724734a20dc86bb3e9aac312e7d01eaaa747e9 Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 3 Dec 2025 07:58:21 +0100 Subject: [PATCH] refactor for monolog user create --- config/packages/monolog.yaml | 102 ++++++++++--- src/Controller/UserController.php | 206 +++++++++++++++++---------- src/Service/EmailService.php | 8 +- src/Service/LoggerService.php | 142 ++++++++++++++++++ src/Service/OrganizationsService.php | 5 +- src/Service/UserService.php | 188 +++++++++++++++++++++++- 6 files changed, 547 insertions(+), 104 deletions(-) create mode 100644 src/Service/LoggerService.php diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index c533f37..34dd8f7 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -12,25 +12,87 @@ monolog: - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists when@dev: - monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - channels: ["!event"] - # uncomment to get logging in your browser - # you may have to allow bigger header sizes in your Web server configuration - #firephp: - # type: firephp - # level: info - #chromephp: - # type: chromephp - # level: info - console: - type: console - process_psr_3_messages: false - channels: ["!event", "!doctrine", "!console"] + monolog: + handlers: + critical_errors: + type: fingers_crossed + action_level: critical + handler: error_nested + buffer_size: 50 + + error_nested: + type: rotating_file + path: "%kernel.logs_dir%/error.log" + level: debug + max_files: 30 + + error: + type: rotating_file + path: "%kernel.logs_dir%/error.log" + level: error # logs error, critical, alert, emergency + max_files: 30 + channels: [ error ] + php_errors: + type: rotating_file + path: "%kernel.logs_dir%/php_error.log" + level: warning # warnings, errors, fatals… + max_files: 30 + channels: [ php ] + # User Management + user_management: + type: rotating_file + path: "%kernel.logs_dir%/user_management.log" + level: debug + channels: [ user_management ] + max_files: 30 + + # Authentication + authentication: + type: rotating_file + path: "%kernel.logs_dir%/authentication.log" + level: debug + channels: [ authentication ] + max_files: 30 + + # Organization Management + organization_management: + type: rotating_file + path: "%kernel.logs_dir%/organization_management.log" + level: debug + channels: [ organization_management ] + max_files: 30 + + # Access Control + access_control: + type: rotating_file + path: "%kernel.logs_dir%/access_control.log" + level: debug + channels: [ access_control ] + max_files: 30 + + # Email Notifications + email_notifications: + type: rotating_file + path: "%kernel.logs_dir%/email_notifications.log" + level: debug + channels: [ email_notifications ] + max_files: 30 + + # Admin Actions + admin_actions: + type: rotating_file + path: "%kernel.logs_dir%/admin_actions.log" + level: debug + channels: [ admin_actions ] + max_files: 30 + + # Security + security: + type: rotating_file + path: "%kernel.logs_dir%/security.log" + level: debug + channels: [ security ] + max_files: 30 when@test: monolog: @@ -57,7 +119,7 @@ when@prod: error_nested: type: rotating_file - path: "%kernel.logs_dir%/error.log" + path: "%kernel.logs_dir%/critical.log" level: debug max_files: 30 diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 31a8488..93c9a35 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -16,6 +16,7 @@ use App\Repository\UsersOrganizationsRepository; use App\Service\ActionService; use App\Service\AwsService; use App\Service\EmailService; +use App\Service\LoggerService; use App\Service\OrganizationsService; use App\Service\UserOrganizationAppService; use App\Service\UserOrganizationService; @@ -51,11 +52,8 @@ class UserController extends AbstractController private readonly OrganizationsRepository $organizationRepository, private readonly LoggerInterface $userManagementLogger, private readonly LoggerInterface $organizationManagementLogger, - private readonly LoggerInterface $accessControlLogger, - private readonly LoggerInterface $EmailNotificationLogger, - private readonly LoggerInterface $adminActionsLogger, private readonly LoggerInterface $errorLogger, - private readonly LoggerInterface $SecurityLogger, + private readonly LoggerService $loggerService, private readonly EmailService $emailService, private readonly AwsService $awsService, private readonly OrganizationsService $organizationsService, @@ -251,72 +249,130 @@ class UserController extends AbstractController public function new(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ADMIN'); + try { $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); - if ($this->userService->hasAccessTo($actingUser)) { - $user = new User(); - $form = $this->createForm(UserForm::class, $user); - $form->handleRequest($request); - $orgId = $request->get('organizationId'); - if ($orgId){ - $org = $this->organizationRepository->find($orgId) ?? throw new NotFoundHttpException(sprintf('%s not found', $orgId)); - } - if ($form->isSubmitted() && $form->isValid()) { - $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); - if ($existingUser && $orgId) { - $this->userService->handleExistingUser($existingUser, $org); - - $this->actionService->createAction("Create new user", $existingUser, $org, "Added user to organization" . $existingUser->getUserIdentifier() . " for organization " . $org->getName()); - $this->logger->notice("User added to organization " . $org->getName()); - $this->emailService->sendExistingUserNotificationEmail($existingUser, $org); - $this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier()); - $data = ['user' => $existingUser, 'organization' => $org]; - $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED'); - return $this->redirectToRoute('organization_show', ['id' => $orgId]); - } - - - // Handle file upload - $picture = $form->get('pictureUrl')->getData(); - $this->userService->formatNewUserData($user, $picture, true); - - if ($orgId) { - $uo = new UsersOrganizations(); - $uo->setUsers($user); - $uo->setOrganization($org); - $uo->setStatut("INVITED"); - $uo->setIsActive(false); - $uo->setModifiedAt(new \DateTimeImmutable('now')); - $this->entityManager->persist($uo); - $this->actionService->createAction("Create new user", $user, $org, "Added user to organization" . $user->getUserIdentifier() . " for organization " . $org->getName()); - $this->logger->notice("User added to organization " . $org->getName()); - $this->emailService->sendPasswordSetupEmail($user, $orgId); - $this->logger->notice("Password setup email sent to " . $user->getUserIdentifier()); - $data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()]; - $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED'); - } - $this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier()); - $this->logger->notice("User created " . $user->getUserIdentifier()); - $this->entityManager->persist($user); - $this->entityManager->flush(); - - if ($orgId) { - return $this->redirectToRoute('organization_show', ['organizationId' => $orgId]); - } - return $this->redirectToRoute('user_index'); - } + if (!$this->userService->hasAccessTo($actingUser)) { + throw $this->createAccessDeniedException(self::ACCESS_DENIED); } + + $user = new User(); + $form = $this->createForm(UserForm::class, $user); + $form->handleRequest($request); + + $orgId = $request->get('organizationId'); + $org = $orgId ? $this->organizationRepository->find($orgId) : null; + + if (!$org && $orgId) { + $this->loggerService->logCritical('Organization not found for user creation', [ + 'organization_id' => $orgId, + 'acting_user_id' => $actingUser->getId(), + 'ip' => $request->getClientIp(), + ]); + throw $this->createNotFoundException('Organization not found'); + } + + if ($form->isSubmitted() && $form->isValid()) { + $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); + + // Case : User exists + has organization context + if ($existingUser && $org) { + $this->userService->addExistingUserToOrganization( + $existingUser, + $org, + $actingUser, + $request->getClientIp() + ); + + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin( + $existingUser->getId(), + $org->getId(), + $actingUser->getId(), + $request->getClientIp(), + "Super Admin linked user to organization", + ); + } + + return $this->redirectToRoute('organization_show', ['id' => $orgId]); + } + + // Case : User exists but NO organization context → error + if ($existingUser) { + $this->loggerService->logError('Attempt to create user with existing email without organization', [ + 'target_user_email' => $user->getid(), + 'acting_user_id' => $actingUser->getId(), + 'ip' => $request->getClientIp(), + ]); + + $form->get('email')->addError( + new \Symfony\Component\Form\FormError( + 'This email is already in use. Add the user to an organization instead.' + ) + ); + + return $this->render('user/new.html.twig', [ + 'user' => $user, + 'form' => $form->createView(), + 'organizationId' => $orgId, + ]); + } + + $picture = $form->get('pictureUrl')->getData(); + $this->userService->createNewUser($user, $actingUser, $picture, $request->getClientIp()); + + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin( + $user->getId(), + null, + $actingUser->getId(), + $request->getClientIp(), + "Super Admin created new user", + + ); + } + + // Link to organization if provided + if ($org) { + $this->userService->linkUserToOrganization( + $user, + $org, + $actingUser, + $request->getClientIp() + ); + + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin( + $user->getId(), + $org->getId(), + $actingUser->getId(), + $request->getClientIp(), + "Super Admin linked user to organization", + ); + } + + return $this->redirectToRoute('organization_show', ['id' => $orgId]); + } + + return $this->redirectToRoute('user_index'); + } + return $this->render('user/new.html.twig', [ - 'user' => $user, - 'form' => $form->createView(), - 'organizationId' => $orgId + 'user' => $user, + 'form' => $form->createView(), + 'organizationId' => $orgId, ]); + } catch (\Exception $e) { - $this->logger->error($e->getMessage()); - if ($orgId) { + $this->loggerService->logError("Error on user creation route: " . $e->getMessage(), [ + 'ip' => $request->getClientIp(), + ]); + + if ($orgId ?? null) { return $this->redirectToRoute('organization_show', ['id' => $orgId]); } + return $this->redirectToRoute('user_index'); } } @@ -730,7 +786,8 @@ class UserController extends AbstractController $uo->setModifiedAt(new \DateTimeImmutable()); try { $data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()]; - $this->emailService->sendPasswordSetupEmail($user, $orgId); + $token = $this->userService->generatePasswordToken($user, $org->getId()); + $this->emailService->sendPasswordSetupEmail($user, $token); $this->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName()); $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED'); return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK); @@ -759,19 +816,20 @@ class UserController extends AbstractController throw $this->createNotFoundException('Invalid or expired invitation token.'); } $orgId = $this->userService->getOrgFromToken($token); - $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]); - if (!$uo || $uo->getStatut() !== 'INVITED') { - $this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId); - throw $this->createNotFoundException('No pending invitation found for this user and organization.'); + if ($orgId) { + $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]); + if (!$uo || $uo->getStatut() !== 'INVITED') { + $this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId); + throw $this->createNotFoundException('No pending invitation found for this user and organization.'); + } + $uo->setModifiedAt(new \DateTimeImmutable()); + $uo->setStatut("ACCEPTED"); + $uo->setIsActive(true); + $this->entityManager->persist($uo); + $this->entityManager->flush(); + $this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId); } - $uo->setModifiedAt(new \DateTimeImmutable()); - $uo->setStatut("ACCEPTED"); - $uo->setIsActive(true); - $this->entityManager->persist($uo); - $this->entityManager->flush(); - $this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId); - - return $this->render('user/show.html.twig', ['user' => $user, 'orgId' => $orgId]); + return $this->render('security/login.html.twig'); } } diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php index d8c942e..61d750e 100644 --- a/src/Service/EmailService.php +++ b/src/Service/EmailService.php @@ -14,15 +14,12 @@ class EmailService { public function __construct( private readonly MailerInterface $mailer, - private readonly UserService $userService, private readonly LoggerInterface $logger, private UrlGeneratorInterface $urlGenerator ) {} - public function sendPasswordSetupEmail(User $user, int $orgId): void + public function sendPasswordSetupEmail(User $user, string $token): void { - $token = $this->userService->generatePasswordToken($user, $orgId); - // Generate absolute URL for the password setup route $link = $this->urlGenerator->generate( 'password_setup', @@ -52,9 +49,8 @@ class EmailService } } - public function sendExistingUserNotificationEmail(User $existingUser, Organizations $org): void + public function sendExistingUserNotificationEmail(User $existingUser, Organizations $org, $token): void { - $token = $this->userService->generatePasswordToken($existingUser, $org->getId()); $link = $this->urlGenerator->generate('user_accept',[ 'id' => $existingUser->getId(), 'token' => $token diff --git a/src/Service/LoggerService.php b/src/Service/LoggerService.php new file mode 100644 index 0000000..016fa21 --- /dev/null +++ b/src/Service/LoggerService.php @@ -0,0 +1,142 @@ +userManagementLogger->notice("New user created: $userId", [ + 'target_user_id' => $userId, + 'acting_user_id' => $actingUserId, + 'ip' => $ip, + '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 + { + $this->organizationManagementLogger->notice('User-Organization link created', [ + 'target_user_id' => $userId, + 'organization_id' => $orgId, + 'acting_user_id' => $actingUserId, + 'uo_id' => $uoId, + 'ip' => $ip, + 'timestamp' => $this->now(), + ]); + } + + public function logExistingUserAddedToOrg(int $userId, int $orgId, int $actingUserId, int $uoId, ?string $ip): 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, + 'timestamp' => $this->now(), + ]); + } + + // Email Notification Logs + public function logPasswordSetupEmailSent(int $userId, ?int $orgId, ?string $ip): void + { + $this->emailNotificationLogger->notice("Password setup email sent to $userId", [ + 'target_user_id' => $userId, + 'organization_id' => $orgId, + 'ip' => $ip, + 'timestamp' => $this->now(), + ]); + } + + public function logExistingUserNotificationSent(int $userId, int $orgId, ?string $ip): void + { + $this->emailNotificationLogger->notice("Existing user notification email sent to $userId", [ + 'target_user_id' => $userId, + 'organization_id' => $orgId, + 'ip' => $ip, + 'timestamp' => $this->now(), + ]); + } + + public function logAdminNotified(int $adminUserId, int $targetUserId, int $orgId, int $actingUserId, ?string $ip): void + { + $this->emailNotificationLogger->notice('Organization admin notified', [ + 'admin_user_id' => $adminUserId, + 'target_user_id' => $targetUserId, + 'organization_id' => $orgId, + 'acting_user_id' => $actingUserId, + 'ip' => $ip, + 'timestamp' => $this->now(), + ]); + } + + public function logSuperAdmin(int $userId, ?int $orgId, int $actingUserId, ?string $ip, string $message): void + { + $this->adminActionsLogger->notice($message, [ + 'target_user_id' => $userId, + 'organization_id' => $orgId, + 'acting_user_id' => $actingUserId, + 'ip' => $ip, + 'timestamp' => $this->now(), + ]); + } + + // Error Logs + public function logError(string $message, array $context = []): void + { + $this->errorLogger->error($message, array_merge($context, [ + 'timestamp' => $this->now(), + ])); + } + + public function logCritical(string $message, array $context = []): void + { + $this->errorLogger->critical($message, array_merge($context, [ + 'timestamp' => $this->now(), + ])); + } + + // Security Logs + public function logAccessDenied(int $targetId, ?int $actingUserId, ?string $ip): void + { + $this->securityLogger->warning('Access denied', [ + 'target_id' => $targetId, + 'acting_user_id' => $actingUserId, + 'ip' => $ip, + 'timestamp' => $this->now(), + ]); + } + + // Helper + private function now(): string + { + return (new \DateTimeImmutable('now'))->format(DATE_ATOM); + } +} diff --git a/src/Service/OrganizationsService.php b/src/Service/OrganizationsService.php index ece09b3..9384809 100644 --- a/src/Service/OrganizationsService.php +++ b/src/Service/OrganizationsService.php @@ -6,6 +6,7 @@ use App\Entity\Apps; use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\UserOrganizatonApp; +use App\Entity\UsersOrganizations; use App\Repository\UsersOrganizationsRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\File\Exception\FileException; @@ -62,7 +63,7 @@ class OrganizationsService } - public function notifyOrganizationAdmins(array $data, string $type): void + public function notifyOrganizationAdmins(array $data, string $type): UsersOrganizations { $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); @@ -130,6 +131,8 @@ class OrganizationsService } } + + return $adminUO; } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index ae5e908..228b8b2 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -27,7 +27,14 @@ class UserService public function __construct(private readonly EntityManagerInterface $entityManager, private readonly Security $security, - private readonly AwsService $awsService + private readonly AwsService $awsService, + private readonly LoggerService $loggerService, + private readonly ActionService $actionService, + private readonly EmailService $emailService, + private readonly OrganizationsService $organizationsService, + + + ) { @@ -330,7 +337,7 @@ class UserService return $formatted; } - public function generatePasswordToken(User $user, int $orgId): string + public function generatePasswordToken(User $user, int $orgId = null): string { $orgString = "o" . $orgId . "@"; $token = $orgString . bin2hex(random_bytes(32)); @@ -434,7 +441,7 @@ class UserService * @param Organizations $organization * @return void */ - public function handleExistingUser(User $user, Organizations $organization): void + public function handleExistingUser(User $user, Organizations $organization): int { if (!$user->isActive()) { $user->setIsActive(true); @@ -448,6 +455,8 @@ class UserService $uo->setModifiedAt(new \DateTimeImmutable('now')); $this->entityManager->persist($uo); $this->entityManager->flush(); + + return $uo->getId(); } /** @@ -479,4 +488,177 @@ class UserService $this->handleProfilePicture($user, $picture); } } + + /** + * Handle existing user being added to an organization + */ + public function addExistingUserToOrganization( + User $existingUser, + Organizations $org, + User $actingUser, + ?string $ip + ): int { + try { + $uoId = $this->handleExistingUser($existingUser, $org); + + $this->loggerService->logExistingUserAddedToOrg( + $existingUser->getId(), + $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); + + return $uoId; + } catch (\Exception $e) { + $this->loggerService->logError('Error linking existing user to organization: ' . $e->getMessage(), [ + 'target_user_id' => $existingUser->getId(), + 'organization_id' => $org->getId(), + 'acting_user_id' => $actingUser->getId(), + 'ip' => $ip, + ]); + throw $e; + } + } + + /** + * Create a brand-new user + */ + public function createNewUser(User $user, User $actingUser, $picture, ?string $ip): void + { + try { + $this->formatNewUserData($user, $picture, true); + $this->entityManager->persist($user); + $this->entityManager->flush(); + + $this->loggerService->logUserCreated($user->getId(), $actingUser->getId(), $ip); + $token = $this->generatePasswordToken($user); + $this->emailService->sendPasswordSetupEmail($user, $token); + $this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier()); + } catch (\Exception $e) { + $this->loggerService->logError('Error creating new user: ' . $e->getMessage(), [ + 'target_user_email' => $user->getEmail(), + 'acting_user_id' => $actingUser->getId(), + 'ip' => $ip, + ]); + throw $e; + } + } + + /** + * Link newly created user to an organization + */ + public function linkUserToOrganization( + User $user, + Organizations $org, + User $actingUser, + ?string $ip + ): UsersOrganizations { + try { + $uo = new UsersOrganizations(); + $uo->setUsers($user); + $uo->setOrganization($org); + $uo->setStatut("INVITED"); + $uo->setIsActive(false); + $uo->setModifiedAt(new \DateTimeImmutable('now')); + $this->entityManager->persist($uo); + $this->entityManager->flush(); + + $this->loggerService->logUserOrganizationLinkCreated( + $user->getId(), + $org->getId(), + $actingUser->getId(), + $uo->getId(), + $ip + ); + + $this->actionService->createAction( + "Link user to organization", + $actingUser, + $org, + "Added {$user->getUserIdentifier()} to {$org->getName()}" + ); + + $this->sendNewUserNotifications($user, $org, $actingUser, $ip); + + return $uo; + } catch (\Exception $e) { + $this->loggerService->logError('Error linking user to organization: ' . $e->getMessage(), [ + '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 + { + try { + $token = $this->generatePasswordToken($user, $org->getId()); + $this->emailService->sendExistingUserNotificationEmail($user, $org, $token); + $this->loggerService->logExistingUserNotificationSent($user->getId(), $org->getId(), $ip); + } 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); + } + + private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser, ?string $ip): 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); + } + + private function notifyOrgAdmins(User $user, Organizations $org, User $actingUser, ?string $ip): void + { + try { + $data = ['user' => $user, 'organization' => $org]; + $adminsUos = $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED'); + + foreach ($adminsUos as $adminUo) { + $this->loggerService->logAdminNotified( + $adminUo->getUsers()->getId(), + $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, + ]); + } + } + }