Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions sources/AppBundle/Association/Form/LoginType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace AppBundle\Association\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
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;

/**
* Le POST est intercepté par le firewall (cf. legacy_secured_area dans config/packages/security.yaml) : ce
* formulaire n'est jamais soumis à Symfony Forms, il n'est là que pour le rendu. Les noms des champs sont donc
* un contrat avec la configuration du firewall, qui reste inchangée : d'où le préfixe vide (les champs sont
* postés à plat, sans préfixe de formulaire) et le jeton CSRF nommé et identifié comme l'attend le firewall.
*
* @extends AbstractType<array<string, mixed>>
*/
class LoginType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('utilisateur', TextType::class, [
'label' => "Email ou nom d'utilisateur",
])
->add('mot_de_passe', PasswordType::class, [
'label' => 'Mot de passe',
])
->add('_target_path', HiddenType::class)
->add('submit', SubmitType::class, ['label' => 'Se connecter'])
;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_field_name' => '_csrf_token',
'csrf_token_id' => 'authenticate',
]);
}

public function getBlockPrefix(): string
{
return '';
}
}
19 changes: 14 additions & 5 deletions sources/AppBundle/Controller/Auth/LoginAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

namespace AppBundle\Controller\Auth;

use AppBundle\Association\Form\LoginType;
use AppBundle\Twig\ViewRenderer;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

final readonly class LoginAction
{
public function __construct(
private AuthenticationUtils $authenticationUtils,
private FormFactoryInterface $formFactory,
private UrlGeneratorInterface $urlGenerator,
private ViewRenderer $view,
) {}

Expand All @@ -29,13 +34,17 @@ public function __invoke(Request $request): Response
$noDomain = parse_url($targetUri, PHP_URL_HOST) === null;
$targetPath = $targetUri !== $actualUrl && $noDomain ? $targetUri : null;

// Le formulaire n'est pas soumis à Symfony : c'est le firewall qui intercepte le POST sur cette même URL.
$form = $this->formFactory->create(LoginType::class, [
'utilisateur' => $lastUsername,
'_target_path' => $targetPath,
], [
'action' => $this->urlGenerator->generate('app_login'),
]);

return $this->view->render('site/auth/login.html.twig', [
'last_username' => $lastUsername,
'form' => $form->createView(),
'error' => $error,
'target_path' => $targetPath,
'title' => 'Connexion',
'page' => 'connexion',
'class' => 'panel-page',
]);
}
}
57 changes: 26 additions & 31 deletions templates/site/auth/login.html.twig
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
{% extends 'layouts/site.html.twig' %}
{% form_theme form 'form_themes/tailwind.html.twig' %}

{% block content %}

<div class="container mx-auto flex flex-col items-center px-4 my-5 gap-4">
<h1 class="font-titre font-normal text-5xl text-afup-800 text-center">{{ "Connexion"|trans }}</h1>

<form method="post" novalidate="novalidate">
{% block title %}Connexion - AFUP{% endblock %}

<twig:Card class="flex flex-col gap-8 px-6 py-12 sm:px-28 sm:py-12 my-5 sm:mb-10 w-full sm:w-200">
{% block content %}

<div class="container mx-auto flex flex-col items-center px-4 my-5 gap-4">
<h1 class="font-titre text-5xl text-afup-800 text-center">{{ "Connexion"|trans }}</h1>

{% if error is not null %}
<twig:Alert type="error" class="w-full">{{ error.messageKey|trans(error.messageData, 'security') }}</twig:Alert>
{% endif %}
{{ form_start(form, {attr: {class: 'w-full sm:w-200'}}) }}

<twig:Card class="flex flex-col gap-8 px-6 py-12 sm:px-28 sm:py-12 my-5 sm:mb-10 w-full">

<div class="flex w-full flex-col gap-1.5">
<label for="utilisateur" class="flex items-start gap-1 text-sm leading-4 font-medium text-neutre-700">Email ou nom d'utilisateur</label>
<input type="text" id="utilisateur" name="utilisateur" value="{{ last_username }}" required="required" class="w-full rounded-lg border-2 bg-white px-3 text-sm text-gray-700 placeholder:text-neutre-400 focus:border-afup-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-neutre-200 disabled:text-neutre-400 border-neutre-300 h-10">
</div>
{% if error is not null %}
<twig:Alert type="error" class="w-full">{{ error.messageKey|trans(error.messageData, 'security') }}</twig:Alert>
{% endif %}

<div class="flex w-full flex-col gap-1.5">
<div class="flex items-center justify-between">
<label for="mot_de_passe" class="flex items-start gap-1 text-sm leading-4 font-medium text-neutre-700">Mot de passe</label>
<a href="{{ url('app_password_reset') }}" class="text-xs text-afup-500 underline hover:no-underline">Mot de passe perdu ?</a>
{{ form_row(form.utilisateur) }}

<div class="flex w-full flex-col gap-1.5">
<div class="flex items-center justify-between">
{{ form_label(form.mot_de_passe) }}
<a href="{{ path('app_password_reset') }}" class="text-xs text-afup-500 underline hover:no-underline">Mot de passe perdu ?</a>
</div>
{{ form_widget(form.mot_de_passe) }}
{{ form_errors(form.mot_de_passe) }}
</div>
<input type="password" id="mot_de_passe" name="mot_de_passe" required="required" class="w-full rounded-lg border-2 bg-white px-3 text-sm text-gray-700 placeholder:text-neutre-400 focus:border-afup-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-neutre-200 disabled:text-neutre-400 border-neutre-300 h-10">
</div>

<input type="hidden" name="connexion" value="1" />
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
{% if target_path is not null %}
<input type="hidden" name="_target_path" value="{{ target_path }}" />
{% endif %}
<div class="flex flex-col-reverse sm:flex-row items-center justify-between gap-4 border-t-2 border-neutre-300 pt-5">
<p class="text-sm text-neutre-700">Pas encore de compte ? <a href="{{ path('become_member') }}" class="text-afup-500 underline hover:no-underline">Adhésion à l'AFUP</a></p>
{{ form_widget(form.submit) }}
</div>

</twig:Card>

<div class="flex flex-col-reverse sm:flex-row items-center justify-between gap-4 border-t-2 border-neutre-300 pt-5">
<p class="font-sans font-normal text-sm text-neutre-700">Pas encore de compte ? <a href="{{ url('become_member') }}" class="text-afup-500 underline hover:no-underline">Adhésion à l'AFUP</a></p>
<button class="items-center justify-center rounded-lg border border-transparent bg-clip-border whitespace-nowrap bg-ruby-500 text-white hover:bg-ruby-700 px-8 py-3 text-base font-semibold" type="submit" name="soumettre">Se connecter</button>
</div>
{{ form_end(form) }}
</div>

</twig:Card>
</form>
{% endblock %}
Loading