display users for super admin

This commit is contained in:
Charles 2025-08-22 11:45:29 +02:00
parent 8a19b01893
commit 3ca1446b91
6 changed files with 176 additions and 145 deletions

View File

@ -2,7 +2,9 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Organizations;
use App\Entity\User; use App\Entity\User;
use App\Entity\UsersOrganizations;
use App\Form\UserForm; use App\Form\UserForm;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\UserOrganizationService; use App\Service\UserOrganizationService;
@ -25,7 +27,8 @@ class UserController extends AbstractController
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService, private readonly UserService $userService,
private readonly ActionService $actionService, private readonly ActionService $actionService,
) { )
{
} }
#[Route('/', name: 'index', methods: ['GET'])] #[Route('/', name: 'index', methods: ['GET'])]
@ -33,14 +36,20 @@ class UserController extends AbstractController
{ {
$this->denyAccessUnlessGranted('ROLE_USER'); $this->denyAccessUnlessGranted('ROLE_USER');
$user = $this->getUser(); $user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if ($this->isGranted('ROLE_SUPER_ADMIN')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$usersByOrganization = $this->userService->getUsersGroupedForIndex(); $uo = $this->entityManager->getRepository(UsersOrganizations::class)->findUsersWithOrganization();
$noOrgUsers = $this->userService->formatNoOrgUsersAsAssoc(
$this->entityManager->getRepository(User::class)->findActiveUsersWithoutOrganization());
$usersByOrganization = $this->userService->groupByOrganization($uo);
$usersByOrganization += $noOrgUsers;
//Log action
$this->actionService->createAction("View all users", $user, null, "All" );
} elseif ($this->isGranted('ROLE_ADMIN')) { } elseif ($this->isGranted('ROLE_ADMIN')) {
$usersByOrganization = $this->userService->getUsersGroupedForAdmin($user); dd("dsaf");
} else { } else {
$usersByOrganization = []; $usersByOrganization = [];
} }

View File

@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use App\Entity\UsersOrganizations;
/** /**
* @extends ServiceEntityRepository<User> * @extends ServiceEntityRepository<User>
@ -33,16 +34,6 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
} }
// Rewrote this function to return less data
public function getAllActiveUsers(): array{
$queryBuilder = $this->createQueryBuilder('u')
->select('u.surname', 'u.email', 'u.id', 'u.isActive', 'u.name', 'u.pictureUrl')
->where('u.isActive = :isActive')
->orderBy('u.surname', 'ASC');
$queryBuilder->setParameter('isActive', true);
return $queryBuilder->getQuery()->getResult();
}
/** /**
* Returns active users that are NOT in any UsersOrganizations mapping. * Returns active users that are NOT in any UsersOrganizations mapping.
* Returns User entities. * Returns User entities.
@ -53,7 +44,7 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
{ {
$qb = $this->createQueryBuilder('u') $qb = $this->createQueryBuilder('u')
->select('u') ->select('u')
->leftJoin('App\\Entity\\UsersOrganizations', 'uo', 'WITH', 'uo.users = u') ->leftJoin(UsersOrganizations::class, 'uo', 'WITH', 'uo.users = u')
->where('u.isActive = :uActive') ->where('u.isActive = :uActive')
->andWhere('u.isDeleted = :uDeleted') ->andWhere('u.isDeleted = :uDeleted')
->andWhere('uo.id IS NULL') ->andWhere('uo.id IS NULL')
@ -63,32 +54,4 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
/**
* Get all active users with their organization relationships
* @return array
*/
public function getAllActiveUsersWithOrganizations(): array
{
$queryBuilder = $this->createQueryBuilder('u')
->select('u', 'uo', 'o')
->leftJoin('App\Entity\UsersOrganizations', 'uo', 'WITH', 'u.id = uo.users')
->leftJoin('App\Entity\Organizations', 'o', 'WITH', 'uo.organization = o.id')
->where('u.isActive = :isActive')
->andWhere('u.isDeleted = :isDeleted')
->andWhere('(uo.isActive = :uoActive OR uo.isActive IS NULL)')
->andWhere('(o.isActive = :oActive OR o.isActive IS NULL)')
->andWhere('(o.isDeleted = :oDeleted OR o.isDeleted IS NULL)')
->orderBy('o.name', 'ASC')
->addOrderBy('u.surname', 'ASC');
$queryBuilder->setParameter('isActive', true);
$queryBuilder->setParameter('isDeleted', false);
$queryBuilder->setParameter('uoActive', true);
$queryBuilder->setParameter('oActive', true);
$queryBuilder->setParameter('oDeleted', false);
return $queryBuilder->getQuery()->getResult();
}
} }

View File

@ -22,20 +22,18 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
* *
* @return UsersOrganizations[] * @return UsersOrganizations[]
*/ */
public function findActiveWithUserAndOrganization(): array public function findUsersWithOrganization(): array
{ {
$qb = $this->createQueryBuilder('uo') $qb = $this->createQueryBuilder('uo')
->addSelect('u', 'o') ->addSelect('u', 'o')
->leftJoin('uo.users', 'u') ->leftJoin('uo.users', 'u')
->leftJoin('uo.organization', 'o') ->leftJoin('uo.organization', 'o')
->where('uo.isActive = :uoActive')
->andWhere('u.isActive = :uActive') ->andWhere('u.isActive = :uActive')
->andWhere('u.isDeleted = :uDeleted') ->andWhere('u.isDeleted = :uDeleted')
->andWhere('o.isActive = :oActive') ->andWhere('o.isActive = :oActive')
->andWhere('o.isDeleted = :oDeleted') ->andWhere('o.isDeleted = :oDeleted')
->orderBy('o.name', 'ASC') ->orderBy('o.name', 'ASC')
->addOrderBy('u.surname', 'ASC') ->addOrderBy('u.surname', 'ASC')
->setParameter('uoActive', true)
->setParameter('uActive', true) ->setParameter('uActive', true)
->setParameter('uDeleted', false) ->setParameter('uDeleted', false)
->setParameter('oActive', true) ->setParameter('oActive', true)

View File

@ -3,17 +3,26 @@
namespace App\Service; namespace App\Service;
use App\Entity\Roles;
use App\Entity\User; use App\Entity\User;
use App\Entity\UserOrganizatonApp; use App\Entity\UserOrganizatonApp;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Exception;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken; use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use Random\RandomException; use Random\RandomException;
use Symfony\Bundle\SecurityBundle\Security;
class UserService class UserService
{ {
public function __construct(private readonly EntityManagerInterface $entityManager) public const NOT_FOUND = 'Entity not found';
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
)
{ {
// Constructor logic if needed // Constructor logic if needed
} }
@ -45,7 +54,7 @@ class UserService
*/ */
public function isUserConnected(string $userIdentifier): bool public function isUserConnected(string $userIdentifier): bool
{ {
$now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Paris')); $now = new DateTimeImmutable();
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([ $tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
'userIdentifier' => $userIdentifier, 'userIdentifier' => $userIdentifier,
'revoked' => false 'revoked' => false
@ -62,28 +71,103 @@ class UserService
} }
/** /**
* Returns UsersOrganizations rows joined with User and Organization, grouped by organization name. * Check if the user have the rights to access the page
* @return array<string, array{ organization: ?object, users: array<int, array{ users: User, userOrganization: UsersOrganizations, is_connected: bool }> }> *
* @param User $user
* @return bool
* @throws Exception
*/ */
private function groupUserOrganizationsByOrganizationFromRows(array $rows): array public function hasAccessTo(User $user): bool{
if($user->getUserIdentifier() === $this->security->getUser()->getUserIdentifier()){
return true;
}
$userOrganization = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['user' => $user]);
if($userOrganization) {
foreach ($userOrganization as $uo) {
if ($this->isAdminOfOrganization($uo)) {
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(UsersOrganizations $usersOrganizations): bool{
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['user' => $actingUser]);
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['role' => 'ADMIN']);
if ($uo){
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo,
'role'=> $roleAdmin]);
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(['userIdentifier' => $userIdentifier]);
if (!$user) {
throw new EntityNotFoundException(self::NOT_FOUND);
}
return $user;
}
/**
* Get users grouped by their organizations for the index page.
* This method should return an array of users grouped by their organizations.
*
* @return array
*/
public function groupByOrganization(array $usersOrganizations): array
{ {
$grouped = []; $grouped = [];
foreach ($rows as $userOrg) {
$organization = $userOrg->getOrganization();
$user = $userOrg->getUsers();
$orgName = $organization?->getName() ?? 'No Organization';
if (!isset($grouped[$orgName])) { foreach ($usersOrganizations as $userOrg) {
$grouped[$orgName] = [ $org = $userOrg->getOrganization();
'organization' => $organization, if (!$org) {
continue;
}
$orgId = $org->getId();
$orgName = $org->getName();
if (!isset($grouped[$orgId])) {
$grouped[$orgId] = [
'id' => $orgId,
'name' => $orgName,
'users' => [], 'users' => [],
]; ];
} }
$grouped[$orgName]['users'][] = [ $user = $userOrg->getUsers();
'users' => $user, $grouped[$orgId]['users'][] = [
'userOrganization' => $userOrg, 'entity' => $user,
'is_connected' => $this->isUserConnected($user->getEmail()), 'connected' => $this->isUserConnected($user->getUserIdentifier()),
'isActive' => (bool) $userOrg->isActive()
]; ];
} }
@ -91,64 +175,27 @@ class UserService
} }
/** /**
* Returns users that have no UsersOrganizations mapping. * Format users without organization for admin view.
* @return array<int, array{ users: User, userOrganization: null, is_connected: bool }> *
* @param array $users
* @return array
*/ */
private function buildUsersWithoutOrganization(): array public function formatNoOrgUsersAsAssoc(array $noOrgUsers): array
{ {
$userRepository = $this->entityManager->getRepository(User::class); $group = [
$users = $userRepository->findActiveUsersWithoutOrganization(); 'id' => null,
'name' => 'Utilisateurs',
'users' => [],
];
$list = []; foreach ($noOrgUsers as $user) {
foreach ($users as $user) { $group['users'][] = [
$list[] = [ 'entity' => $user,
'users' => $user, 'connected' => $this->isUserConnected($user->getUserIdentifier()),
'userOrganization' => null,
'is_connected' => $this->isUserConnected($user->getEmail()),
]; ];
} }
return $list; // Use a fixed key (e.g., 0 or 'none') to avoid collisions with real org IDs
} return ['none' => $group];
/**
* Public API: build the final structure for the index view.
* Groups users by organization and adds a "No Organization" group for users without mapping.
* @return array<string, array{ organization: ?object, users: array<int, array{ users: User, userOrganization: UsersOrganizations|null, is_connected: bool }> }>
*/
public function getUsersGroupedForIndex(): array
{
$usersOrganizationsRepo = $this->entityManager->getRepository(UsersOrganizations::class);
$rows = $usersOrganizationsRepo->findActiveWithUserAndOrganization();
$grouped = $this->groupUserOrganizationsByOrganizationFromRows($rows);
$noOrgUsers = $this->buildUsersWithoutOrganization();
if (!empty($noOrgUsers)) {
$grouped['No Organization'] = [
'organization' => null,
'users' => $noOrgUsers,
];
}
ksort($grouped);
return $grouped;
}
/**
* For admins: return users grouped only for organizations where the given user is ADMIN.
* @return array<string, array{ organization: ?object, users: array<int, array{ users: User, userOrganization: UsersOrganizations, is_connected: bool }> }>
*/
public function getUsersGroupedForAdmin(User $adminUser): array
{
$uoaRepo = $this->entityManager->getRepository(UserOrganizatonApp::class);
$orgIds = $uoaRepo->findAdminOrganizationIdsForUser($adminUser);
$uoRepo = $this->entityManager->getRepository(UsersOrganizations::class);
$rows = $uoRepo->findActiveWithUserAndOrganizationByOrganizationIds($orgIds);
$grouped = $this->groupUserOrganizationsByOrganizationFromRows($rows);
ksort($grouped);
return $grouped;
} }
} }

View File

@ -7,7 +7,7 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1>Gestion Utilisateurs</h1> <h1>Gestion Utilisateurs</h1>
{% if is_granted('ROLE_SUPER_ADMIN') %} {% if is_granted('ROLE_SUPER_ADMIN') %}
<a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a> {# <a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a>#}
{% endif %} {% endif %}
</div> </div>
@ -15,14 +15,13 @@
{% if usersByOrganization|length == 0 %} {% if usersByOrganization|length == 0 %}
<div class="alert alert-info"> <div class="alert alert-info">
<h4>Aucun utilisateur trouvé</h4> <h4>Aucun utilisateur trouvé</h4>
<p>Il n'y a actuellement aucun utilisateur correspondant à votre périmètre.</p>
</div> </div>
{% else %} {% else %}
{% for orgName, orgData in usersByOrganization %} {% for org in usersByOrganization %}
{% include 'user/userList.html.twig' with { {% include 'user/userList.html.twig' with {
title: orgName, title: org.name,
org: orgData, organizationId: org.id|default(null),
organizationId: orgData.organization ? orgData.organization.id : null users: org.users
} %} } %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -6,7 +6,7 @@
<div class="card-title d-flex justify-content-between align-items-center "> <div class="card-title d-flex justify-content-between align-items-center ">
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
{% if organizationId %} {% if organizationId %}
{# <span class="badge bg-primary">ID: {{ organizationId }}</span>#}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -24,43 +24,58 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if org.users|length == 0 %} {% if users|length == 0 %}
<tr> <tr>
<td colspan="6" class="text-center">Aucun utilisateur trouvé dans cette organisation.</td> <td colspan="6" class="text-center">Aucun utilisateur trouvé dans cette organisation.</td>
</tr> </tr>
{% else %} {% else %}
{% for userData in org.users %} {% for user in users %}
<tr> <tr>
<td> <td>
{% if userData.users.pictureUrl %} {% if user.entity.pictureUrl %}
<img src="{{ asset(userData.users.pictureUrl) }}" alt="User profile pic" <img src="{{ asset(user.entity.pictureUrl) }}" alt="User profile pic"
class="rounded-circle" class="rounded-circle"
style="width:40px; height:40px;"> style="width:40px; height:40px;">
{% else %} {% else %}
<div class="rounded-circle bg-secondary d-flex align-items-center justify-content-center" <div class="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
style="width:40px; height:40px;"> style="width:40px; height:40px;">
<span class="text-white">{{ userData.users.name|first|upper }}{{ userData.users.surname|first|upper }}</span> <span class="text-white">{{ user.entity.name|first|upper }}{{ user.entity.surname|first|upper }}</span>
</div> </div>
{% endif %} {% endif %}
</td> </td>
<td>{{ userData.users.surname }}</td> <td>{{ user.entity.surname }}</td>
<td>{{ userData.users.name }}</td> <td>{{ user.entity.name }}</td>
<td>{{ userData.users.email }}</td> <td>{{ user.entity.email }}</td>
{# Logic for status #}
<td> <td>
{% if userData.is_connected %} {# check if the user is active in the organization link #}
{% if user.isActive is defined %}
{% if user.isActive %}
{% if user.connected %}
<span class="badge bg-success">Actif</span> <span class="badge bg-success">Actif</span>
{% else %} {% else %}
<span class="badge bg-secondary">Inactif</span> <span class="badge bg-secondary">Inactif</span>
{% endif %} {% endif %}
{% else %}
<span class="badge bg-danger">Désactivé</span>
{% endif %}
{# if no organization link #}
{% else %}
{% if user.connected %}
<span class="badge bg-success">Actif</span>
{% else %}
<span class="badge bg-secondary">Inactif</span>
{% endif %}
{% endif %}
</td> </td>
<td> <td>
{# {% if organizationId is defined and organizationId %} #} {# {% if organizationId is defined and organizationId %} #}
{# <a href="{{ path('user_show', {'id': userData.users.id, 'organizationId': organizationId}) }}"#} {# <a href="{{ path('user_show', {'id': user.entity.id, 'organizationId': organizationId}) }}" #}
{# class="p-3 align-middle color-primary"> #} {# class="p-3 align-middle color-primary"> #}
{# {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} #} {# {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} #}
{# </a> #} {# </a> #}
{# {% else %} #} {# {% else %} #}
{# <a href="{{ path('user_show', {'id': userData.users.id}) }}"#} {# <a href="{{ path('user_show', {'id': user.entity.id}) }}" #}
{# class="p-3 align-middle color-primary"> #} {# class="p-3 align-middle color-primary"> #}
{# {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} #} {# {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} #}
{# </a> #} {# </a> #}