Créer une SPA avec authentification par rôles avec Laravel et Vue.js

Benoît Ripoche
20 min readSep 12, 2018

--

######################

Edit du 5 aout 2020

Ce tutoriel commence à être un peu ancien, et je n’ai pas beaucoup de temps pour le maintenir à jour (idem pour le répository Git). Il y a surement pas mal de choses à adapter en fonction des évolutions au niveau des packages et des propres spécificités de votre code :)

Note : Si vous êtes intéressés pour maintenir le répo Git, contactez moi via GitHub.

######################

Edit du 6 octobre 2019 : Mise à jour du tutoriel pour Laravel 6

Une des différences impactantes pour ce tutoriel lors du passage à Laravel 6 est la séparation de l’interface utilisateur dans un package composer séparé.

Les quelques différences concernant Laravel 6 sont listées ci dessous, le reste du tutoriel fonctionne correctement.

- Dans le fichier User.php, l’attribut “$casts” est maintenant présent de base dans Laravel. Pensez-y lors de la modification de ce fichier et si vous remplacez tout le contenu par celui de ce tutoriel, n’oubliez pas alors de rajouter le code suivant :

protected $casts = [
‘email_verified_at’ => ‘datetime’,
];

- Ajouter le package laravel/ui afin d’avoir Vue présent dans votre projet avec les commandes suivantes :

composer require laravel/ui

php artisan ui vue

npm install

Le répertoire Github est mis a jour avec la version 6 de Laravel sur cette branche : https://github.com/Pochwar/laravel-vue-spa/tree/laravel-6.1

######################

Dans cet article, je vais expliquer comment j’ai mis en place une SPA (Single Page Application) disposant d’un système d’authentification par rôles avec Laravel et Vue.js.

Le résultat de ce tutoriel est disponible sur GitHub à cette adresse : https://github.com/Pochwar/laravel-vue-spa.

English version available here: https://medium.com/@ripoche.b/create-a-spa-with-role-based-authentication-with-laravel-and-vue-js-ac4b260b882f.

Pour cet exemple, je vais utiliser Laravel 5.7 qui inclut Vue.js par défaut lors de son installation.

Je vais considérer que vous connaissez déjà Laravel et son environnement pour la suite de cet article, si ce n’est pas le cas, allez faire un tour sur la documentation ;)

Installation de Laravel

Dans un terminal, lancez la commande suivante pour installer un nouveau projet Laravel :

laravel new laravel-vue-spa

Note : pour utiliser la commande précédente, vous devez avoir installé le package ‘laravel-installer’ en global sur votre poste :

composer global require "laravel/installer"

Création des utilisateurs et des rôles

Une fois Laravel installé, nous allons ajouter un rôle aux utilisateurs et créer quelques utilisateurs de tests.

Pour les rôles, je vais me contenter d’un champ ‘rôle’ dans la table ‘users’, mais n’importe quel package de gestion des rôles pour Laravel pourra faire l’affaire.
Dans cet exemple, un utilisateur aura le rôle ‘1’ et un administrateur le rôle ‘2’.

Ajouter la ligne suivante à la migration ‘create_users_table’ :

$table->integer('role')->default(1);

Dans le but de simplifier au maximum cet exemple, j’ai rajouté l’option ‘nullable()’ au champ ‘name’ pour ne pas avoir a fournir de nom lors de l’enregistrement :

$table->string('name')->nullable();

Modifier le fichier DatabaseSeeder.php pour créer un utilisateur et un administrateur :

<?php

use App\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class DatabaseSeeder extends Seeder
{
public function run()
{
User::create([
'name' => 'Admin',
'email' => 'admin@test.com',
'password' => Hash::make('admin'),
'role' => 2
]);

User::create([
'name' => 'User',
'email' => 'user@test.com',
'password' => Hash::make('secret'),
'role' => 1
]);
}
}

Après avoir configuré l’accès à la BDD dans le fichier ‘.env’, lancer la commande suivante pour créer la table ‘users’ avec les 2 utilisateurs définis :

php artisan migrate --seed

Protection des routes de l’API avec JWT

Je vais utiliser le package tymondesigns/jwt-auth pour gérer l’authentification de l’API. Dans cet exemple j’utilise la version ‘dev-develop’ de ce package.

composer require tymon/jwt-auth:dev-develop

Publier la configuration de JWT avec la commande suivante :

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Cela va créer le fichier ‘config/jwt.php’.

Puis créer la clef secrète utilisée par JWT avec la commande :

php artisan jwt:secret

Cela va générer une variable d’environnement dans le fichier ‘.env’.

Ensuite, dans le fichier ‘config/auth.php’, remplacer le guard par défaut par ‘api’ :

'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],

Puis le driver du guard de l’API par ‘jwt’ :

'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],

'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],

Modifier le modèle User pour qu’il implémente l’interface ‘JWTSubject’ :

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
use Notifiable;

/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];

/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];

public function getJWTIdentifier()
{
return $this->getKey();
}

public function getJWTCustomClaims()
{
return [];
}
}

Note : ne pas oublier d’ajouter les deux méthodes getJWTIdentifier() et getJWTCustomClaims() requises par l’interface.

Dans le fichier ‘routes/api.php’, la route ‘api/user’ déjà existante utilise le middleware ‘auth:api’, maintenant configuré avec JWT.

Si on tente d’accéder à cette route dans le navigateur (http://127.0.0.1:8000/api/user), l’accès sera refusé et l’on obtient une page d’erreur avec le message : ‘Route [login] not defined.’

Note : pour accéder à l’application, lancer le serveur interne de Laravel avec la commande ‘php artisan serve’. L’URL par défaut est ‘http://127.0.0.1:8000’.

Ce comportement est défini dans le fichier ‘app/Http/Middleware/Authenticate.php’. Comme l’idée est de mettre en place une API renvoyant des réponses au format json et que la page pour se connecter sera mise en place avec Vue, il est possible (mais optionnel) de modifier ce fichier pour renvoyer une réponse au format json :

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware
{
public function handle($request, Closure $next, ...$guards)
{
if ($this->authenticate($request, $guards) === 'authentication_error') {
return response()->json(['error'=>'Unauthorized']);
}

return $next($request);
}

protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}

foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}

return 'authentication_error';
}
}
Capture d’écran de l’application sur la route de l’API ‘api/users/2’ après modification du fichier Authenticate.php

Création des endpoints d’authentification

Nous allons maintenant créer les endpoints servant à l’authentification. Dans le fichier ‘routes/api.php’, ajouter les routes suivantes :

Route::prefix('auth')->group(function () {
Route::post('register', 'AuthController@register');
Route::post('login', 'AuthController@login');
Route::get('refresh', 'AuthController@refresh');

Route::group(['middleware' => 'auth:api'], function(){
Route::get('user', 'AuthController@user');
Route::post('logout', 'AuthController@logout');
});
});

La route ‘api/auth/register’ va servir à créer un nouvel utilisateur.
La route ‘’api/auth/login’ va servir à se connecter.
La route ‘api/auth/refresh’ va servir à rafraîchir le token

Ces trois routes sont publiques.

La route ‘api/auth/user’ va servir a récupérer les informations de l’utilisateur.
La route ‘api/auth/logout’ va servir a se déconnecter.

Ces deux routes ne sont accessibles que pour un utilisateur connecté.

Il faut ensuite créer le contrôleur qui va traiter ces différentes requêtes :

php artisan make:controller AuthController

Cela va créer le fichier ‘app/Http/Controllers/AuthController.php’

Ajouter les méthodes requises tel que présenté ci-dessous :

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{

public function register(Request $request)
{
$v = Validator::make($request->all(), [
'email' => 'required|email|unique:users',
'password' => 'required|min:3|confirmed',
]);

if ($v->fails())
{
return response()->json([
'status' => 'error',
'errors' => $v->errors()
], 422);
}

$user = new User;
$user->email = $request->email;
$user->password = bcrypt($request->password);
$user->save();

return response()->json(['status' => 'success'], 200);
}

public function login(Request $request)
{
$credentials = $request->only('email', 'password');

if ($token = $this->guard()->attempt($credentials)) {
return response()->json(['status' => 'success'], 200)->header('Authorization', $token);
}

return response()->json(['error' => 'login_error'], 401);
}

public function logout()
{
$this->guard()->logout();

return response()->json([
'status' => 'success',
'msg' => 'Logged out Successfully.'
], 200);
}

public function user(Request $request)
{
$user = User::find(Auth::user()->id);

return response()->json([
'status' => 'success',
'data' => $user
]);
}

public function refresh()
{
if ($token = $this->guard()->refresh()) {
return response()
->json(['status' => 'successs'], 200)
->header('Authorization', $token);
}

return response()->json(['error' => 'refresh_token_error'], 401);
}

private function guard()
{
return Auth::guard();
}
}

Note : Dans un but de simplification, je ne fais ici que très peu de vérifications. J’utilise un validateur sur la méthode ‘register()’ en guise d’exemple, mais dans des cas d’utilisation réels, il faudrait le faire également pour la méthode ‘login()’ et utiliser des blocs ‘try/catch’ pour gérer les autres cas d’erreur (serveur, base de données, …)

La méthode ‘register()’ est relativement simple, elle se contente de créer un utilisateur avec les champs renvoyés en prenant soin de hacher le mot de passe auparavant.

La méthode ‘login()’ fait appel à la méthode Auth::guard() qui utilise JWT.
Ainsi, la méthode ‘attempt()’ qui vérifie les identifiants fournis va générer un token qui sera renvoyé dans les headers de la réponse en cas de succès.

La méthode ‘logout()’ va servir à déconnecter les utilisateurs en invalidant le token.

La méthode ‘user()’ va récupérer les informations de l’utilisateur connecté et les renvoyer dans les datas de la réponse.

La méthode ‘refresh()’ va servir a rafraîchir le token si celui ci est expiré. il est possible de définir la durée de validité du token dans le fichier ‘config/jwt.php’.

Ainsi, avec utilitaire comme Postman, il est possible de tester les endpoints et de s’enregistrer et se connecter.

Capture d’écran de Postman — Requête d’enregistrement avec des valeurs erronées
Capture d’écran de Postman — Requête d’enregistrement avec des valeurs correctes
Capture d’écran de Postman — Requête de connexion avec des valeurs erronées
Capture d’écran de Postman — Requête de connexion avec des valeurs correctes
Capture d’écran de Postman — Requête de connexion avec des valeurs correctes / visualisation du token dans les Headers

Protection des endpoints en fonction des rôles

Nous allons ensuite créer deux middlewares dont le but sera d’autoriser l’accès à telle ou telle ressource de l’API en fonction du rôle de l’utilisateur connecté.

Par exemple, un administrateur doit pouvoir accéder à la liste de tous les utilisateurs, tandis qu’un utilisateur ne doit pouvoir accéder qu’à ses propres informations.

Création des deux middlewares :

php artisan make:middleware CheckIsAdmin
php artisan make:middleware CheckIsAdminOrSelf

Cela va générer deux fichiers dans ‘app/http/Middleware’

Dans le premier, ‘CheckIsAdmin.php’, nous allons faire une simple vérification de si l’utilisateur connecté à le rôle administrateur. Remplacer le contenu du fichier par ce code :

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckIsAdmin
{
public function handle($request, Closure $next)
{
if(Auth::user()->role === 2) {
return $next($request);
}

else {
return response()->json(['error' => 'Unauthorized'], 403);
}
}
}

Dans le second middleware, ‘CheckIsAdminOrSelf.php’, nous allons vérifier que l’utilisateur est un administrateur ou bien que la ressource qu’il cherche a consulter ou altérer le concerne. Remplacer le contenu du fichier par ce code :

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckIsAdminOrSelf
{
public function handle($request, Closure $next)
{
$requestedUserId = $request->route()->parameter('id');

if(
Auth::user()->role === 2 ||
Auth::user()->id == $requestedUserId
) {
return $next($request);
}

else {
return response()->json(['error' => 'Unauthorized'], 403);
}
}
}

Il faut ensuite déclarer ces middlewares, dans le fichier ‘app/Http/kernel.php’. Ajouter ces deux lignes au tableau ‘$routeMiddleware’ :

'isAdmin' => \App\Http\Middleware\CheckIsAdmin::class,
'isAdminOrSelf' => \App\Http\Middleware\CheckIsAdminOrSelf::class,

Ensuite, nous allons créer les routes pour accéder aux infos des utilisateurs et les protéger avec les middlewares adéquats :

Route::group(['middleware' => 'auth:api'], function(){
// Users
Route::get('users', 'UserController@index')->middleware('isAdmin');
Route::get('users/{id}', 'UserController@show')->middleware('isAdminOrSelf');
});

Enfin, nous allons créer le contrôleur pour les utilisateur et y définir les méthodes ‘index()’ et ‘show()’.

php artisan make:controller UserController

Cela va générer le fichier ‘app/Http/Controllers/UserController.php’ dont voici le contenu :

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
public function index()
{
$users = User::all();

return response()->json(
[
'status' => 'success',
'users' => $users->toArray()
], 200);
}

public function show(Request $request, $id)
{
$user = User::find($id);

return response()->json(
[
'status' => 'success',
'user' => $user->toArray()
], 200);
}


}

Ainsi, le endpoint ‘api/users’ ne sera accessible que pour un administrateur, et le endpoint ‘api/users/2’ ne sera accessible que pour un administrateur et pour l’utilisateur dont l’id est le 2.

Capture d’écran de Postman — Requête d’accès à la ressource ‘user/2’ en étant connecté avec l’utilisateur dont l’id est le ‘3’

Mise en place du front avec Vue.js

Nous allons commencer par configurer Laravel pour qu’il utilise Vue. Celui ci est déjà installé, mais il va falloir implémenter son utilisation.

Tout d’abord, installer les dépendances front avec la commande :

npm install

Ensuite, ouvrir le fichier ‘resources/views/welcome.blade.php’ et remplacer son contenu par le code suivant :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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') }}" defer></script>

<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" rel="stylesheet" type="text/css">

<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<index></index>
</div>
</body>
</html>

Note : Ce qui est important ici est l’inclusion du script ‘js/app.js’, la balise ‘<div id=”app”>’ et la balise ‘<index>’

Créer ensuite le fichier ‘resources/js/Index.vue’ et y insérer le code suivant :

<template>
<div id="main">
<header id="header">
<h1>
Laravel Vue SPA
</h1>
</header>
<div id="content">
Bienvenue !
</div>
</div>
</template>

<script>
export default {
data() {
return {
//
}
},
components: {
//
}
}
</script>

Afin de compiler les fichiers js au fur et à mesure de la création de l’interface avec Vue, lancez la commande suivante :

npm run watch

Cela va faire tourner un script en continu qui compilera les fichiers à chaque fois qu’ils seront sauvegardés.

Ouvrir le fichier ‘resources/js/app.js’ et remplacer le contenu par le code suivant :

import './bootstrap'
import Vue from 'vue'
import Index from './Index'

// Set Vue globally
window.Vue = Vue

// Load Index
Vue.component('index', Index)

const app = new Vue({
el: '#app'
});

Après la compilation des fichiers, en accedant à la racine du site, vous devriez voir les éléments définis dans le fichier ‘Index.vue’.

Capture d’écran de l’application

Mise en place de l’authentification côté front

Pour gérer l’authentification et la gestion des droits d’accès côté front, nous allons utiliser le package ‘websanova/vue-auth’ ainsi que plusieurs autres dépendances : ‘vue-router’, ‘vue-axios’, ‘axios’ et ‘es6-promise’.

Installer les dépendances avec la commande suivante :

npm i @websanova/vue-auth vue-router vue-axios axios es6-promise

Créer le fichier ‘resources/js/auth.js’ et y insérer le code suivant :

import bearer from '@websanova/vue-auth/drivers/auth/bearer'
import axios from '@websanova/vue-auth/drivers/http/axios.1.x'
import router from '@websanova/vue-auth/drivers/router/vue-router.2.x'

// Auth base configuration some of this options
// can be override in method calls
const config = {
auth: bearer,
http: axios,
router: router,
tokenDefaultName: 'laravel-vue-spa',
tokenStore: ['localStorage'],
rolesVar: 'role',
registerData: {url: 'auth/register', method: 'POST', redirect: '/login'},
loginData: {url: 'auth/login', method: 'POST', redirect: '', fetchUser: true},
logoutData: {url: 'auth/logout', method: 'POST', redirect: '/', makeRequest: true},
fetchData: {url: 'auth/user', method: 'GET', enabled: true},
refreshData: {url: 'auth/refresh', method: 'GET', enabled: true, interval: 30}
}

export default config

Ce fichier sert à configurer ‘vue-auth’. Quelques précisions à ce sujet :
- ‘rolesVar’ sert a déterminer quel champ du modèle user est utilisé pour définir le rôle des utilisateurs. Si vous avez nommé votre champ différemment ou utilisé un package pour la gestion des rôles dans Laravel, pensez à modifier sa valeur en conséquence. (Note : Dans les dernières version du package, ‘rolesVar’ est à remplacer par ‘rolesKey’)
- ‘registerData’, ‘loginData’, ‘logoutData’, ‘fetchData’ et ‘refreshData’ servent à définir les endpoints de l’API que Vue-Auth va utiliser. J’ai ici défini les endpoints créés auparavant. Vous trouverez plus d’infos sur la configuration de Vue-Auth sur la documentation officielle :)

Créer le fichier ‘resources/js/router.js’ et y insérer le code suivant :

import VueRouter from 'vue-router'

// Pages
import Home from './pages/Home'
import Register from './pages/Register'
import Login from './pages/Login'
import Dashboard from './pages/user/Dashboard'
import AdminDashboard from './pages/admin/Dashboard'

// Routes
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: {
auth: undefined
}
},
{
path: '/register',
name: 'register',
component: Register,
meta: {
auth: false
}
},
{
path: '/login',
name: 'login',
component: Login,
meta: {
auth: false
}
},
// USER ROUTES
{
path: '/dashboard',
name: 'dashboard',
component: Dashboard,
meta: {
auth: true
}
},
// ADMIN ROUTES
{
path: '/admin',
name: 'admin.dashboard',
component: AdminDashboard,
meta: {
auth: {roles: 2, redirect: {name: 'login'}, forbiddenRedirect: '/403'}
}
},
]

const router = new VueRouter({
history: true,
mode: 'history',
routes,
})

export default router

Ce fichier sert à définir les différentes routes de l’application, et quels composants sont utilisés pour chacune. Le paramètre ‘meta’ permet de définir les règles d’accès pour chaque route.
- ‘auth:undefined’ sera utilisé pour les routes publiques
- ‘auth:true’ sera utilisé pour les routes accessibles uniquement a un utilisateur connecté ;
- ‘auth:false’ sera utilisé pour les routes accessibles uniquement a un utilisateur non connecté ;
- ‘auth: {roles: 2, …}’ sera utilisé pour les routes accessibles uniquement a un administrateur. On peut indiquer dans cet objet les regles de redirection si l’utilisateur n’est pas connecté ou si l’utilisateur n’a pas les droits.

Nous allons donc devoir créer ces composants.

Fichier ‘resources/js/pages/Home.vue’ :

<template>
<div class="container">
<div class="card card-default">
<div class="card-header">Bienvenue</div>

<div class="card-body">
<p>
American Main Barbary Coast scuttle hardtack spanker fire ship grapple jack code of conduct port. Port red ensign Shiver me timbers provost salmagundi bring a spring upon her cable pillage cog crow's nest lateen sail. Barbary Coast quarterdeck lass coffer keel hulk mizzen me square-rigged loot.
</p>
<p>
Yardarm starboard keelhaul list schooner prow booty cackle fruit gabion topmast. Plunder shrouds Nelsons folly jack Arr parley warp grog blossom ballast pressgang. Knave crack Jennys tea cup flogging log man-of-war hearties killick long clothes six pounders hulk.
</p>
</div>
</div>
</div>
</template>

Ce fichier sera la page d’accueil de l’application.

Fichier ‘resources/js/pages/Register.vue’ :

<template>
<div class="container">
<div class="card card-default">
<div class="card-header">Inscription</div>

<div class="card-body">
<div class="alert alert-danger" v-if="has_error && !success">
<p v-if="error == 'registration_validation_error'">Erreur(s) de validation, veuillez consulter le(s) message(s) ci-dessous.</p>
<p v-else>Erreur, impossible de s'inscrire pour le moment. Si le problème persiste, veuillez contacter un administrateur.</p>
</div>

<form autocomplete="off" @submit.prevent="register" v-if="!success" method="post">

<div class="form-group" v-bind:class="{ 'has-error': has_error && errors.email }">
<label for="email">E-mail</label>
<input type="email" id="email" class="form-control" placeholder="user@example.com" v-model="email">
<span class="help-block" v-if="has_error && errors.email">{{ errors.email }}</span>
</div>

<div class="form-group" v-bind:class="{ 'has-error': has_error && errors.password }">
<label for="password">Mot de passe</label>
<input type="password" id="password" class="form-control" v-model="password">
<span class="help-block" v-if="has_error && errors.password">{{ errors.password }}</span>
</div>

<div class="form-group" v-bind:class="{ 'has-error': has_error && errors.password }">
<label for="password_confirmation">Confirmation mot de passe</label>
<input type="password" id="password_confirmation" class="form-control" v-model="password_confirmation">
</div>

<button type="submit" class="btn btn-default">Inscription</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
name: '',
email: '',
password: '',
password_confirmation: '',
has_error: false,
error: '',
errors: {},
success: false
}
},

methods: {
register() {
var app = this
this.$auth.register({
data: {
email: app.email,
password: app.password,
password_confirmation: app.password_confirmation
},
success: function () {
app.success = true
this.$router.push({name: 'login', params: {successRegistrationRedirect: true}})
},
error: function (res) {
console.log(res.response.data.errors)
app.has_error = true
app.error = res.response.data.error
app.errors = res.response.data.errors || {}
}
})
}
}
}
</script>

Note : J’ai effectué la gestion des erreurs uniquement dans ce composant, de la même manière que je n’ai géré la validation côté API que pour la méthode ‘register()’. Dans un cas d’utilisation réelle, il faudra gérer les erreurs à tous les niveaux !

Fichier ‘resources/js/pages/Login.vue’ :

<template>
<div class="container">
<div class="card card-default">
<div class="card-header">Connexion</div>

<div class="card-body">
<div class="alert alert-danger" v-if="has_error">
<p>Erreur, impossible de se connecter avec ces identifiants.</p>
</div>
<form autocomplete="off" @submit.prevent="login" method="post">
<div class="form-group">
<label for="email">E-mail</label>
<input type="email" id="email" class="form-control" placeholder="user@example.com" v-model="email" required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" class="form-control" v-model="password" required>
</div>
<button type="submit" class="btn btn-default">Connexion</button>
</form>
</div>
</div>
</div>
</template>

<script>
export default {
data() {
return {
email: null,
password: null,
has_error: false
}
},

mounted() {
//
},

methods: {
login() {
// get the redirect object
var redirect = this.$auth.redirect()
var app = this
this.$auth.login({
params: {
email: app.email,
password: app.password
},
success: function() {
// handle redirection
const redirectTo = redirect ? redirect.from.name : this.$auth.user().role === 2 ? 'admin.dashboard' : 'dashboard'

this.$router.push({name: redirectTo})
},
error: function() {
app.has_error = true
},
rememberMe: true,
fetchUser: true
})
}
}
}
</script>

Fichier ‘resources/js/pages/user/Dashboard.vue’ :

<template>
<div class="container">
<div class="card card-default">
<div class="card-header">Dashboard</div>

<div class="card-body">
Bienvenue
</div>
</div>
</div>
</template>

<script>

export default {
data() {
return {
//
}
},
components: {
//
}
}
</script>

Fichier ‘resources/js/pages/admin/Dashboard.vue’ :

<template>
<div class="container">
<div class="card card-default">
<div class="card-header">Admin Dashboard</div>

<div class="card-body">
Bienvenue sur votre dashboard administrateur
</div>
</div>
</div>
</template>

<script>
export default {
mounted() {
//
}
}
</script>

Nous allons également modifier le fichier ‘resources/js/Index.vue’ et remplacer la chaine de caractère “Bienvenue” par le composant ‘<router-view>’ qui sert a afficher les composants en fonction de la route demandée.
La route ‘/’ apelle le composant ‘Home’, celui ci sera donc automatiquement chargé sur la page d’accueil.

Le fichier ‘Index.vue’ ressemble maintenant à cela :

<template>
<div id="main">
<header id="header">
<h1>
Laravel Vue SPA
</h1>
</header>
<div id="content">
<router-view></router-view>
</div>
</div>
</template>

<script>
export default {
data() {
return {
//
}
},
components: {
//
}
}
</script>

Enfin, il faut mettre à jour le fichier ‘resources/js/app.js’ afin d’implémenter ‘vue-auth’ et ‘vue-router’ :

import 'es6-promise/auto'
import axios from 'axios'
import './bootstrap'
import Vue from 'vue'
import VueAuth from '@websanova/vue-auth'
import VueAxios from 'vue-axios'
import VueRouter from 'vue-router'
import Index from './Index'
import auth from './auth'
import router from './router'

// Set Vue globally
window.Vue = Vue

// Set Vue router
Vue.router = router
Vue.use(VueRouter)

// Set Vue authentication
Vue.use(VueAxios, axios)
axios.defaults.baseURL = `${process.env.MIX_APP_URL}/api`
Vue.use(VueAuth, auth)

// Load Index
Vue.component('index', Index)

const app = new Vue({
el: '#app',
router
});

Note : La variable ‘axios.defaults.baseURL’ utilise la variable d’environnement ‘MIX_APP_URL’. Il faut définir cette variable dans le fichier ‘.env’

MIX_APP_URL="${APP_URL}"

Cette variable utilise elle même la variable ‘APP_URL’ utile à Laravel.
Il faut donc définir cette variable, pour ma part j’utilise l’URL fournie par la commande ‘php artisan serve’ qui est par défaut ‘http://127.0.0.1:8000’.

APP_URL=http://127.0.0.1:8000

Si vous utilisez un virtual host, pensez à modifier cette valeur en conséquence.

Ainsi, lors de la compilation des fichiers .js avec la commande ‘npm run dev’ ou ‘npm run watch’, toutes les variables d’environnement commençant par ‘MIX_’ seront prises en compte par le front.

Si vous actualisez votre page d’accueil, vous devriez voir apparaître le contenu du composant ‘Home.vue’.

Capture d’écran de l’application

Cependant, si vous essayez d’accéder à la page ‘/login’, vous devriez avoir une erreur. En effet, Laravel n’est actuellement configuré pour ne renvoyer qu’une seule route, l’index, qui affiche le template ‘welcome.blade.php’.

Pour résoudre cela, il faut éditer le fichier ’routes/web.php’ et ajouter les lignes suivantes :

// Route to handle page reload in Vue except for api routes
Route::get('/{any?}', function (){
return view('welcome');
})->where('any', '^(?!api\/)[\/\w\.-]*');

Ainsi, n’importe quelle URL renverra la view ‘welcome’ et vue-router se chargera d’afficher le composant correspondant.

La dernière ligne sert à ce que les routes de l’API ne soient pas prises en compte, ce qui empêcherait le bon fonctionnement de l’application !

Si vous essayez d’accéder à la page ‘/login’ vous devriez maintenant voir le formulaire de login. Et si vous tentez d’accéder à ‘/dashboard’ ou ‘/admin/dashboard’ vous devriez être renvoyé vers la page de login.

Capture d’écran de l’application — Page login

Si vous vous loguez, vous devriez accéder au dashboard correspondant à votre rôle. Ceci est géré dans le fichier ‘Login.vue’, dans la fonction callback de success, ligne 52 :

const redirectTo = redirect ? redirect.from.name : this.$auth.user().role === 2 ? 'admin.dashboard' : 'dashboard'

Cette ligne permet :
- soit de rediriger l’utilisateur vers la page d’où il venait auparavant s’il avait été redirigé vers la page de login. Ceci est géré par la variable ‘redirect’ déclarée plus haut avec ‘this.$auth.redirect()’. Cette méthode de Vue-Auth permet de savoir si l’utilisateur à été redirigé vers la page de login après avoir tenté d’accéder à une page protégée sans être connecté ;
- soit de rediriger vers le dashboard correspondant, en fonction du rôle, si l’utilisateur est arrivé tout seul sur la page de login.

Ajout des menus de navigation

L’application est fonctionnelle, il ne reste plus qu’à pouvoir naviguer. Pour cela nous allons ajouter un menu.

Créer le fichier ‘resources/js/components/Menu.Vue’ et y inserer le code suivant :

<template>
<nav id="nav">
<ul>
<!--UNLOGGED-->
<li v-if="!$auth.check()" v-for="(route, key) in routes.unlogged" v-bind:key="route.path">
<router-link :to="{ name : route.path }" :key="key">
{{route.name}}
</router-link>
</li>
<!--LOGGED USER-->
<li v-if="$auth.check(1)" v-for="(route, key) in routes.user" v-bind:key="route.path">
<router-link :to="{ name : route.path }" :key="key">
{{route.name}}
</router-link>
</li>
<!--LOGGED ADMIN-->
<li v-if="$auth.check(2)" v-for="(route, key) in routes.admin" v-bind:key="route.path">
<router-link :to="{ name : route.path }" :key="key">
{{route.name}}
</router-link>
</li>
<!--LOGOUT-->
<li v-if="$auth.check()">
<a href="#" @click.prevent="$auth.logout()">Logout</a>
</li>
</ul>
</nav>
</template>

<script>
export default {
data() {
return {
routes: {
// UNLOGGED
unlogged: [
{
name: 'Inscription',
path: 'register'
},
{
name: 'Connexion',
path: 'login'
}
],

// LOGGED USER
user: [
{
name: 'Dashboard',
path: 'dashboard'
}
],
// LOGGED ADMIN
admin: [
{
name: 'Dashboard',
path: 'admin.dashboard'
}
]
}
}
},
mounted() {
//
}
}
</script>

Puis, dans le fichier ‘resources/js/index.vue’, remplacer le contenu par le code suivant :

<template>
<div id="main">
<header id="header">
<h1>
<router-link :to="{name: 'home'}">
Laravel Vue SPA
</router-link>
</h1>
<navigationMenu></navigationMenu>
</header>
<div id="content">
<router-view></router-view>
</div>
</div>
</template>

<script>
import navigationMenu from './components/Menu.vue'
export default {
data() {
return {
//
}
},
components: {
navigationMenu
}
}
</script>

On importe le composant de navigation créé pour l’afficher en dessous du titre dans le header. On ajoute également un lien sur le titre, avec la balise ‘<router-link>’ pour rediriger vers la page d’accueil.

Capture d’écran de l’application

Call axios pour récupérer des datas depuis l’API

Maintenant que le système d’authentification est géré et en back et en front, nous allons pouvoir récupérer des datas depuis l’API pour les afficher dans Vue.

Nous allons récupérer la liste des utilisateurs pour l’afficher dans le dashboard admin.

Créer le fichier ‘resources/js/components/user-list.vue’ et y insérer le contenu suivant :

<template>
<div>
<h3>Liste de utilisateurs</h3>
<div class="alert alert-danger" v-if="has_error">
<p>Erreur, impossible de récupérer la liste des utilisateurs.</p>
</div>

<table class="table">
<tr>
<th scope="col">Id</th>
<th scope="col">Nom</th>
<th scope="col">Email</th>
<th scope="col">Date d'inscription</th>
</tr>
<tr v-for="user in users" v-bind:key="user.id" style="margin-bottom: 5px;">
<th scope="row">{{ user.id }}</th>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.created_at}}</td>

</tr>
</table>

</div>
</template>

<script>
export default {
data() {
return {
has_error: false,
users: null
}
},

mounted() {
this.getUsers()
},

methods: {
getUsers() {
this.$http({
url: `users`,
method: 'GET'
})
.then((res) => {
this.users = res.data.users
}, () => {
this.has_error = true
})
}
}
}
</script>

Note : on utilise la méthode ‘$http()’ de vue-auth qui va automatiquement ajouter le token dans le header de la requête auprès de l’API.

Puis, dans le fichier ‘resources/js/pages/admin/Dashboard.vue’, remplacer le contenu par le code suivant :

<template>
<div class="container">
<div class="card card-default">
<div class="card-header">Admin Dashboard</div>

<div class="card-body">
Bienvenue sur votre dashboard administrateur
</div>

</div>
<div class="card card-default">
<div class="card-header">Liste des utilisateurs</div>

<div class="card-body">
<userList></userList>
</div>
</div>
</div>
</template>

<script>
import userList from '../../components/user-list.vue'
export default {
mounted() {
//
},
components: {
userList
}
}
</script>

Ainsi, lorsqu’un administrateur se connecte à son dashboard, il peut maintenant voir la liste des utilisateurs.

Capture d’écran de l’application

Note : si vous essayez de faire la même chose dans le dashboard utilisateur et que vous vous connectez avec un utilisateur standard, vous devriez obtenir un message d’erreur. En effet, le endpoint ‘/api/users’ est protégé et uniquement accessible pour les administrateurs !

Conclusion

Ce tutoriel est terminé, j’espère qu’il vous aidera à mettre en place de belles applications avec Laravel et Vue :)

Ceci est mon premier article sur Medium, n’hésitez pas à me faire des retours, que ce soit au niveau de la rédaction ou sur la qualité du code.

je me suis inspiré de plusieurs tutoriels traitant du même sujet mais n’allant pas assez loin à mon sens dans la protection des routes et dans la gestion des rôles.

Si vous pensez que j’ai mal expliqué une partie, que j’ai fait des erreurs dans la manière de procéder, ou si vous n’arrivez pas au résultat escompté après avoir suivi à la lettre ce tutoriel, faites le moi savoir et je m’efforcerais de vous répondre !

--

--