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`.
This commit is contained in:
2026-03-03 14:13:04 +01:00
parent 063d6528b7
commit cd29e3150a
5 changed files with 99 additions and 18 deletions

View File

@@ -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<string, list<string>>
*/
#[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