736 lines
26 KiB
PHP
736 lines
26 KiB
PHP
<?php
|
||
|
||
namespace App\Service;
|
||
|
||
|
||
use App\Entity\Organizations;
|
||
use App\Entity\Roles;
|
||
use App\Entity\User;
|
||
use App\Entity\UserOrganizatonApp;
|
||
use App\Entity\UsersOrganizations;
|
||
use App\Repository\RolesRepository;
|
||
use DateTimeImmutable;
|
||
use DateTimeZone;
|
||
use Doctrine\ORM\EntityManagerInterface;
|
||
use Doctrine\ORM\EntityNotFoundException;
|
||
use Exception;
|
||
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
|
||
use Random\RandomException;
|
||
use RuntimeException;
|
||
use Symfony\Bundle\SecurityBundle\Security;
|
||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||
use App\Event\UserCreatedEvent;
|
||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||
|
||
class UserService
|
||
{
|
||
|
||
public const NOT_FOUND = 'Entity not found';
|
||
|
||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||
private readonly Security $security,
|
||
private readonly LoggerService $loggerService,
|
||
private readonly ActionService $actionService,
|
||
private readonly EmailService $emailService,
|
||
private readonly OrganizationsService $organizationsService,
|
||
private readonly EventDispatcherInterface $eventDispatcher, private readonly RolesRepository $rolesRepository
|
||
)
|
||
{
|
||
|
||
}
|
||
|
||
/**
|
||
* Generate a random password for a new user until they set their own.
|
||
* @throws RandomException
|
||
*/
|
||
public function generateRandomPassword(): string
|
||
{
|
||
return bin2hex(random_bytes(32));
|
||
}
|
||
|
||
/** Check if the user is admin in any organization.
|
||
* Return true if the user is admin in at least one organization, false otherwise.
|
||
*
|
||
* @param User $user
|
||
* @return bool
|
||
* @throws Exception
|
||
*/
|
||
// TODO: pas sur de l'utiliser, à vérifier
|
||
public function isAdminInAnyOrganization(User $user): bool
|
||
{
|
||
$roleAdmin = $this->rolesRepository->findOneBy(['name' => 'ADMIN']);
|
||
$uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy([
|
||
'users' => $user,
|
||
'isActive' => true,
|
||
'role'=> $roleAdmin]);
|
||
return $uoAdmin !== null;
|
||
}
|
||
|
||
/**
|
||
* Check if the user is currently connected.
|
||
* This method check if the user is currently connected to one of the applications.
|
||
*
|
||
* @param String $userIdentifier
|
||
* @return bool
|
||
*/
|
||
public function isUserConnected(string $userIdentifier): bool
|
||
{
|
||
$now = new DateTimeImmutable();
|
||
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
|
||
'userIdentifier' => $userIdentifier,
|
||
'revoked' => false
|
||
]);
|
||
|
||
foreach ($tokens as $token) {
|
||
// Assuming $token->getExpiry() returns a DateTimeInterface
|
||
if ($token->getExpiry() > $now) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Determines if the currently logged-in user has permission to manage or view a target User.
|
||
* * Access is granted if:
|
||
* 1. The current user is a Super Admin.
|
||
* 2. The current user is the target user itself.
|
||
* 3. The current user is an active Admin of an organization the target user belongs to.
|
||
*
|
||
* @param User $user The target User object we are checking access against.
|
||
* * @return bool True if access is permitted, false otherwise.
|
||
* @throws Exception If database or security context issues occur.
|
||
*/
|
||
public function hasAccessTo(User $user): bool
|
||
{
|
||
if ($this->security->isGranted('ROLE_ADMIN')) {
|
||
return true;
|
||
}
|
||
// S'il s'agit de son propre compte, on lui donne accès
|
||
if ($user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) {
|
||
return true;
|
||
}
|
||
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||
if ($userOrganizations) {
|
||
foreach ($userOrganizations as $uo) {
|
||
//l'utilisateur doit être actif dans l'org, avoir le statut ACCEPTED (double vérif) et être admin de l'org
|
||
if ($uo->getStatut() === "ACCEPTED" && $uo->isActive() && $this->isAdminOfOrganization($uo->getOrganization())) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
|
||
}
|
||
|
||
/* Return if the current user is an admin of the target user.
|
||
* This is true if the current user is an admin of at least one organization that the target user belongs to.
|
||
*
|
||
* @param User $user
|
||
* @return bool
|
||
* @throws Exception
|
||
*/
|
||
public function isAdminOfUser(User $user): bool
|
||
{
|
||
$actingUser = $this->security->getUser();
|
||
|
||
if (!$actingUser instanceof User) {
|
||
return false;
|
||
}
|
||
|
||
// Reuse the cached/fetched role
|
||
$adminRole = $this->rolesRepository->findOneBy(['name' => 'ADMIN']);
|
||
|
||
if (!$adminRole) {
|
||
return false;
|
||
}
|
||
|
||
// Call the optimized repository method
|
||
return $this->entityManager
|
||
->getRepository(UsersOrganizations::class)
|
||
->isUserAdminOfTarget($actingUser, $user, $adminRole);
|
||
}
|
||
|
||
/**
|
||
* Check if the acting user is an admin of the organization
|
||
* A user is considered an admin of an organization if they have an active UsersOrganizations link with the role of ADMIN for that organization.
|
||
*
|
||
* @param Organizations $organizations
|
||
* @return bool
|
||
* @throws Exception
|
||
*/
|
||
public function isAdminOfOrganization(Organizations $organizations): bool
|
||
{
|
||
$actingUser =$this->security->getUser();
|
||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||
$uoAdmin = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser,
|
||
'organization' => $organizations,
|
||
'role'=> $roleAdmin,
|
||
'isActive' => true]);
|
||
return $uoAdmin !== null;
|
||
|
||
}
|
||
|
||
/**
|
||
* Get the user by their identifier.
|
||
*
|
||
* @param string $userIdentifier
|
||
* @return User|null
|
||
* @throws Exception
|
||
*/
|
||
public function getUserByIdentifier(string $userIdentifier): ?User
|
||
{
|
||
$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;
|
||
}
|
||
|
||
|
||
/**
|
||
* Format users without organization for admin view.
|
||
*
|
||
* @param array $users
|
||
* @return array
|
||
*/
|
||
public function formatNoOrgUsersAsAssoc(array $noOrgUsers): array
|
||
{
|
||
$group = [
|
||
'id' => null,
|
||
'name' => 'Utilisateurs',
|
||
'users' => [],
|
||
];
|
||
|
||
foreach ($noOrgUsers as $user) {
|
||
$group['users'][] = [
|
||
'entity' => $user,
|
||
'connected' => $this->isUserConnected($user->getUserIdentifier()),
|
||
];
|
||
}
|
||
|
||
// Use a fixed key (e.g., 0 or 'none') to avoid collisions with real org IDs
|
||
return ['none' => $group];
|
||
}
|
||
|
||
public function handleProfilePicture(User $user, UploadedFile $picture): void
|
||
{
|
||
// 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures')
|
||
$destinationDir = 'uploads/profile_pictures';
|
||
|
||
// 2. Create the directory if it doesn't exist
|
||
if (!file_exists($destinationDir)) {
|
||
// 0755 is the standard permission (Owner: read/write/exec, Others: read/exec)
|
||
if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) {
|
||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir));
|
||
}
|
||
}
|
||
|
||
// 3. Get extension and create a safe filename
|
||
$extension = $picture->guessExtension();
|
||
|
||
// Sanitize the filename to remove special characters/spaces to prevent filesystem errors
|
||
$safeName = preg_replace('/[^a-zA-Z0-9]/', '', $user->getName() . $user->getSurname());
|
||
$customFilename = $safeName . '_' . date('dmyHis') . '.' . $extension;
|
||
|
||
try {
|
||
// 4. Move the file to the destination directory
|
||
// The move() method is standard in Symfony/Laravel UploadedFile objects
|
||
$picture->move($destinationDir, $customFilename);
|
||
|
||
// 5. Update the user entity with the relative path
|
||
// Ensure you store the path relative to your public folder usually
|
||
$user->setPictureUrl('/'.$destinationDir . '/' . $customFilename);
|
||
|
||
} catch (\Exception $e) {
|
||
// 6. Log the critical error as requested
|
||
$this->loggerService->logError('File upload failed',[
|
||
'target_user_id' => $user->getId(),
|
||
'message' => $e->getMessage(),
|
||
'file_name' => $customFilename,
|
||
]);
|
||
|
||
// Optional: Re-throw the exception if you want the controller/user to know the upload failed
|
||
throw new FileException('File upload failed.');
|
||
}
|
||
}
|
||
|
||
public function deleteProfilePicture(User $user): void
|
||
{
|
||
// 1. Get the current picture path from the user entity
|
||
$currentPicturePath = $user->getPictureUrl();
|
||
|
||
// If the user doesn't have a picture, simply return (nothing to delete)
|
||
if (!$currentPicturePath) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 2. Check if the file exists on the server before trying to delete
|
||
// Note: Ensure $currentPicturePath is relative to the script execution or absolute.
|
||
if (file_exists($currentPicturePath)) {
|
||
|
||
// 3. Delete the file
|
||
if (!unlink($currentPicturePath)) {
|
||
throw new \Exception(sprintf('Could not unlink "%s"', $currentPicturePath));
|
||
}
|
||
} else {
|
||
// Optional: Log a warning if the DB had a path but the file was missing
|
||
$this->loggerService->logError('File not found on disk during deletion request', [
|
||
'target_user_id' => $user->getId(),
|
||
'missing_path' => $currentPicturePath
|
||
]);
|
||
}
|
||
|
||
// 4. Update the user entity to remove the reference
|
||
$user->setPictureUrl("");
|
||
|
||
} catch (\Exception $e) {
|
||
// 5. Log the critical error
|
||
// We log it, but strictly speaking, we might still want to nullify the DB
|
||
// if the file is corrupted/un-deletable to prevent broken images on the frontend.
|
||
$this->loggerService->logError('File deletion failed', [
|
||
'target_user_id' => $user->getId(),
|
||
'message' => $e->getMessage(),
|
||
'file_path' => $currentPicturePath,
|
||
]);
|
||
|
||
// Re-throw if you want to stop execution/show error to user
|
||
throw new \RuntimeException('Unable to remove profile picture.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Format users of a specific organization.
|
||
*
|
||
* @param UsersOrganizations[] $userOrganizations
|
||
* @return array
|
||
*/
|
||
public function formatOrgUsers(array $userOrganizations): array
|
||
{
|
||
$users = [];
|
||
|
||
foreach ($userOrganizations as $uo) {
|
||
$user = $uo->getUsers();
|
||
|
||
$users[] = [
|
||
'entity' => $user,
|
||
'connected' => $this->isUserConnected($user->getUserIdentifier()),
|
||
'isActive' => (bool)$uo->isActive(),
|
||
];
|
||
}
|
||
|
||
return $users;
|
||
}
|
||
|
||
/**
|
||
* Handle user's role synchronization
|
||
*
|
||
* @param User $user
|
||
* @param string $role
|
||
* @param boolean $add
|
||
* @return void
|
||
*/
|
||
public function syncUserRoles(User $user, string $role, bool $add): void
|
||
{
|
||
$roleFormatted = $this->formatRoleString($role);
|
||
|
||
if ($add) {
|
||
// Add the main role if not already present
|
||
if (!in_array($roleFormatted, $user->getRoles(), true)) {
|
||
$user->setRoles(array_merge($user->getRoles(), [$roleFormatted]));
|
||
}
|
||
|
||
// If SUPER ADMIN is given → ensure ADMIN is also present
|
||
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)
|
||
->findBy([
|
||
'userOrganization' => $uo,
|
||
'isActive' => true,
|
||
'role' => $this->entityManager->getRepository(Roles::class)
|
||
->findOneBy(['name' => $role]),
|
||
]);
|
||
if ($uoa) {
|
||
$hasRole = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Only remove globally if no other app gives this role
|
||
if (!$hasRole) {
|
||
$roles = $user->getRoles();
|
||
$roles = array_filter($roles, fn($r) => $r !== $roleFormatted);
|
||
$user->setRoles($roles);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Format role string to match the ROLE_ convention
|
||
*/
|
||
public function formatRoleString(string $role): string
|
||
{
|
||
$role = str_replace(' ', '_', trim($role));
|
||
$role = strtoupper($role);
|
||
if (str_starts_with($role, 'ROLE_')) {
|
||
return $role;
|
||
}
|
||
return 'ROLE_' . $role;
|
||
}
|
||
|
||
|
||
public function formatStatutForOrganizations(array $rows): array
|
||
{
|
||
$formatted = array_map(function (UsersOrganizations $uo) {
|
||
$user = $uo->getUsers();
|
||
|
||
if ($uo->getStatut() === "INVITED") {
|
||
$statut = "INVITED";
|
||
// if user invited but not accepted in 15 days, set statut to EXPIRED
|
||
$now = new DateTimeImmutable();
|
||
$invitationTime = $uo->getModifiedAt();
|
||
$expiryTime = $invitationTime->modify('+15 days');
|
||
if ($now > $expiryTime) {
|
||
$statut = "EXPIRED";
|
||
}
|
||
} else {
|
||
$statut = $uo->isActive() ? "ACTIVE" : "INACTIVE";
|
||
}
|
||
return [
|
||
'pictureUrl' => $user->getPictureUrl(),
|
||
'name' => $user->getSurname(),
|
||
'prenom' => $user->getName(),
|
||
'email' => $user->getEmail(),
|
||
'isConnected' => $this->isUserConnected($user->getUserIdentifier()),
|
||
'statut' => $statut,
|
||
'showUrl' => '/user/view/' . $user->getId() . '?organizationId=' . $uo->getOrganization()->getId(),
|
||
'id' => $user->getId(),
|
||
];
|
||
}, $rows);
|
||
return $formatted;
|
||
}
|
||
|
||
public function generatePasswordToken(User $user, int $orgId = null): string
|
||
{
|
||
$orgString = "o" . $orgId . "@";
|
||
$token = $orgString . bin2hex(random_bytes(32));
|
||
$user->setPasswordToken($token);
|
||
$user->setTokenExpiry(new DateTimeImmutable('+1 hour', new \DateTimeZone('Europe/Paris')));
|
||
$this->entityManager->persist($user);
|
||
$this->entityManager->flush();
|
||
return $token;
|
||
}
|
||
|
||
public function isPasswordTokenValid(User $user, string $token): bool
|
||
{
|
||
if ($user->getPasswordToken() !== $token || $user->getTokenExpiry() < new DateTimeImmutable()) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
public function isPasswordStrong(string $newPassword): bool
|
||
{
|
||
$pewpew = 0;
|
||
if (preg_match('/\w/', $newPassword)) { //Find any alphabetical letter (a to Z) and digit (0 to 9)
|
||
$pewpew++;
|
||
}
|
||
if (preg_match('/\W/', $newPassword)) { //Find any non-alphabetical and non-digit character
|
||
$pewpew++;
|
||
}
|
||
if (strlen($newPassword) > 8) {
|
||
$pewpew++;
|
||
}
|
||
return $pewpew >= 3;
|
||
}
|
||
|
||
public function updateUserPassword(User $user, string $newPassword): void
|
||
{
|
||
$user->setPassword(password_hash($newPassword, PASSWORD_BCRYPT));
|
||
$user->setModifiedAt(new DateTimeImmutable('now', new DateTimeZone('Europe/Paris')));
|
||
$user->setPasswordToken(null);
|
||
$user->setTokenExpiry(null);
|
||
$this->entityManager->persist($user);
|
||
$this->entityManager->flush();
|
||
}
|
||
|
||
public 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;
|
||
}
|
||
|
||
/**
|
||
* Get roles array for a user, optionally including super admin roles.
|
||
* ViewSAdminRoles flag determines if super admin roles should be included.
|
||
*
|
||
* @param User $actingUser
|
||
* @param bool $viewSAdminRoles
|
||
* @return array
|
||
*/
|
||
public function getRolesArrayForUser(User $actingUser, bool $viewSAdminRoles = false): array
|
||
{
|
||
$roles = $this->entityManager->getRepository(Roles::class)->findAll();
|
||
$rolesArray = [];
|
||
foreach ($roles as $role) {
|
||
if (!$viewSAdminRoles && $role->getName() === 'SUPER ADMIN') {
|
||
continue;
|
||
}
|
||
$rolesArray[] = [
|
||
'id' => $role->getId(),
|
||
'name' => $role->getName(),
|
||
];
|
||
}
|
||
|
||
return $rolesArray;
|
||
}
|
||
|
||
public function canEditRolesCheck(User $actingUser, User $user, bool $isAdmin, UsersOrganizations $uo = null, $org = null): bool
|
||
{
|
||
$userRoles = $user->getRoles();
|
||
$actingUserRoles = $actingUser->getRoles();
|
||
// if acting user is admin, he can´t edit super admin roles
|
||
if (!in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true) && in_array('ROLE_SUPER_ADMIN', $userRoles, true)) {
|
||
return false;
|
||
}
|
||
if ($uo && $this->isAdminOfOrganization($uo->getOrganization())) {
|
||
return true;
|
||
}
|
||
return $isAdmin && !empty($org);
|
||
|
||
}
|
||
|
||
/**
|
||
* Handle already existing user when creating a user.
|
||
* If the user exists but is inactive, reactivate them.
|
||
*
|
||
* @param User $user
|
||
* @param Organizations $organization
|
||
* @return void
|
||
*/
|
||
public function handleExistingUser(User $user, Organizations $organization): int
|
||
{
|
||
if (!$user->isActive()) {
|
||
$user->setIsActive(true);
|
||
$this->entityManager->persist($user);
|
||
}
|
||
$uo = $this->linkUserToOrganization($user, $organization);
|
||
|
||
return $uo->getId();
|
||
}
|
||
|
||
/**
|
||
* Format new user data.
|
||
* Capitalize name and surname.
|
||
* Trim strings.
|
||
* Set random password
|
||
* Handle picture if provided
|
||
*
|
||
* @param User $user
|
||
* @return void
|
||
*/
|
||
public function formatUserData(User $user, $picture, bool $setPassword = false): void
|
||
{
|
||
// capitalize name and surname
|
||
$user->setName(ucfirst(strtolower($user->getName())));
|
||
$user->setSurname(ucfirst(strtolower($user->getSurname())));
|
||
|
||
// trim strings
|
||
$user->setName(trim($user->getName()));
|
||
$user->setSurname(trim($user->getSurname()));
|
||
$user->setEmail(trim($user->getEmail()));
|
||
if ($setPassword) {
|
||
//FOR SETTING A DEFAULT RANDOM PASSWORD OF 50 CHARACTERS until user set his own password
|
||
try {
|
||
$user->setPassword(bin2hex(random_bytes(50)));
|
||
} catch (RandomException $e) {
|
||
$this->loggerService->logError('Error generating random password: ' . $e->getMessage(), [
|
||
'target_user_id' => $user->getId(),
|
||
]);
|
||
throw new RuntimeException('Error generating random password: ' . $e->getMessage());
|
||
}
|
||
|
||
}
|
||
if ($picture) {
|
||
$this->handleProfilePicture($user, $picture);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle existing user being added to an organization
|
||
*/
|
||
public function addExistingUserToOrganization(
|
||
User $existingUser,
|
||
Organizations $org,
|
||
): int
|
||
{
|
||
try {
|
||
$uoId = $this->handleExistingUser($existingUser, $org);
|
||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||
$this->loggerService->logExistingUserAddedToOrg(
|
||
$existingUser->getId(),
|
||
$org->getId(),
|
||
$actingUser->getId(),
|
||
$uoId,
|
||
);
|
||
$this->actionService->createAction(
|
||
"Add existing user to organization",
|
||
$actingUser,
|
||
$org,
|
||
"Added user {$existingUser->getUserIdentifier()} to {$org->getName()}"
|
||
);
|
||
$this->sendExistingUserNotifications($existingUser, $org, $actingUser);
|
||
|
||
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(),
|
||
]);
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a brand-new user
|
||
*/
|
||
public function createNewUser(User $user, User $actingUser, $picture): void
|
||
{
|
||
try {
|
||
$this->formatUserData($user, $picture, true);
|
||
|
||
$user->setisActive(false);
|
||
$this->generatePasswordToken($user); // Set token on the entity
|
||
|
||
$this->entityManager->persist($user);
|
||
$this->entityManager->flush();
|
||
|
||
$this->eventDispatcher->dispatch(new UserCreatedEvent($user, $actingUser));
|
||
|
||
} catch (\Exception $e) {
|
||
$this->loggerService->logError('Error creating user: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Link newly created user to an organization
|
||
*/
|
||
public function linkUserToOrganization(
|
||
User $user,
|
||
Organizations $org,
|
||
): UsersOrganizations
|
||
{
|
||
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
||
try {
|
||
$roleUser = $this->rolesRepository->findOneBy(['name' => 'USER']);
|
||
$uo = new UsersOrganizations();
|
||
$uo->setUsers($user);
|
||
$uo->setOrganization($org);
|
||
$uo->setStatut("INVITED");
|
||
$uo->setIsActive(false);
|
||
$uo->setRole($roleUser);
|
||
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||
$this->entityManager->persist($uo);
|
||
$this->entityManager->flush();
|
||
|
||
$this->loggerService->logUserOrganizationLinkCreated(
|
||
$user->getId(),
|
||
$org->getId(),
|
||
$actingUser->getId(),
|
||
$uo->getId(),
|
||
|
||
);
|
||
|
||
$this->actionService->createAction(
|
||
"Link user to organization",
|
||
$actingUser,
|
||
$org,
|
||
"Added {$user->getUserIdentifier()} to {$org->getName()}"
|
||
);
|
||
$auRoles = $actingUser->getRoles();
|
||
if (in_array('ROLE_ADMIN', $auRoles, true)) {
|
||
$this->loggerService->logSuperAdmin(
|
||
$user->getId(),
|
||
$actingUser->getId(),
|
||
"Admin linked user to organization during creation",
|
||
$org->getId()
|
||
);
|
||
}
|
||
|
||
$this->sendNewUserNotifications($user, $org, $actingUser);
|
||
|
||
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(),
|
||
]);
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
// Private helpers for email notifications
|
||
public 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());
|
||
$this->organizationsService->notifyOrganizationAdmins(['user' => $user, 'acting_user_id' => $actingUser->getId(),
|
||
'organization' => $org], 'USER_INVITED');
|
||
} catch (\Exception $e) {
|
||
$this->loggerService->logError("Error sending existing user notification: " . $e->getMessage(), [
|
||
'target_user_id' => $user->getId(),
|
||
'organization_id' => $org->getId(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
private function sendNewUserNotifications(User $user, Organizations $org, User $actingUser): void
|
||
{
|
||
try {
|
||
$token = $this->generatePasswordToken($user, $org->getId());
|
||
$this->emailService->sendPasswordSetupEmail($user, $token);
|
||
$this->organizationsService->notifyOrganizationAdmins(['user' => $user, 'acting_user_id' => $actingUser->getId(),
|
||
'organization' => $org], 'USER_INVITED');
|
||
} catch (\Exception $e) {
|
||
$this->loggerService->logError("Error sending password setup email: " . $e->getMessage(), [
|
||
'target_user_id' => $user->getId(),
|
||
'organization_id' => $org->getId(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
}
|