feat: Implémente la fonctionnalité de réinitialisation du mot de passe
Ce commit introduit la fonctionnalité de réinitialisation du mot de passe pour les utilisateurs. Les modifications apportées sont les suivantes : - Ajout de l'entité `AccountResetPasswordRequest` pour gérer les requêtes de réinitialisation de mot de passe. - Ajout du repository `AccountResetPasswordRequestRepository` pour interagir avec l'entité `AccountResetPasswordRequest`. - Ajout du formulaire `RequestPasswordRequestType` pour permettre aux utilisateurs de demander une réinitialisation de mot de passe. - Ajout de l'événement `ResetPasswordEvent` pour déclencher le processus de réinitialisation du mot de passe. - Ajout de la route `/forgot-password` dans le `HomeController` pour gérer la demande de réinitialisation. - Création des templates twig `admin/forgot-password.twig` et `admin/base.twig` et `form_tailwind.twig` pour la gestion de l'affichage du formulaire et de la base de l'interface admin. - Modification des templates twig `admin/login.twig` pour ajouter un lien vers la page de réinitialisation de mot de passe. - Mise à jour du fichier `assets/app.scss` pour inclure des styles CSS personnalisés. - Ajout de tests unitaires pour l'entité, le repository et le formulaire. - Ajout de la configuration twig pour prendre en charge les formulaires avec tailwind - Ajout des règles d'exclusions sonar dans `sonar-project.properties`
This commit is contained in:
@@ -1,2 +1,25 @@
|
||||
@import "tailwindcss";
|
||||
@import url('https://fonts.googleapis.com/css2?family=DynaPuff:wght@400..700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap');
|
||||
|
||||
h1,h2,h3,h4,h5,h6,
|
||||
label,span,input,{
|
||||
font-family: 'Intel One Mono', monospace;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 1rem;
|
||||
}
|
||||
input {
|
||||
background: oklch(21% 0.034 264.665);
|
||||
color: white;
|
||||
&::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
form_themes:
|
||||
- 'form_tailwind.twig'
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
|
||||
@@ -25,7 +25,7 @@ sonar.coverage.exclusions= \
|
||||
src/Service/**/*.php, \
|
||||
src/Command/*.php
|
||||
|
||||
sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6
|
||||
sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6,e7,e8
|
||||
sonar.issue.ignore.multicriteria.e1.ruleKey=php:S103
|
||||
sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.php
|
||||
|
||||
@@ -43,3 +43,9 @@ sonar.issue.ignore.multicriteria.e5.resourceKey=**/*.php
|
||||
|
||||
sonar.issue.ignore.multicriteria.e6.ruleKey=javascript:S1128
|
||||
sonar.issue.ignore.multicriteria.e6.resourceKey=**/*.js
|
||||
|
||||
sonar.issue.ignore.multicriteria.e7.ruleKey=php:S117
|
||||
sonar.issue.ignore.multicriteria.e7.resourceKey=**/*.php
|
||||
|
||||
sonar.issue.ignore.multicriteria.e8.ruleKey=php:S116
|
||||
sonar.issue.ignore.multicriteria.e8.resourceKey=**/*.php
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Attribute\Mainframe;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
@@ -24,4 +29,19 @@ class HomeController extends AbstractController
|
||||
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/forgot-password',name: 'app_forgotpassword',methods: ['GET', 'POST'])]
|
||||
public function forgotPassword(EventDispatcherInterface $eventDispatcher,Request $request): Response
|
||||
{
|
||||
$requestPasswordResetEvent = new ResetPasswordEvent();
|
||||
$form = $this->createForm(RequestPasswordRequestType::class,$requestPasswordResetEvent);
|
||||
$form->handleRequest($request);
|
||||
if($form->isSubmitted() && $form->isValid()) {
|
||||
$eventDispatcher->dispatch($requestPasswordResetEvent);
|
||||
$this->addFlash('success', 'Si votre compte existe, vous recevrez un email pour réinitialiser votre mot de passe.');
|
||||
return $this->redirectToRoute('app_forgotpassword');
|
||||
}
|
||||
return $this->render('admin/forgot-password.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
82
src/Entity/AccountResetPasswordRequest.php
Normal file
82
src/Entity/AccountResetPasswordRequest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AccountResetPasswordRequestRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AccountResetPasswordRequestRepository::class)]
|
||||
class AccountResetPasswordRequest
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?Account $Account = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $token = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $requestedAt = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $expiresAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account
|
||||
{
|
||||
return $this->Account;
|
||||
}
|
||||
|
||||
public function setAccount(?Account $Account): static
|
||||
{
|
||||
$this->Account = $Account;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getToken(): ?string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setToken(?string $token): static
|
||||
{
|
||||
$this->token = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRequestedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->requestedAt;
|
||||
}
|
||||
|
||||
public function setRequestedAt(\DateTimeImmutable $requestedAt): static
|
||||
{
|
||||
$this->requestedAt = $requestedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExpiresAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->expiresAt;
|
||||
}
|
||||
|
||||
public function setExpiresAt(\DateTimeImmutable $expiresAt): static
|
||||
{
|
||||
$this->expiresAt = $expiresAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
25
src/Form/RequestPasswordRequestType.php
Normal file
25
src/Form/RequestPasswordRequestType.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class RequestPasswordRequestType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('email',EmailType::class,[
|
||||
'required' => true,
|
||||
'label' => 'Email de votre compte',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('data_class',ResetPasswordEvent::class);
|
||||
}
|
||||
}
|
||||
18
src/Repository/AccountResetPasswordRequestRepository.php
Normal file
18
src/Repository/AccountResetPasswordRequestRepository.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AccountResetPasswordRequest>
|
||||
*/
|
||||
class AccountResetPasswordRequestRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AccountResetPasswordRequest::class);
|
||||
}
|
||||
}
|
||||
24
src/Service/ResetPassword/Event/ResetPasswordEvent.php
Normal file
24
src/Service/ResetPassword/Event/ResetPasswordEvent.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace App\Service\ResetPassword\Event;
|
||||
|
||||
|
||||
class ResetPasswordEvent
|
||||
{
|
||||
private string $email;
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
*/
|
||||
public function setEmail(string $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
25
templates/admin/base.twig
Normal file
25
templates/admin/base.twig
Normal file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mainframe - {% block title %}{% endblock %}</title>
|
||||
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link rel="icon" type="image/png" href="{{ asset('favicon/favicon-96x96.png') }}" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon/favicon.svg') }}" />
|
||||
<link rel="shortcut icon" href="{{ asset('favicon/favicon.ico') }}" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('favicon/apple-touch-icon.png') }}" />
|
||||
<meta name="apple-mobile-web-app-title" content="Mainframe" />
|
||||
<link rel="manifest" href="{{ asset('favicon/site.webmanifest') }}" />
|
||||
{{ vite_asset('app.js',[]) }}
|
||||
</head>
|
||||
{# Changement ici : fond noir, texte blanc #}
|
||||
<body class="bg-gray-900 font-sans antialiased text-gray-100">
|
||||
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
39
templates/admin/forgot-password.twig
Normal file
39
templates/admin/forgot-password.twig
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends 'admin/base.twig' %}
|
||||
{% block title %}Mot de passe perdu{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
|
||||
<div class="text-center">
|
||||
<img class="mx-auto h-12 w-auto" src="{{ asset('assets/logo.png') | imagine_filter('webp') }}" alt="Logo Mainframe">
|
||||
|
||||
{# Changement ici : texte du titre blanc #}
|
||||
<h2 class="mt-6 text-3xl font-extrabold text-white">
|
||||
Mot de de passe perdu
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="w-full px-4 py-3 rounded-md text-sm font-medium
|
||||
{% if label == 'success' %}
|
||||
text-green-100 bg-green-600
|
||||
{% elseif label == 'error' %}
|
||||
text-red-100 bg-red-600
|
||||
{% else %}
|
||||
text-gray-100 bg-gray-600
|
||||
{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.email) }}
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 dark:bg-indigo-500 dark:hover:bg-indigo-600"
|
||||
>
|
||||
Valider
|
||||
</button>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,95 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mainframe - Connexion</title>
|
||||
{% extends 'admin/base.twig' %}
|
||||
{% block title %}Connexion{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
|
||||
<div class="text-center">
|
||||
<img class="mx-auto h-12 w-auto" src="{{ asset('assets/logo.png') | imagine_filter('webp') }}" alt="Logo Mainframe">
|
||||
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
{# Changement ici : texte du titre blanc #}
|
||||
<h2 class="mt-6 text-3xl font-extrabold text-white">
|
||||
Connectez-vous à votre compte
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<link rel="icon" type="image/png" href="{{ asset('favicon/favicon-96x96.png') }}" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon/favicon.svg') }}" />
|
||||
<link rel="shortcut icon" href="{{ asset('favicon/favicon.ico') }}" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('favicon/apple-touch-icon.png') }}" />
|
||||
<meta name="apple-mobile-web-app-title" content="Mainframe" />
|
||||
<link rel="manifest" href="{{ asset('favicon/site.webmanifest') }}" />
|
||||
{{ vite_asset('app.js',[]) }}
|
||||
</head>
|
||||
{# Changement ici : fond noir, texte blanc #}
|
||||
<body class="bg-gray-900 font-sans antialiased text-gray-100">
|
||||
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
{# Changement ici : fond du formulaire gris foncé, ombre plus subtile #}
|
||||
<div class="max-w-md w-full space-y-8 p-10 bg-gray-800 rounded-xl shadow-xl z-10">
|
||||
<div class="text-center">
|
||||
<img class="mx-auto h-12 w-auto" src="{{ asset('assets/logo.png') | imagine_filter('webp') }}" alt="Logo Mainframe">
|
||||
{% if error %}
|
||||
{# Changement ici : couleurs de l'alerte pour le thème sombre #}
|
||||
<div class="bg-red-900 border border-red-700 text-red-200 px-4 py-3 rounded relative" role="alert">
|
||||
<strong class="font-bold">Erreur de connexion !</strong>
|
||||
<span class="block sm:inline">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Changement ici : texte du titre blanc #}
|
||||
<h2 class="mt-6 text-3xl font-extrabold text-white">
|
||||
Connectez-vous à votre compte
|
||||
</h2>
|
||||
<form data-turbo="false" class="mt-8 space-y-6" action="{{ path('app_login') }}" method="POST">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">Utilisateur</label>
|
||||
{# Changement ici : champs de saisie pour le thème sombre #}
|
||||
<input id="username" name="_username" type="text" autocomplete="text" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Utilisateur" value="{{ last_username }}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">Mot de passe</label>
|
||||
{# Changement ici : champs de saisie pour le thème sombre #}
|
||||
<input id="password" name="_password" type="password" autocomplete="current-password" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Mot de passe">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
{# Changement ici : couleurs de l'alerte pour le thème sombre #}
|
||||
<div class="bg-red-900 border border-red-700 text-red-200 px-4 py-3 rounded relative" role="alert">
|
||||
<strong class="font-bold">Erreur de connexion !</strong>
|
||||
<span class="block sm:inline">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form data-turbo="false" class="mt-8 space-y-6" action="{{ path('app_login') }}" method="POST">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">Utilisateur</label>
|
||||
{# Changement ici : champs de saisie pour le thème sombre #}
|
||||
<input id="username" name="_username" type="text" autocomplete="text" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Utilisateur" value="{{ last_username }}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">Mot de passe</label>
|
||||
{# Changement ici : champs de saisie pour le thème sombre #}
|
||||
<input id="password" name="_password" type="password" autocomplete="current-password" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Mot de passe">
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
{# Changement ici : case à cocher pour le thème sombre #}
|
||||
<input id="remember_me" name="_remember_me" type="checkbox"
|
||||
class="h-4 w-4 text-indigo-500 focus:ring-indigo-500 border-gray-600 rounded bg-gray-700">
|
||||
{# Changement ici : texte du label blanc #}
|
||||
<label for="remember_me" class="ml-2 block text-sm text-gray-100">
|
||||
Se souvenir de moi
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
{# Changement ici : case à cocher pour le thème sombre #}
|
||||
<input id="remember_me" name="_remember_me" type="checkbox"
|
||||
class="h-4 w-4 text-indigo-500 focus:ring-indigo-500 border-gray-600 rounded bg-gray-700">
|
||||
{# Changement ici : texte du label blanc #}
|
||||
<label for="remember_me" class="ml-2 block text-sm text-gray-100">
|
||||
Se souvenir de moi
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
{# Changement ici : couleur du lien plus claire #}
|
||||
<a href="#" class="font-medium text-indigo-400 hover:text-indigo-300">
|
||||
Mot de passe oublié ?
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{# Changement ici : couleur du lien plus claire #}
|
||||
<a href="{{ path('app_forgotpassword') }}" class="font-medium text-indigo-400 hover:text-indigo-300">
|
||||
Mot de passe oublié ?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{# Le bouton reste indigo pour un bon contraste et une visibilité #}
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<div>
|
||||
{# Le bouton reste indigo pour un bon contraste et une visibilité #}
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
136
templates/form_tailwind.twig
Normal file
136
templates/form_tailwind.twig
Normal file
@@ -0,0 +1,136 @@
|
||||
{% use 'form_div_layout.html.twig' %}
|
||||
|
||||
{# ---------- FORM START / END ---------- #}
|
||||
{% block form_start -%}
|
||||
{{ parent() }}
|
||||
{%- endblock %}
|
||||
{% block form_end -%}
|
||||
{{ parent() }}
|
||||
{%- endblock %}
|
||||
|
||||
{# ---------- ROW ---------- #}
|
||||
{% block form_row %}
|
||||
<div class="mb-5">
|
||||
{{ form_label(form) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
{% if not compound and not form.vars.valid %}
|
||||
<p class="text-sm text-red-500 mt-1">{{ form_errors(form) }}</p>
|
||||
{% else %}
|
||||
{{ form_errors(form) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- LABEL ---------- #}
|
||||
{% block form_label %}
|
||||
{% if label is not same as(false) %}
|
||||
<label for="{{ id }}" class="block text-sm font-medium text-gray-200 dark:text-gray-300">
|
||||
{{ label|trans({}, translation_domain) }}
|
||||
{% if required %}
|
||||
<span class="text-red-400">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- ERRORS ---------- #}
|
||||
{% block form_errors %}
|
||||
{% if errors|length > 0 %}
|
||||
<ul class="mt-1 text-sm text-red-500">
|
||||
{% for error in errors %}
|
||||
<li>{{ error.message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- WIDGET DISPATCH ---------- #}
|
||||
{% block form_widget %}
|
||||
{% if compound %}
|
||||
{{ block('form_widget_compound') }}
|
||||
{% else %}
|
||||
{{ block('form_widget_simple') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- SIMPLE INPUTS (text, email, number...) ---------- #}
|
||||
{% block form_widget_simple %}
|
||||
{% set type = type|default('text') %}
|
||||
<input
|
||||
type="{{ type }}"
|
||||
{{ block('widget_attributes') }}
|
||||
value="{{ value }}"
|
||||
class="form-input mt-1 block w-full px-3 py-2 bg-gray-800 border border-gray-700 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- TEXTAREA ---------- #}
|
||||
{% block textarea_widget %}
|
||||
<textarea
|
||||
{{ block('widget_attributes') }}
|
||||
class="form-textarea mt-1 block w-full px-3 py-2 bg-gray-800 border border-gray-700 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
>{{ value }}</textarea>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- SELECT ---------- #}
|
||||
{% block choice_widget_collapsed %}
|
||||
<select
|
||||
{{ block('widget_attributes') }}
|
||||
class="form-select mt-1 block w-full px-3 py-2 bg-gray-800 border border-gray-700 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
>
|
||||
{% if placeholder is not none %}
|
||||
<option value="" {% if required and value is empty %}selected{% endif %}>
|
||||
{{ placeholder != '' ? (placeholder|trans({}, translation_domain)) : '' }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% for group_label, choice in choices %}
|
||||
{% if choice is iterable %}
|
||||
<optgroup label="{{ group_label|trans({}, translation_domain) }}">
|
||||
{% for nested_choice in choice %}
|
||||
<option value="{{ nested_choice.value }}" {% if nested_choice is selectedchoice(value) %}selected{% endif %}>
|
||||
{{ nested_choice.label|trans({}, translation_domain) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% else %}
|
||||
<option value="{{ choice.value }}" {% if choice is selectedchoice(value) %}selected{% endif %}>
|
||||
{{ choice.label|trans({}, translation_domain) }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- CHECKBOX ---------- #}
|
||||
{% block checkbox_widget %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox"
|
||||
{{ block('widget_attributes') }}
|
||||
{% if value not in ['', null] %} value="{{ value }}"{% endif %}
|
||||
{% if checked %}checked="checked"{% endif %}
|
||||
class="form-checkbox h-5 w-5 text-indigo-500 bg-gray-800 border-gray-700 rounded focus:ring-indigo-500">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- RADIO ---------- #}
|
||||
{% block radio_widget %}
|
||||
<input type="radio"
|
||||
{{ block('widget_attributes') }}
|
||||
value="{{ value }}"
|
||||
{% if checked %}checked="checked"{% endif %}
|
||||
class="form-radio h-5 w-5 text-indigo-500 bg-gray-800 border-gray-700 focus:ring-indigo-500">
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- FILE ---------- #}
|
||||
{% block file_widget %}
|
||||
<input type="file"
|
||||
{{ block('widget_attributes') }}
|
||||
class="block w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-indigo-600 file:text-white
|
||||
hover:file:bg-indigo-700
|
||||
bg-gray-800 border border-gray-700 rounded-md">
|
||||
{% endblock %}
|
||||
37
tests/Entity/AccountResetPasswordRequestTest.php
Normal file
37
tests/Entity/AccountResetPasswordRequestTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
class AccountResetPasswordRequestTest extends TestCase
|
||||
{
|
||||
public function testEntitySettersAndGetters(): void
|
||||
{
|
||||
$account = new Account(); // Ou crée un mock si la classe est complexe
|
||||
$token = 'reset-token-example';
|
||||
$now = new \DateTimeImmutable();
|
||||
$expires = $now->modify('+1 hour');
|
||||
|
||||
$resetRequest = new AccountResetPasswordRequest();
|
||||
$resetRequest->setAccount($account);
|
||||
$resetRequest->setToken($token);
|
||||
$resetRequest->setRequestedAt($now);
|
||||
$resetRequest->setExpiresAt($expires);
|
||||
|
||||
$refClass = new ReflectionClass($resetRequest);
|
||||
$idProp = $refClass->getProperty('id');
|
||||
$idProp->setAccessible(true);
|
||||
$idProp->setValue($resetRequest, 123);
|
||||
|
||||
$this->assertSame($account, $resetRequest->getAccount());
|
||||
$this->assertSame($token, $resetRequest->getToken());
|
||||
$this->assertSame($now, $resetRequest->getRequestedAt());
|
||||
$this->assertSame($expires, $resetRequest->getExpiresAt());
|
||||
$this->assertSame(123, $resetRequest->getId());
|
||||
|
||||
}
|
||||
}
|
||||
51
tests/Form/RequestPasswordRequestTypeTest.php
Normal file
51
tests/Form/RequestPasswordRequestTypeTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Form;
|
||||
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Symfony\Component\Form\Test\TypeTestCase;
|
||||
|
||||
class RequestPasswordRequestTypeTest extends TypeTestCase
|
||||
{
|
||||
public function testSubmitValidData(): void
|
||||
{
|
||||
$formData = [
|
||||
'email' => 'user@example.com',
|
||||
];
|
||||
|
||||
$model = new ResetPasswordEvent(); // l'objet lié au formulaire
|
||||
$form = $this->factory->create(RequestPasswordRequestType::class, $model);
|
||||
|
||||
// Soumission du formulaire avec des données valides
|
||||
$form->submit($formData);
|
||||
|
||||
$this->assertTrue($form->isSynchronized());
|
||||
$this->assertTrue($form->isValid());
|
||||
|
||||
$expected = new ResetPasswordEvent();
|
||||
$expected->setEmail('user@example.com'); // Assure-toi que la méthode setEmail existe
|
||||
|
||||
$this->assertEquals($expected, $model);
|
||||
|
||||
// Vérification de la création des champs dans le formulaire
|
||||
$view = $form->createView();
|
||||
$children = $view->children;
|
||||
|
||||
$this->assertArrayHasKey('email', $children);
|
||||
}
|
||||
|
||||
public function testSubmitInvalidEmail(): void
|
||||
{
|
||||
$formData = [
|
||||
'email' => 'not-an-email',
|
||||
];
|
||||
|
||||
$model = new ResetPasswordEvent();
|
||||
$form = $this->factory->create(RequestPasswordRequestType::class, $model);
|
||||
|
||||
$form->submit($formData);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Repository\AccountResetPasswordRequestRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class AccountResetPasswordRequestRepositoryTest extends KernelTestCase
|
||||
{
|
||||
private ?EntityManagerInterface $entityManager;
|
||||
private ?AccountResetPasswordRequestRepository $accountResetPasswordRequestRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->entityManager = self::getContainer()->get('doctrine')->getManager();
|
||||
$this->accountResetPasswordRequestRepository = $this->entityManager->getRepository(AccountResetPasswordRequest::class);
|
||||
}
|
||||
|
||||
public function testRepositoryExistsAndIsCorrectInstance(): void
|
||||
{
|
||||
$this->assertInstanceOf(AccountResetPasswordRequestRepository::class, $this->accountResetPasswordRequestRepository);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
$this->entityManager->close();
|
||||
$this->entityManager = null; // Avoid memory leaks
|
||||
$this->accountResetPasswordRequestRepository = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user