Shopping : l’authentification

On a vu lors des deux précédents articles comment le schéma des données est organisé. On a aussi garni la base avec des données d’exemples. Toutes les relations sont en place. On va commencer maintenant à s’intéresser au visuel de la boutique.

Le visuel est toujours une affaire de goût personnel. Personnellement j’aime bien Materialize. Alors on va partir avec ce framework pour l’apparence de la boutique. On fera un autre choix pour l’administration.

Vous pouvez télécharger un ZIP du projet ici.

Materialize

On charge Materialize avec npm :

npm i -D materialize-css

On le retrouve ainsi dans le dossier node_modules.

Mais comme on va changer certaines variables pour changer les couleurs de base on va en faire une copie dans le dossier resources/sass. Mais juste la partie utile :

Donc le dossier components et le fichier materialize.scss.

Au passage on supprime le fichier _variables.scss qui ne nous sera plus utile.

Et dans le fichier app.scss on remplace ainsi le code :

@import url("https://fonts.googleapis.com/icon?family=Material+Icons");

//Materialize
@import "materialize-css/materialize.scss";

Le fichier où on va intervenir pour changer l’aspect est materialize-css/components/_variables.scss.

On va changer certaines couleurs de base :

$primary-color: color("brown", "darken-2") !default;
$secondary-color: color("purple", "darken-2") !default;

$card-link-color: color("purple", "accent-3") !default;

Pour le Javascript on va aussi copier le fichier complet issu de dist pour éviter qu’une mise à jour ultérieure vienne mettre la panique :

On modifie le fichier resources/js/app.js avec ce code :

require('./materialize.js');

document.addEventListener('DOMContentLoaded', function() {
  M.AutoInit();
  
  var elems = document.querySelectorAll('.tooltipped');
  M.Tooltip.init(elems);
});

Il ne reste plus qu’à générer :

npm run dev

Évidemment là on a tout cassé ! Il ne nous reste plus qu’à arranger déjà l’authentification !

Le layout

On va commencer par modifier le layout (views/layouts/app.blade.php) :

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    {{-- <meta name="csrf-token" content="{{ csrf_token() }}"> --}}

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}"></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">

    @yield('css')

</head>
<body>
  <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
    @csrf
  </form>

  <nav>
    <div class="nav-wrapper">
      <a href="#" class="brand-logo"><img src="/images/logo1.png" width="210px" alt="Logo"></a>
      <a href="#" data-target="mobile" class="sidenav-trigger"><i class="material-icons">menu</i></a>
      <ul class="right hide-on-med-and-down">
      @guest        
        <li><a href="{{ route('login') }}"><i class="material-icons left">perm_identity</i>Connexion</a></li>
      @else
        <li><a href="{{ route('logout') }}"
          onclick="event.preventDefault();
          document.getElementById('logout-form').submit();">
          <i class="material-icons left">perm_identity</i>
          Déconnexion
        </a></li>
      @endguest
      
    </ul>
    </div>
  </nav>

  <ul class="sidenav" id="mobile">
    @guest
      <li><a href="{{ route('login') }}">Connexion</a></li>
    @else
      <li><a href="{{ route('logout') }}"
        onclick="event.preventDefault();
        document.getElementById('logout-form').submit();">
        Déconnexion
      </a></li>
    @endguest
  </ul>

  <main>
    @yield('content')
  </main>

  <footer class="page-footer">
    <div class="container">
      <div class="row">
        <div class="col l6 s12">
          <h5 class="white-text">Nom de la boutique</h5>
          <ul>
            <li class="grey-text text-lighten-3">Adresse de la boutique</li>
            <li class="grey-text text-lighten-3">Appelez-nous...</li>
            <li class="grey-text text-lighten-3">Écrivez-nous...</li>
            <br>
            <li><img src="/images/paiement.png" alt="Modes de paiement" width="250px"></li>
          </ul>
        </div>
        <div class="col l4 offset-l2 s12">
          <h5 class="white-text">Informations</h5>
        </div>
      </div>
    </div>
    <div class="footer-copyright">
      <div class="container">
        © 2020 Nom de la boutique
        <a class="grey-text text-lighten-4 right" href="#" target="_blank"><img src="/images/facebook.png" alt="Facebook"></a>
      </div>
    </div>
  </footer>

  @yield('javascript')

</body>
</html>

On complètera ça au fur et à mesure…

Un composant

Laravel 7 a ajouté la possibilité de créer des composants pour les vues alors on ne va pas s’en priver :

php artisan make:component Input

On a la création d’une classe :

Et d’une vue :

On complète ainsi le code de la classe :

<?php

namespace App\View\Components;

use Illuminate\View\Component;

class Input extends Component
{
    public $name;
    public $type;
    public $icon;
    public $label;
    public $required;
    public $autofocus;
    public $value;

    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct($name, $type, $icon, $label, $value = '', $required = false, $autofocus = false)
    {
        $this->name = $name;
        $this->type = $type;
        $this->icon = $icon;
        $this->label = $label;
        $this->value = $value;
        $this->required = $required;
        $this->autofocus = $autofocus;
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\View\View|string
     */
    public function render()
    {
        return view('components.input');
    }
}

Et celui de la vue :

<div class="row">
  <div class="input-field col s12">
    <i class="material-icons prefix">{{ $icon }}</i>
    <input 
      id="{{ $name }}" 
      type="{{ $type }}" 
      name="{{ $name }}" 
      value="{{ old($name, $value !== '' ? $value : '') }}" 
      class="{{ $errors->has($name) ? 'invalid' : '' }}" 
      {{ $required ? 'required' : '' }} 
      {{ $autofocus ? 'autofocus' : '' }}>
    <label for="{{ $name }}">{{ $label }}</label>
    <span class="red-text">{{ $errors->has($name) ? $errors->first($name): '' }}</span>
  </div>
</div>

L’enregistrement

On peut maintenant modifier le code pour la vue de l’enregistrement (views/auth/register.blade.php) :

@extends('layouts.app')

@section('content')
<div class="container">

  <div class="row">
    <div class="col s12 m10 offset-m1 l8 offset-l2">
      <div class="card">
        <form  method="POST" action="{{ route('register') }}">
          <div class="card-content">
            @csrf
            <span class="card-title">Créez votre compte</span>

            <hr>

            <x-input
              name="firstname"
              type="text"
              icon="person"
              label="Prénom"
              required="true"
            ></x-input>

            <x-input
              name="name"
              type="text"
              icon="person"
              label="Nom"
              required="true"
            ></x-input>

            <x-input
              name="email"
              type="email"
              icon="mail"
              label="Adresse mail"
              required="true"
            ></x-input>

            <x-input
              name="password"
              type="password"
              icon="lock"
              label="Mot de passe"
              required="true"
            ></x-input>

            <div class="row">
              <div class="input-field col s12">
                <i class="material-icons prefix">lock</i>
                <input id="password-confirm" type="password" name="password_confirmation" required>
                <label for="password-confirm">Confirmation du mot de passe</label>
              </div>
            </div>

            <div class="row col s12">
              <label>
                <input type="checkbox" name="newsletter" id="newsletter" {{ old('newsletter') ? 'checked' : '' }}>
                <span>Je désire recevoir votre lettre d'information</span>
              </label>
            </div>

            <div class="row col s12">
              <label>
                <input type="checkbox" name="rgpd" id="rgpd" {{ old('rgpd') ? 'checked' : '' }}>
                <span>J'accepte les termes et conditions de <a href="{{ route('page', 'politique-de-confidentialite') }}" target="_blank">la politique de confidentialité</a>.</span>
              </label>
            </div>
            
            <p>
              <button class="btn waves-effect waves-light disabled" style="width: 100%" type="submit" name="action">
                Enregistrer
              </button>
            </p>

          </div>
        </form>
      </div>
    </div>
  </div>
</div>
@endsection

@section('javascript')
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const rgpd = document.querySelector('#rgpd');
      rgpd.checked = false;
      rgpd.addEventListener('click', () => document.querySelector('button[type=submit]').classList.toggle('disabled'));
    });
  </script>
@endsection

On a déjà quelque chose de plus sympathique :

Le Javascript sert juste à rendre le bouton d’enregistrement actif seulement si l’utilisateur a coché la case d’acceptation de la politique de confidentialité (pour le moment le lien est inactif).

Mais comme on a ajouté des colonnes dans la table users on va avoir un souci pour l’enregistrement !

On va donc modifier ainsi le contrôleur RegisterController :

use App\Models\User;

...

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:255'],
        'firstname' => ['required', 'string', 'max:255'],
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', 'string', 'min:8', 'confirmed'],
    ]);
}

protected function create(array $data)
{
    return User::create([
        'name' => $data['name'],
        'firstname' => $data['firstname'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
        'newsletter' => array_key_exists('newsletter', $data),
    ]);
}

On peut maintenant tenter d’enregistrer un nouvel utilisateur ! Chez moi ça fonctionne :

La connexion

On peut maintenant passer à la connexion avec la vue views/auth/login.blade.php :

@extends('layouts.app')

@section('content')
<div class="container">

  <div class="row">
    <div class="col s12 m10 offset-m1 l8 offset-l2">
      <div class="card">
        <form  method="POST" action="{{ route('login') }}">
          <div class="card-content">
            @csrf
            <span class="card-title">Connexion</span>

            <hr>

            <x-input
              name="email"
              type="email"
              icon="mail"
              label="Adresse mail"
              required="true"
              autofocus="true" 
            ></x-input>

            <x-input
              name="password"
              type="password"
              icon="lock"
              label="Mot de passe"
              required="true"
            ></x-input>

            <div class="row col s12">
              <label>
                <input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
                <span>Se rappeler de moi</span>
              </label>
            </div>

            <p>
              <button class="btn waves-effect waves-light" style="width:100%" type="submit" name="action">Connexion
                <i class="material-icons right">lock_open</i>
              </button>
            </p>

            <br>

          </div>

          <div class="card-action">
            <p><a href="{{ route('password.request') }}">Mot de passe oublié ?</a></p>
            <p><a href="{{ route('register') }}">Pas de compte ? Créez-en un</a></p>
          </div>

        </form>
      </div>
    </div>
  </div>
</div>
@endsection

Oubli du mot de passe

On va aussi modifier la vue views/auth/passwords/email.blade.php :

@extends('layouts.app')

@section('content')
<div class="container">

  @if (session('status'))
    <div class="card">
      <div class="card green darken-1">
        <div class="card-content white-text">
          {{ session('status') }}
        </div>
      </div>
    </div>
  @endif

  <div class="row">
    <div class="col s12 m10 offset-m1 l8 offset-l2">
        <div class="card">
          <form  method="POST" action="{{ route('password.email') }}">
            <div class="card-content">
              @csrf
              <span class="card-title">Renouvellement du mot de passe</span>

              <hr>

              <x-input
                name="email"
                type="email"
                icon="mail"
                label="Adresse mail"
                required="true"
                autofocus="true" 
              ></x-input>

              <p>
                <button class="btn waves-effect waves-light right" type="submit" name="action">
                  Envoyer le lien de renouvellement
                  <i class="material-icons right">lock_open</i>
                </button>
              </p>

              <br><br>

            </div>
          </form>
      </div>
    </div>
  </div>
</div>
@endsection

Pour faire l’essai il faut prévoir de renseigner les données de Mailtrap dans le fichier .env :

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME="username ici"
MAIL_PASSWORD="mot de passe ici"
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=from@example.com
MAIL_FROM_NAME=Example

Lorsque l’email est parti on affiche la jolie banière verte :

Bon c’est encore en anglais, on va aller chercher les traductions en français. On place le dossier ici :

On change aussi dans config/app.php :

'locale' => 'fr',

Et maintenant ça va mieux :

Un email en français

Par contre le contenu de l’email est tout en anglais :

On peut créer un fichier de traduction :

Avec ce code (le source est en grande partie dans laravel/framework/src/illuminate/Auth/Notifications/ResetPassword.php) :

{
  "Reset Password Notification": "Renouvellement du mot de passe",
  "You are receiving this email because we received a password reset request for your account.": "Vous recevez ce message parce que nous avons reçu une demande de renouvellement du mot de passe pour votre compte.",
  "Reset Password": "Renouvellement",
  "If you did not request a password reset, no further action is required." : "Si vous n'avez pas fait cette demande, aucune action complémentaire n'est requise.",
  "Hello!": "Bonjour !",
  "Regards": "Cordialement",
  "This password reset link will expire in :count minutes.": "Ce lien de renouvellement expirera dans :count minutes.",
  "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si vous avez un problème pour cliquer le boutton \":actionText\", copiez et collez le lien ci-dessous\ndans votre navigateur :",
  "All rights reserved.": "Tous droits réservés"
}

C’est mieux :

On a encore deux petits soucis :

  • C’est signé « Laravel »
  • Un bas de page qui n’apparaît pas sur l’image affiche : © 2020 Laravel. Tous droits réservés

Pour la signature et le bas de page ça se passe dans la configuration, la valeur est par défaut dans le fichier .env :

APP_NAME=Laravel

Dans un souci d’homogénéité on serait sans doute amené à intégrer cet email à ceux qu’il faudra créer pour la boutique, parce qu’on ne va pas se contenter du système de notification de Laravel. On abordera cette question dans un article utltérieur.

Renouvellement du mot de passe

On va aussi modifier la vue pour le renouvellement du mot de passe (views/auth/passwords/reset.blade.php) :

@extends('layouts.app')

@section('content')
<div class="container">

  <div class="row">
    <div class="col s12 m10 offset-m1 l8 offset-l2">
        <div class="card">
          <form  method="POST" action="{{ route('password.update') }}">
            <div class="card-content">
              @csrf
              <input type="hidden" name="token" value="{{ $token }}">

              <span class="card-title">Renouvellement du mot de passe</span>

              <hr>

              <x-input
                name="email"
                type="email"
                icon="mail"
                label="Adresse mail"
                required="true"
                autofocus="true" 
              ></x-input>

              <x-input
                name="password"
                type="password"
                icon="lock"
                label="Mot de passe"
                required="true"
              ></x-input>

              <div class="row">
                <div class="input-field col s12">
                  <i class="material-icons prefix">lock</i>
                  <input id="password-confirm" type="password" name="password_confirmation" required>
                  <label for="password-confirm">Confirmation du mot de passe</label>
                </div>
              </div>

              <p>
                <button class="btn waves-effect waves-light" type="submit" name="action">Renouveler !
                  <i class="material-icons right">lock_open</i>
                </button>
              </p>

            </div>
          </form>
      </div>
    </div>
  </div>
</div>
@endsection

Parfait !

Les urls

J’ai dit qu’on veut un site strictement en français. Ca concerne aussi les urls. Il faudrait éviter login, register, reset

Par défaut toutes les routes de l’authentification sont générées avec ce simple code :

Auth::routes();

On génère ainsi tout ça :

On se rend compte aussi qu’il y a des routes (confirm) qu’on n’utilisera pas.

Ces routes sont générées dans le fichier vendor\laravel\ui\src\AuthRouteMethods.php.

Pour franciser les urls une seule solution, réécrire ces routes :

Route::post('deconnexion', 'Auth\LoginController@logout')->name('logout');
Route::middleware('guest')->group(function () {
    Route::prefix('connexion')->group(function () {
        Route::get('/', 'Auth\LoginController@showLoginForm')->name('login');
        Route::post('/', 'Auth\LoginController@login');
    });
    Route::prefix('inscription')->group(function () {
        Route::get('/', 'Auth\RegisterController@showRegistrationForm')->name('register');
        Route::post('/', 'Auth\RegisterController@register');
    });
});
Route::prefix('passe')->group(function () {
    Route::get('renouvellement', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
    Route::post('email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
    Route::get('renouvellement/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
    Route::post('renouvellement', 'Auth\ResetPasswordController@reset')->name('password.update');
});

Un peu de style

Pour terminer on va un peu améliorer le style de nos pages (app.scss). Lancez npm run watch pour constater les changements.

On va déjà un peu décaler le contenu de nos pages par rapport à la barre de menu :

main { padding-top: 40px; };

On va aussi changer la couleur du fond :

body { background-color: #f1f1ff; }

On va aussi aérer le footer :

.page-footer { margin-top: 100px; }

Conclusion

Notre authentification est maintenant en place, on est donc prêts à accueillir des clients ! Il va falloir maintenant leur proposer des produits à la vente et gérer leurs achats…

Print Friendly, PDF & Email

7 commentaires sur “Shopping : l’authentification

  1. Bonsoir BestMomo, j’essaie d’envoyer le mail de renouvellement de mot de passe et je suis confronter à une erreur, j’ai essayer de m’en sortir toute seule mais j’y suis pas arriver. Voici l’erreur : Failed to authenticate on SMTP server with username « *** » using 3 possible authenticators. Authenticator CRAM-MD5 returned Expected response code 235 but got code « 535 », with message « 535 5.7.0 Invalid login or password « . Authenticator LOGIN returned Expected response code 250 but got an empty response. Authenticator PLAIN returned Expected response code 250 but got an empty response.

    J’ai besoin de votre aide s’il vous plait. Merci

      1. Bonsoir,
        Merci beaucoup pour votre aide grâce à elle je comprends mieux le mécanisme d’envoi de mail. Par contre même après avoir crée mon compte sur Mailtrap je reçoit toujours la même avec à la place des *** j’ai mon username. J’ai fait un php artisan cache:clear mais le problème persiste toujours.
        Voici l’erreur :
        Failed to authenticate on SMTP server with username « hamdiyatou » using 3 possible authenticators. Authenticator CRAM-MD5 returned Expected response code 235 but got code « 535 », with message « 535 5.7.0 Invalid login or password
        « . Authenticator LOGIN returned Expected response code 250 but got an empty response. Authenticator PLAIN returned Expected response code 250 but got an empty response.

        Merci d’avance.

  2. je ne suis qu’un débutant laravel
    J’ai ce problème

    ERROR Failed to compile with 1 errors 14:27:30

    This relative module was not found:

    * ./materialize.js in ./resources/js/app.js
    Asset Size Chunks Chunk Names
    /css/app.css 158 KiB /js/app [emitted] /js/app
    /js/app.js 4.85 KiB /js/app [emitted] /js/app

    ERROR in ./resources/js/app.js
    Module not found: Error: Can’t resolve ‘./materialize.js’ in ‘C:\laragon\www\assi
    vito\resources\js’
    @ ./resources/js/app.js 1:0-27
    @ multi ./resources/js/app.js ./resources/sass/app.scss
    npm ERR! code ELIFECYCLE
    npm ERR! errno 2
    npm ERR! @ development: `cross-env NODE_ENV=development node_modules/webpack/bin/
    webpack.js –progress –hide-modules –config=node_modules/laravel-mix/setup/webp
    ack.config.js`
    npm ERR! Exit status 2
    npm ERR!
    npm ERR! Failed at the @ development script.
    npm ERR! This is probably not a problem with npm. There is likely additional logg
    ing output above.

    npm ERR! A complete log of this run can be found in:
    npm ERR! C:\Users\client\AppData\Roaming\npm-cache\_logs\2020-05-27T13_27_30_
    681Z-debug.log
    npm ERR! code ELIFECYCLE
    npm ERR! errno 2
    npm ERR! @ dev: `npm run development`
    npm ERR! Exit status 2
    npm ERR!
    npm ERR! Failed at the @ dev script.
    npm ERR! This is probably not a problem with npm. There is likely additional logg
    ing output above.

    npm ERR! A complete log of this run can be found in:
    npm ERR! C:\Users\client\AppData\Roaming\npm-cache\_logs\2020-05-27T13_27_30_
    774Z-debug.log

Laisser un commentaire