Logomark

LARAVEL

Un framework qui rend heureux
Voir cette catégorie
Vers le bas
Voir cette série
Shop V2 : l'authentification
Samedi 1 août 2026 18:38

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

Article précédent : Shop V2 : la page d'accueil
Article suivant : Shop V2 : oubli du mot de passe