['me:read']], ), new Get( security: "is_granted('ROLE_USER')", normalizationContext: ['groups' => ['user:list']], ), new GetCollection( paginationEnabled: false, security: "is_granted('ROLE_USER')", normalizationContext: ['groups' => ['user:list']], ), new Post(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class), new Patch(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class), new Delete(security: "is_granted('ROLE_ADMIN')"), new Get( uriTemplate: '/users/{id}/rbac', security: "is_granted('core.users.manage')", normalizationContext: ['groups' => ['user:rbac:read']], ), new Patch( uriTemplate: '/users/{id}/rbac', security: "is_granted('core.users.manage')", normalizationContext: ['groups' => ['user:rbac:read']], denormalizationContext: ['groups' => ['user:rbac:write']], processor: UserRbacProcessor::class, ), ], denormalizationContext: ['groups' => ['user:write']], )] #[Auditable] #[ORM\Entity(repositoryClass: DoctrineUserRepository::class)] #[ORM\Table(name: '`user`')] class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedUserInterface, LeaveProfileInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'absence_request:read', 'absence_balance:read', 'commercial_report:read'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] #[Groups(['me:read', 'task:read', 'user:list', 'user:write', 'time_entry:read', 'absence_request:read', 'absence_balance:read', 'commercial_report:read'])] private ?string $username = null; #[ORM\Column(length: 100, nullable: true)] #[Groups(['me:read', 'user:list', 'user:write'])] private ?string $firstName = null; #[ORM\Column(length: 100, nullable: true)] #[Groups(['me:read', 'user:list', 'user:write'])] private ?string $lastName = null; /** @var list */ #[ORM\Column] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private array $roles = []; #[ORM\Column] #[AuditIgnore] private ?string $password = null; #[Groups(['user:write'])] #[AuditIgnore] private ?string $plainPassword = null; #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] private ?DateTimeImmutable $createdAt = null; #[ORM\Column(length: 64, unique: true, nullable: true)] #[Groups(['me:read'])] #[AuditIgnore] private ?string $apiToken = null; #[ORM\Column(length: 255, nullable: true)] private ?string $avatarFileName = null; // --- HR / absence management fields (readable only by an admin or the user themselves) --- /** Whether this user is an employee subject to absence management. */ #[ORM\Column] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private bool $isEmployee = false; /** Hiring date — start of paid-leave acquisition. */ #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private ?DateTimeImmutable $hireDate = null; #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private ?DateTimeImmutable $endDate = null; #[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: ContractType::class)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private ?ContractType $contractType = null; /** Work-time ratio: 1.0 = full time, 0.8 = 4 days out of 5. */ #[ORM\Column(type: Types::FLOAT)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private float $workTimeRatio = 1.0; /** Yearly paid-leave entitlement in worked days (default 25 = jours ouvrés). */ #[ORM\Column(type: Types::FLOAT)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private float $annualLeaveDays = 25.0; /** Reference period start as MM-DD (default 06-01, 1st of June). */ #[ORM\Column(length: 5)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private string $referencePeriodStart = '06-01'; /** Paid-leave already acquired when the module is rolled out. */ #[ORM\Column(type: Types::FLOAT)] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[Groups(['me:read', 'user:list', 'user:write'])] private float $initialLeaveBalance = 0.0; /** * @var Collection */ #[ORM\ManyToMany(targetEntity: Role::class, fetch: 'EAGER')] #[ORM\JoinTable(name: 'user_role')] #[Groups(['user:rbac:read', 'user:rbac:write'])] private Collection $rbacRoles; /** * @var Collection */ #[ORM\ManyToMany(targetEntity: Permission::class, fetch: 'EAGER')] #[ORM\JoinTable(name: 'user_permission')] #[Groups(['user:rbac:read', 'user:rbac:write'])] private Collection $directPermissions; public function __construct() { $this->createdAt = new DateTimeImmutable(); $this->rbacRoles = new ArrayCollection(); $this->directPermissions = new ArrayCollection(); } public function getId(): ?int { return $this->id; } public function getUsername(): ?string { return $this->username; } public function setUsername(string $username): static { $this->username = $username; return $this; } public function getFirstName(): ?string { return $this->firstName; } public function setFirstName(?string $firstName): static { $this->firstName = $firstName; return $this; } public function getLastName(): ?string { return $this->lastName; } public function setLastName(?string $lastName): static { $this->lastName = $lastName; return $this; } public function getUserIdentifier(): string { return (string) $this->username; } /** @return list */ public function getRoles(): array { $roles = $this->roles; // Every authenticated user gets ROLE_USER. $roles[] = 'ROLE_USER'; return array_values(array_unique($roles)); } /** @param list $roles */ public function setRoles(array $roles): static { $this->roles = $roles; return $this; } public function getPassword(): ?string { return $this->password; } public function setPassword(string $password): static { $this->password = $password; return $this; } public function getCreatedAt(): ?DateTimeImmutable { return $this->createdAt; } public function setCreatedAt(DateTimeImmutable $createdAt): static { $this->createdAt = $createdAt; return $this; } public function getApiToken(): ?string { return $this->apiToken; } public function setApiToken(?string $apiToken): static { $this->apiToken = $apiToken; return $this; } public function getAvatarFileName(): ?string { return $this->avatarFileName; } public function setAvatarFileName(?string $avatarFileName): static { $this->avatarFileName = $avatarFileName; return $this; } #[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'absence_request:read', 'absence_balance:read'])] public function getAvatarUrl(): ?string { if (null === $this->avatarFileName) { return null; } return '/api/users/'.$this->id.'/avatar'; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): static { $this->plainPassword = $plainPassword; return $this; } public function eraseCredentials(): void { $this->plainPassword = null; } public function getIsEmployee(): bool { return $this->isEmployee; } public function setIsEmployee(bool $isEmployee): static { $this->isEmployee = $isEmployee; return $this; } public function getHireDate(): ?DateTimeImmutable { return $this->hireDate; } public function setHireDate(?DateTimeImmutable $hireDate): static { $this->hireDate = $hireDate; return $this; } public function getEndDate(): ?DateTimeImmutable { return $this->endDate; } public function setEndDate(?DateTimeImmutable $endDate): static { $this->endDate = $endDate; return $this; } public function getContractType(): ?ContractType { return $this->contractType; } public function setContractType(?ContractType $contractType): static { $this->contractType = $contractType; return $this; } public function getWorkTimeRatio(): float { return $this->workTimeRatio; } public function setWorkTimeRatio(float $workTimeRatio): static { $this->workTimeRatio = $workTimeRatio; return $this; } public function getAnnualLeaveDays(): float { return $this->annualLeaveDays; } public function setAnnualLeaveDays(float $annualLeaveDays): static { $this->annualLeaveDays = $annualLeaveDays; return $this; } public function getReferencePeriodStart(): string { return $this->referencePeriodStart; } public function setReferencePeriodStart(string $referencePeriodStart): static { $this->referencePeriodStart = $referencePeriodStart; return $this; } public function getInitialLeaveBalance(): float { return $this->initialLeaveBalance; } public function setInitialLeaveBalance(float $initialLeaveBalance): static { $this->initialLeaveBalance = $initialLeaveBalance; return $this; } /** * @return Collection */ public function getRbacRoles(): Collection { return $this->rbacRoles; } public function addRbacRole(Role $role): void { if (!$this->rbacRoles->contains($role)) { $this->rbacRoles->add($role); } } public function removeRbacRole(Role $role): void { $this->rbacRoles->removeElement($role); } /** * @return Collection */ public function getDirectPermissions(): Collection { return $this->directPermissions; } public function addDirectPermission(Permission $permission): void { if (!$this->directPermissions->contains($permission)) { $this->directPermissions->add($permission); } } public function removeDirectPermission(Permission $permission): void { $this->directPermissions->removeElement($permission); } /** * Permissions effectives = union (rôles RBAC → permissions) ∪ (permissions directes), triée, dédupliquée. * * @return list */ #[Groups(['me:read', 'user:rbac:read'])] public function getEffectivePermissions(): array { $codes = []; foreach ($this->rbacRoles as $role) { foreach ($role->getPermissions() as $permission) { $codes[$permission->getCode()] = true; } } foreach ($this->directPermissions as $permission) { $codes[$permission->getCode()] = true; } $keys = array_keys($codes); sort($keys); return $keys; } }