Easy_solution/src/Service/UserService.php

482 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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\Service\AwsService;
use DateTimeImmutable;
use DateTimeZone;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Exception;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use Random\RandomException;
use SebastianBergmann\CodeCoverage\Util\DirectoryCouldNotBeCreatedException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
class UserService
{
public const NOT_FOUND = 'Entity not found';
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
private readonly AwsService $awsService
)
{
}
/**
* Generate a random password for a new user until they set their own.
* @throws RandomException
*/
public function generateRandomPassword(): string
{
$length = 50; // Length of the password
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+';
$charactersLength = strlen($characters);
$randomPassword = '';
for ($i = 0; $i < $length; $i++) {
$randomPassword .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomPassword;
}
/**
* 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;
}
/**
* Check if the user have the rights to access the page
* Self check can be skipped when checking access for the current user
*
* @param User $user
* @param bool $skipSelfCheck
* @return bool
* @throws Exception
*/
public function hasAccessTo(User $user, bool $skipSelfCheck = false): bool
{
if (!$skipSelfCheck && $user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()) {
return true;
}
$userOrganizations = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
if ($userOrganizations) {
foreach ($userOrganizations as $uo) {
if ($this->isAdminOfOrganization($uo->getOrganization())) {
return true;
}
}
}
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
return true;
}
return false;
}
/**
* Check if the user is an admin of the organization
* A user is considered an admin of an organization if they have the 'ROLE_ADMIN' AND have the link to the
* entity role 'ROLE_ADMIN' in the UsersOrganizationsApp entity
* (if he is admin for any application of the organization).
*
* @param UsersOrganizations $usersOrganizations
* @return bool
* @throws Exception
*/
public function isAdminOfOrganization(Organizations $organizations): bool
{
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $actingUser, 'organization' => $organizations]);
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
if ($uo) {
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo,
'role' => $roleAdmin,
'isActive' => true]);
if ($uoa && $this->security->isGranted('ROLE_ADMIN')) {
return true;
}
}
return false;
}
/**
* 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) {
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];
}
//TODO: reset function
public function handleProfilePicture(User $user, $picture): void
{
// Get file extension
$extension = $picture->guessExtension();
// Create custom filename: userNameUserSurname_ddmmyyhhmmss
$customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension;
// $customFilename = $user->getName() . $user->getSurname() . "." .$extension;
try {
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/');
$user->setPictureUrl('profile/' . $customFilename);
} catch (FileException $e) {
// Handle upload error
throw new FileException('File upload failed: ' . $e->getMessage());
}
//
//
}
/**
* 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']));
}
} 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 revokeUserTokens(string $userIdentifier)
{
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
'userIdentifier' => $userIdentifier,
'revoked' => false
]);
foreach ($tokens as $token) {
$token->revoke();
}
}
public function formatStatutForOrganizations(array $rows): array
{
$formatted = array_map(function (UsersOrganizations $uo) {
$user = $uo->getUsers();
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
if ($uo->getStatut() === "INVITED") {
$statut = "INVITED";
// if user invited but not accepted in 1 hour, set statut to EXPIRED
$now = new DateTimeImmutable();
$invitationTime = $uo->getModifiedAt();
$expiryTime = $invitationTime->modify('+1 hour');
if ($now > $expiryTime) {
$statut = "EXPIRED";
}
} else {
$statut = $uo->isActive() ? "ACTIVE" : "INACTIVE";
}
return [
'pictureUrl' => $picture,
'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): 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, $org, bool $isAdmin): 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', $userRoles, true) && !in_array('ROLE_SUPER_ADMIN', $actingUserRoles, true)) {
return false;
}
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): void
{
if (!$user->isActive()) {
$user->setIsActive(true);
$this->entityManager->persist($user);
}
$uo = new UsersOrganizations();
$uo->setUsers($user);
$uo->setOrganization($organization);
$uo->setStatut("INVITED");
$uo->setIsActive(false);
$uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo);
$this->entityManager->flush();
}
/**
* Format new user data.
* Capitalize name and surname.
* Trim strings.
* Set random password
* Handle picture if provided
*
* @param User $user
* @return void
*/
public function formatNewUserData(User $user, $picture): 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()));
//FOR SETTING A DEFAULT RANDOM PASSWORD OF 50 CHARACTERS until user set his own password
$user->setPassword($this->generateRandomPassword());
if($picture) {
$this->handleProfilePicture($user, $picture);
}
}
}