Laravel 4 : chapitre 24 : Un blog : authentification

Nous allons dans ce chapitre poursuivre l’exemple du blog en ajoutant l’authentification. Je tiendrai compte des changements dus à la sortie de la version 4.1.26 de Laravel qui a changé certains éléments. Comme cet article comporte beaucoup de code je l’ai rassemblé (ainsi que tous les éléments des articles précédents) dans un fichier compressé téléchargeable.

Contrôleur HomeController

Pour les besoins du fonctionnement du blog on avait prévu une authentification automatique dans le contrôleur HomeController au niveau de la méthode article. On va donc supprimer cette disposition maintenant qu’on va mettre en place l’authentification :

<?php
public function article($cat_id, Article $article)
{
  $comments = $article->comments()->orderBy('comments.created_at', 'desc')
                                  ->join('users', 'users.id', '=', 'comments.user_id')
                                  ->select('users.username', 'title', 'text', 'comments.created_at')
                                  ->get();
  return View::make('article', array('categories' => Categorie::all(), 'article' => $article, 'actif' => $cat_id, 'comments' => $comments));
}

Vues

La navigation

On va conserver les mêmes vues. On va juste ajouter un bouton au niveau de la navigation pour la connexion ou la déconnexion :

@section('navigation')
  <li>{{ link_to_route('accueil', 'Accueil', null, ($actif == 0)? array('class' => 'actif'): null) }}</li>

  @foreach ($categories as $categorie)
    <li>{{ link_to('cat/'.$categorie->id, $categorie->title, ($actif == $categorie->id)? array('class' => 'actif'): null) }}</li>
  @endforeach

  @if (Auth::check())
    <li>{{ link_to('auth/logout', 'Deconnexion') }}</li>
  @else
    <li>{{ link_to('auth/login', 'Connexion', ($actif == -1)? array('class' => 'actif'): null) }}</li>
  @endif
@stop

La méthode check de la classe Auth permet de savoir si l’utilisateur est connecté. On peut ainsi adapter le menu en conséquence. On fait référence à des routes qui n’existent pas encore.

Quand je parle de la classe Auth ce n’est pas vraiment exact puisqu’il s’agit d’une façade qui renvoie à la classe Illuminate\Auth\AuthManager. Je parlerai des façades dans un article ultérieur. Vous pouvez trouver la correspondance entre les façades et les classes dans cette partie de la documentation.

Le formulaire de connexion

Nous allons avoir besoin de vues pour l’authentification, on va toutes les loger dans un dossier views/auth. Nous allons commencer par la vue views/auth/login.blade.php. Cette vue sera chargée d’afficher le formulaire pour la connexion :

@extends('template_blog')

@include('navigation')

@section('content')

    <div class="col-md-12">
        Pour vous connecter au site veuillez entrer votre pseudo et votre mot de passe dans le formulaire ci-dessous :
    </div>
    <br>
    <div class="row col-md-12">
        @if (Session::has('flash_error'))
            <div class="col-md-7">
                <div class="alert alert-danger">
                    {{ Session::get('flash_error') }}
                </div>
            </div>
        @endif
        <div class="col-md-7">
            {{ Form::open(array('url' => 'auth/login', 'method' => 'POST', 'class' => 'form-horizontal well')) }}
            <div class="form-group">
                {{ Form::label('username', 'Pseudo :', array('class' => 'col-md-3 control-label')) }}
                <div class="col-md-9">
                    {{ Form::text('username', '', $attributes = array('class' => 'form-control')) }}
                </div>
            </div>
            <div class="form-group">
                {{ Form::label('password', 'Mot de passe :', array('class' => 'col-md-3 control-label')) }}
                <div class="col-md-9">
                    {{ Form::password('password', $attributes = array('class' => 'form-control')) }}
                </div>
            </div>
            <div class="checkbox pull-right">
                {{ Form::checkbox('souvenir') }}Se rappeler de moi
            </div>
            <div class="form-group">
                <div class="col-md-offset-3 col-md-9">
                    {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}
                </div>
            </div>
            {{ Form::close()}}
        </div>
        <div class="col-md-5">
            <p>
                {{ link_to('remind/remind', 'J\'ai oublié mon mot de passe...', array('class' => 'btn btn-success')) }}
            </p>
        </div>
    </div>
    <div class="col-md-12">
        Si vous n'avez pas encore de compte vous pouvez en créer un en cliquant sur ce bouton :
        {{ link_to('auth/inscription', 'Je m\'inscris !', array('class' => 'btn btn-info')) }}
    </div>
    
@stop

La langue

Pour avoir les messages en Français il faut aller chercher le dossier fr ici. Il faut placer ce dossier dans celui des langues :

img93

Et ensuite informer le fichier app/config/app.php :

<?php
'locale' => 'fr',

Les routes

Nous allons nous en occuper en ajoutant cette ligne :

<?php
Route::controller('auth', 'AuthController');

On prévoit ici un contrôleur RESTful avec la méthode controller. Il s’agit d’un contrôleur un peu particulier. Le nom de ses méthodes doivent répondre à une certaine syntaxe : ils doivent débuter par le verbe de la requête (get, post, put…) et se terminer par un nom. Prenons un exemple avec la méthode getLogin. Cette méthode est composée du verbe get et du nom Login. Comme on a nommé le contrôleur auth la route correspondante sera auth/login avec le verbe get. On se rend compte qu’avec une simple ligne de code on peut avoir autant de routes que l’on veut en créant des méthodes dans le contrôleur.

Il est possible de nommer les routes pour un contrôleur RESTful, par exemple on pourrait écrire :

<?php
Route::controller('auth', 'AuthController', ['getLogin' => 'login']);

Le contrôleur AuthController

Maintenant que nous avons déclaré la route passons au contrôleur :

<?php

class AuthController extends BaseController {

	public function __construct()
  {
		$this->beforeFilter('auth', array('only' => 'getLogout'));
		$this->beforeFilter('guest', array('except' => 'getLogout'));
		$this->beforeFilter('csrf', array('on' => 'post'));
  }

  /**
	* Affiche le formulaire de login
	*
	* @return View
	*/
	protected function createView($name)
	{
		return View::make($name, array('categories' => Categorie::all(), 'actif' => -1)); 
	}  

  /**
	* Affiche le formulaire de login
	*
	* @return View
	*/
	public function getLogin()
	{
		return $this->createView('auth.login');
	}

  /**
	* Affiche le formulaire d'inscription
	*
	* @return View
	*/
	public function getInscription()
	{
		return $this->createView('auth.inscription');
	}

  /**
	* Traitement du formulaire de login
	*
	* @return Redirect
	*/
	public function postLogin()
	{
		$user = array('username' => Input::get('username'), 'password' => Input::get('password'));
		if (Auth::attempt($user, Input::get('souvenir'))) {
		  return Redirect::intended('/')
			->with('flash_notice', 'Vous avez été correctement connecté avec le pseudo ' . Auth::user()->username);
		}
		return Redirect::to('guest/login')->with('flash_error', 'Pseudo ou mot de passe non correct !')->withInput();       
	}

  /**
	* Traitement du formulaire d'inscription
	*
	* @return Redirect
	*/
	public function postInscription()
	{
		$v = User::validate(Input::all()); 
		if ($v->passes()) {
			$user = new User; 
			$user->username = Input::get('username'); 
			$user->email = Input::get('email'); 
			$user->password = Hash::make(Input::get('password')); 
			$user->save(); 
			return Redirect::route('accueil')->with('flash_notice', 'Votre compte a été créé.'); 
		}
		return Redirect::to('guest/inscription')->withErrors($v)->withInput(); 
	}  
   
  /**
	* Effectue le logout
	*
	* @return Redirect
	*/
	public function getLogout()
	{
		Auth::logout(); 
		return Redirect::route('accueil')->with('flash_notice', 'Vous avez été correctement déconnecté.');
	}

}

Nous allons expliciter peu à peu ce code.

La connexion

Lorsqu’on clique sur l’option « Connexion » du menu on arrive sur ce formulaire en passant par la méthode getLogin du contrôleur :

img78

Le traitement du formulaire est assuré par la méthode postLogin du contrôleur AuthController. Comme on a un contrôleur RESTful ça correspond donc à la route auth/login avec le verbe post. Voici cette méthode :

public function postLogin()
{
	$user = array('username' => Input::get('username'), 'password' => Input::get('password'));
	if (Auth::attempt($user, Input::get('souvenir'))) {
	  return Redirect::intended('/')
		->with('flash_notice', 'Vous avez été correctement connecté avec le pseudo ' . Auth::user()->username);
	}
	return Redirect::to('guest/login')->with('flash_error', 'Pseudo ou mot de passe non correct !')->withInput();       
}

La méthode attempt de la classe Auth assure d’une part la vérification des données entrées et de la connexion si tout est correct. On a donc deux situations à considérer : réussite ou échec

Échec de la connexion

Si on entre des valeurs incorrectes on renvoie le formulaire avec un message d’erreur : img79

Réussite de la connexion

Normalement vous devez avoir dans votre table des utilisateurs l’un d’entre eux nommé « admin » avec le passe « admin ». Vous devriez donc pouvoir vous connecter avec ces valeurs. Il y a deux possibilités de redirection selon la requête qui a amené sur la page de connexion. La méthode intended de la classe Redirect tient compte du fait qu’une url a pu être mémorisée et redirigée parce que l’utilisateur n’était pas connecté. Si vous regardez le filtre auth vous avez ce code :

Route::filter('auth', function()
{
	if (Auth::guest()) return Redirect::guest('login');
});

La redirection ici s’effectue avec la méthode guest. L’url est mise en mémoire pour la méthode intended. S’il n’y a rien en mémoire alors on prend en compte l’url passé en paramètre, dans notre cas l’url de base du site. J’ai choisi de renvoyer sur la page d’accueil avec un message pour signaler que la connexion a bien eu lieu :

img80

Remarquez aussi qu’on a la possibilité de mémoriser cette connexion dans un cookie si le deuxième paramètre de la méthode attempt a la valeur true. il suffit d’envoyer la valeur de la case à cocher prévue dans le formulaire pour affecter cette valeur.

La déconnexion

Quand un utilisateur est connecté on fait apparaître un item « Deconnexion » au niveau du menu :

img81

C’est la méthode getLogout du contrôleur AuthController qui assure le traitement :

public function getLogout()
{
	Auth::logout(); 
	return Redirect::route('accueil')->with('flash_notice', 'Vous avez été correctement déconnecté.');
}

On fait appel à la méthode logout de la classe Auth. On redirige ensuite sur la page d’accueil avec un message :

img82

Inscription d’un utilisateur

Si on clique sur le bouton « Je m’inscris ! » prévu dans la page de connexion on peut s’inscrire sur le site. La méthode getInscription du contrôleur AuthController provoque la génération du formulaire à partir de la vue.

La vue

Voici la vue auth/inscription.blade.php :

@extends('template_blog')

@include('navigation')

@section('content')
    <div class="col-md-12">
    	Pour vous inscrire veuillez remplir ce formulaire :
    </div>
    <br>
    <div class="row col-md-12">
        @if ($errors->count() > 0)
            <div class="col-md-9">
                <div class="alert alert-danger">
                    @foreach($errors->all() as $message)
                        {{ $message }}<br />
                    @endforeach           
                </div>
            </div>
        @endif
        <div class="col-md-9">
            {{ Form::open(array('url' => 'auth/inscription', 'method' => 'POST', 'class' => 'form-horizontal well')) }}
            <div class="form-group {{ $errors->has('username') ? 'error' : '' }}">
    			{{ Form::label('username', 'Pseudo :', array('class' => 'col-md-4 control-label')) }}
    			<div class="col-md-8">
    				{{ Form::text('username', '', $attributes = array('class' => 'form-control')) }}
    			</div>
    		</div>
    		<div class="form-group {{ $errors->has('email') ? 'error' : '' }}">
    			{{ Form::label('email', 'Mail :', array('class' => 'col-md-4 control-label')) }}
    			<div class="col-md-8">
    				{{ Form::text('email', '', $attributes = array('class' => 'form-control')) }}
    			</div>
    		</div>
    		<div class="form-group {{ $errors->has('password') ? 'error' : '' }}">
    			{{ Form::label('password', 'Mot de passe :', array('class' => 'col-md-4 control-label')) }}
    			<div class="col-md-8">
    				{{ Form::password('password', $attributes = array('class' => 'form-control')) }}
    			</div>
    		</div>
    		<div class="form-group">
    			{{ Form::label('password_confirmation', 'Confirmation passe :', array('class' => 'col-md-4 control-label')) }}
    			<div class="col-md-8">
    				{{ Form::password('password_confirmation', $attributes = array('class' => 'form-control')) }}
    			</div>
    		</div>
            <div class="form-group">
                <div class="col-md-offset-4 col-md-8">
                    {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}
                </div>
            </div>
    		{{ Form::close()}}
        </div>
    </div>
@stop

Avec cet aspect :

img83

La validation

On va prévoir la méthode pour la validation dans le modèle User, de même que des messages personnalisés :

<?php
public static function validate($input)
{
	$rules = array(
		'username' => 'required|Alpha|between:6,64|unique:users',
		'email' => 'required|email|unique:users',
		'password' => 'required|AlphaNum|between:6,20|confirmed'
	);
	$messages = array(
		'username.required' => 'Nous avons besoin de votre pseudo.',
		'username.Alpha' => 'Le pseudo doit être composé de caractères alphabétiques.',
		'username.between' => 'Le nombre de caractères du pseudo doit être compris entre :min et :max.',
		'username.unique' => 'Ce pseudo est déjà utilisé.',
		'email.required' => 'Nous avons besoin de votre adresse mail.',
		'email.email' => 'Le format de l\'adresse mail n\'est pas correct.',
		'email.unique' => 'Cette adresse mail est déjà utilisée.',
		'password.required' => 'Nous avons besoin d\'un mot de passe.',
		'password.Alphanum' => 'Le pseudo doit être composé de caractères alphanumériques.',
		'password.between' => 'Le nombre de caractères du mot de passe doit être compris entre :min et :max.',
		'password.confirmed' => 'La confirmation du mot de passe n\'est pas correcte.'
	);
	return Validator::make($input, $rules, $messages);
}

La localisation de la validation a déjà alimenté les discussions et continuera à le faire. Il y a plusieurs possibilités, nous en verrons d’autres dans les articles ultérieurs.

Le traitement

Le traitement du formulaire est assuré par la méthode postInscription du contrôleur AuthController :

<?php
public function postInscription()
{
	$v = User::validate(Input::all()); 
	if ($v->passes()) {
		$user = new User; 
		$user->username = Input::get('username'); 
		$user->email = Input::get('email'); 
		$user->password = Hash::make(Input::get('password')); 
		$user->save(); 
		return Redirect::route('accueil')->with('flash_notice', 'Votre compte a été créé.'); 
	}
	return Redirect::to('auth/inscription')->withErrors($v)->withInput(); 
}

En cas d’erreur de saisie on renvoie le formualire avec un message regroupant les problèmes rencontrés :

img84

Si tout est correct on renvoie sur la page d’accueil avec un message :

img85

La réinitialisation du mot de passe

Vous devez normalement déjà avoir dans la base la table password_reminders que nous avons créé dans un article précédent.

Le contrôleur et la route

On peut créer le contrôleur pour la réintialisation du mot de passe avec cette commande d’Artisan :

php artisan auth:reminders-controller

Ce qui a pour effet de créer un contrôleur nommé RemindersController. On va juste le modifier pour l’adapter à notre application :

<?php

class RemindersController extends Controller {

	/**
	 * Affiche le formulaire d'oubli
	 *
	 * @return Response
	 */
	public function getRemind()
	{
		return View::make('auth.remind', array('categories' => Categorie::all(), 'actif' => -1));
	}

	/**
	 * Traitement du formulaire d'oubli
	 *
	 * @return Response
	 */
	public function postRemind()
	{
		$response = Password::remind(Input::only('email'), function($message)
		{
		  $message->subject('Oubli du mot de passe.');
		});
		switch ($response)
		{
			case Password::INVALID_USER:
				return Redirect::back()->with('error', Lang::get($response))->withInput();

			case Password::REMINDER_SENT:
				return Redirect::back()->with('status', Lang::get($response))->withInput();
		}
	}

	/**
	 * Display the password reset view for the given token.
	 *
	 * @param  string  $token
	 * @return Response
	 */
	public function getReset($token = null)
	{
		if (is_null($token)) App::abort(404);

		return View::make('auth.reset', array('categories' => Categorie::all(), 'actif' => -1, 'token' => $token));
	}

	/**
	 * Handle a POST request to reset a user's password.
	 *
	 * @return Response
	 */
	public function postReset()
	{
		$credentials = Input::only(
			'email', 'password', 'password_confirmation', 'token'
		);

		$response = Password::reset($credentials, function($user, $password)
		{
			$user->password = Hash::make($password);

			$user->save();
		});

		switch ($response)
		{
			case Password::INVALID_PASSWORD:
			case Password::INVALID_TOKEN:
			case Password::INVALID_USER:
				return Redirect::back()->with('error', Lang::get($response))->withInput();

			case Password::PASSWORD_RESET:
				return Redirect::to('auth/login');
		}
	}

}

Comme il s’agit d’un contrôleur RESTful on va prévoir une route adaptée :

<?php
Route::controller('remind', 'RemindersController');

Le formulaire de demande de réinitialisation

Lorsqu’on clique sur le bouton « J’ai oublié mon mot de passe… » placé sur la page de connexion on active la méthode getRemind du contrôleur RemindersController :

<?php
public function getRemind()
{
	return View::make('auth.remind', array('categories' => Categorie::all(), 'actif' => -1));
}

Ce qui a pour effet d’activer la vue auth/remind.blade.php :

@extends('template_blog')

@include('navigation')

@section('content')

<br>
<div class="row col-md-12">
	<h3 class="col-md-12">
		Nouveau mot de passe
	</h3>
	@if (!Session::has('flash_notice'))
		<div class="col-md-12">
			Il semblerait que vous ayez oublié votre mot de passe. Ne vous affolez pas, nous allons vous donner la possibilité d'en créer un nouveau. Veuillez entrer dans ce formulaire l'adresse mail que vous avez utilisée pour votre inscription :
		</div>
	@endif
	<br />
	@if (Session::has('error'))
    <div class="col-md-8">
      <div class="alert alert-danger">
				{{ Session::get('error') }}         
      </div>
    </div>
	@endif
	@if (Session::has('status'))
		<div class="col-md-8">
			<div class="alert alert-success">
				{{ Session::get('status') }}
			</div>
		</div>
	@else
		<div class="col-md-8">
      {{ Form::open(array('url' => 'remind/remind', 'method' => 'POST', 'class' => 'form-horizontal well')) }}
			<div class="form-group {{ $errors->has('email') ? 'error' : '' }}">
				{{ Form::label('email', 'Mail :', array('class' => 'col-md-2 control-label')) }}
				<div class="col-md-10">
					{{ Form::text('email', '', $attributes = array('class' => 'form-control')) }}
				</div>
			</div>
      <div class="form-group">
          <div class="col-md-offset-2 col-md-10">
              {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}
          </div>
      </div>
			{{ Form::close()}}
		</div>
	@endif
</div>
@stop

Et la création de ce formulaire :

img86

Le traitement de la demande

Le traitement du formulaire est assuré par la méthode postRemind du contrôleur RemindersController :

<?php
public function postRemind()
{
	$response = Password::remind(Input::only('email'), function($message)
	{
	  $message->subject('Oubli du mot de passe.');
	});
	switch ($response)
	{
		case Password::INVALID_USER:
			return Redirect::back()->with('error', Lang::get($response))->withInput();

		case Password::REMINDER_SENT:
			return Redirect::back()->with('status', Lang::get($response))->withInput();
	}
}

Si l’utilisateur n’est pas trouvé, donc l’Email n’est pas valide ou n’existe pas dans la table,  on renvoie le formulaire avec mention de l’erreur :

img87

Si l’utilisateur est trouvé on lui présente un message de confirmation :

img88

On mémorise un jeton d’identification dans la table password_reminders :

img89

Et un Email est envoyé constitué avec la vue emails/auth/reminder.blade.php :

<!DOCTYPE html>
<html lang="fr-FR">
	<head>
		<meta charset="utf-8">
	</head>
	<body>
		<h2>Renouvellement de mot de passe</h2>

		<div>
			Pour renouveller votre mot de passe veuillez utiliser ce lien : 
			<p>
				{{ url('remind/reset/'.$token) }}
			</p>
		</div>
	</body>
</html>

J’ai adapté le code pour notre exemple et je l’ai francisé.

Lorsque l’utilisateur clique sur le lien il est dirigé sur la méthode getReset qui affiche le formulaire de saisi du nouveau mot de passe avec la vue auth/reset.blade.php :

@extends('template_blog')

@include('navigation')

@section('content')
	<div class="col-md-12">
		Pour entrer un nouveau mot de passe veuillez remplir ce formulaire :
	</div>
	<br>
	<div class="row col-md-12">
		@if (Session::has('error'))
			<div class="col-md-9">
        <div class="alert alert-danger">
    			{{ Session::get('error') }}
				</div>
			</div>
		@endif
		<div class="col-md-9">
	    {{ Form::open(array('url' => 'remind/reset/'.$token, 'method' => 'POST', 'class' => 'form-horizontal well')) }}
			{{ Form::hidden('token', $token)}}
			<div class="form-group">
				{{ Form::label('email', 'Mail :', array('class' => 'col-md-4 control-label')) }}
				<div class="col-md-8">
					{{ Form::text('email', '', $attributes = array('class' => 'form-control')) }}
				</div>
			</div>
			<div class="form-group">
				{{ Form::label('password', 'Mot de passe :', array('class' => 'col-md-4 control-label')) }}
				<div class="col-md-8">
					{{ Form::password('password', $attributes = array('class' => 'form-control')) }}
				</div>
			</div>
			<div class="form-group">
				{{ Form::label('password_confirmation', 'Confirmation passe :', array('class' => 'col-md-4 control-label')) }}
				<div class="col-md-8">
					{{ Form::password('password_confirmation', $attributes = array('class' => 'form-control')) }}
				</div>
			</div>
      <div class="form-group">
          <div class="col-md-offset-4 col-md-8">
              {{ Form::submit('Envoyer', array('class' => 'btn btn-success')) }}
          </div>
      </div>
			{{ Form::close()}}
		</div>
	</div>
@stop

Avec cet aspect :

img90

Le traitement se fait dans la méthode postReset :

<?php
public function postReset()
{
	$credentials = Input::only(
		'email', 'password', 'password_confirmation', 'token'
	);

	$response = Password::reset($credentials, function($user, $password)
	{
		$user->password = Hash::make($password);

		$user->save();
	});

	switch ($response)
	{
		case Password::INVALID_PASSWORD:
		case Password::INVALID_TOKEN:
		case Password::INVALID_USER:
			return Redirect::back()->with('error', Lang::get($response))->withInput();

		case Password::PASSWORD_RESET:
			return Redirect::to('auth/login');
	}
}

Là est effectuée une validation. Par exemple on peut obtenir ce résultat :

img91

On vérifie aussi si le jeton est valide sinon on envoie aussi un message :

img92

Si tout se passe bien on renvoie sur la page de connexion pour que l’utilisateur puisse se connecter avec son nouveau mot de passe.

4 réflexions sur “Laravel 4 : chapitre 24 : Un blog : authentification

  1. bestmomo dit :

    Salut,

    L’envoie de l’email se fait par la méthode Password::remind.

    En ce qui concerne le message d’erreur il faut renseigner la ligne :

    'from' => array('address' => null, 'name' => null),

    du fichier app/config/mail.php.

  2. alibaba dit :

    Bonjour,
    vous dîtes que si l’utilisateur est trouvé on lui présente un message de confirmation et un email est envoyé constitué avec la vue emails/auth/reminder.blade.php.
    Mes questions sont : Comment se fait l’envoie de mail?
    Quel est le script d’envoie de mail?
    Où se trouve le fichier qui contient le script d’envoie du mail?

    • alibaba dit :

      Je précise que je reçois un message d’erreur quand je clique sur le bouton Envoyer pour renouveler le mot de passe.
      Et le message d’erreur en question est :

      Swift_TransportException
      Cannot send message without a sender address

Laisser un commentaire