first commit

This commit is contained in:
2025-07-28 17:10:56 +02:00
commit 7d55ac027a
124 changed files with 19397 additions and 0 deletions

View File

@ -0,0 +1,66 @@
<?php
namespace App\Command;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
#[AsCommand(
name: 'app:init',
description: 'Initialisation of the app',
)]
class InitCommand extends Command
{
private EntityManagerInterface $em;
private ParameterBagInterface $params;
private UserPasswordHasherInterface $passwordHasher;
public function __construct(EntityManagerInterface $em, ParameterBagInterface $params, UserPasswordHasherInterface $passwordHasher)
{
$this->em = $em;
$this->params = $params;
$this->passwordHasher = $passwordHasher;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('APP:INIT');
$io->text('Initialisation of the app');
$io->text('');
$admins = array_map('trim', explode(',', $this->params->get('appAdmin')));
foreach ($admins as $admin) {
$user = $this->em->getRepository("App\Entity\User")->findOneBy(['username' => $admin]);
if (!$user) {
$io->text('> Création du compte admin par defaut = '.$admin);
$user = new User();
$hashedPassword = $this->passwordHasher->hashPassword(
$user,
$this->params->get('appSecret')
);
$user->setUsername($admin);
$user->setPassword($hashedPassword);
$user->setAvatar('medias/avatar/admin.jpg');
$user->setEmail($this->params->get('appNoreply'));
$this->em->persist($user);
}
$user->setRoles(['ROLE_ADMIN']);
$this->em->flush();
}
return Command::SUCCESS;
}
}

0
src/Controller/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,38 @@
<?php
namespace App\Controller;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'app_home')]
public function home(Request $request): Response
{
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.');
}
$projects = $user->getProjects();
return $this->render('home/home.html.twig', [
'usemenu' => true,
'usesidebar' => false,
'projects' => $projects,
]);
}
#[Route('/admin', name: 'app_admin')]
public function admin(): Response
{
return $this->render('home/blank.html.twig', [
'usemenu' => true,
'usesidebar' => true,
]);
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Controller;
use App\Entity\Project;
use App\Entity\User;
use App\Form\ProjectType;
use App\Repository\ProjectRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
class ProjectController extends AbstractController
{
#[Route('/admin/project', name: 'app_admin_project')]
public function list(ProjectRepository $projectRepository): Response
{
$projects = $projectRepository->findAll();
return $this->render('project/list.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Liste des Projets',
'routesubmit' => 'app_admin_project_submit',
'routeupdate' => 'app_admin_project_update',
'projects' => $projects,
]);
}
#[Route('/admin/project/submit', name: 'app_admin_project_submit')]
public function submit(Request $request, EntityManagerInterface $em): Response
{
$project = new Project();
$project->addUser($this->getUser());
$form = $this->createForm(ProjectType::class, $project, ['mode' => 'submit']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($project);
$em->flush();
return $this->redirectToRoute('app_admin_project');
}
return $this->render('project/edit.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Création Projet',
'routecancel' => 'app_admin_project',
'routedelete' => 'app_admin_project_delete',
'mode' => 'submit',
'form' => $form,
]);
}
#[Route('/admin/project/update/{id}', name: 'app_admin_project_update')]
public function update(int $id, Request $request, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
{
$project = $projectRepository->find($id);
if (!$project) {
throw new NotFoundHttpException('La ressource demandée est introuvable.');
}
$form = $this->createForm(ProjectType::class, $project, ['mode' => 'update']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->flush();
return $this->redirectToRoute('app_admin_project');
}
return $this->render('project/edit.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Modification Projet = '.$project->getTitle(),
'routecancel' => 'app_admin_project',
'routedelete' => 'app_admin_project_delete',
'mode' => 'update',
'form' => $form,
'project' => $project,
]);
}
#[Route('/admin/project/delete/{id}', name: 'app_admin_project_delete')]
public function delete(int $id, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
{
$project = $projectRepository->find($id);
if (!$project) {
throw new NotFoundHttpException('La ressource demandée est introuvable.');
}
$users = $em->getRepository(User::class)->findBy(['project' => $project]);
foreach ($users as $user) {
$user->setProject(null);
$em->flush();
}
// Tentative de suppression
try {
$em->remove($project);
$em->flush();
} catch (\Exception $e) {
$this->addflash('error', $e->getMessage());
return $this->redirectToRoute('app_admin_project_update', ['id' => $id]);
}
return $this->redirectToRoute('app_admin_project');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace App\Controller;
use App\Service\ImageService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class UploadController extends AbstractController
{
private ImageService $imageService;
public function __construct(ImageService $imageService)
{
$this->imageService = $imageService;
}
#[Route('/user/upload/crop01/{endpoint}', name: 'app_user_upload_crop01')]
public function crop01(string $endpoint, Request $request): Response
{
$reportThumb = $request->get('reportThumb');
return $this->render('upload\crop01.html.twig', [
'useheader' => false,
'usemenu' => false,
'usesidebar' => false,
'endpoint' => $endpoint,
'reportThumb' => $reportThumb,
]);
}
#[Route('/user/upload/crop02', name: 'app_user_upload_crop02')]
public function crop02(Request $request): Response
{
$reportThumb = $request->get('reportThumb');
$path = $request->get('path');
$file = $request->get('file');
$image = $this->getParameter('kernel.project_dir').'/public/'.$path.'/'.$file;
$thumb = $this->getParameter('kernel.project_dir').'/public/'.$path.'/thumb_'.$file;
// Redimentionner
$this->imageService->resizeImage($image, 700, 700);
// Construction du formulaire
$form = $this->createFormBuilder()
->add('submit', SubmitType::class, ['label' => 'Valider', 'attr' => ['class' => 'btn btn-success']])
->add('x1', HiddenType::class)
->add('y1', HiddenType::class)
->add('x2', HiddenType::class)
->add('y2', HiddenType::class)
->add('w', HiddenType::class)
->add('h', HiddenType::class)
->getForm();
// Récupération des data du formulaire
$form->handleRequest($request);
$toReport = false;
// Sur validation on généère la miniature croppée
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$toReport = true;
$this->imageService->cropImage($image, $thumb, $data['x1'], $data['y1'], $data['w'], $data['h'], 150, 150);
}
return $this->render('upload\crop02.html.twig', [
'useheader' => false,
'usemenu' => false,
'usesidebar' => false,
'reportThumb' => $reportThumb,
'image' => $path.'/'.$file,
'thumb' => $path.'/thumb_'.$file,
'form' => $form,
'toReport' => $toReport,
]);
}
}

View File

@ -0,0 +1,191 @@
<?php
namespace App\Controller;
use App\Entity\Project;
use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
class UserController extends AbstractController
{
#[Route('/admin/user', name: 'app_admin_user')]
public function list(UserRepository $userRepository): Response
{
$users = $userRepository->findAll();
return $this->render('user/list.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Liste des Utilisateurs',
'routesubmit' => 'app_admin_user_submit',
'routeupdate' => 'app_admin_user_update',
'users' => $users,
]);
}
#[Route('/admin/user/submit', name: 'app_admin_user_submit')]
public function submit(Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $em): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user, ['mode' => 'submit', 'modeAuth' => $this->getParameter('modeAuth')]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$password = $user->getPassword();
if ('CAS' === $this->getParameter('modeAuth')) {
$password = Uuid::uuid4();
}
$hashedPassword = $passwordHasher->hashPassword(
$user,
$password
);
$user->setPassword($hashedPassword);
$em->persist($user);
$em->flush();
return $this->redirectToRoute('app_admin_user');
}
return $this->render('user/edit.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Création Utilisateur',
'routecancel' => 'app_admin_user',
'routedelete' => 'app_admin_user_delete',
'mode' => 'submit',
'form' => $form,
]);
}
#[Route('/admin/user/update/{id}', name: 'app_admin_user_update')]
public function update(int $id, Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $em): Response
{
$user = $em->getRepository(User::class)->find($id);
if (!$user) {
return $this->redirectToRoute('app_admin_user');
}
$hashedPassword = $user->getPassword();
$form = $this->createForm(UserType::class, $user, ['mode' => 'update', 'modeAuth' => $this->getParameter('modeAuth')]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
if ($user->getPassword()) {
$hashedPassword = $passwordHasher->hashPassword(
$user,
$user->getPassword()
);
}
$user->setPassword($hashedPassword);
$em->flush();
return $this->redirectToRoute('app_admin_user');
}
return $this->render('user/edit.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Modification Utilisateur = '.$user->getUsername(),
'routecancel' => 'app_admin_user',
'routedelete' => 'app_admin_user_delete',
'mode' => 'update',
'form' => $form,
]);
}
#[Route('/admin/user/delete/{id}', name: 'app_admin_user_delete')]
public function delete(int $id, EntityManagerInterface $em): Response
{
$user = $em->getRepository(User::class)->find($id);
if (!$user) {
return $this->redirectToRoute('app_admin_user');
}
// Tentative de suppression
try {
$em->remove($user);
$em->flush();
} catch (\Exception $e) {
$this->addflash('error', $e->getMessage());
return $this->redirectToRoute('app_admin_user_update', ['id' => $id]);
}
return $this->redirectToRoute('app_admin_user');
}
#[Route('/user', name: 'app_user_profil')]
public function profil(Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $em): Response
{
$user = $em->getRepository(User::class)->find($this->getUser());
if (!$user) {
return $this->redirectToRoute('app_home');
}
$hashedPassword = $user->getPassword();
$form = $this->createForm(UserType::class, $user, ['mode' => 'profil', 'modeAuth' => $this->getParameter('modeAuth')]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
if ($user->getPassword()) {
$hashedPassword = $passwordHasher->hashPassword(
$user,
$user->getPassword()
);
}
$user->setPassword($hashedPassword);
$em->flush();
return $this->redirectToRoute('app_home');
}
return $this->render('user/edit.html.twig', [
'usemenu' => true,
'usesidebar' => false,
'title' => 'Profil = '.$user->getUsername(),
'routecancel' => 'app_home',
'routedelete' => '',
'mode' => 'profil',
'form' => $form,
]);
}
#[Route('/user/selectproject', name: 'app_user_selectproject')]
public function selectproject(Request $request, EntityManagerInterface $em): JsonResponse
{
$id = $request->get('id');
$project = $em->getRepository(Project::class)->find($id);
if (!$project) {
return new JsonResponse(['status' => 'KO', 'message' => 'ID non fourni'], Response::HTTP_NOT_FOUND);
}
$user = $this->getUser();
if (!$user instanceof User) {
throw new \LogicException('L\'utilisateur actuel n\'est pas une instance de App\Entity\User.');
}
$projects = $user->getProjects();
if (!$projects->contains($project)) {
return new JsonResponse(['status' => 'KO', 'message' => 'Projet non autorisée'], Response::HTTP_FORBIDDEN);
}
$user->setProject($project);
$em->flush();
return new JsonResponse(['status' => 'OK', 'message' => 'Projet selectionnée'], Response::HTTP_OK);
}
}

0
src/Entity/.gitignore vendored Normal file
View File

112
src/Entity/Project.php Normal file
View File

@ -0,0 +1,112 @@
<?php
namespace App\Entity;
use App\Repository\ProjectRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_project_title', fields: ['title'])]
class Project
{
#[ORM\Id]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $title = null;
#[ORM\Column()]
private int $status;
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $updateAt = null;
/**
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'projects')]
private Collection $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function setId(int $id): static
{
$this->id = $id;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getUpdateAt(): ?\DateTimeInterface
{
return $this->updateAt;
}
public function setUpdateAt(?\DateTimeInterface $updateAt): static
{
$this->updateAt = $updateAt;
return $this;
}
public function getStatus(): ?int
{
return $this->status;
}
public function setStatus(int $status): static
{
$this->status = $status;
return $this;
}
/**
* @return Collection<int, User>
*/
public function getUsers(): Collection
{
return $this->users;
}
public function addUser(User $user): static
{
if (!$this->users->contains($user)) {
$this->users->add($user);
$user->addProject($this);
}
return $this;
}
public function removeUser(User $user): static
{
if ($this->users->removeElement($user)) {
$user->removeProject($this);
}
return $this;
}
}

207
src/Entity/User.php Normal file
View File

@ -0,0 +1,207 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_USERNAME', fields: ['username'])]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
#[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé.')]
#[UniqueEntity(fields: ['username'], message: 'Ce nom dutilisateur est déjà pris.')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $username = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $apikey = null;
#[ORM\Column]
private array $roles = [];
#[ORM\Column]
private ?string $password = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $avatar = null;
#[ORM\Column(length: 255, unique: true)]
#[Assert\Email(message: 'Veuillez entrer un email valide.')]
private ?string $email = null;
#[ORM\ManyToMany(targetEntity: Project::class, inversedBy: 'users')]
private ?Collection $projects;
#[ORM\ManyToOne()]
#[ORM\JoinColumn(nullable: true)]
private ?Project $project = null;
public function __construct()
{
$this->projects = 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;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/**
* @see UserInterface
*
* @return list<string>
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* @param list<string> $roles
*/
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(?string $password): static
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getApikey(): ?string
{
return $this->apikey;
}
public function setApikey(?string $apikey): static
{
$this->apikey = $apikey;
return $this;
}
public function getAvatar(): ?string
{
return $this->avatar ? $this->avatar : 'medias/avatar/noavatar.png';
}
public function setAvatar(?string $avatar): static
{
$this->avatar = $avatar;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
/**
* @return Collection<int, Project>
*/
public function getProjects(): ?Collection
{
return $this->projects;
}
public function addProject(Project $project): static
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
}
return $this;
}
public function removeProject(Project $project): static
{
$this->projects->removeElement($project);
return $this;
}
public function getProject(): ?Project
{
if (!$this->projects) {
return null;
}
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\EventListener;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Http\Event\LogoutEvent;
final class LogoutListener
{
private ParameterBagInterface $parameterBag;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->parameterBag = $parameterBag;
}
#[AsEventListener(event: LogoutEvent::class)]
public function onLogoutEvent(LogoutEvent $event): void
{
if ('CAS' == $this->parameterBag->get('modeAuth')) {
$request = $event->getRequest();
$host = $request->headers->get('host');
$scheme = $request->headers->get('X-Forwarded-Proto') ?? $request->getScheme();
$url = $scheme.'://'.$host;
\phpCAS::client(CAS_VERSION_2_0, $this->parameterBag->get('casHost'), (int) $this->parameterBag->get('casPort'), $this->parameterBag->get('casPath'), $url, false);
\phpCAS::setNoCasServerValidation();
$url.=$request->getBaseUrl();
\phpCAS::logoutWithRedirectService($url);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\EventListener;
use Oneup\UploaderBundle\Event\PostPersistEvent;
use Oneup\UploaderBundle\Event\ValidationEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;
final class UploadListener
{
private string $projectDir;
public function __construct(KernelInterface $kernel)
{
// Utiliser le projectDir pour construire le chemin
$this->projectDir = $kernel->getProjectDir();
}
#[AsEventListener(event: 'oneup_uploader.validation')]
public function onOneupUploaderValidation(ValidationEvent $event): void
{
// On s'assure que le repertoire de destination existe bien
$fs = new Filesystem();
$fs->mkdir($this->projectDir.'/public/uploads');
$fs->mkdir($this->projectDir.'/public/uploads/'.$event->getType());
}
#[AsEventListener(event: 'oneup_uploader.post_persist')]
public function onOneupUploaderPostPersit(PostPersistEvent $event): void
{
$file = $event->getFile();
$type = $event->getType();
$filename = $file->getFilename();
$response = $event->getResponse();
$response['file'] = $filename;
$response['path'] = 'uploads/'.$type;
$response['filepath'] = 'uploads/'.$type.'/'.$filename;
}
}

46
src/Form/ProjectType.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Form;
use App\Entity\Project;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('submit', SubmitType::class, [
'label' => 'Valider',
'attr' => ['class' => 'btn btn-success no-print'],
])
->add('title', TextType::class, [
'label' => 'Titre',
])
->add('users', EntityType::class, [
'label' => 'Propriétaires',
'class' => User::class,
'choice_label' => 'username',
'multiple' => true,
'attr' => ['class' => 'select2'],
'required' => false,
'by_reference' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Project::class,
'mode' => 'submit',
]);
}
}

View File

@ -0,0 +1,999 @@
<?php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FontawsomeType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => $this->getFontAwesomeIcons(),
]);
}
public function getParent(): string
{
return ChoiceType::class;
}
private function getFontAwesomeIcons(): array
{
$icons = [
'ad',
'address-book',
'address-card',
'adjust',
'air-freshener',
'align-center',
'align-justify',
'align-left',
'align-right',
'allergies',
'ambulance',
'american-sign-language-interpreting',
'anchor',
'angle-double-down',
'angle-double-left',
'angle-double-right',
'angle-double-up',
'angle-down',
'angle-left',
'angle-right',
'angle-up',
'angry',
'ankh',
'apple-alt',
'archive',
'archway',
'arrow-alt-circle-down',
'arrow-alt-circle-left',
'arrow-alt-circle-right',
'arrow-alt-circle-up',
'arrow-circle-down',
'arrow-circle-left',
'arrow-circle-right',
'arrow-circle-up',
'arrow-down',
'arrow-left',
'arrow-right',
'arrow-up',
'arrows-alt',
'arrows-alt-h',
'arrows-alt-v',
'assistive-listening-systems',
'asterisk',
'at',
'atlas',
'atom',
'audio-description',
'award',
'baby',
'baby-carriage',
'backspace',
'backward',
'bacon',
'bahai',
'balance-scale',
'balance-scale-left',
'balance-scale-right',
'ban',
'band-aid',
'barcode',
'bars',
'baseball-ball',
'basketball-ball',
'bath',
'battery-empty',
'battery-full',
'battery-half',
'battery-quarter',
'battery-three-quarters',
'bed',
'beer',
'bell',
'bell-slash',
'bezier-curve',
'bible',
'bicycle',
'biking',
'binoculars',
'biohazard',
'birthday-cake',
'blender',
'blender-phone',
'blind',
'blog',
'bold',
'bolt',
'bomb',
'bone',
'bong',
'book',
'book-dead',
'book-medical',
'book-open',
'book-reader',
'bookmark',
'border-all',
'border-none',
'border-style',
'bowling-ball',
'box',
'box-open',
'boxes',
'braille',
'brain',
'bread-slice',
'briefcase',
'briefcase-medical',
'broadcast-tower',
'broom',
'brush',
'bug',
'building',
'bullhorn',
'bullseye',
'burn',
'bus',
'bus-alt',
'business-time',
'calculator',
'calendar',
'calendar-alt',
'calendar-check',
'calendar-day',
'calendar-minus',
'calendar-plus',
'calendar-times',
'calendar-week',
'camera',
'camera-retro',
'campground',
'candy-cane',
'cannabis',
'capsules',
'car',
'car-alt',
'car-battery',
'car-crash',
'car-side',
'caravan',
'caret-down',
'caret-left',
'caret-right',
'caret-square-down',
'caret-square-left',
'caret-square-right',
'caret-square-up',
'caret-up',
'carrot',
'cart-arrow-down',
'cart-plus',
'cash-register',
'cat',
'certificate',
'chair',
'chalkboard',
'chalkboard-teacher',
'charging-station',
'chart-area',
'chart-bar',
'chart-line',
'chart-pie',
'check',
'check-circle',
'check-double',
'check-square',
'cheese',
'chess',
'chess-bishop',
'chess-board',
'chess-king',
'chess-knight',
'chess-pawn',
'chess-queen',
'chess-rook',
'chevron-circle-down',
'chevron-circle-left',
'chevron-circle-right',
'chevron-circle-up',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up',
'child',
'church',
'circle',
'circle-notch',
'city',
'clinic-medical',
'clipboard',
'clipboard-check',
'clipboard-list',
'clock',
'clone',
'closed-captioning',
'cloud',
'cloud-download-alt',
'cloud-meatball',
'cloud-moon',
'cloud-moon-rain',
'cloud-rain',
'cloud-showers-heavy',
'cloud-sun',
'cloud-sun-rain',
'cloud-upload-alt',
'cocktail',
'code',
'code-branch',
'coffee',
'cog',
'cogs',
'coins',
'columns',
'comment',
'comment-alt',
'comment-dollar',
'comment-dots',
'comment-medical',
'comment-slash',
'comments',
'comments-dollar',
'compact-disc',
'compass',
'compress',
'compress-alt',
'compress-arrows-alt',
'concierge-bell',
'cookie',
'cookie-bite',
'copy',
'copyright',
'couch',
'credit-card',
'crop',
'crop-alt',
'cross',
'crosshairs',
'crow',
'crown',
'crutch',
'cube',
'cubes',
'cut',
'database',
'deaf',
'democrat',
'desktop',
'dharmachakra',
'diagnoses',
'dice',
'dice-d20',
'dice-d6',
'dice-five',
'dice-four',
'dice-one',
'dice-six',
'dice-three',
'dice-two',
'digital-tachograph',
'directions',
'divide',
'dizzy',
'dna',
'dog',
'dollar-sign',
'dolly',
'dolly-flatbed',
'donate',
'door-closed',
'door-open',
'dot-circle',
'dove',
'download',
'drafting-compass',
'dragon',
'draw-polygon',
'drum',
'drum-steelpan',
'drumstick-bite',
'dumbbell',
'dumpster',
'dumpster-fire',
'dungeon',
'edit',
'egg',
'eject',
'ellipsis-h',
'ellipsis-v',
'envelope',
'envelope-open',
'envelope-open-text',
'envelope-square',
'equals',
'eraser',
'ethernet',
'euro-sign',
'exchange-alt',
'exclamation',
'exclamation-circle',
'exclamation-triangle',
'expand',
'expand-alt',
'expand-arrows-alt',
'external-link-alt',
'external-link-square-alt',
'eye',
'eye-dropper',
'eye-slash',
'fan',
'fast-backward',
'fast-forward',
'fax',
'feather',
'feather-alt',
'female',
'fighter-jet',
'file',
'file-alt',
'file-archive',
'file-audio',
'file-code',
'file-contract',
'file-csv',
'file-download',
'file-excel',
'file-export',
'file-image',
'file-import',
'file-invoice',
'file-invoice-dollar',
'file-medical',
'file-medical-alt',
'file-pdf',
'file-powerpoint',
'file-prescription',
'file-signature',
'file-upload',
'file-video',
'file-word',
'fill',
'fill-drip',
'film',
'filter',
'fingerprint',
'fire',
'fire-alt',
'fire-extinguisher',
'first-aid',
'fish',
'fist-raised',
'flag',
'flag-checkered',
'flag-usa',
'flask',
'flushed',
'folder',
'folder-minus',
'folder-open',
'folder-plus',
'font',
'football-ball',
'forward',
'frog',
'frown',
'frown-open',
'funnel-dollar',
'futbol',
'gamepad',
'gas-pump',
'gavel',
'gem',
'genderless',
'ghost',
'gift',
'gifts',
'glass-cheers',
'glass-martini',
'glass-martini-alt',
'glass-whiskey',
'glasses',
'globe',
'globe-africa',
'globe-americas',
'globe-asia',
'globe-europe',
'golf-ball',
'gopuram',
'graduation-cap',
'greater-than',
'greater-than-equal',
'grimace',
'grin',
'grin-alt',
'grin-beam',
'grin-beam-sweat',
'grin-hearts',
'grin-squint',
'grin-squint-tears',
'grin-stars',
'grin-tears',
'grin-tongue',
'grin-tongue-squint',
'grin-tongue-wink',
'grin-wink',
'grip-horizontal',
'grip-lines',
'grip-lines-vertical',
'grip-vertical',
'guitar',
'h-square',
'hamburger',
'hammer',
'hamsa',
'hand-holding',
'hand-holding-heart',
'hand-holding-usd',
'hand-lizard',
'hand-middle-finger',
'hand-paper',
'hand-peace',
'hand-point-down',
'hand-point-left',
'hand-point-right',
'hand-point-up',
'hand-pointer',
'hand-rock',
'hand-scissors',
'hand-spock',
'hands',
'hands-helping',
'handshake',
'hanukiah',
'hard-hat',
'hashtag',
'hat-cowboy',
'hat-cowboy-side',
'hat-wizard',
'hdd',
'heading',
'headphones',
'headphones-alt',
'headset',
'heart',
'heart-broken',
'heartbeat',
'helicopter',
'highlighter',
'hiking',
'hippo',
'history',
'hockey-puck',
'holly-berry',
'home',
'horse',
'horse-head',
'hospital',
'hospital-alt',
'hospital-symbol',
'hot-tub',
'hotdog',
'hotel',
'hourglass',
'hourglass-end',
'hourglass-half',
'hourglass-start',
'house-damage',
'hryvnia',
'i-cursor',
'ice-cream',
'icicles',
'icons',
'id-badge',
'id-card',
'id-card-alt',
'igloo',
'image',
'images',
'inbox',
'indent',
'industry',
'infinity',
'info',
'info-circle',
'italic',
'jedi',
'joint',
'journal-whills',
'kaaba',
'key',
'keyboard',
'khanda',
'kiss',
'kiss-beam',
'kiss-wink-heart',
'kiwi-bird',
'landmark',
'language',
'laptop',
'laptop-code',
'laptop-medical',
'laugh',
'laugh-beam',
'laugh-squint',
'laugh-wink',
'layer-group',
'leaf',
'lemon',
'less-than',
'less-than-equal',
'level-down-alt',
'level-up-alt',
'life-ring',
'lightbulb',
'link',
'lira-sign',
'list',
'list-alt',
'list-ol',
'list-ul',
'location-arrow',
'lock',
'lock-open',
'long-arrow-alt-down',
'long-arrow-alt-left',
'long-arrow-alt-right',
'long-arrow-alt-up',
'low-vision',
'luggage-cart',
'magic',
'magnet',
'mail-bulk',
'male',
'map',
'map-marked',
'map-marked-alt',
'map-marker',
'map-marker-alt',
'map-pin',
'map-signs',
'marker',
'mars',
'mars-double',
'mars-stroke',
'mars-stroke-h',
'mars-stroke-v',
'mask',
'medal',
'medkit',
'meh',
'meh-blank',
'meh-rolling-eyes',
'memory',
'menorah',
'mercury',
'meteor',
'microchip',
'microphone',
'microphone-alt',
'microphone-alt-slash',
'microphone-slash',
'microscope',
'minus',
'minus-circle',
'minus-square',
'mitten',
'mobile',
'mobile-alt',
'money-bill',
'money-bill-alt',
'money-bill-wave',
'money-bill-wave-alt',
'money-check',
'money-check-alt',
'monument',
'moon',
'mortar-pestle',
'mosque',
'motorcycle',
'mountain',
'mouse',
'mouse-pointer',
'mug-hot',
'music',
'network-wired',
'neuter',
'newspaper',
'not-equal',
'notes-medical',
'object-group',
'object-ungroup',
'oil-can',
'om',
'otter',
'outdent',
'pager',
'paint-brush',
'paint-roller',
'palette',
'pallet',
'paper-plane',
'paperclip',
'parachute-box',
'paragraph',
'parking',
'passport',
'pastafarianism',
'paste',
'pause',
'pause-circle',
'paw',
'peace',
'pen',
'pen-alt',
'pen-fancy',
'pen-nib',
'pen-square',
'pencil-alt',
'pencil-ruler',
'people-carry',
'pepper-hot',
'percent',
'percentage',
'person-booth',
'phone',
'phone-alt',
'phone-slash',
'phone-square',
'phone-square-alt',
'phone-volume',
'photo-video',
'piggy-bank',
'pills',
'pizza-slice',
'place-of-worship',
'plane',
'plane-arrival',
'plane-departure',
'play',
'play-circle',
'plug',
'plus',
'plus-circle',
'plus-square',
'podcast',
'poll',
'poll-h',
'poo',
'poo-storm',
'poop',
'portrait',
'pound-sign',
'power-off',
'pray',
'praying-hands',
'prescription',
'prescription-bottle',
'prescription-bottle-alt',
'print',
'procedures',
'project-diagram',
'puzzle-piece',
'qrcode',
'question',
'question-circle',
'quidditch',
'quote-left',
'quote-right',
'quran',
'radiation',
'radiation-alt',
'rainbow',
'random',
'receipt',
'record-vinyl',
'recycle',
'redo',
'redo-alt',
'registered',
'remove-format',
'reply',
'reply-all',
'republican',
'restroom',
'retweet',
'ribbon',
'ring',
'road',
'robot',
'rocket',
'route',
'rss',
'rss-square',
'ruble-sign',
'ruler',
'ruler-combined',
'ruler-horizontal',
'ruler-vertical',
'running',
'rupee-sign',
'sad-cry',
'sad-tear',
'satellite',
'satellite-dish',
'save',
'school',
'screwdriver',
'scroll',
'sd-card',
'search',
'search-dollar',
'search-location',
'search-minus',
'search-plus',
'seedling',
'server',
'shapes',
'share',
'share-alt',
'share-alt-square',
'share-square',
'shekel-sign',
'shield-alt',
'ship',
'shipping-fast',
'shoe-prints',
'shopping-bag',
'shopping-basket',
'shopping-cart',
'shower',
'shuttle-van',
'sign',
'sign-in-alt',
'sign-language',
'sign-out-alt',
'signal',
'signature',
'sim-card',
'sitemap',
'skating',
'skiing',
'skiing-nordic',
'skull',
'skull-crossbones',
'slash',
'sleigh',
'sliders-h',
'smile',
'smile-beam',
'smile-wink',
'smog',
'smoking',
'smoking-ban',
'sms',
'snowboarding',
'snowflake',
'snowman',
'snowplow',
'socks',
'solar-panel',
'sort',
'sort-alpha-down',
'sort-alpha-down-alt',
'sort-alpha-up',
'sort-alpha-up-alt',
'sort-amount-down',
'sort-amount-down-alt',
'sort-amount-up',
'sort-amount-up-alt',
'sort-down',
'sort-numeric-down',
'sort-numeric-down-alt',
'sort-numeric-up',
'sort-numeric-up-alt',
'sort-up',
'spa',
'space-shuttle',
'spell-check',
'spider',
'spinner',
'splotch',
'spray-can',
'square',
'square-full',
'square-root-alt',
'stamp',
'star',
'star-and-crescent',
'star-half',
'star-half-alt',
'star-of-david',
'star-of-life',
'step-backward',
'step-forward',
'stethoscope',
'sticky-note',
'stop',
'stop-circle',
'stopwatch',
'store',
'store-alt',
'stream',
'street-view',
'strikethrough',
'stroopwafel',
'subscript',
'subway',
'suitcase',
'suitcase-rolling',
'sun',
'superscript',
'surprise',
'swatchbook',
'swimmer',
'swimming-pool',
'synagogue',
'sync',
'sync-alt',
'syringe',
'table',
'table-tennis',
'tablet',
'tablet-alt',
'tablets',
'tachometer-alt',
'tag',
'tags',
'tape',
'tasks',
'taxi',
'teeth',
'teeth-open',
'temperature-high',
'temperature-low',
'tenge',
'terminal',
'text-height',
'text-width',
'th',
'th-large',
'th-list',
'theater-masks',
'thermometer',
'thermometer-empty',
'thermometer-full',
'thermometer-half',
'thermometer-quarter',
'thermometer-three-quarters',
'thumbs-down',
'thumbs-up',
'thumbtack',
'ticket-alt',
'times',
'times-circle',
'tint',
'tint-slash',
'tired',
'toggle-off',
'toggle-on',
'toilet',
'toilet-paper',
'toolbox',
'tools',
'tooth',
'torah',
'torii-gate',
'tractor',
'trademark',
'traffic-light',
'trailer',
'train',
'tram',
'transgender',
'transgender-alt',
'trash',
'trash-alt',
'trash-restore',
'trash-restore-alt',
'tree',
'trophy',
'truck',
'truck-loading',
'truck-monster',
'truck-moving',
'truck-pickup',
'tshirt',
'tty',
'tv',
'umbrella',
'umbrella-beach',
'underline',
'undo',
'undo-alt',
'universal-access',
'university',
'unlink',
'unlock',
'unlock-alt',
'upload',
'user',
'user-alt',
'user-alt-slash',
'user-astronaut',
'user-check',
'user-circle',
'user-clock',
'user-cog',
'user-edit',
'user-friends',
'user-graduate',
'user-injured',
'user-lock',
'user-md',
'user-minus',
'user-ninja',
'user-nurse',
'user-plus',
'user-secret',
'user-shield',
'user-slash',
'user-tag',
'user-tie',
'user-times',
'users',
'users-cog',
'utensil-spoon',
'utensils',
'vector-square',
'venus',
'venus-double',
'venus-mars',
'vial',
'vials',
'video',
'video-slash',
'vihara',
'voicemail',
'volleyball-ball',
'volume-down',
'volume-mute',
'volume-off',
'volume-up',
'vote-yea',
'vr-cardboard',
'walking',
'wallet',
'warehouse',
'water',
'wave-square',
'weight',
'weight-hanging',
'wheelchair',
'wifi',
'wind',
'window-close',
'window-maximize',
'window-minimize',
'window-restore',
'wine-bottle',
'wine-glass',
'wine-glass-alt',
'won-sign',
'wrench',
'x-ray',
'yen-sign',
'yin-yang', ];
$tbicons = [];
foreach ($icons as $value) {
$tbicons[$value] = 'fas fa-'.$value;
}
// Liste d'exemples d'icônes Font Awesome
return $tbicons;
}
}

88
src/Form/UserType.php Normal file
View File

@ -0,0 +1,88 @@
<?php
namespace App\Form;
use App\Entity\Project;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Regex;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('submit', SubmitType::class, [
'label' => 'Valider',
'attr' => ['class' => 'btn btn-success no-print'],
])
->add('username', TextType::class, [
'label' => 'Login',
])
->add('apikey', TextType::class, [
'label' => 'apikey',
'required' => false,
])
->add('avatar', HiddenType::class)
->add('email', EmailType::class, [
'label' => 'Email',
]);
if ('profil' != $options['mode']) {
$builder
->add('roles', ChoiceType::class, [
'choices' => ['ROLE_ADMIN' => 'ROLE_ADMIN', 'ROLE_MASTER' => 'ROLE_MASTER', 'ROLE_USER' => 'ROLE_USER'],
'multiple' => true,
'expanded' => true,
])
->add('projects', EntityType::class, [
'label' => 'Projets',
'class' => Project::class,
'choice_label' => 'title',
'multiple' => true,
'attr' => ['class' => 'select2'],
]);
}
if ('SQL' === $options['modeAuth']) {
$builder
->add('password', RepeatedType::class, [
'type' => PasswordType::class,
'required' => ('submit' == $options['mode'] ? true : false),
'options' => ['always_empty' => true],
'first_options' => ['label' => 'Mot de Passe', 'attr' => ['class' => 'form-control', 'style' => 'margin-bottom:15px', 'autocomplete' => 'new-password']],
'second_options' => ['label' => 'Confirmer Mot de Passe', 'attr' => ['class' => 'form-control', 'style' => 'margin-bottom:15px']],
'constraints' => [
new Regex([
'pattern' => '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\W).{8,}$/',
'message' => 'Le mot de passe doit contenir au moins 8 caractères, une lettre majuscule, une lettre minuscule et un caractère spécial.',
]),
],
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'mode' => 'submit',
'modeAuth' => 'SQL',
]);
}
}

11
src/Kernel.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

0
src/Repository/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,18 @@
<?php
namespace App\Repository;
use App\Entity\Project;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Project>
*/
class ProjectRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Project::class);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
}
$user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class CasUserProvider implements UserProviderInterface
{
private UserRepository $userRepository;
private EntityManagerInterface $em;
private ParameterBagInterface $parameterBag;
public function __construct(UserRepository $userRepository, EntityManagerInterface $em, ParameterBagInterface $parameterBag)
{
$this->userRepository = $userRepository;
$this->em = $em;
$this->parameterBag = $parameterBag;
}
/**
* Charge un utilisateur par son identifiant CAS.
*/
public function loadUserByIdentifier(string $identifier): UserInterface
{
// Récupérer l'utilisateur depuis la base de données
$user = $this->userRepository->findOneBy(['username' => $identifier]);
if (!$user) {
throw new UserNotFoundException(sprintf('User with username "%s" not found.', $identifier));
}
return $user;
}
/**
* Charge un utilisateur par son identifiant CAS et autosubmit autoupdate en fonction des attributs CAS
*/
public function loadUserByIdentifierAndAttributes(string $identifier, array $attributes): UserInterface
{
// Charger l'utilisateur existant depuis la base de données
$user = $this->userRepository->findOneBy(['username' => $identifier]);
if (!$user) {
// Créer un nouvel utilisateur avec les attributs CAS
$user = new User();
$user->setUsername($identifier);
$user->setPassword(Uuid::uuid4()->toString());
$user->setRoles(['ROLE_USER']);
// Persister l'utilisateur en base si nécessaire
$this->em->persist($user);
}
$user->setEmail($attributes[$this->parameterBag->get('casMail')] ?? null);
$this->em->flush();
return $user;
}
/**
* Permet de recharger un utilisateur déjà authentifié (si nécessaire).
*/
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new \InvalidArgumentException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
// Recharge l'utilisateur en base si nécessaire (ex. : rôles mis à jour)
return $this->userRepository->find($user->getId());
}
/**
* Indique si ce provider supporte un type d'utilisateur donné.
*/
public function supportsClass(string $class): bool
{
return User::class === $class;
}
}

View File

@ -0,0 +1,130 @@
<?php
namespace App\Security;
use App\Repository\UserRepository;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
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\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class DynamicAuthenticator extends AbstractAuthenticator
{
private string $modeAuth;
private UserRepository $userRepository;
private CasUserProvider $casUserProvider;
private ParameterBagInterface $parameterBag;
public function __construct(string $modeAuth, UserRepository $userRepository, CasUserProvider $casUserProvider, ParameterBagInterface $parameterBag)
{
$this->modeAuth = $modeAuth;
$this->userRepository = $userRepository;
$this->casUserProvider = $casUserProvider;
$this->parameterBag = $parameterBag;
}
public function supports(Request $request): ?bool
{
// Vérifie si l'utilisateur est déjà connecté
if ($request->getSession()->get('_security_main')) {
return false; // L'utilisateur est déjà authentifié
}
// Exclure les routes de login et logout pour éviter les boucles
$currentPath = $request->getPathInfo();
if (in_array($currentPath, ['/login', '/logout'])) {
return false;
}
return true;
}
public function authenticate(Request $request): Passport
{
switch ($this->modeAuth) {
case 'SQL':
return $this->authenticateWithSql($request);
case 'CAS':
return $this->authenticateWithCas($request);
default:
throw new \InvalidArgumentException('Invalid authentication method');
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
throw $exception;
}
private function authenticateWithSql(Request $request): Passport
{
$username = $request->request->get('_username', '');
$password = $request->request->get('_password', '');
if (!$username || !$password) {
throw new AuthenticationException('Username and password are required.');
}
// Charger l'utilisateur via Doctrine
$user = $this->userRepository->findOneBy(['username' => $username]);
if (!$user) {
throw new AuthenticationException('User not found.');
}
return new Passport(
new UserBadge($username, function ($userIdentifier) {
return $this->userRepository->findOneBy(['username' => $userIdentifier]);
}),
new PasswordCredentials($password)
);
}
private function authenticateWithCas(Request $request): Passport
{
// Récupérer l'hôte d'origine derrière le reverse proxy
$host = $request->headers->get('host');
$scheme = $request->headers->get('X-Forwarded-Proto') ?? $request->getScheme();
// Construire l'URL
$url = $scheme.'://'.$host;
// \phpCAS::setDebug('/tmp/logcas.log');
\phpCAS::client(
CAS_VERSION_2_0,
$this->parameterBag->get('casHost'),
(int) $this->parameterBag->get('casPort'),
$this->parameterBag->get('casPath'),
$url,
false);
\phpCAS::setNoCasServerValidation();
\phpCAS::forceAuthentication();
$username = \phpCAS::getUser();
$attributes = \phpCAS::getAttributes();
if (!$username) {
throw new AuthenticationException('CAS authentication failed.');
}
$userBadge = new UserBadge($username, function ($userIdentifier) use ($attributes) {
return $this->casUserProvider->loadUserByIdentifierAndAttributes($userIdentifier, $attributes);
});
return new SelfValidatingPassport($userBadge);
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace App\Service;
class ImageService
{
// Hauteur d'une image
public function getHeight(string $image): int
{
$size = getimagesize($image);
$height = $size[1];
return $height;
}
// Largeur d'une imgage
public function getWidth(string $image): int
{
$size = getimagesize($image);
$width = $size[0];
return $width;
}
public function resizeImage(string $image, int $maxWidth, int $maxHeight): void
{
list($width, $height, $imageType) = getimagesize($image);
$imageType = image_type_to_mime_type($imageType);
// Définir le pourcentage de réduction de l'image
$scale = $maxHeight / $height;
if (($width * $scale) > $maxWidth) {
$scale = $maxWidth / $width;
}
// Création de l'image redimentionnée
$newImageWidth = (int) ceil($width * $scale);
$newImageHeight = (int) ceil($height * $scale);
$newImage = imagecreatetruecolor($newImageWidth, $newImageHeight);
switch ($imageType) {
case 'image/gif':
$source = imagecreatefromgif($image);
break;
case 'image/pjpeg':
case 'image/jpeg':
case 'image/jpg':
$source = imagecreatefromjpeg($image);
break;
case 'image/png':
case 'image/x-png':
$source = imagecreatefrompng($image);
break;
case 'image/webp':
$source = imagecreatefromwebp($image);
break;
default:
$source= imagecreatefromgif($image);
break;
}
imagecopyresampled($newImage, $source, 0, 0, 0, 0, $newImageWidth, $newImageHeight, $width, $height);
switch ($imageType) {
case 'image/gif':
imagegif($newImage, $image);
break;
case 'image/pjpeg':
case 'image/jpeg':
case 'image/jpg':
imagejpeg($newImage, $image, 90);
break;
case 'image/png':
case 'image/x-png':
imagepng($newImage, $image);
break;
case 'image/webp':
imagewebp($newImage, $image, 90);
}
}
public function cropImage(string $image, string $thumb, int $x, int $y, int $cropWidth, int $cropHeight, int $maxWidth, int $maxHeight): void
{
list($width, $height, $imageType) = getimagesize($image);
$imageType = image_type_to_mime_type($imageType);
// Définir le pourcentage de réduction de l'image
$scale = $maxHeight / $cropHeight;
if (($cropWidth * $scale) > $maxWidth) {
$scale = $maxWidth / $cropWidth;
}
// Création de l'image redimentionnée
$newImageWidth = (int) ceil($cropWidth * $scale);
$newImageHeight = (int) ceil($cropHeight * $scale);
$newImage = imagecreatetruecolor($newImageWidth, $newImageHeight);
switch ($imageType) {
case 'image/gif':
$source = imagecreatefromgif($image);
break;
case 'image/pjpeg':
case 'image/jpeg':
case 'image/jpg':
$source = imagecreatefromjpeg($image);
break;
case 'image/png':
case 'image/x-png':
$source = imagecreatefrompng($image);
break;
case 'image/webp':
$source = imagecreatefromwebp($image);
break;
default:
$source= imagecreatefromgif($image);
break;
}
imagecopyresampled($newImage, $source, 0, 0, $x, $y, $newImageWidth, $newImageHeight, $cropWidth, $cropHeight);
switch ($imageType) {
case 'image/gif':
imagegif($newImage, $thumb);
break;
case 'image/pjpeg':
case 'image/jpeg':
case 'image/jpg':
imagejpeg($newImage, $thumb, 90);
break;
case 'image/png':
case 'image/x-png':
imagepng($newImage, $thumb);
break;
case 'image/webp':
imagewebp($newImage, $thumb, 90);
}
}
}