refactor for monolog user create
This commit is contained in:
parent
cb34a18948
commit
47724734a2
|
|
@ -14,23 +14,85 @@ monolog:
|
||||||
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
|
||||||
|
handler: error_nested
|
||||||
|
buffer_size: 50
|
||||||
|
|
||||||
|
error_nested:
|
||||||
|
type: rotating_file
|
||||||
|
path: "%kernel.logs_dir%/error.log"
|
||||||
level: debug
|
level: debug
|
||||||
channels: ["!event"]
|
max_files: 30
|
||||||
# uncomment to get logging in your browser
|
|
||||||
# you may have to allow bigger header sizes in your Web server configuration
|
error:
|
||||||
#firephp:
|
type: rotating_file
|
||||||
# type: firephp
|
path: "%kernel.logs_dir%/error.log"
|
||||||
# level: info
|
level: error # logs error, critical, alert, emergency
|
||||||
#chromephp:
|
max_files: 30
|
||||||
# type: chromephp
|
channels: [ error ]
|
||||||
# level: info
|
php_errors:
|
||||||
console:
|
type: rotating_file
|
||||||
type: console
|
path: "%kernel.logs_dir%/php_error.log"
|
||||||
process_psr_3_messages: false
|
level: warning # warnings, errors, fatals…
|
||||||
channels: ["!event", "!doctrine", "!console"]
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
|
||||||
|
if (!$this->userService->hasAccessTo($actingUser)) {
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$form = $this->createForm(UserForm::class, $user);
|
$form = $this->createForm(UserForm::class, $user);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
$orgId = $request->get('organizationId');
|
$orgId = $request->get('organizationId');
|
||||||
if ($orgId){
|
$org = $orgId ? $this->organizationRepository->find($orgId) : null;
|
||||||
$org = $this->organizationRepository->find($orgId) ?? throw new NotFoundHttpException(sprintf('%s not found', $orgId));
|
|
||||||
|
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()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
$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());
|
// Case : User exists + has organization context
|
||||||
$this->logger->notice("User added to organization " . $org->getName());
|
if ($existingUser && $org) {
|
||||||
$this->emailService->sendExistingUserNotificationEmail($existingUser, $org);
|
$this->userService->addExistingUserToOrganization(
|
||||||
$this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier());
|
$existingUser,
|
||||||
$data = ['user' => $existingUser, 'organization' => $org];
|
$org,
|
||||||
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
$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]);
|
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(),
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle file upload
|
$form->get('email')->addError(
|
||||||
$picture = $form->get('pictureUrl')->getData();
|
new \Symfony\Component\Form\FormError(
|
||||||
$this->userService->formatNewUserData($user, $picture, true);
|
'This email is already in use. Add the user to an organization instead.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
}
|
||||||
$this->logger->error($e->getMessage());
|
|
||||||
if ($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('organization_show', ['id' => $orgId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('user_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('user/new.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'organizationId' => $orgId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->loggerService->logError("Error on user creation route: " . $e->getMessage(), [
|
||||||
|
'ip' => $request->getClientIp(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($orgId ?? null) {
|
||||||
|
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute('user_index');
|
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,6 +816,7 @@ 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);
|
||||||
|
if ($orgId) {
|
||||||
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||||
if (!$uo || $uo->getStatut() !== 'INVITED') {
|
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->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId);
|
||||||
|
|
@ -770,8 +828,8 @@ class UserController extends AbstractController
|
||||||
$this->entityManager->persist($uo);
|
$this->entityManager->persist($uo);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId);
|
$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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue