Add/Remove admin from orgs
This commit is contained in:
parent
056325bcf3
commit
3df22c2dbf
|
|
@ -1,7 +1,15 @@
|
|||
import {Controller} from '@hotwired/stimulus';
|
||||
import Choices from 'choices.js';
|
||||
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||
import {activateUserIcon, deactivateUserIcon, eyeIconLink, sendEmailIcon, TABULATOR_FR_LANG} from "../js/global.js";
|
||||
import {
|
||||
activateUserIcon,
|
||||
deactivateUserIcon,
|
||||
eyeIconLink,
|
||||
sendEmailIcon,
|
||||
TABULATOR_FR_LANG,
|
||||
trashIconForm
|
||||
} from "../js/global.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
|
||||
export default class extends Controller {
|
||||
|
|
@ -18,7 +26,7 @@ export default class extends Controller {
|
|||
orgId: Number
|
||||
}
|
||||
|
||||
static targets = ["select", "statusButton"];
|
||||
static targets = ["select", "statusButton", "modal", "userSelect"];
|
||||
|
||||
connect() {
|
||||
this.roleSelect();
|
||||
|
|
@ -34,6 +42,9 @@ export default class extends Controller {
|
|||
if (this.listOrganizationValue) {
|
||||
this.tableOrganization()
|
||||
}
|
||||
if (this.hasModalTarget) {
|
||||
this.modal = new Modal(this.modalTarget);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -499,8 +510,9 @@ export default class extends Controller {
|
|||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
const orgId = this.orgIdValue;
|
||||
if (url) {
|
||||
eyeIconLink(url);
|
||||
return trashIconForm(url, orgId);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
@ -918,4 +930,92 @@ export default class extends Controller {
|
|||
btn.dataset.active = "false";
|
||||
}
|
||||
}
|
||||
|
||||
async openAddAdminModal() {
|
||||
this.modal.show();
|
||||
await this.loadAvailableUsers();
|
||||
}
|
||||
|
||||
async loadAvailableUsers() {
|
||||
try {
|
||||
const response = await fetch(`/organization/${this.orgIdValue}/users`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.users) {
|
||||
this.userSelectTarget.innerHTML = '<option value="">Choisir un utilisateur...</option>' +
|
||||
data.users.map(user => `
|
||||
<option value="${user.id}">${user.email}</option>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
this.userSelectTarget.innerHTML = '<option value="">Erreur de chargement</option>';
|
||||
}
|
||||
}
|
||||
|
||||
async submitAddAdmin(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
const userId = formData.get('userId');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/user/organization/admin/${userId}`, {
|
||||
method: 'POST',
|
||||
body: formData, // Sends userId, status="add", and organizationId
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.modal.hide();
|
||||
// If you use Tabulator, refresh it here
|
||||
// e.g., this.table.setData();
|
||||
location.reload();
|
||||
} else {
|
||||
if (response.status === 409) {
|
||||
alert("Cet utilisateur est déjà administrateur de l'organisation.");
|
||||
return;
|
||||
}
|
||||
alert("Erreur lors de l'ajout de l'administrateur.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Une erreur est survenue.");
|
||||
}
|
||||
}
|
||||
|
||||
async removeAdmin(event) {
|
||||
// 1. Prevent any default behavior
|
||||
event.preventDefault();
|
||||
|
||||
const button = event.currentTarget;
|
||||
const url = button.dataset.url;
|
||||
const organizationId = button.dataset.orgId;
|
||||
|
||||
if (!confirm("Voulez-vous vraiment retirer les droits d'administrateur à cet utilisateur ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Prepare the payload (matching your previous hidden fields)
|
||||
const formData = new FormData();
|
||||
formData.append('status', 'remove');
|
||||
formData.append('organizationId', organizationId);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// 3. Success! Reload the page to see the updated list
|
||||
location.reload();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert("Erreur: " + (errorData.error || "Impossible de supprimer les droits."));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Une erreur réseau est survenue.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ export function pencilIcon() {
|
|||
`
|
||||
}
|
||||
|
||||
export function trashIcon(url) {
|
||||
export function trashIcon() {
|
||||
return `
|
||||
<span class="align-middle color-delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
|
||||
|
|
@ -50,6 +50,18 @@ export function trashIcon(url) {
|
|||
`
|
||||
}
|
||||
|
||||
export function trashIconForm(url, organizationId) {
|
||||
return `
|
||||
<button class="btn btn-link p-0 border-0 color-delete align-middle"
|
||||
data-action="click->user#removeAdmin"
|
||||
data-url="${url}"
|
||||
data-org-id="${organizationId}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
|
||||
|
||||
</button>`;
|
||||
}
|
||||
|
||||
|
||||
export function deactivateUserIcon() {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use App\Entity\UserOrganizatonApp;
|
|||
use App\Entity\UsersOrganizations;
|
||||
use App\Form\OrganizationForm;
|
||||
use App\Repository\OrganizationsRepository;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\AwsService;
|
||||
use App\Service\LoggerService;
|
||||
|
|
@ -43,7 +44,7 @@ class OrganizationController extends AbstractController
|
|||
private readonly ActionService $actionService,
|
||||
private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly OrganizationsRepository $organizationsRepository,
|
||||
private readonly LoggerService $loggerService)
|
||||
private readonly LoggerService $loggerService, private readonly UsersOrganizationsRepository $usersOrganizationsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -323,4 +324,31 @@ class OrganizationController extends AbstractController
|
|||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/users', name: 'users', methods: ['GET'])]
|
||||
public function users($id): JsonResponse{
|
||||
$this->denyAccessUnlessGranted("ROLE_USER");
|
||||
$actingUser = $this->getUser();
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->loggerService->logEntityNotFound('Organization', [
|
||||
'org_id' => $id,
|
||||
'message' => 'Organization not found for users endpoint'
|
||||
], $actingUser->getUserIdentifier());
|
||||
return $this->json(['error' => self::NOT_FOUND], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) {
|
||||
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
|
||||
return $this->json(['error' => self::ACCESS_DENIED], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
$uos = $this->usersOrganizationsRepository->findBy(['organization' => $organization, 'isActive' => true]);
|
||||
$users = array_map(function (UsersOrganizations $uo) {
|
||||
$user = $uo->getUsers();
|
||||
return [
|
||||
'id' => $user->getId(),
|
||||
'email' => $user->getEmail()
|
||||
];
|
||||
}, $uos);
|
||||
return $this->json(['users' => $users]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -603,30 +603,24 @@ class UserController extends AbstractController
|
|||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($this->userService->isAdminOfOrganization($org) || $this->isGranted("ROLE_ADMIN")) {
|
||||
$uos = $this->uoRepository->findBy(['organization' => $org]);
|
||||
if ($this->userService->isAdminOfOrganization($org) || $this->isGranted("ROLE_ADMIN")){
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
$users = [];
|
||||
foreach ($uos as $uo) {
|
||||
if ($this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin])) {
|
||||
$users[] = $uo;
|
||||
if (!$roleAdmin) {
|
||||
$this->loggerService->logEntityNotFound('Role', ['name' => 'ADMIN'], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Map to array (keep isConnected)
|
||||
$data = array_map(function (UsersOrganizations $uo) {
|
||||
$user = $uo->getUsers();
|
||||
$uos = $this->uoRepository->findBy(['organization' => $org, 'role' => $roleAdmin, 'statut' => "ACCEPTED", 'isActive' => true]);
|
||||
$data = array_map(function (UsersOrganizations $uos) {
|
||||
$user = $uos->getUsers();
|
||||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||
return [
|
||||
'pictureUrl' => $user->getPictureUrl(),
|
||||
'email' => $user->getEmail(),
|
||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||
'showUrl' => $this->generateUrl('user_organization_admin_status', ['id' => $user->getId()]),
|
||||
'initials' => strtoupper($initials),
|
||||
];
|
||||
}, $users);
|
||||
|
||||
}, $uos);
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
]);
|
||||
|
|
@ -774,5 +768,70 @@ class UserController extends AbstractController
|
|||
}
|
||||
return $this->render('security/login.html.twig', ['error'=> null, 'last_username' => $user->getEmail()]);
|
||||
}
|
||||
|
||||
#[Route(path: '/organization/admin/{id}', name: 'organization_admin_status', methods: ['POST'])]
|
||||
public function organizationAdminStatus(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->getUser();
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
$this->loggerService->logEntityNotFound('User', ['id' => $id], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$orgId = $request->request->get('organizationId');
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user,'organization' => $org]);
|
||||
if (!$uo) {
|
||||
$this->loggerService->logEntityNotFound('UsersOrganization', ['user_id' => $id, 'organization_id' => $uo->getOrganization()->getId()], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$status = $request->request->get('status');
|
||||
if ($status === 'add') {
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
if (!$roleAdmin) {
|
||||
$this->loggerService->logEntityNotFound('Role', ['name' => 'ADMIN'], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
if ($uo->getRole() == $roleAdmin) {
|
||||
return new JsonResponse(['error' => 'User is already admin'], Response::HTTP_CONFLICT);
|
||||
}
|
||||
$uo->setRole($roleAdmin);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getUserIdentifier(), "Role ADMIN added to user with uo id : {$uo->getId()}");
|
||||
$this->actionService->createAction("Grant organization admin rights", $actingUser, $uo->getOrganization(), "for user " . $uo->getUSers()->getUserIdentifier());
|
||||
return new JsonResponse(['status' => 'added'], Response::HTTP_OK);
|
||||
}
|
||||
|
||||
if ($status === 'remove') {
|
||||
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
||||
if (!$roleUser) {
|
||||
$this->loggerService->logEntityNotFound('Role', ['name' => 'ADMIN'], $actingUser->getUserIdentifier());
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo->setRole($roleUser);
|
||||
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||
$this->entityManager->persist($uo);
|
||||
$this->entityManager->flush();
|
||||
$this->loggerService->logOrganizationInformation($uo->getOrganization()->getId(), $actingUser->getUserIdentifier(), "Role ADMIN removed from user with uo id : {$uo->getId()}");
|
||||
$this->actionService->createAction("Revoke organization admin rights", $actingUser, $uo->getOrganization(), "for user " . $uo->getUSers()->getUserIdentifier());
|
||||
return new JsonResponse(['status' => 'removed'], Response::HTTP_OK);
|
||||
}
|
||||
//invalid status
|
||||
$this->loggerService->logError('Invalid status provided for organizationAdminStatus', [
|
||||
'requested_status' => $status,
|
||||
'target_user_id' => $uo->getUsers()->getId(),
|
||||
'organization_id' => $uo->getOrganization()->getId(),
|
||||
]);
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,18 +69,49 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-3 card no-header-bg">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
Administrateurs
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userListSmallAdmin" data-controller="user"
|
||||
data-user-aws-value="{{ aws_url }}"
|
||||
<div class="col mb-3 card no-header-bg"
|
||||
data-controller="user"
|
||||
data-user-org-id-value="{{ organization.id }}"
|
||||
data-user-admin-value="true"
|
||||
data-user-list-small-value="true"
|
||||
data-user-org-id-value="{{ organization.id }}">
|
||||
data-user-list-small-value="true">
|
||||
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Administrateurs</h2>
|
||||
<button type="button" class="btn btn-primary" data-action="click->user#openAddAdminModal">
|
||||
Ajouter un administrateur
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userListSmallAdmin"></div>
|
||||
</div>
|
||||
|
||||
{# Modal for Adding Admin #}
|
||||
<div class="modal fade" id="addAdminModal" tabindex="-1" aria-hidden="true" data-user-target="modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter un administrateur</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form data-action="submit->user#submitAddAdmin">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sélectionner l'utilisateur</label>
|
||||
<select name="userId" class="form-select" data-user-target="userSelect" required>
|
||||
<option value="">Chargement...</option>
|
||||
</select>
|
||||
</div>
|
||||
{# Hidden Fields #}
|
||||
<input type="hidden" name="status" value="add">
|
||||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue