feat(permissions) : add role-based access control system

Backend:
- Add role hierarchy (ADMIN > GESTIONNAIRE > VIEWER > USER) in security.yaml
- Add password authentication on profile activation (SessionProfileController)
- Add SessionProfileAuthenticator with stateless API firewall
- Add ProfilePasswordHasher state processor for API Platform
- Add security annotations on all 18 API Platform entities
- Add denyAccessUnlessGranted on all 13 custom controllers
- Add AdminProfileController for profile/role management (/api/admin/profiles)
- Add InitProfilePasswordsCommand for initial admin setup
- Simplify SessionProfilesController to list-only (removed create/delete)

Frontend (submodule update):
- Add usePermissions composable (isAdmin, canEdit, canView, isGranted)
- Add password login modal on profiles page
- Add admin backoffice page for profile management
- Disable all form fields for ROLE_VIEWER across all edit/create pages
- Show navigation buttons for all roles, hide destructive actions for viewers
- Add readonly mode to ModelTypeForm and site/constructeur modals
- Guard /admin routes in middleware
- Configure Vite proxy for API requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-26 13:37:12 +01:00
parent adc44b99d3
commit a3e440c254
36 changed files with 762 additions and 110 deletions

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Entity\Profile;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
final class SessionProfileAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly ProfileRepository $profiles,
) {}
public function supports(Request $request): ?bool
{
if (!$request->hasSession()) {
return false;
}
return $request->getSession()->has('profileId');
}
public function authenticate(Request $request): Passport
{
$profileId = $request->getSession()->get('profileId');
return new SelfValidatingPassport(
new UserBadge($profileId, function (string $id): Profile {
$profile = $this->profiles->find($id);
if (!$profile || !$profile->isActive()) {
throw new CustomUserMessageAuthenticationException('Profil introuvable ou inactif.');
}
return $profile;
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// Let the request continue normally
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse(
['message' => $exception->getMessageKey()],
JsonResponse::HTTP_UNAUTHORIZED,
);
}
}