From cd29e3150a9db62fcdb655b2aa7c38c673fbb61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 3 Mar 2026 14:13:04 +0100 Subject: [PATCH] Add role management for users and update related translations and templates - Added a `roles` column to the `users` table using a JSONB data type. - Updated the `User` entity to include role management methods (`getRoles`, `addRole`, `removeRole`). - Modified the `UserType` form to allow selecting special roles. - Updated Twig templates to display assigned roles in the user list. - Added new translations for roles and updated French translation keys in `messages.fr.yml`. --- src/Bundle/ChillMainBundle/Entity/User.php | 55 +++++++++++++------ src/Bundle/ChillMainBundle/Form/UserType.php | 11 +++- .../Resources/views/User/index.html.twig | 11 +++- .../migrations/Version20260302202811.php | 33 +++++++++++ .../translations/messages.fr.yml | 7 +++ 5 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20260302202811.php diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index b5539aa83..1439d106e 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -17,6 +17,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Selectable; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use libphonenumber\PhoneNumber; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; @@ -38,20 +39,22 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email'; public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest'; + public const ROLE_SEE_AUDIT_TRAILS = 'ROLE_SEE_AUDIT_TRAILS'; + #[ORM\Id] - #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] + #[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\GeneratedValue(strategy: 'AUTO')] protected ?int $id = null; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $absenceStart = null; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $absenceEnd = null; /** * Array where SAML attributes's data are stored. */ - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] + #[ORM\Column(type: Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] private array $attributes = []; #[ORM\ManyToOne(targetEntity: Civility::class)] @@ -60,13 +63,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter #[ORM\ManyToOne(targetEntity: Location::class)] private ?Location $currentLocation = null; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 150, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 150, nullable: true)] private ?string $email = null; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 150, nullable: true, unique: true)] + #[ORM\Column(type: Types::STRING, length: 150, nullable: true, unique: true)] private ?string $emailCanonical = null; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)] + #[ORM\Column(type: Types::BOOLEAN)] private bool $enabled = true; /** @@ -76,10 +79,10 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')] private Collection $groupCenters; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 200)] + #[ORM\Column(type: Types::STRING, length: 200)] private string $label = ''; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)] // sf4 check: in yml was false by default !? + #[ORM\Column(type: Types::BOOLEAN)] // sf4 check: in yml was false by default !? private bool $locked = true; #[ORM\ManyToOne(targetEntity: Center::class)] @@ -94,13 +97,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserScopeHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection&Selectable $scopeHistories; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)] + #[ORM\Column(type: Types::STRING, length: 255)] private string $password = ''; /** * @internal must be set to null if we use bcrypt */ - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] private ?string $salt = null; /** @@ -109,10 +112,10 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserJobHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection&Selectable $jobHistories; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 80)] + #[ORM\Column(type: Types::STRING, length: 80)] private string $username = ''; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 80, unique: true, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 80, unique: true, nullable: true)] private ?string $usernameCanonical = null; /** @@ -125,15 +128,18 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter /** * @var array> */ - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] + #[ORM\Column(type: Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] private array $notificationFlags = []; /** * User's preferred locale. */ - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 5, nullable: false, options: ['default' => 'fr'])] + #[ORM\Column(type: Types::STRING, length: 5, nullable: false, options: ['default' => 'fr'])] private string $locale = 'fr'; + #[ORM\Column(type: Types::JSON, nullable: false, options: ['default' => "'[]'::jsonb", 'jsonb' => true])] + private array $roles = []; + /** * User constructor. */ @@ -280,7 +286,24 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter public function getRoles(): array { - return ['ROLE_USER']; + return [...$this->roles, 'ROLE_USER']; + } + + public function addRole(string $role): void + { + // we do not accept "ROLE_USER" + if ('ROLE_USER' === $role) { + return; + } + + if (!in_array($role, $this->roles, true)) { + $this->roles[] = $role; + } + } + + public function removeRole(string $role): void + { + $this->roles = array_diff($this->roles, [$role]); } public function getSalt(): ?string diff --git a/src/Bundle/ChillMainBundle/Form/UserType.php b/src/Bundle/ChillMainBundle/Form/UserType.php index 79f95c20e..90636ddd6 100644 --- a/src/Bundle/ChillMainBundle/Form/UserType.php +++ b/src/Bundle/ChillMainBundle/Form/UserType.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillPhoneNumberType; @@ -107,6 +108,14 @@ class UserType extends AbstractType 'required' => false, 'input' => 'datetime_immutable', 'label' => 'absence.Absence end', + ]) + ->add('roles', ChoiceType::class, [ + 'label' => 'user.profile.special_roles', + 'choices' => [ + 'roles.role_see_audit_trails' => User::ROLE_SEE_AUDIT_TRAILS, + ], + 'multiple' => true, + 'expanded' => true, ]); // @phpstan-ignore-next-line @@ -172,7 +181,7 @@ class UserType extends AbstractType public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ - 'data_class' => \Chill\MainBundle\Entity\User::class, + 'data_class' => User::class, ]); $resolver diff --git a/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig index 66ffc030b..bd3414a4b 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig @@ -12,7 +12,7 @@ {% block table_entities_thead_tr %} {{ 'Active'|trans }} {{ 'absence.Is absent'|trans }} - {{ 'Username'|trans }} + {{ 'user.profile.label'|trans }} {{ 'Datas'|trans }} {{ 'Actions'|trans }} {% endblock %} @@ -46,6 +46,15 @@