Dans cette partie du développement de notre boutique, nous allons intégrer les formulaires d'authentification. Bien que Laravel propose des kits d'authentification prêts à l'emploi, je préfère adopter une approche sur mesure. Cette décision nous permet de créer un système d'authentification parfaitement adapté à nos besoins spécifiques, tout en offrant une flexibilité maximale.
Le code complet à la fin de cet article peut être téléchargé ici.
Créer un compte
Un email de confirmation
Il faut prévoir l'envoi d'un email à l'utilisateur pour confirmer que l'inscription s'est bien déroulée.
La classe
On crée la classe qui va gérer l'envoi de l'email :
php artisan make:mail UserRegistered

Avec ce code :
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Mail\Mailables\Address;
use App\Models\Shop;
class UserRegistered extends Mailable
{
use Queueable, SerializesModels;
Public Shop $shop;
public function __construct()
{
$this->shop = Shop::firstOrFail();
}
public function envelope(): Envelope
{
return new Envelope(
from: new Address($this->shop->email, $this->shop->name),
subject: trans('Vous êtes bien enregistré'),
);
}
public function content(): Content
{
return new Content(
view: 'mails.registered',
);
}
public function attachments(): array
{
return [];
}
}
On transmet les données de la boutique pour les renseignements essentiels à donner au nouvel inscrit.
La vue
On doit créer la vue associée à cet email :

Comme le code est assez volumineux, je vous propose d'aller chercher le fichier ZIP. Pour créer ce fichier, je me suis appuyé sur l'excellent site Beefree. Dans la version gratuite, on peut créer dix emails, et faire six exportations chaque mois. On peut ainsi créer le design et, après exportation, apporter les modifications nécessaires pour adapter l'email à nos données.
D'autre part, pour vos essais, vous aurez besoin d'une plateforme pour recevoir les emails. J'aime bien utiliser Mailtrap. Avec un compte gratuit, on a droit à 1000 emails par mois, ce qui est assez généreux. Les fonctionnalités sont nombreuses et faciles à utiliser. pour que ça fonctionne, vous devez entrer les bons paramètres dans le fichier .env.
MAIL_MAILER=smtp
MAIL_SCHEME=null
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=445ccd3e72a7b2
MAIL_PASSWORD=******************
Vous trouvez votre identifiant et votre mot de passe dans votre boîte de réception sur le site. Ainsi, tous les emails qui partent de votre projet arrivent dans cette boîte de réception.
Le composant pour la création d'un compte
On crée le composant pour la création d'un compte :
php artisan make:livewire pages::auth.register

Et voilà le code :
<?php
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Title;
use Livewire\Component;
use App\Mail\UserRegistered;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Rules\Password;
use Illuminate\Support\Str;
new #[Title('Inscription')]
class extends Component {
public string $firstname = '';
public string $name = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public bool $newsletter = false;
public ?string $generatedPassword = null;
public ?string $gender = null;
/**
* Enregistre un nouvel utilisateur dans la base de données.
*/
public function register()
{
if ($this->gender) {
abort(403);
}
$data = $this->validate([
'firstname' => 'required|string|max:255',
'name' => 'required|string|max:255',
'newsletter' => 'nullable|boolean',
'email' => 'required|string|email|unique:users',
'password' => [
'required',
'confirmed',
Password::min(8)
->mixedCase() // Majuscules & Minuscules
->numbers() // Chiffres
->symbols() // Caractères spéciaux
->uncompromised(), // Vérifie via "Have I Been Pwned" si le mot de passe a fuité sur le web
],
'password_confirmation' => 'required',
]);
$data['password'] = Hash::make($data['password']);
$user = User::create($data);
auth()->login($user);
request()->session()->regenerate();
Mail::to($user)->send(new UserRegistered());
session()->flash('registered', __('Votre compte a été créé avec succès. Un e-mail de confirmation vous a été envoyé.'));
return redirect('/');
}
/**
* Génère un mot de passe aléatoire et le remplit dans les champs de mot de passe.
*/
public function generatePassword(): void
{
$this->generatedPassword = Str::password(14);
$this->password = $this->generatedPassword;
$this->password_confirmation = $this->generatedPassword;
}
}; ?>
<div class="flex items-center justify-center min-h-[calc(100vh-16rem)] py-12 px-4">
<div class="w-full max-w-xl">
<x-card
title="{{ __('Créer un compte') }}"
subtitle="{{ __('Renseignez vos informations pour passer une commande sur notre boutique') }}"
class="shadow-xl bg-base-100"
separator
progress-indicator
>
<x-form wire:submit="register" x-data="{ rgpd: false }" class="space-y-4">
{{-- Champ Honeypot Anti-Bot --}}
<div class="hidden">
<x-input wire:model="gender" type="text" />
</div>
{{-- Prénom & Nom sur la même ligne en Desktop --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<x-input
label="{{ __('Prénom') }}"
wire:model="firstname"
icon="o-user"
required
/>
<x-input
label="{{ __('Nom') }}"
wire:model="name"
icon="o-user"
required
/>
</div>
<x-input
label="{{ __('Adresse e-mail') }}"
wire:model="email"
icon="o-envelope"
type="email"
required
/>
<div class="space-y-3">
<x-password
label="{{ __('Mot de passe') }}"
wire:model="password"
password-icon="o-key"
hint="{{ __('8 caractères min., majuscule, minuscule, chiffre et symbole.') }}"
clearable
required
/>
<x-password
label="{{ __('Confirmer le mot de passe') }}"
wire:model="password_confirmation"
password-icon="o-key"
clearable
required
/>
<div class="flex justify-end">
<x-button
type="button"
label="{{ __('Générer un mot de passe sécurisé') }}"
wire:click="generatePassword"
icon="o-wrench"
class="btn-ghost btn-xs text-primary"
/>
</div>
{{-- Alerte d'affichage du mot de passe généré --}}
@if($generatedPassword)
<div
x-data="{ copied: false }"
class="alert alert-warning shadow-sm mt-3 flex items-center justify-between p-3 rounded-xl"
>
<div class="flex items-center gap-3">
<x-icon name="o-key" class="w-5 h-5 shrink-0" />
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span class="font-semibold text-xs text-base-content whitespace-nowrap">
{{ __('Mot de passe généré :') }}
</span>
<code class="font-mono bg-base-100 px-2.5 py-1 rounded-md text-sm font-bold text-base-content select-all tracking-wider border border-base-300">
{{ $generatedPassword }}
</code>
</div>
</div>
{{-- Bouton Copier en HTML/DaisyUI pur pour éviter tout bug avec Livewire/MaryUI --}}
<button
type="button"
class="btn btn-xs gap-1 transition-all"
:class="copied ? 'btn-success text-white' : 'btn-ghost bg-base-100/60 hover:bg-base-100 text-base-content'"
@click="
navigator.clipboard.writeText('{{ $generatedPassword }}');
copied = true;
setTimeout(() => copied = false, 2000);
"
>
<template x-if="!copied">
<span class="flex items-center gap-1">
<x-icon name="o-document-duplicate" class="w-3.5 h-3.5" />
<span>{{ __('Copier') }}</span>
</span>
</template>
<template x-if="copied">
<span class="flex items-center gap-1">
<x-icon name="o-check" class="w-3.5 h-3.5" />
<span>{{ __('Copié !') }}</span>
</span>
</template>
</button>
</div>
@endif
</div>
<div class="divider my-2"></div>
<div class="space-y-2">
<x-checkbox
label="{{ __('Je souhaite recevoir la newsletter') }}"
wire:model="newsletter"
class="checkbox-primary checkbox-sm"
/>
<x-checkbox
label="{!! __('J\'accepte les conditions générales et la politique de confidentialité') !!}"
x-model="rgpd"
class="checkbox-primary checkbox-sm"
/>
</div>
<p class="text-xs text-base-content/60 pt-2">
<span class="text-error">*</span> @lang('Champs obligatoires')
</p>
<x-slot:actions>
<div class="flex flex-col sm:flex-row items-center justify-between gap-3 w-full pt-2">
<x-button
label="{{ __('Déjà inscrit ?') }}"
class="btn-ghost btn-sm w-full sm:w-auto order-2 sm:order-1"
link="/login"
/>
<x-button
x-show="rgpd"
label="{{ __('Créer mon compte') }}"
type="submit"
icon-right="o-arrow-right"
class="btn-primary btn-sm w-full sm:w-auto order-1 sm:order-2"
spinner="register"
/>
</div>
</x-slot:actions>
</x-form>
</x-card>
</div>
</div>
On ajoute la route :
...
Route::livewire('/', 'pages::index')->name('home');
Route::middleware('guest')->group(function () {
Route::livewire('/register', 'pages::auth.register');
});
Et maintenant avec l'url .../register, on arrive sur le formulaire :

Un message pour rassurer
Le nouvel inscrit va recevoir un email de confirmation, mais il est judicieux lors du renvoi sur la page d'accueil, même s'il se retrouve déjà connecté dans la boutique, de lui afficher un message. Dans le composant d'inscription, on a prévu un message en session :
session()->flash('registered', __('Votre compte a été créé avec succès. Un e-mail de confirmation vous a été envoyé.'));
On l'a déjà prévu dans notre composant index de l'accueil.
Le pot de miel
Si vous êtes observateur, vous avez remarqué la présence insolite d'une propriété gender qui semble inutile. C'est un moyen appelé "pot de miel" pour tromper les robots.
Les robots automatisés (spambots) remplissent souvent tous les champs de formulaire qu'ils trouvent, sans se soucier de leur visibilité. En ajoutant un champ de saisie masqué (le pot de miel), vous pouvez détecter si un robot a rempli le formulaire. Si le champ gender contient une valeur lors de la soumission du formulaire, cela signifie probablement que le formulaire a été rempli par un robot, car un utilisateur humain ne verrait pas ce champ et ne le remplirait pas.
En utilisant cette technique, vous pouvez ajouter une couche de sécurité supplémentaire à vos formulaires. Si le champ gender est rempli, vous pouvez rejeter la soumission du formulaire et empêcher les spambots de soumettre des données indésirables. On le fait dans le composant avec ce code :
public function register()
{
if ($this->gender) {
abort(403);
}
Fonctionnement
Il ne nous reste plus qu'à vérifier que tout fonctionne correctement. Les validations :

On peut créer automatiquement un mot de passe sécurisé et le copier :

Vérifiez l'enregistrement effectif dans la base :

Et le message sur la page d'accueil :

Ainsi que la cohérence de l'email reçu :

La connexion
À présent qu'on peut s'inscrire dans la boutique, on doit prévoir la possibilité de se connecter. On crée un nouveau composant :
php artisan make:livewire pages::auth.login

Entrez ce code :
<?php
use Livewire\Attributes\{Validate, Title};
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\{Auth, RateLimiter, Session};
use Illuminate\Validation\ValidationException;
new
#[Title('Connexion')]
class extends Component {
#[Validate('required|email')]
public string $email = '';
#[Validate('required')]
public string $password = '';
#[Validate('boolean')]
public bool $remember = false;
public function login()
{
$this->validate();
$this->authenticate();
Session::regenerate();
$this->redirectIntended(default: url('/'), navigate: true);
}
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
protected function throttleKey(): string
{
return Str::lower($this->email).'|'.request()->ip();
}
}; ?>
<div class="flex items-center justify-center min-h-[calc(100vh-16rem)] py-12 px-4">
<div class="w-full max-w-md">
<x-card
title="{{ __('Connexion') }}"
subtitle="{{ __('Accédez à votre compte pour gérer vos commandes') }}"
class="shadow-xl bg-base-100"
separator
progress-indicator
>
<x-form wire:submit="login" class="space-y-4">
<x-input
label="{{ __('Adresse e-mail') }}"
wire:model="email"
icon="o-envelope"
type="email"
required
/>
<x-password
label="{{ __('Mot de passe') }}"
wire:model="password"
clearable
required
/>
<div class="flex items-center justify-between pt-1">
<x-checkbox
label="{{ __('Rester connecté') }}"
wire:model="remember"
class="checkbox-primary checkbox-sm"
/>
<x-button
label="{{ __('Mot de passe oublié ?') }}"
class="btn-ghost btn-xs text-base-content/70 hover:text-primary"
link="/forgot-password"
/>
</div>
<p class="text-xs text-base-content/60 pt-2">
<span class="text-error">*</span> @lang('Champs obligatoires')
</p>
<x-slot:actions>
<div class="flex flex-col sm:flex-row items-center justify-between gap-3 w-full pt-2">
<x-button
label="{{ __('Créer un compte') }}"
class="btn-ghost btn-sm w-full sm:w-auto order-2 sm:order-1"
link="/register"
/>
<x-button
label="{{ __('Se connecter') }}"
type="submit"
icon-right="o-arrow-right-end-on-rectangle"
class="btn-primary btn-sm w-full sm:w-auto order-1 sm:order-2"
spinner="login"
/>
</div>
</x-slot:actions>
</x-form>
</x-card>
</div>
</div>
Et la route :
Route::middleware('guest')->group(function () {
Route::livewire('/login', 'pages::auth.login')->name('login');
...
});
On obtient ce joli formulaire :

Vérifiez la validation :

Et évidemment aussi que la connexion et la déconnexion fonctionnent bien.
Conclusion
Dans ce chapitre, on a mis en place les fondamentaux de l'authentification avec l'inscription à la boutique et la connexion. Dans le prochain chapitre, on s'occupera de l'oubli du mot de passe.
Par bestmomo
Aucun commentaire