[#322] Page horaire (#4)
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|        #322          |        Page horaire         |

## Description de la PR
[#322] Page horaire

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #4
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #4.
This commit is contained in:
2026-02-20 11:23:52 +00:00
committed by Autin
parent f6c1f7eead
commit ee16779777
85 changed files with 6232 additions and 242 deletions

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Entity\Employee;
use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
class EmployeeScopeService
{
public const string SITE_ACCESS_ROLE = 'SITE_ACCESS';
/**
* Règle métier centrale d'accès à un employé.
* - Admin : accès global
* - Self : uniquement son employé lié
* - Site : uniquement les employés des sites autorisés.
*/
public function canAccessEmployee(User $user, Employee $employee): bool
{
if (in_array('ROLE_ADMIN', $user->getRoles(), true)) {
return true;
}
if (in_array('ROLE_SELF', $user->getRoles(), true)) {
return $user->getEmployee()?->getId() === $employee->getId();
}
$employeeSiteId = $employee->getSite()?->getId();
if (!$employeeSiteId) {
return false;
}
return in_array($employeeSiteId, $this->getAllowedSiteIds($user), true);
}
/**
* Retourne la liste des sites accessibles via user_site_roles.
*
* @return list<int>
*/
public function getAllowedSiteIds(User $user): array
{
$siteIds = [];
foreach ($user->getSiteRoles() as $siteRole) {
if (self::SITE_ACCESS_ROLE !== $siteRole->getRole()) {
continue;
}
$siteId = $siteRole->getSite()?->getId();
if ($siteId) {
$siteIds[] = $siteId;
}
}
return array_values(array_unique($siteIds));
}
/**
* Applique le scope directement sur un QueryBuilder Doctrine.
* Cette méthode est utilisée pour filtrer les collections SQL
* avant sérialisation (plus sûr et plus performant).
*/
public function applyEmployeeScope(QueryBuilder $qb, string $employeeAlias, string $paramPrefix, User $user): void
{
if (in_array('ROLE_ADMIN', $user->getRoles(), true)) {
return;
}
if (in_array('ROLE_SELF', $user->getRoles(), true)) {
$employeeId = $user->getEmployee()?->getId();
if (!$employeeId) {
$qb->andWhere('1 = 0');
return;
}
$qb->andWhere(sprintf('%s.id = :%s_employee_id', $employeeAlias, $paramPrefix))
->setParameter(sprintf('%s_employee_id', $paramPrefix), $employeeId)
;
return;
}
$siteIds = $this->getAllowedSiteIds($user);
if ([] === $siteIds) {
$qb->andWhere('1 = 0');
return;
}
$qb->andWhere(sprintf('%s.site IN (:%s_site_ids)', $employeeAlias, $paramPrefix))
->setParameter(sprintf('%s_site_ids', $paramPrefix), $siteIds)
;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\Absence;
use App\Entity\User;
use App\Security\EmployeeScopeService;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class AbsenceVoter extends Voter
{
public const string VIEW = 'ABSENCE_VIEW';
public const string EDIT = 'ABSENCE_EDIT';
public function __construct(
private readonly Security $security,
private readonly EmployeeScopeService $employeeScopeService,
) {}
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT], true) && $subject instanceof Absence;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
$user = $this->security->getUser();
if (!$user instanceof User) {
return false;
}
if (!$subject instanceof Absence) {
return false;
}
$employee = $subject->getEmployee();
if (null === $employee) {
return false;
}
return $this->employeeScopeService->canAccessEmployee($user, $employee);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\User;
use App\Entity\WorkHour;
use App\Security\EmployeeScopeService;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class WorkHourVoter extends Voter
{
public const string VIEW = 'WORK_HOUR_VIEW';
public const string EDIT = 'WORK_HOUR_EDIT';
public function __construct(
private readonly Security $security,
private readonly EmployeeScopeService $employeeScopeService,
) {}
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT], true) && $subject instanceof WorkHour;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
// On ne traite que des utilisateurs applicatifs authentifiés.
$user = $this->security->getUser();
if (!$user instanceof User) {
return false;
}
if (!$subject instanceof WorkHour) {
return false;
}
$employee = $subject->getEmployee();
if (null === $employee) {
return false;
}
// Délégation de la règle au service de scope unique (évite la duplication).
return $this->employeeScopeService->canAccessEmployee($user, $employee);
}
}