refactor for monolog user create

This commit is contained in:
Charles 2025-12-03 07:58:21 +01:00
parent cb34a18948
commit 47724734a2
6 changed files with 547 additions and 104 deletions

View File

@ -12,25 +12,87 @@ monolog:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev: when@dev:
monolog: monolog:
handlers: handlers:
main: critical_errors:
type: stream type: fingers_crossed
path: "%kernel.logs_dir%/%kernel.environment%.log" action_level: critical
level: debug handler: error_nested
channels: ["!event"] buffer_size: 50
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration error_nested:
#firephp: type: rotating_file
# type: firephp path: "%kernel.logs_dir%/error.log"
# level: info level: debug
#chromephp: max_files: 30
# type: chromephp
# level: info error:
console: type: rotating_file
type: console path: "%kernel.logs_dir%/error.log"
process_psr_3_messages: false level: error # logs error, critical, alert, emergency
channels: ["!event", "!doctrine", "!console"] 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: when@test:
monolog: monolog:
@ -57,7 +119,7 @@ when@prod:
error_nested: error_nested:
type: rotating_file type: rotating_file
path: "%kernel.logs_dir%/error.log" path: "%kernel.logs_dir%/critical.log"
level: debug level: debug
max_files: 30 max_files: 30

View File

@ -16,6 +16,7 @@ use App\Repository\UsersOrganizationsRepository;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\AwsService; use App\Service\AwsService;
use App\Service\EmailService; use App\Service\EmailService;
use App\Service\LoggerService;
use App\Service\OrganizationsService; use App\Service\OrganizationsService;
use App\Service\UserOrganizationAppService; use App\Service\UserOrganizationAppService;
use App\Service\UserOrganizationService; use App\Service\UserOrganizationService;
@ -51,11 +52,8 @@ class UserController extends AbstractController
private readonly OrganizationsRepository $organizationRepository, private readonly OrganizationsRepository $organizationRepository,
private readonly LoggerInterface $userManagementLogger, private readonly LoggerInterface $userManagementLogger,
private readonly LoggerInterface $organizationManagementLogger, private readonly LoggerInterface $organizationManagementLogger,
private readonly LoggerInterface $accessControlLogger,
private readonly LoggerInterface $EmailNotificationLogger,
private readonly LoggerInterface $adminActionsLogger,
private readonly LoggerInterface $errorLogger, private readonly LoggerInterface $errorLogger,
private readonly LoggerInterface $SecurityLogger, private readonly LoggerService $loggerService,
private readonly EmailService $emailService, private readonly EmailService $emailService,
private readonly AwsService $awsService, private readonly AwsService $awsService,
private readonly OrganizationsService $organizationsService, private readonly OrganizationsService $organizationsService,
@ -251,72 +249,130 @@ class UserController extends AbstractController
public function new(Request $request): Response public function new(Request $request): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_ADMIN');
try { try {
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); $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()) { if (!$this->userService->hasAccessTo($actingUser)) {
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); throw $this->createAccessDeniedException(self::ACCESS_DENIED);
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');
}
} }
$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', [ return $this->render('user/new.html.twig', [
'user' => $user, 'user' => $user,
'form' => $form->createView(), 'form' => $form->createView(),
'organizationId' => $orgId 'organizationId' => $orgId,
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error($e->getMessage()); $this->loggerService->logError("Error on user creation route: " . $e->getMessage(), [
if ($orgId) { 'ip' => $request->getClientIp(),
]);
if ($orgId ?? null) {
return $this->redirectToRoute('organization_show', ['id' => $orgId]); return $this->redirectToRoute('organization_show', ['id' => $orgId]);
} }
return $this->redirectToRoute('user_index'); return $this->redirectToRoute('user_index');
} }
} }
@ -730,7 +786,8 @@ class UserController extends AbstractController
$uo->setModifiedAt(new \DateTimeImmutable()); $uo->setModifiedAt(new \DateTimeImmutable());
try { try {
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()]; $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->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName());
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED'); $this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK); 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.'); throw $this->createNotFoundException('Invalid or expired invitation token.');
} }
$orgId = $this->userService->getOrgFromToken($token); $orgId = $this->userService->getOrgFromToken($token);
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]); if ($orgId) {
if (!$uo || $uo->getStatut() !== 'INVITED') { $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
$this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId); if (!$uo || $uo->getStatut() !== 'INVITED') {
throw $this->createNotFoundException('No pending invitation found for this user and organization.'); $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()); return $this->render('security/login.html.twig');
$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]);
} }
} }

View File

@ -14,15 +14,12 @@ class EmailService
{ {
public function __construct( public function __construct(
private readonly MailerInterface $mailer, private readonly MailerInterface $mailer,
private readonly UserService $userService,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private UrlGeneratorInterface $urlGenerator 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 // Generate absolute URL for the password setup route
$link = $this->urlGenerator->generate( $link = $this->urlGenerator->generate(
'password_setup', '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',[ $link = $this->urlGenerator->generate('user_accept',[
'id' => $existingUser->getId(), 'id' => $existingUser->getId(),
'token' => $token 'token' => $token

View File

@ -0,0 +1,142 @@
<?php
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
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,
) {}
// User Management Logs
public function logUserCreated(int $userId, int $actingUserId, ?string $ip): void
{
$this->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);
}
}

View File

@ -6,6 +6,7 @@ use App\Entity\Apps;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Entity\Roles; use App\Entity\Roles;
use App\Entity\UserOrganizatonApp; use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations;
use App\Repository\UsersOrganizationsRepository; use App\Repository\UsersOrganizationsRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException; 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']); $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
@ -130,6 +131,8 @@ class OrganizationsService
} }
} }
return $adminUO;
} }
} }

View File

@ -27,7 +27,14 @@ class UserService
public function __construct(private readonly EntityManagerInterface $entityManager, public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly Security $security, 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; return $formatted;
} }
public function generatePasswordToken(User $user, int $orgId): string public function generatePasswordToken(User $user, int $orgId = null): string
{ {
$orgString = "o" . $orgId . "@"; $orgString = "o" . $orgId . "@";
$token = $orgString . bin2hex(random_bytes(32)); $token = $orgString . bin2hex(random_bytes(32));
@ -434,7 +441,7 @@ class UserService
* @param Organizations $organization * @param Organizations $organization
* @return void * @return void
*/ */
public function handleExistingUser(User $user, Organizations $organization): void public function handleExistingUser(User $user, Organizations $organization): int
{ {
if (!$user->isActive()) { if (!$user->isActive()) {
$user->setIsActive(true); $user->setIsActive(true);
@ -448,6 +455,8 @@ class UserService
$uo->setModifiedAt(new \DateTimeImmutable('now')); $uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo); $this->entityManager->persist($uo);
$this->entityManager->flush(); $this->entityManager->flush();
return $uo->getId();
} }
/** /**
@ -479,4 +488,177 @@ class UserService
$this->handleProfilePicture($user, $picture); $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,
]);
}
}
} }