La mise en place d'un système de réinitialisation de mot de passe est un élément important à prendre en considération lors de la conception d'un site web ou d'une application en ligne. Les utilisateurs peuvent oublier leurs mots de passe pour diverses raisons (absence prolongée, informations sensibles oubliées, etc.). En offrant une fonctionnalité de réinitialisation de mot de passe, vous améliorez l'expérience utilisateur en facilitant leur accès au site ou à l'application.
Le code complet à la fin de cet article peut être téléchargé ici.
On va commencer par créer un composant Blade pour les messages en session :
php artisan make:component session-status --view

@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif
Pour l'oubli du mot de passe, on crée un composant Livewire :
php artisan make:livewire pages::auth.forgot-password

Avec ce code :
<?php
use Illuminate\Support\Facades\Password;
use Livewire\Attributes\{Validate, Title};
use Livewire\Component;
new
#[Title('Mot de passe oublié')]
class extends Component {
#[Validate('required|email')]
public string $email = '';
public function sendPasswordResetLink(): void
{
$this->validate();
// 1. Protection contre le spam (Rate Limiting)
// On limite à 3 demandes toutes les 10 minutes par e-mail + IP
$throttleKey = Str::lower($this->email).'|'.request()->ip();
if (RateLimiter::tooManyAttempts($throttleKey, 3)) {
$seconds = RateLimiter::availableIn($throttleKey);
$this->addError('email', __('Veuillez patienter : trop de demandes. Réessayez dans ' . ceil($seconds / 60) . ' min.'));
return;
}
$status = Password::sendResetLink(
$this->only('email')
);
if ($status !== Password::RESET_LINK_SENT) {
RateLimiter::hit($throttleKey, 600); // On marque une tentative
// 2. Sécurité : On ne révèle pas si l'utilisateur existe ou non
// On affiche un message générique même en cas d'erreur
session()->flash('status', __('Un lien de réinitialisation vous a été envoyé si votre adresse e-mail est enregistrée.'));
return;
}
RateLimiter::clear($throttleKey);
$this->reset('email');
session()->flash('status', __($status));
}
}; ?>
<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="{{ __('Mot de passe oublié') }}"
subtitle="{{ __('Saisissez votre e-mail pour recevoir un lien de réinitialisation') }}"
class="shadow-xl bg-base-100"
separator
progress-indicator
>
{{-- Message de confirmation d'envoi du mail --}}
@if (session('status'))
<x-alert icon="o-check-circle" class="alert-success text-xs mb-4">
{{ session('status') }}
</x-alert>
@endif
<x-form wire:submit="sendPasswordResetLink" class="space-y-4">
<x-input
label="{{ __('Adresse e-mail') }}"
wire:model="email"
icon="o-envelope"
type="email"
required
/>
<p class="text-xs text-base-content/60 pt-1">
<span class="text-error">*</span> @lang('Champ obligatoire')
</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="{{ __('Retour') }}"
type="button"
icon="o-arrow-left"
class="btn-ghost btn-sm w-full sm:w-auto order-2 sm:order-1"
link="{{ route('login') }}"
/>
<x-button
label="{{ __('Envoyer le lien') }}"
type="submit"
icon-right="o-paper-airplane"
class="btn-primary btn-sm w-full sm:w-auto order-1 sm:order-2"
spinner="sendPasswordResetLink"
/>
</div>
</x-slot:actions>
</x-form>
</x-card>
</div>
</div>
Et on ajoute la route, toujours dans le même groupe :
Route::middleware('guest')->group(function () {
...
Route::livewire('/forgot-password', 'pages::auth.forgot-password');
});
Voilà le formulaire généré :

Si vous utilisez le formulaire, vous allez tomber sur une erreur à la soumission :
Route [password.reset] not defined.
Et effectivement, on n'a pas encore codé cette partie, alors faisons-le :
php artisan make:livewire pages::auth.reset-password

Avec ce code :
<?php
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\{Hash, Password, Session};
use Illuminate\Support\Str;
use Livewire\Attributes\{Locked, Title};
use Livewire\Component;
use Illuminate\Validation\Rules\Password as PasswordRule;
new
#[Title('Réinitialisation du mot de passe')]
class extends Component {
#[Locked]
public string $token = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public ?string $generatedPassword = null;
public function mount(string $token): void
{
$this->token = $token;
$this->email = request()->input('email', '');
}
/**
* 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;
}
public function resetPassword(): void
{
$this->validate([
'token' => ['required'],
'email' => ['required', 'string', 'email'],
'password' => [
'required',
'confirmed',
PasswordRule::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
],
]);
$status = Password::reset(
$this->only('email', 'password', 'password_confirmation', 'token'),
function ($user) {
$user->forceFill([
'password' => Hash::make($this->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
if (Password::PASSWORD_RESET != $status) {
$this->addError('email', __($status));
return;
}
Session::flash('status', __($status));
$this->redirectRoute('login', navigate: true);
}
}; ?>
<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="{{ __('Réinitialiser le mot de passe') }}"
subtitle="{{ __('Saisissez votre e-mail et choisissez votre nouveau mot de passe') }}"
class="shadow-xl bg-base-100"
separator
progress-indicator
>
@if (session('status'))
<x-alert icon="o-check-circle" class="alert-success text-xs mb-4">
{{ session('status') }}
</x-alert>
@endif
<x-form wire:submit="resetPassword" class="space-y-4">
<x-input
label="{{ __('Adresse e-mail') }}"
wire:model="email"
icon="o-envelope"
type="email"
required
readonly
/>
<div class="space-y-3">
<x-password
label="{{ __('Nouveau 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>
{{-- 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>
<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>
<p class="text-xs text-base-content/60 pt-1">
<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="{{ __('Annuler') }}"
type="button"
icon="o-arrow-left"
class="btn-ghost btn-sm w-full sm:w-auto order-2 sm:order-1"
link="{{ route('login') }}"
/>
<x-button
label="{{ __('Réinitialiser le mot de passe') }}"
type="submit"
icon-right="o-check"
class="btn-primary btn-sm w-full sm:w-auto order-1 sm:order-2"
spinner="resetPassword"
/>
</div>
</x-slot:actions>
</x-form>
</x-card>
</div>
</div>
On ajoute la route :
Route::middleware('guest')->group(function () {
...
Route::livewire('/reset-password/{token}', 'pages::auth.reset-password')->name('password.reset');
});
Maintenant l'envoi du lien fonctionne et on a un message qui apparaît :

Voici l'aspect de l'email :

En utilisant le lien, on aboutit sur ce formulaire :

Tant qu'à faire, j'ai rendu l'email non modifiable. Après réinitialisation, on se retrouve sur la page de connexion.
Conclusion
Dans ce chapitre, on a créé la logistique pour le renouvellement du mot de passe.
Par bestmomo
Aucun commentaire