Create a SPA with role-based authentication with Laravel and Vue.js

Benoît Ripoche
Sep 18, 2018 · 18 min read

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

Edit of 6 october 2019 : Update tutorial for Laravel 6

One of the main difference for this tutorial when switching to version 6 of Laravel is the separation of the UI in a separate composer package.

The few differences concerning Laravel 6 are listed below, the rest of the tutorial works fine.

- In file User.php, the attribute “$casts” is now present in Laravel as a base. Think about it while modifying this file, if you replace the whole content by the one of this tutorial, don’t forget to add the followinf code:

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

- Add the package laravel/ui to get Vue in your project with the following commands:

composer require laravel/ui

php artisan ui vue

npm install

Github repository is updated with the version 6 of Laravel on this branch: https://github.com/Pochwar/laravel-vue-spa/tree/laravel-6.1

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

In this article, i’m gonna explain how I did implement a SPA (Single Page Application) with a role-based authentication with Laravel and Vue.js.

The result is available on GitHub at this address : https://github.com/Pochwar/laravel-vue-spa.

Version française disponible ici : https://medium.com/@ripoche.b/cr%C3%A9er-une-spa-avec-authentification-par-r%C3%B4les-avec-laravel-et-vue-js-e69782ac6896.

For this example, I’m gonna use Laravel 5.7 which includes Vue.js by default during its installation.

I will consider that you already know Laravel and its environment for the rest of this article, if it is not the case, go take a look at the documentation ;)

Laravel Installation

In a terminal, run the following command to install a fresh Laravel project:

laravel new laravel-vue-spa

Note : to use the previous command, you need to have the ‘laravel-installer’ package globally installed on your computer:

composer global require laravel/installer

Creation of users and roles

Once laravel installed, let’s add roles to users and create some test users.

For the roles, I’m just gonna use a ‘role’ field in the users table, but any role manager package could do the trick.
In this example, a user will have role ‘1’ and an administrator, role ‘2’.

Add the following line to the ‘create_users_table’ migration:

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

In order to simplify this example as much as possible, I added the ‘nullable()’ option to the ‘name’ field. this allow me not to provide a name when registering:

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

Update DatabaseSeeder.php file to create an user and an administrator:

<?phpuse 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
]);
}
}

After configuring database access in the ‘.env’ file, run the following command to create the ‘users’ table with the two defined users:

php artisan migrate --seed

API route protection with JWT

I’ll use tymondesigns/jwt-auth package to handle API authentication. in this example, I use the ‘dev-develop’ version of this package.

composer require tymon/jwt-auth:dev-develop

Publish JWT configuration with the following command:

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

This will create ‘config/jwt.php’ file.

Then, create JWT secret key with this command:

php artisan jwt:secret

This will generate an environment varaible in the ‘.env’ file.

Then, in the ‘config/auth.php’ file, replace default guard by ‘api’:

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

Then the driver of the API guard by ‘jwt’:

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

modify the User model to implement ‘JWTSubject’ interface:

<?phpnamespace 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 : don’t forget to add getJWTIdentifier() and getJWTCustomClaims() methods that are required by the interface.

In the ‘routes/api.php’ file, the already existing ‘api/user’ route uses the ‘auth:api’ middleware, now configured with JWT.

If we try to access this route in the browser (http://127.0.0.1:8000/api/user), access will be refused and we’ll get an error page with this message: ‘Route [login] not defined.’

Note : to access the application, launch the internal Laravel server with the ‘php artisan serve’ command. Default URL is ‘http://127.0.0.1:8000’.

This behaviour is defined in the ‘app/Http/Middleware/Authenticate.php’ file. As the point is to set up an API that send json formatted responses, and that the login page will be handled by Vue, it’s possible (but optionnal) to modify this file to send a json formatted response:

<?phpnamespace 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';
}
}
Application screenshot on the ‘api/users/2’ route after Authenticate.php file modification

Authentication endpoints creation

We will now create authentication endpoints. In the ‘routes/api.php’ file, add the following lines:

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');
});
});

The ‘api/auth/register’ route will be used to create users.
The ‘api/auth/login’ route will be used to login.
The ‘api/auth/refresh’ route will be used to refresh token.

Thoses three routes are public.

The ‘api/auth/user’ route will be used to fetch user’s informations.
The ‘api/auth/logout’ route will be used to logout.

Thoses tow routes are reachable by connected users only.

Then, we need to create the controller that will handle these requests:

php artisan make:controller AuthController

This will create the ‘app/Http/Controllers/AuthController.php’ file.

Add required methods as presented below:

<?phpnamespace 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: For the sake of simplification, I am doing very little checking here. I use a validator on the ‘register()’ method as an example, but in real use cases, it should also be done for the ‘login()’ method and use ‘try / catch’ blocks to manage other error cases (server, database, …).

The ‘register()’ method is quite simple, it merely creates a user with the returned fields, taking care of hashing the password before.

The ‘login()’ method uses the Auth::guard() method that uses JWT.
Thus, the ‘attempt()’ method which checks the provided credentials will generate a token which will be returned in the headers of the response if successful.

The ‘logout()’ method will be used to disconnect users by disabling the token.

The ‘user()’ method will retrieve the logged user’s informations and send it back to the response dataset.

The ‘refresh()’ method will refresh the token if it has expired. It is possible to define the duration of validity of the token in the file ‘config / jwt.php’.

Thus, with utility like Postman, it is possible to test the endpoints to register and connect.

Postman screenshot — Register request with wrong dataset
Postman screenshot — Register request with correct dataset
Postman screenshot —Login request with wrong dataset
Postman screenshot — Login request with correct dataset
Postman screenshot — Login request with correct dataset / token visualisation in Headers

Endpoints protection by role

We will now create two middlewares which purpose will be to allow access to API resources depending on the user’s role.

For example, an administrator should be alble to access the list of all users, while a user should only be able to access his own informations.

Let’s create the middlewares:

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

This will create two files in ‘app/http/Middleware’.

in the first one, ‘CheckIsAdmin.php’, we will do a simple check of wheter the logged user has admin role. Replace file content by this code:

<?phpnamespace 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);
}
}
}

In the other middleware, ‘CheckIsAdminOrSelf.php’, we will check that the user is an admin or the the resource he seeks to consult or update concerns him. Replace file content by this code:

<?phpnamespace 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);
}
}
}

Now let’s declare these middlewares in the ‘app/Http/kernel.php’ file. Add the following lines to the ‘$routeMiddleware’ array:

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

Then, wee will create routes to access user’s informations and protect them with the appropriate middlewares:

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

Finally, we will create the controller for users and define ‘index()’ and ‘show()’ methods.

php artisan make:controller UserController

This will generate the ‘app/Http/Controllers/UserController.php’ file of which here is the content:

<?phpnamespace 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);
}
}

Thus, the ‘api/users’ endpoint will only be accessiblene for administrators, and the ‘api/users/2’ endpoint will only be accessible for administrators and the user with id 2.

Postman screenshot — Request to ‘user/2’ while being connected as user 3

Setup front with Vue.js

We will start by configuring Laravel for using Vue. Vue is already installed but we have to implement its usage.

First, install front dependencies with this command:

npm install

Then, open the ‘resources/views/welcome.blade.php’ file and replace its content by the following code:

<!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 : What matters here is inclusion of script ‘js/app.js’, the ‘<div id=”app”>’ tag and the ‘<index>’ tag.

then create ‘resources/js/Index.vue’ file and add following code:

<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>

To compile js files as creating the interface with Vue, run the following command:

npm run watch

This will run a continuous script that will compile files each time they are saved.

Open the ‘resources/js/app.js’ file and replace its content by the following code:

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'
});

After compilation, by accessign application in browser, you should see elements defined if the ‘Index.vue’ file.

Application screenshot

Setup front-side authentication

To handle authnetication and right access front-side, we will use the package ‘websanova/vue-auth’ as well as several other dependencies: ‘vue-router’, ‘vue-axios’, ‘axios’ and ‘es6-promise’.

Install dependancies with the following command:

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

Create ‘resources/js/auth.js’ file and insert the following code:

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

This file is used for ‘vue-auth’ configuration. Some clarifications:
- ‘rolesVar’ is used to determine which user model field is used to define the user’s role. If you nammed your field differently or use a package to handle roles in Laravel, modify this value consequently.
- ‘registerData’, ‘loginData’, ‘logoutData’, ‘fetchData’ and ‘refreshData’ are used to define API endpoints that Vue-Auth is gonna use. Here I define endpoints previously created. You will find more informations about Vue-Auth configuration on the official documentation :)

Create the ‘resources/js/router.js’ file and insert the following code:

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

This file is used to define the differents routes of the application, and which components are used for each one of them. The ‘meta’ parameter is used to define the access rules for each route.
- ‘auth:undefined’ will be used for public routes;
- ‘auth:true’ will be used for routes only accessible for connected users;
- ‘auth:false’ will be used for routes only accessible for unconnected users;
- ‘auth: {roles: 2, …}’ will be used for routes only accessible for an administrator. In this object, we can provide redirection rules if the user is not conected or if he has’nt the rights to access.

Let’s create theses components.

File ‘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>

This file will be application homepage.

File ‘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 : I handle errors only in this component, in the same way I only managed API validation in the ‘register()’ method. In real use cases, errors have to be handled in every components!

File ‘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>

File ‘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>

File ‘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>

We will now modify the ‘resources/js/Index.vue’ file and replace the string “Bienvenue” by the ‘<router-view>’ component that will display component depending on the requested route. The ‘/’ route calls the ‘Home’ component that will be displayed on the homepage.

The ‘Index.vue’ file looks like this now:

<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>

Finally, we have to update the ‘resources/js/app.js’ file in order to implement ‘vue-auth’ and ‘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 : The varaible ‘axios.defaults.baseURL’ uses the environment variable ‘MIX_APP_URL’. We have to define this variable in the ‘.env’ file.

MIX_APP_URL="${APP_URL}"

this variable uses itself the ‘APP_URL’ variable useful for Laravel.
We have to define this variable, I use the URL I get from the ‘php artisan serve’ command, that is by default ‘http://127.0.0.1:8000’.

APP_URL=http://127.0.0.1:8000

If you use a virtual host, modify this value consequently.

Thus, when compiling .js files with ‘npm run dev’ or ‘npm run watch’ commands, all environment variables starting with ‘MIX_’ will be taken into account by the front.

If you refresh your homepage, you should see the content of the ‘Home.vue’ component.

Application screenshot

However, if you try to access the ‘/login’ page, you should have an error. Indeed, Laravel is currently configured to return only one route, the index, which displys the template ‘welcome.blade.php’.

To solve this, we have to edit the ’routes/web.php’ file and add these lines:

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

Thus, any URL will return the ‘welcome’ view and vue-router will display the corresponding component.

The last line is for the API routes not to be taken in account, which would break the application.

If you try to access the ‘/login’ page, you should now see the login form. And if you try to access ‘/dashboard’ or ‘/admin/dashboard’ you should be redirect to the login page.

Application screenshot — login page

If you log in, you should acces to the dashboard corresponding to your role.
This behaviour is handled in the ‘Login.vue’ component, line 52:

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

this line allows:
- either to redirect the user to the page he came before, if he had been redirected to the login page. This is handled by the ‘redirect’ variable declared few ines before with ‘this.$auth.redirect()’. This Vue-Auth method allows to know if the user has been redirected after attempt to access a restricted page;
- either to redirect to the corresponding dashbord depending on the role.

Add navigation menu

The application is functionnal, now let’s add a nivigation menu.

Create the ‘resources/js/components/Menu.Vue’ file and insert the following code:

<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>

Then in the ‘resources/js/index.vue’ file, replace the content by the following code:

<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>

We import the navigation component to display it under the title in the header. On importe le composant de navigation créé pour l’afficher en dessous du titre dans le header. We also add a link on the title, with the ‘router-link’ tag, to redirect to the home page.

Application screenshot

Fetch API data with axios

Now that authentication system is setup in back and in front, let’s fetch some data from the API to display them in Vue.

We will get the users list to disply it in the admin dashboard.

Create the ‘resources/js/components/user-list.vue’ file and insert the followin code:

<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: we use the ‘$http()’ method from Vue-Auth that will automatically add the token in the request headers.

Then, in the ‘resources/js/pages/admin/Dashboard.vue’ file, replace the content by the following code:

<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>

Thus, when an admin connects to its dashboard, he can now see the users list.

Application screenshot

Note: if you try the same in the user’s dashboard, you should have an error message. Indeed, the endpoint ‘/api/users’ is protected an only accessible for administrators!

Conclusion

This tutorial is finished, I hope it will help you to st up beatiful applications with Laravel and Vue :)

This is my first article on Medium (and the first time I translate a text in english), so do not hesitate to make feedbacks, whether at level of writing, translation or code quality.

I have been inspired by several tutorials dealing with this subject, but not going far enough in my opinion in the protection of routes and in the management of roles.

If you think I did not explain a part correctly, that I made mistakes in the way of proceeding, or if you don’t get the expected result after following this tutorial, let me know and i’ll do my best to answer you!

Benoît Ripoche

Written by

Fullstack web-developer PHP JavaScript

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade