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

View File

@@ -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

View File

@@ -12,7 +12,7 @@
{% block table_entities_thead_tr %}
<th>{{ 'Active'|trans }}</th>
<th>{{ 'absence.Is absent'|trans }}</th>
<th>{{ 'Username'|trans }}</th>
<th>{{ 'user.profile.label'|trans }}</th>
<th>{{ 'Datas'|trans }}</th>
<th>{{ 'Actions'|trans }}</th>
{% endblock %}
@@ -46,6 +46,15 @@
</td>
<td>
<ul class="unbullet">
{# #}
{% if entity.roles|length > 1 %}
<li>
<span class="dt">{{ 'user.profile.special_roles'|trans }}:</span>
{% if constant('Chill\\MainBundle\\Entity\\User::ROLE_SEE_AUDIT_TRAILS') in entity.roles %}
<span class="badge" style="background-color: var(--bs-beige)" >{{ 'roles.role_see_audit_trails'|trans }}</span>
{% endif %}
</li>
{% endif %}
<li>
<span class="dt">login:</span>
{{ entity.username|e('html_attr') }}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260302202811 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add a role column for table users';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE users ADD roles JSONB DEFAULT \'[]\'::jsonb NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP roles');
}
}

View File

@@ -59,6 +59,8 @@ user:
no job: Pas de métier assigné
no scope: Pas de service assigné
notification_preferences: Préférences pour mes notifications
label: Nom affiché
special_roles: Droits spécifiques
locale:
label: Langue de communication
help: Langue utilisée pour les notifications par email et autres communications.
@@ -544,6 +546,9 @@ CHILL_FOO_SEE: Voir un élément
CHILL_FOO_EDIT: Modifier un élément
chill_export: Exports (statistiques)
roles:
role_see_audit_trails: Voir les accès aux éléments
#Show templates
Date: Date
By: Par
@@ -920,6 +925,8 @@ admin:
center_name: Territoire
permissionsGroup_id: Identifiant du groupe de permissions
permissionsGroup_name: Groupe de permissions
mainLanguage: Langue principale
isRoleSeeAuditTrails: Rôle "voir les audits"
job_scope_histories:
Show history: Voir l'historique
index: