Refactor user view

This commit is contained in:
Charles 2025-11-24 15:53:19 +01:00
parent 04b8b26d65
commit 9d394c34f4
5 changed files with 144 additions and 201 deletions

View File

@ -57,150 +57,105 @@ class UserController extends AbstractController
{
}
//TODO : REMOVE DEAD CODE due to the use of tabulator in the frontend
#[Route('/view/{id}', name: 'show', methods: ['GET'])]
public function view(int $id, Request $request): Response
{
// Accès : uniquement utilisateur authentifié
$this->denyAccessUnlessGranted('ROLE_USER');
// Utilisateur courant (acting user) via UserService
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
// Vérification des droits d'accès supplémentaires
if (!$this->userService->hasAccessTo($actingUser)) {
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
}
// Chargement de l'utilisateur cible à afficher
$user = $this->userRepository->find($id);
try {
// Paramètre optionnel de contexte organisationnel
$orgId = $request->query->get('organizationId');
$apps = $this->appsRepository->findAll();
$roles = $this->rolesRepository->findAll();
$data = [
'roles' => $roles,
];
// Liste de toutes les applications (pour créer des groupes même si vides)
$apps = $this->appsRepository->findAll();
$uoList = [];
// Initialisations pour la résolution des UsersOrganizations (UO)
$singleUo = null;
$uoActive = null;
$orgs = [];
// get uo or uoS based on orgId
if ($orgId) {
// Specific organization context
$orgs = $this->organizationRepository->findBy(['id' => $orgId]);
// Contexte organisation précis : récupérer l'organisation et les liens UO
$organization = $this->organizationRepository->findBy(['id' => $orgId]);
$uoList = $this->uoRepository->findBy([
'users' => $user,
'organization' => $orgs,
'users' => $user,
'organization' => $organization,
'isActive' => true,
]);
if (!$uoList) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
// Si contexte org donné, on retient la première UO (singleUo)
$singleUo = $uoList[0];
$data["singleUo"] = $singleUo;
$uoActive = $singleUo->isActive();
} else {
// All active organizations
// Pas de contexte org : récupérer toutes les UO actives de l'utilisateur
$uoList = $this->uoRepository->findBy([
'users' => $user,
'isActive'=> true,
'users' => $user,
'isActive' => true,
]);
foreach ($uoList as $u) {
$orgs[] = $u->getOrganization();
}
if (count($uoList) === 1) {
$singleUo = $uoList[0];
$uoActive = $singleUo->isActive();
}
}
$data['uoList'] = $uoList;
$data['singleUo'] = $singleUo;
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
// Load user-organization-app roles (can be empty)
$uoa = $this->entityManager
->getRepository(UserOrganizatonApp::class)
->findBy([
'userOrganization' => $uoList,
'isActive' => true,
'isActive' => true,
]);
// Group existing UOA per app
$uoas = $this->userOrganizationAppService
->groupUserOrganizationAppsByApplication($uoa);
// Group UOA by app and ensure every app has a group
$data['uoas'] = $this->userOrganizationAppService
->groupUserOrganizationAppsByApplication(
$uoa,
$apps,
$singleUo ? $singleUo->getId() : null
);
// ---------- HERE: create empty groups for apps with no UOA ----------
// Index existing groups by app id
$indexedUoas = [];
foreach ($uoas as $group) {
$indexedUoas[$group['application']->getId()] = $group;
//Build roles based on user permissions.
//Admin can't see or edit a super admin user
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$data['rolesArray'] = $this->rolesRepository->findAll();
} elseif (!$orgId) {
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true);
} else {
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser);
}
// Load all possible roles once
$allRoles = $this->entityManager->getRepository(Roles::class)->findAll();
foreach ($apps as $app) {
$appId = $app->getId();
if (!isset($indexedUoas[$appId])) {
// No UOA for this app yet: create an empty group
$indexedUoas[$appId] = [
'uoId' => $singleUo ? $singleUo->getId() : null,
'application' => $app,
'roles' => [],
'rolesArray' => [],
'selectedRoleIds' => [],
];
foreach ($allRoles as $role) {
// Same security logic: ADMIN cannot assign SUPER ADMIN
if ($this->isGranted('ROLE_ADMIN')
&& !$this->isGranted('ROLE_SUPER_ADMIN')
&& $role->getName() === 'SUPER ADMIN') {
continue;
}
$indexedUoas[$appId]['rolesArray'][] = [
'id' => $role->getId(),
'name' => $role->getName(),
];
}
}
}
if(!$orgId){
$data['singleUo'] = null;
}
// Overwrite $uoas to include groups for *all* apps
$uoas = array_values($indexedUoas);
$data['uoas'] = $uoas;
// -------------------------------------------------------------------
// Compute "can edit" flag: admin AND exactly one UO
$canEditRoles = $this->isGranted('ROLE_ADMIN') && count($uoList) === 1;
$this->actionService->createAction(
"View user information",
$actingUser,
null,
$user->getUserIdentifier()
);
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $organization, $this->isGranted('ROLE_ADMIN'));
} catch (\Exception $e) {
$canEditRoles = false;
// En cas d'erreur, désactiver l'édition et logger l'exception
$canEdit = false;
$this->logger->error($e->getMessage());
}
return $this->render('user/show.html.twig', [
'user' => $user,
'uoas' => $uoas ?? null,
'orgs' => $orgs ?? null,
'user' => $user,
'organizationId' => $orgId ?? null,
'uoActive' => $uoActive ?? null,
'apps' => $apps ?? [],
'data' => $data ?? [],
'canEditRoles' => $canEditRoles ?? false,
'uoActive' => $uoActive ?? null,
'apps' => $apps ?? [],
'data' => $data ?? [],
'canEdit' => $canEdit ?? false,
]);
}
@ -510,9 +465,9 @@ class UserController extends AbstractController
}
if (!empty($selectedRolesIds)) {
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)){
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) {
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
}else{
} else {
$this->userOrganizationAppService->syncRolesForUserOrganizationApp(
$uo,
$application,

View File

@ -19,18 +19,21 @@ class UserOrganizationAppService
}
/**
* Groups UserOrganizationApp entities by Application
* and prepares data for Twig.
* Groups UserOrganizationApp by application and ensures every app has a group (even if empty).
*
* @param UserOrganizatonApp[] $userOrgApps
* @return array
* @param array $userOrgApps Array of UserOrganizatonApp entities
* @param array $allApps Array of all Application entities
* @param int|null $defaultUoId The UserOrganization ID to use for apps with no UOA
* @return array Indexed by app ID: ['uoId' => int|null, 'application' => App, 'selectedRoleIds' => int[]]
*/
public function groupUserOrganizationAppsByApplication(array $userOrgApps): array
{
public function groupUserOrganizationAppsByApplication(
array $userOrgApps,
array $allApps,
?int $defaultUoId = null
): array {
$grouped = [];
foreach ($userOrgApps as $uoa) {
$app = $uoa->getApplication();
$appId = $app->getId();
$roleEntity = $uoa->getRole();
@ -39,39 +42,27 @@ class UserOrganizationAppService
$grouped[$appId] = [
'uoId' => $uoa->getUserOrganization()->getId(),
'application' => $app,
'roles' => [],
'rolesArray' => [],
'selectedRoleIds' => [],
];
}
$grouped[$appId]['roles'][] = [
'id' => $roleEntity->getId(),
'name' => $roleEntity->getName(),
];
$grouped[$appId]['selectedRoleIds'][] = $roleEntity->getId();
}
// Load all possible roles once
$allRoles = $this->entityManager->getRepository(Roles::class)->findAll();
// Ensure every app has a group
foreach ($allApps as $app) {
$appId = $app->getId();
foreach ($grouped as &$appGroup) {
foreach ($allRoles as $role) {
// exclude SUPER ADMIN from assignable roles if current user is just ADMIN
if ($this->security->isGranted('ROLE_ADMIN')
&& !$this->security->isGranted('ROLE_SUPER_ADMIN')
&& $role->getName() === 'SUPER ADMIN') {
continue;
}
$appGroup['rolesArray'][] = [
'id' => $role->getId(),
'name' => $role->getName(),
if (!isset($grouped[$appId])) {
$grouped[$appId] = [
'uoId' => $defaultUoId,
'application' => $app,
'selectedRoleIds' => [],
];
}
}
return array_values($grouped);
return $grouped; // IMPORTANT: keep indexed by appId
}
/**

View File

@ -261,21 +261,6 @@ class UserService
throw new FileException('File upload failed: ' . $e->getMessage());
}
// // Define upload directory
// $uploadDirectory = $this->profileDirectory;
// // Create directory if it doesn't exist
// if (!is_dir($uploadDirectory) && !mkdir($uploadDirectory, 0755, true) && !is_dir($uploadDirectory)) {
// throw new DirectoryCouldNotBeCreatedException(sprintf('Directory "%s" was not created', $uploadDirectory));
// }
// try {
//
// // Move the file to the upload directory
// $picture->move($uploadDirectory, $customFilename);
//
// // Update user entity with the file path (relative to public directory)
// $user->setPictureUrl('uploads/profile/' . $customFilename);
//
//
}
@ -471,4 +456,41 @@ class UserService
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);
}
}

View File

@ -33,15 +33,9 @@
<div class="card border-0 no-header-bg ">
<div class="card-header">
{% if orgs|length >0 %}
<div class="card-title">
<h1>Vos applications</h1>
</div>
{% else %}
<div class="card-title">
<h1>Aucune application</h1>
</div>
{% endif %}
</div>
@ -62,30 +56,23 @@
<div class="card-body">
<div class="row">
<p><b>Description
:</b> {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}
<p>
<b>Description :</b>
{{ app.descriptionSmall|default('Aucune description disponible.')|raw }}
</p>
</div>
{# EDITABLE if admin and exactly one UO #}
{% if canEditRoles and data.singleUo is not null %}
{# find appGroup once, used in both editable and read-only branches #}
{% set appGroup = data.uoas[app.id]|default(null) %}
{% if canEdit %}
<form method="POST"
action="{{ path('user_application_role', { id: data.singleUo.id }) }}">
{# for this app, find its grouped info #}
{# Find the group for this specific app #}
{% set appGroup = null %}
{% for group in data.uoas|default([]) %}
{% if group.application.id == app.id %}
{% set appGroup = group %}
{% endif %}
{% endfor %}
<div class="form-group mb-3">
<label for="roles-{{ app.id }}"><b>Rôles :</b></label>
<div class="form-check">
{% if appGroup %}
{# Use rolesArray: filtered by current user's level (no SUPER ADMIN for plain ADMIN, etc.) #}
{% for role in appGroup.rolesArray %}
{% for role in data.rolesArray %}
<input class="form-check-input" type="checkbox"
name="roles[]"
value="{{ role.id }}"
@ -100,12 +87,8 @@
{% endif %}
</label>
{% endfor %}
{% else %}
<p class="text-muted">Aucun rôle défini pour cette
application.</p>
<p class="text-muted">Aucun rôle défini pour cette application.</p>
{% endif %}
</div>
<button type="submit" name="appId" value="{{ app.id }}"
@ -114,29 +97,18 @@
</button>
</div>
</form>
{# READ ONLY otherwise #}
{% else %}
{% set appGroup = null %}
{% for group in data.uoas|default([]) %}
{% if group.application.id == app.id %}
{% set appGroup = group %}
{% endif %}
{% endfor %}
<div class="form-group mb-3">
<label for="roles-{{ app.id }}"><b>Rôles :</b></label>
<div class="form-check">
{% if appGroup %}
{# Use rolesArray: filtered by current user's level (no SUPER ADMIN for plain ADMIN, etc.) #}
{% for role in appGroup.rolesArray %}
{% for role in data.rolesArray %}
<input class="form-check-input" type="checkbox"
disabled
name="roles[]"
value="{{ role.id }}"
id="role-{{ role.id }}-app-{{ app.id }}"
{% if appGroup and role.id in appGroup.selectedRoleIds %}checked{% endif %}>
{% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>
<label class="form-check"
for="role-{{ role.id }}-app-{{ app.id }}">
{% if role.name == 'USER' %}
@ -147,10 +119,8 @@
</label>
{% endfor %}
{% else %}
<p class="text-muted">Aucun rôle défini pour cette
application.</p>
<p class="text-muted">Aucun rôle défini pour cette application.</p>
{% endif %}
</div>
</div>
{% endif %}

View File

@ -1,23 +1,25 @@
{% block body %}
<div class="card no-header-bg border-0 ">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
{% if user.pictureUrl is not empty %}
<img src="{{ aws_url ~ user.pictureUrl }}" alt="user" class="rounded-circle"
style="width:40px; height:40px;">
{% endif %}
<div class="card-title ">
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
</div>
<div class="card no-header-bg border-0 ">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
{% if user.pictureUrl is not empty %}
<img src="{{ aws_url ~ user.pictureUrl }}" alt="user" class="rounded-circle"
style="width:40px; height:40px;">
{% endif %}
<div class="card-title ">
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
</div>
</div>
<div class="d-flex gap-2">
<div class="d-flex gap-2">
{% if canEdit %}
{% if organizationId is not null %}
{% if uoActive %}
<form method="post" action="{{ path('user_deactivate_organization', {'id': user.id}) }}"
onsubmit="return confirm('Vous allez retirer l\'utilisateur de cette organisation, êtes vous sûre?');">
<input type="hidden" name="organizationId" value="{{ organizationId }}">
<button class="btn btn-secondary" type="submit">Désactiver l'utilisateur de l'organisation
<button class="btn btn-secondary" type="submit">Désactiver l'utilisateur de
l'organisation
</button>
</form>
{% else %}
@ -29,20 +31,23 @@
</form>
{% endif %}
{% endif %}
<a href="{{ path('user_edit', {'id': user.id, 'organizationId': organizationId}) }}"
class="btn btn-primary">Modifier</a>
</div>
</div>
<div class="card-body ">
<p><b>Email: </b>{{ user.email }}</p>
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
à {{ user.lastConnection|date('H:m:s') }} </p>
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
{% endif %}
</div>
</div>
<div class="card-body ">
<p><b>Email: </b>{{ user.email }}</p>
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
à {{ user.lastConnection|date('H:m:s') }} </p>
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
</div>
</div>
{% endblock %}