Easy Jwt authentication & Crud using Laravel & Vue js & MySQL

Sajad Asadi
Vue.js Developers
Published in
21 min readJun 24, 2019

Hello everyone!

In this course, I’m going to teach you how to make a website with Jwt authentication and Vue js as front end and of course Laravel as backend

For this purpose I will use my own Laravel package, my package have some improvement in Vue side of Laravel such as Installed more packages like Vuex and Vue router and so on … and made a boilerplate using data domain view pattern. my package also has some improvement into Laravel MVC sides such as some tools for validating rest API requests and configured JWT and a boilerplate and so on …

This is my packages GitHub link: https://github.com/aratta-studios/aratta-laravel-boilerplate

It’s better to have a little bit of experience with Vue.js, Vuex, Vuetify and the Laravel itself, especially in rest API to get the best result from this tutorial. But if you don’t have, you still can follow the instructions, maybe it’s time to make the first or one of your first experiences.

Also after editing a file in a section usually I will put the final file at the end of that section for that you can see if you haven’t imported a library by a chance or etc.

Project Setup & Configuration

First, install this provided installer:

composer global require aratta-studios/aratta-laravel

For creating the project and initial setup, open your terminal in your working folder and use this command:

aratta-laravel ArattaLaravelProject

Then in phpMyAdmin, create a database named:

aratta_laravel_db

Now in .env file change these fields like this:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=aratta_laravel_db
DB_USERNAME=yourUserName
DB_PASSWORD=yourPassword

Ps: if you using XAMPP for DB and has not changed the configurations, Username is root and no password needed

Next in config/database.php file change these fields in line 51,52,53 like this:

'database' => env('DB_DATABASE', 'aratta_laravel_db'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),

Now your project is ready for coding.

Migrations and creating required tables

Migrations in Laravel are really useful, besides creating the tables and specifying fields and types and relations and so on … we can use this tool if we moved our project in a new server with a new and empty database, this tool will recreate all of our tables just with a command.
Read more in this link:

In our case, we need to create two tables, a users table, and a contacts table.

Create users migration using this command:

php artisan make:migration create_users_table

Create contacts migration using this command:

php artisan make:migration create_contacts_table

After executing these two commands two files will be created in this address:

database/migrations

There is probably another file besides this two you can just delete that file.

In these two files, we need to specify our tables fields before using them to create a table.
By default, they have two attributes: id and timestamps. But we need more fields like username and password for users table.

So we will edit: (maybe your file starts by another date/time in the name)

database/migrations/2019_06_23_150340_create_users_table.php

Like below:

<?phpuse Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('username',25);
$table->string('password');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
*
@return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}

And we will edit: (maybe your file starts by another date/time in the name)

database/migrations/2019_06_23_150353_create_contacts_table.php

Like below:

<?phpuse Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateContactsTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('phone_number',13);
$table->string('name',25);
$table->bigInteger('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
*
@return void
*/
public function down()
{
Schema::dropIfExists('contacts');
}
}

We can have a user and our user can have a number of contacts. actually, we are going to make an online phone book so every user can log in/signup then they can create/read/update/delete their contacts.

Now after specifying our tables, fields, and relations, we need to create them in our DB using this command:

php artisan migrate

This command will create three tables in aratta_laravel_db.

Two tables as we specified above and a table named migrations to manage migrations in the future.

Now we have a database and a well-configured Laravel application with our MySQL database.

Creating initial models

Models are actually our gateways for using the database in the app.

You can read more about models in this link:

In my package of Laravel, you can easily generate models by these two commands:

php artisan krlove:generate:model User --table-name=usersphp artisan krlove:generate:model Contact --table-name=contacts

I have used the eloquent-model-generator library created by a guy named Andrii Mishchenko you can read more about it in GitHub using this link:

After executing the above commands two files will be generated in your app folder Contact.php, User.php.
Also, there is an Example.php that you can delete it I just made it for example.

We will back to these files for crud operation and authentication…

Now let’s create our controller.

Creating an initial controller

You can read about them in https://laravel.com/docs/5.8/controllers

In my package, I provided a generator to generate our controllers for API purposes. actually, it’s not too much but it is honest work :-D
You can easily generate a controller using this command:

php ControllerGenerator.php --name=PhoneBookController

It will generate a PhoneBookController.php file in your app/Http/Controllers/Api folder.

There is another file named ExampleAppController.php you can delete it I made this for example purposes.

Authentication config

I know, I know the course started to get boring.

But we need to be prepared before getting into action.

Now we need to do some configs for authentication purposes.

Open this file:

config/auth.php

Then you need to change the default guard name in line 18 like this:

'defaults' => [
'guard' => 'user'
],

Then change guards object in line 38 like this:

'guards' => [
'user' => [
'driver' => 'jwt',
'provider' => 'users',
]
]
,

then change providers in line 62 like this:

'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
],

Now open the app/User.php file then change line 17 like this:

class User extends Authenticatable implements JWTSubject

Now if you are using PhpStorm just hit Alt+Enter in this line to generate Authenticatable functions.

Then edit getJWTIdentifier function in the file like this:

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

Then edit getJWTCustomClaims function in the file like this:

public function getJWTCustomClaims()
{
return [];
}

Then add getAuthPassword function like this:

/**
* Get the password for the user.
*
*
@return string
*/
public function getAuthPassword()
{
return $this->password;
}

Now your Jwt auth config is set.

Adding some functions in the models

For crud operations or any other DB related purposes, we need to create functions in the model.

Some coders using eloquent in their controllers but this is a mistake especially if you have complicated or conditional queries.

For user registration and adding the user data in DB create this function in the User model, in app/User.php:

public static function create(Request $request)
{
$user = new User();
if (!empty($request->get('username'))) {
$user->username = $request->get('username');
}
if (!empty($request->get('password'))) {
$user->password = bcrypt($request->get('password'));
}
$user->save(); return $user;
}

This function receives the request from a controller function that we will create in the next sections, then adds a user into DB.

This will be enough for our user Model so finished user model looks like this:

<?phpnamespace App;use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Contracts\JWTSubject;
/**
*
@property integer $id
*
@property string $username
*
@property string $password
*
@property string $created_at
*
@property string $updated_at
*
@property Contact[] $contacts
*/
class User extends Authenticatable implements JWTSubject
{
/**
* The "type" of the auto-incrementing ID.
*
*
@var string
*/
protected $keyType = 'integer';
/**
*
@var array
*/
protected $fillable = ['username', 'password', 'created_at', 'updated_at'];
/**
*
@return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function contacts()
{
return $this->hasMany('App\Contact');
}
public static function create(Request $request)
{
$user = new User();
if (!empty($request->get('username'))) {
$user->username = $request->get('username');
}
if (!empty($request->get('password'))) {
$user->password = bcrypt($request->get('password'));
}
$user->save(); return $user;
}
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
*
@return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
*
@return array
*/
public function getJWTCustomClaims()
{
return [];
}
/**
* Get the password for the user.
*
*
@return string
*/
public function getAuthPassword()
{
return $this->password;
}
}

Now let’s create some functions in Contact model inapp/Contact.php like this:

public static function create(Request $request) {
$contact = new Contact();
$contact->user_id = $request->User()->id;
$contact->phone_number = $request->get('phone_number');
$contact->name = $request->get('name');
$contact->save(); return $contact;}public static function updateOne(Request $request) {
$contact = Contact::where('id',$request->get('id'))->first();
$contact->phone_number = $request->get('phone_number');
$contact->name = $request->get('name');
$contact->save(); return $contact;}public static function deleteOne(Request $request) {
Contact::where('id',$request->get('id'))->delete();
}
public static function readAll(Request $request) {
return Contact::where('user_id',$request->User()->id)->get();
}

The first function named create, this function will create a contact in our contacts table.

The second function named updateOne will update a contact by given id in our contacts table.

The third function named the deleteOne will delete a contact by given id from our contacts table.

The fourth function named the readAll will return all contacts that created by a User using given user_id.

Now we’re done with the Contact model so the final file looks like this:

<?phpnamespace App;use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
/**
*
@property integer $id
*
@property integer $user_id
*
@property string $phone_number
*
@property string $name
*
@property string $created_at
*
@property string $updated_at
*
@property User $user
*/
class Contact extends Model
{
/**
* The "type" of the auto-incrementing ID.
*
*
@var string
*/
protected $keyType = 'integer';
/**
*
@var array
*/
protected $fillable = ['user_id', 'phone_number', 'name', 'created_at', 'updated_at'];
/**
*
@return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User');
}
public static function create(Request $request) {
$contact = new Contact();
$contact->user_id = $request->User()->id;
$contact->phone_number = $request->get('phone_number');
$contact->name = $request->get('name');
$contact->save(); return $contact; } public static function updateOne(Request $request) {
$contact = Contact::where('id',$request->get('id'))->first();
$contact->phone_number = $request->get('phone_number');
$contact->name = $request->get('name');
$contact->save(); return $contact; } public static function deleteOne(Request $request) {
Contact::where('id',$request->get('id'))->delete();
}
public static function readAll(Request $request) {
return Contact::where('user_id',$request->User()->id)->get();
}
}

Adding controller functions

We are getting closer and closer to the action.

Now we need to create functions that will return data to front-end or validation purposes using models if it’s needed.

Open the“app/Http/Controllers/API/PhoneBookController.php” file.

Then add a function for signup like this:

/**
* Register a new user
*
@param Request $request
*
@return JsonResponse
*/
public function signUp(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->username,
$request->password
],
function (Request $request) {
User::create($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
}

This function will check if valid request parameters received from front-end using my InputHelper then if it’s valid the callback function will be called as we see above, the function will call Create, a function that we just made in previous steps in the user model. then Returns a success message to front-end.

Now for login purpose, we need to add these two functions.

The first one checks if the user entered the correct username and password using the guard function. If the combination is correct then this function will return a successful HTTP response with an Authorization header having token inside to the user.

The second is the guard function that produces a JWT token if the username and password combination is correct.

/**
* Login user and return a token
*
@param Request $request
*
@return JsonResponse
*/
public function login(Request $request)
{
$token = $this->guard($request->username, $request->password);
if ($token) {
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->header('Authorization', $token)->send();
} else {
return ResponseHelper::jsonResponse(null, Response::HTTP_BAD_REQUEST, config('messages.fail'))->send();
}
}
/**
* Return auth guard
*
@param $username
*
@param $password
*
@return Auth
*/
private function guard($username, $password)
{
return Auth::guard('user')->attempt(array('username' => $username, 'password' => $password));
}

Now we need to do contacts related controller functions.

public function createContact(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->phone_number,
$request->name
],
function (Request $request) {
Contact::create($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
}public function updateContact(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->id,
$request->phone_number,
$request->name
],
function (Request $request) {
Contact::updateOne($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
}public function deleteContact(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->id
],
function (Request $request) {
Contact::deleteOne($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
}public function readContacts(Request $request)
{
InputHelper::inputChecker(
$request,null,
function (Request $request) {
$contacts = Contact::readAll($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, $contacts)->send();
});
}

These functions just doing the crud operation using functions that we just created in the previous step in the Contact model.

Now we have done with the controller.

Our “app/Http/Controllers/API/PhoneBookController.php” file looks like this:

<?phpnamespace App\Http\Controllers\API;use App\Contact;
use App\Http\Controllers\Controller;
use App\Http\Helpers\InputHelper;
use App\Http\Helpers\ResponseHelper;
use App\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class PhoneBookController extends Controller
{
/**
* Register a new user
*
@param Request $request
*
@return JsonResponse
*/
public function signUp(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->username,
$request->password
],
function (Request $request) {
User::create($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
} /**
* Login user and return a token
*
@param Request $request
*
@return JsonResponse
*/
public function login(Request $request)
{
$token = $this->guard($request->username, $request->password);
if ($token) {
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->header('Authorization', $token)->send();
} else {
return ResponseHelper::jsonResponse(null, Response::HTTP_BAD_REQUEST, config('messages.fail'))->send();
}
} public function createContact(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->phone_number,
$request->name
],
function (Request $request) {
Contact::create($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
} public function updateContact(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->id,
$request->phone_number,
$request->name
],
function (Request $request) {
Contact::updateOne($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
} public function deleteContact(Request $request)
{
InputHelper::inputChecker(
$request,
[
$request->id
],
function (Request $request) {
Contact::deleteOne($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, config('messages.success'))->send();
});
} public function readContacts(Request $request)
{
InputHelper::inputChecker(
$request,null,
function (Request $request) {
$contacts = Contact::readAll($request);
return ResponseHelper::jsonResponse(null, Response::HTTP_OK, $contacts)->send();
});
}
/**
* Return auth guard
*
@param $username
*
@param $password
*
@return Auth
*/
private function guard($username, $password)
{
return Auth::guard('user')->attempt(array('username' => $username, 'password' => $password));
}
}

Adding routes

To make our made controllers and models in previous sections usable, we need to specify routes (URLs) for users or front-end apps to access and use them.

For this purpose we will open this file:

routes/api.php

Then edit the file in this way

<?php
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// Below mention routes are public, user can access those without any restriction.
// Create New User
Route::post('v1/sign-up', 'Api\PhoneBookController@signUp');
// Login User
Route::post('v1/login', 'Api\PhoneBookController@login');
Route::middleware('auth:user')->prefix('v1/')->group(function () {
Route::post('/create-contact', 'API\PhoneBookController@createContact');
Route::post('/update-contact', 'API\PhoneBookController@updateContact');
Route::post('/delete-contact', 'API\PhoneBookController@deleteContact');
Route::post('/read-contacts', 'API\PhoneBookController@readContacts');
});
Route::get('/login',function(){
return ('Login Fail !!!');
})->name('login');

Some routes like login and signup that user can’t be authenticated to use them, so we put them outside Route::middleware.

Now we are done with back-end (boring) part, let’s do some fun actions.

Vue pattern, files, folders introduction

At first, if you don’t know anything from vuex or flux pattern and states, It’s better to read these links:

But if you don’t have time, don’t worry! you can see them in action in the next sections.

For working with front-end (Vue side of the project) this is our folder:

resources/js

In this folder, we have three folders:

  • Data
  • UI
  • Store

All of our data for using in logic or showing in UI will Come from classes in the data folder like APIs, local storage, conversions and so on…

In the UI folder, we have two folders Components, Pages.
Vue classes in the pages folder are usually connected to Vuex stores (Logic) and they are for a specified purpose like log in or panel or dashboard.
But Components folder classes are for general purposes like showing a chart or a button, that can be used in multiple pages. these classes are not connected to Vuex (Logic) and these classes are Dependent to pages for data.

In the Stores folder, we keep our Vuex store modules. each page has a Vuex module to do the related logic for that page through its actions and mutations and stores related states for that page.

Arrows are data.

Cleanup Vue example files

I have created some example files to show how my Vue pattern works but for this tutorial, We don’t need them so we will delete some files and edit some other to prepare out phone book app.

Go to

resources/js/ui/pages

And delete these files:

dashboard.vue
panel.vue
sample.vue

Then go to

resources/js/ui/components

And delete all files.

Then go to

resources/js/store

And delete these files:

dashboard.js
sample.js

Then open

resources/js/router.js

And replace all contents with this:

import Vue from "vue"
import VueRouter from "vue-router"
import PhoneBookPage from './ui/pages/phoneBook';
import LoginPage from './ui/pages/login';
import store from './store/index';
Vue.use(VueRouter);function guard(to, from, next){
if(store.state.login.isLoggedIn === true) {
// or however you store your logged in state
next(); // allow to enter route
} else{
next('/login'); // go to '/login';
}
}
export default new VueRouter({
mode: 'history',
routes: [
{
path: '',
name: 'PhoneBook',
component: PhoneBookPage,
beforeEnter: guard
},
{
path: '/login',
name: 'Login',
component: LoginPage
}
]
})

Then open

resources/js/store/index.js

And replace all content with this

import Vue from "vue"
import Vuex from "vuex"
import axios from 'axios'
import VueAxios from 'vue-axios'
import login from './login';
Vue.use(Vuex, VueAxios, axios);export default new Vuex.Store({
namespaced: true,
modules: {
login
},
state: {
windowHeight: 0,
windowWidth: 0
},
actions: {
// Shared actions
},
mutations: {
[
'setWindowHeight'](state, {windowHeight}) {
state.windowHeight = windowHeight;
},
['setWindowWidth'](state, {windowWidth}) {
state.windowWidth = windowWidth;
}
}
,
getters: {
// Shared getters
}
})

Generating a Vue page and store

The last sections were too fast.

Now we need to generate a page and store to be able to show contacts in there.

You can generate these just by this command:

php VuePageGenerator.php --name=phoneBook

This command adds a file named phoneBook.Vue inside resources/js/UI/pages folder

And a file named phonebook.js inside resources/js/store

You have to import the file inside resources/js/store/index.js file like this:

import Vue from "vue"
import Vuex from "vuex"
import axios from 'axios'
import VueAxios from 'vue-axios'
import login from './login';
import phoneBook from './phoneBook';
Vue.use(Vuex, VueAxios, axios);export default new Vuex.Store({
namespaced: true,
modules: {
login,
phoneBook
},
state: {
windowHeight: 0,
windowWidth: 0
},
actions: {
// Shared actions
},
mutations: {
[
'setWindowHeight'](state, {windowHeight}) {
state.windowHeight = windowHeight;
},
['setWindowWidth'](state, {windowWidth}) {
state.windowWidth = windowWidth;
}
}
,
getters: {
// Shared getters
}
})

Let’s see what we have

Now it’s time to see some pictures :-D

If we run this command:

php artisan serve

And this command in another command line window:

npm run watch

Then if we open this link in our browser:

http://localhost:8000

We will be redirected to this link:

http://localhost:8000/login

And we will see the login page.

It happens because we don’t have any token saved in our localStorage.

If you don’t know what localStorage is, read this:

Our app will check if our authorization token saved in the local storage or not.

If not, we will be redirected to the login/signup page to fetch and save one by logging in.

If yes well, we will continue to the main page.

Adding signup to the login page and making them work

Open login store module file from this address:

resources/js/store/login.js

Add mode field to the states function like this:

const state = {
isLoggedIn: !!getToken(),
pending: false,
showPassword: false,
password: '',
username: '',
mode:'login'
};

Now we can use this state to switch functionality of the form.

Then add these three mutations to switch this state to the mutations object like this:

const mutations = {
[
'login'](state) {
state.pending = true;
},
['loginSuccess'](state) {
state.isLoggedIn = true;
state.pending = false;
},
['logout'](state) {
state.isLoggedIn = false;
},
['setUsername'](state, username) {
state.username = username;
},
['setPassword'](state, password) {
state.password = password;
},
['togglePassword'](state) {
state.showPassword = !state.showPassword;
},
['loginMode'](state) {
state.mode = 'login';
},
['signUpMode'](state) {
state.mode = 'signUp';
},
['toggleMode'](state) {
state.mode = state.mode === 'signUp'? 'login': 'signUp';
}
}
;

Then add a SignUp and loginOrSignUp function to the actions object and uncomment login function content like this:

// actions
const actions = {
loginOrSignUp({dispatch,state}) {
if (state.mode === 'login') {
dispatch('login')
}
else {
dispatch('signUp')
}
}
,
login({commit}) {
commit('login');
fetchLogin().then(jsonResponse => {
saveToken(jsonResponse.headers.authorization);
commit('loginSuccess');
router.push('/')
})
;
},
logout({commit}) {
commit('logout');
removeToken();
router.push('/login')
}
,
signUp({commit}) {
commit('login')
fetchSignUp().then(jsonResponse => {
commit('loginSuccess');
commit('loginMode');
})
}
}
;

Import two fetchLogin and fetchSignUp functions at the top of the file:

import {fetchLogin, fetchSignUp} from "../data/api";

Now open

resources/js/data/api.js

then delete fetchCars function cause we don’t need it and it’s just for example purposes.

Now edit the fetchLogin function and add a fetchSignup function this way:

export function fetchLogin() {
let state = store.state.login;
return post('/api/v1/login', {
username: state.username,
password: state.password
});
}
export function fetchSignUp() {
let state = store.state.login;
return post('/api/v1/sign-up', {
username: state.username,
password: state.password
});
}

Now let’s prepare UI for our changes.

Open

resources/js/ui/pages/login.vue

Edit it this way:

<template>
<
v-layout align-center justify-center>
<
v-card>
<
v-card-title primary-title>
<
v-layout column>
<
v-radio-group @change="toggleMode" v-model="mode" row>
<
v-radio label="Login" value="login"></v-radio>
<
v-radio label="Sign Up" value="signUp"></v-radio>
</
v-radio-group>
<
v-img height="250px" width="250px" style="border-radius: 50%;"
src="https://avatars1.githubusercontent.com/u/52023818">
<
template v-slot:placeholder>
<
v-layout
fill-height
align-center
justify-center
ma-0
>
<
v-progress-circular indeterminate color="black darken-5"></v-progress-circular>
</
v-layout>
</
template>
</
v-img>
<
v-form
ref="form"
lazy-validation
id="login_form"
method="POST"
@submit.prevent="loginOrSignUp"
>
<v-text-field
:value="username"
label="Username"
required
text-align="right"
name="username"
@input="setUsername"
data-vv-name="username"
></v-text-field> <v-text-field
:value="password"
:type="showPassword ? 'text' : 'password'"
:append-icon="showPassword ? 'visibility' : 'visibility_off'"
label="Password"
required
data-vv-name="password"
name="password"
@input="setPassword"
text-align="right"
@click:append="togglePassword"
></v-text-field>
<v-btn
fill-width
color="success"
type="submit"
>
<
v-progress-circular v-show="this.pending === true" indeterminate
color="white"></v-progress-circular>
<
div v-show="this.pending === false">{{mode=== 'login'? 'Login' : 'signUp'}}</div>
</
v-btn>
</
v-form>
</v-layout>
</
v-card-title>
</
v-card>
</
v-layout>
</
template>
<script>
import {mapActions, mapMutations, mapState} from "vuex";
import router from '../../router';
export default {
name: "login",
methods: {
...mapMutations({
setUsername: 'login/setUsername',
setPassword: 'login/setPassword',
togglePassword: 'login/togglePassword',
loginMode: 'login/loginMode',
signUpMode: 'login/signUpMode',
toggleMode: 'login/toggleMode'
}),
...mapActions({
loginOrSignUp: 'login/loginOrSignUp'
}),
},
computed: {
...mapState({
isLoggedIn: state => state.login.isLoggedIn,
pending: state => state.login.pending,
showPassword: state => state.login.showPassword,
password: state => state.login.password,
username: state => state.login.username,
mode: state => state.login.mode
})
}
}
</
script>
<style scoped></style>

Now our login page is something like this:

We can easily do a signup/login

After a successful signup/login, we encounter a white screen.

Actually, it’s okay because our phoneBook page is empty.

Creating the phonebook page

First, we must create the required API function in resources/js/data/api.js

So we add this function

export function fetchReadContacts() {
return post('/api/v1/read-contacts');
}

Then we must use this function to fetch contacts so we will edit our resources/js/store/phoneBook.js in this way:

import {fetchReadContacts} from "../data/api";

const state = {
contacts:[],
pending: false
};
// getters
const getters = {
}
;
// actions
const actions = {
readContacts({commit}) {
commit('fetchingContacts');
fetchReadContacts().then(jsonResponse => {
commit('setContacts',jsonResponse.data.data)
})
;
}
}
;
// mutations
const mutations = {
[
'fetchingContacts'](state) {
state.pending = true;
},
['setContacts'](state,contacts) {
state.pending = false;
state.contacts = contacts;
}
}
;
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

Now we can use the data in our page so we can edit our page like this:

<template>
<
v-container>
<
v-layout align-center justify-center column>
<
v-card
title="Simple Table"
text="Here is a subtitle for this table"
class="scroll-y"
width="1000px"
height="500px"
align-center
justify-center
>
<
v-data-table
:headers="headers"
:items="contacts"
hide-actions
>
<
template
slot="headerCell"
slot-scope="{ header }"
>
<
span class="subheading font-weight-light text-success text--darken-3"
v-text="header.text"
></span>
</
template>
<
template
slot="items"
slot-scope="{ item }"
>
<
td>{{ item.name }}</td>
<
td>{{ item.phone_number }}</td>
</
template>
</
v-data-table>
</
v-card>
</
v-layout>
</
v-container>
</
template>
<script>
import {mapActions, mapMutations, mapState} from "vuex";
export default {
name: "phoneBook",
methods: {
...mapMutations({
// setUsername: 'phoneBook/setUsername',
// setPassword: 'phoneBook/setPassword',
// togglePassword: 'phoneBook/togglePassword'
}),
...mapActions({
readContacts: 'phoneBook/readContacts'
}),
},
mounted: function () {
this.readContacts();
},
data(){
return {
headers: [
{
text: 'Name',
align: 'center',
value: 'name'
},
{text: 'Phone Number', value: 'phone_number',align: 'center'}
]
}
}
,
computed: {
...mapState({
contacts: state => state.phoneBook.contacts,
pending: state => state.phoneBook.pending,
})
}
}
</
script>

Finally, if we successfully logged in we will see something like this ( I have inserted some contacts for my user in DB to see them for now )

Completing the CRUD

To complete crud, we must first add required functions inside

resources/js/data/api.js

After that, your file looks like this

import axios from "axios";
import {getToken} from "../data/localStorage";
import store from '../store/index';

export function fetchLogin() {
let state = store.state.login;
return post('/api/v1/login', {
username: state.username,
password: state.password
});
}

export function fetchSignUp() {
let state = store.state.login;
return post('/api/v1/sign-up', {
username: state.username,
password: state.password
});
}

export function fetchReadContacts() {
return post('/api/v1/read-contacts');
}

export function fetchCreateContact() {
let state = store.state.phoneBook;
return post('/api/v1/create-contact', {
name: state.selectedItem.name,
phone_number: state.selectedItem.phone_number
});
}


export function fetchUpdateContact() {
let state = store.state.phoneBook;
return post('/api/v1/update-contact', {
id: state.selectedItem.id,
name: state.selectedItem.name,
phone_number: state.selectedItem.phone_number
});
}


export function fetchDeleteContact() {
let state = store.state.phoneBook;
return post('/api/v1/delete-contact', {
id: state.selectedItem.id
});
}



export function post(url,creds) {
return axios.post(url, creds, {
headers: {
Authorization: getToken()
}
}
)
;

}

export function get(url) {
return axios.get(url, {
headers: {
Authorization: getToken()
}
}
)
;
}

Then add required methods and functions into

resources/js/store/phoneBook.js

After that, your file looks like this

import {fetchCreateContact, fetchDeleteContact, fetchReadContacts, fetchUpdateContact} from "../data/api";

const state = {
contacts: [],
pending: false,
selectedItem: {name: null, phone_number: null},
isEditMode: false,
dataDialog: false,
deleteDialog: false
};

// getters
const getters = {};

// actions
const actions = {
readContacts({commit}) {
commit('fetchingContacts');
fetchReadContacts().then(jsonResponse => {
commit('setContacts', jsonResponse.data.data)
})
;
},
addOrEdit({state, commit, dispatch}) {
if(state.isEditMode === true) {
dispatch('edit');
} else {
dispatch('add');
}
}
,
deleteContact({commit,dispatch}) {
commit('setPending',true);
fetchDeleteContact().then(()=> {
commit('setPending',false);
commit('hideDeleteDialog');
dispatch('readContacts');
})
}
,
add({commit,dispatch}) {
commit('setPending',true);
fetchCreateContact().then(()=> {
commit('setPending',false);
commit('hideDataDialog');
dispatch('readContacts');
})
}
,
edit({commit,dispatch}) {
commit('setPending',true);
fetchUpdateContact().then(()=> {
commit('setPending',false);
commit('hideDataDialog');
dispatch('readContacts');
})
}
}
;

// mutations
const mutations = {
[
'fetchingContacts'](state) {
state.pending = true;
},
['setContacts'](state, contacts) {
state.pending = false;
state.contacts = contacts;
},
['setSelectedItem'](state, selectedItem) {
state.selectedItem = selectedItem;
},
['setPhoneNumber'](state, phone_number) {
state.selectedItem.phone_number = phone_number;
},
['setName'](state, name) {
state.selectedItem.name = name;
},
['setEditMode'](state, editMode) {
state.isEditMode = editMode;
},
['setPending'](state, pending) {
state.pending = pending;
},
['hideDeleteDialog'](state) {
state.deleteDialog = false
},
['showDeleteDialog'](state) {
state.deleteDialog = true
},
['hideDataDialog'](state) {
state.dataDialog = false
},
['showDataDialog'](state) {
state.dataDialog = true
},
};

export default {
namespaced: true,
state,
getters,
actions,
mutations
}

Then add required Vue codes inside

resources/js/ui/pages/phoneBook.vue

After that, your file looks like this

<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<
v-container>
<
v-layout align-center justify-center column>
<
v-card
title="Simple Table"
text="Here is a subtitle for this table"
class="scroll-y"
width="1000px"
height="500px"
align-center
justify-center
>
<
v-data-table
:headers="headers"
:items="contacts"
hide-actions
>
<
template
slot="headerCell"
slot-scope="{ header }"
>
<
span class="subheading font-weight-light text-success text--darken-3"
v-text="header.text"
></span>
</
template>
<
template
slot="items"
slot-scope="{ item }"
>
<
td>{{ item.name }}</td>
<
td>{{ item.phone_number }}</td>
<
td class="justify-center layout px-0">
<
v-icon
small
class="mr-2"
@click="editItem(item)"
>
edit
</v-icon>
<
v-icon
small
@click="deleteItem(item)"
>
delete
</v-icon>
</
td>
</
template>
</
v-data-table>

</
v-card>

<v-btn color="primary" dark class="mt-2" @click="addItem">New Contact</v-btn>

</v-layout>
<
v-dialog
v-model="dataDialog"
width="500"
>
<
v-card>
<
v-card-text>
<
v-container grid-list-md>
<
v-layout wrap>
<
v-flex xs12 sm6 md4>
<
v-text-field @change="setName" v-model="selectedItem.name" label="Name"></v-text-field>
</
v-flex>
<
v-flex xs12 sm6 md4>
<
v-text-field @change="setPhoneNumber" v-model="selectedItem.phone_number" label="Phone Number"></v-text-field>
</
v-flex>
</
v-layout>
</
v-container>
</
v-card-text>

<
v-card-actions>
<
v-spacer></v-spacer>
<
v-btn color="blue darken-1" flat @click="hideDataDialog">Cancel</v-btn>
<
v-btn color="blue darken-1" flat @click="addOrEdit">
<
v-progress-circular v-show="this.pending === true" indeterminate
color="white"></v-progress-circular>
<
div v-show="this.pending === false">Save</div>
</
v-btn>
</
v-card-actions>
</
v-card>
</
v-dialog>
<
v-dialog
v-model="deleteDialog"
width="500"
>

<
v-card>
<
v-card-text>
Are you sure that you want delete this contact?
</v-card-text>

<
v-card-actions>
<
v-spacer></v-spacer>
<
v-btn color="blue darken-1" flat @click="hideDeleteDialog">Cancel</v-btn>
<
v-btn color="blue darken-1" flat @click="deleteContact">
<
v-progress-circular v-show="this.pending === true" indeterminate
color="white"></v-progress-circular>
<
div v-show="this.pending === false">Delete</div>
</
v-btn>
</
v-card-actions>
</
v-card>
</
v-dialog>

</
v-container>
</
template>


<
script>
import {mapActions, mapMutations, mapState} from "vuex";

export default {
name: "phoneBook",
methods: {
...mapMutations({
setName: 'phoneBook/setName',
setPhoneNumber: 'phoneBook/setPhoneNumber',
setContacts: 'phoneBook/setContacts',
setSelectedItem: 'phoneBook/setSelectedItem',
setEditMode: 'phoneBook/setEditMode',
setPending: 'phoneBook/setPending',
hideDeleteDialog: 'phoneBook/hideDeleteDialog',
showDeleteDialog: 'phoneBook/showDeleteDialog',
hideDataDialog: 'phoneBook/hideDataDialog',
showDataDialog: 'phoneBook/showDataDialog'

}),
...mapActions({
readContacts: 'phoneBook/readContacts',
addOrEdit: 'phoneBook/addOrEdit',
deleteContact: 'phoneBook/deleteContact'
}),
editItem (item) {
this.setSelectedItem(item);
this.setEditMode(true);
this.showDataDialog();
},
addItem () {
this.setSelectedItem ({name: '',phone_number: ''}) ;
this.setEditMode(false);
this.showDataDialog();
},
deleteItem (item) {
this.setSelectedItem(item);
this.showDeleteDialog()
}
,
},
mounted: function () {
this.readContacts();
},
data(){
return {
headers: [
{
text: 'Name',
align: 'center',
value: 'name'
},
{text: 'Phone Number', value: 'phone_number',align: 'center'}
]
,
}
}
,
computed: {
...mapState({
contacts: state => state.phoneBook.contacts,
pending: state => state.phoneBook.pending,
selectedItem: state => state.phoneBook.selectedItem,
isEditMode: state => state.phoneBook.isEditMode,
dataDialog: state => state.phoneBook.dataDialog,
deleteDialog: state => state.phoneBook.deleteDialog
})

}
}
</
script>

Now if we run the project we will see something like below pictures

Well, now we have finished this tutorial.
I have just pasted a bunch of codes in the last section. It was on purpose, the best way of using that is you try to analyze codes yourself and relate them to other sections…

Example Github repo

If you have any problem or issue there is a working version of this project that I have made it at the same time of producing this tutorial that you can see in this link

https://github.com/aratta-studios/laravel-vue-jwt-crud-example-project

Peace

--

--