Laravel
24-11-2021
Laravel fortify authentication Api with SPA(Nuxt js) and mailgun
Make laravel authentication Api and use it in Nuxt js SPA

Laravel Api

step 1

create a new laravel project

laravel new LaravelApi

step 2

Install laravel fortify

composer require laravel/fortify
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"

step 3

Add sanctum middleware to api middleware group in your
app/Http/Kernel.php

'api' => [
           \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
           'throttle:api',
           \Illuminate\Routing\Middleware\SubstituteBindings::class,
           \Illuminate\Session\Middleware\StartSession::class
       ],

step 4

add FortifyServiceProvider class to your conig/app.php file

/*
        * Application Service Providers...
        */
       App\Providers\AppServiceProvider::class,
       App\Providers\AuthServiceProvider::class,
       // App\Providers\BroadcastServiceProvider::class,
       App\Providers\EventServiceProvider::class,
       App\Providers\RouteServiceProvider::class,
       App\Providers\FortifyServiceProvider::class,

step 5

create .env file and add the url to your SPA apllication

SPA_URL=http://localhost:3000

Here our SPA is running on port 3000.

step 6

In your config/fortify.php replace this line

'home' => RouteServiceProvider::HOME,

by

'home' => env('SPA_URL'),

so that you can be redirect to your SPA root page after logging.
PS: Feel free to redirect the user to another route on your SPA after logging
Example if you want to redirect your user to ‘/dashbord’ route after logging just do like this

'home' => env('SPA_URL') . ‘/dashbord’,

After turn also the views to false as we don’t need these routes in our project

>> 'views' => false,

step 7

Change your routes/api.php to

<?php
 
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laravel\Fortify\Http\Controllers\RegisteredUserController;
use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController;
use Laravel\Fortify\Http\Controllers\PasswordResetLinkController;
use Laravel\Fortify\Http\Controllers\NewPasswordController;
 
use Laravel\Fortify\Features;
/*
/*
|--------------------------------------------------------------------------
| 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!
|
*/
 
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
   return $request->user();
});
 
// Register
 
if (Features::enabled(Features::registration())) {
 
   Route::post('/register', [RegisteredUserController::class, 'store'])
       ->middleware(['guest:'.config('fortify.guard')]);
}
 
// Login
 
$limiter = config('fortify.limiters.login');
 
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
   ->middleware(array_filter([
       'guest:'.config('fortify.guard'),
       $limiter ? 'throttle:'.$limiter : null,
]));
 
// Reset Password
if (Features::enabled(Features::resetPasswords())) {
  
   Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])
       ->middleware(['guest:'.config('fortify.guard')]);
 
   Route::post('/reset-password', [NewPasswordController::class, 'store'])
       ->middleware(['guest:'.config('fortify.guard')]);
}
 

step 8

Manage reset password link which will be send to your email address by changing your
**App\Providers\AuthServiceProvider ** to

<?php
 
namespace App\Providers;
 
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Auth\Notifications\ResetPassword;
class AuthServiceProvider extends ServiceProvider
{
   /**
    * The policy mappings for the application.
    *
    * @var array
    */
   protected $policies = [
       // 'App\Models\Model' => 'App\Policies\ModelPolicy',
   ];
 
   /**
    * Register any authentication / authorization services.
    *
    * @return void
    */
   public function boot()
   {
       $this->registerPolicies();
 
       ResetPassword::createUrlUsing(function (object $notifiable, string $token) {
           return env('SPA_URL') . '/reset-password?token=' . $token . "&&email=" .$notifiable->getEmailForPasswordReset();
       });
      
   }
}

step 9

Add mailgun mail service provider to send rest password email to your users

in your ** .env** file add

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=your_mailGun_smtp_username
 
MAIL_PASSWORD=your_mailGun_smtp_password
MAIL_ENCRYPTION=tls
MAIL_FROM_NAME="${APP_NAME}"
MAIL_FROM_ADDRESS=test@test.com

PS: Sanbox subdomain is used for test only so to do so you have to register your receiver’s email address so that the mail can be sent.

Nuxt js SPA

step 1

Create your nuxtjs project

yarn create nuxt-app NuxtSpa

step 2

Add dotenv package

 yarn add dotenv

In your nuxt.config.js file
add before the ‘export default{}’ the following line

require("dotenv").config();

step 3

create a .env file and add this

API_URL = http://localhost:8000/api
APP_ENV=local
APP_NAME=NuxtSpa

PS: API_URL is the url of your laravel Api backend

Now in your nuxt.config.js file add the following line of code:

env: {
   baseUrl: process.env.API_URL || 'laravel Api'
 }

step 4

Create a new folder shared/config and create the file api.config.js with the following code

export const API_URL = process.env.baseUrl;

step 5

In the store folder
create a new folder called auth comtaining

actions.js file with the code

import { AUTHENTICATION_MODULE_MUTATIONS } from "./mutations";
import authService from "~/shared/services/auth.service";
 
export const AUTHENTICATION_MODULE_ACTIONS = {
 REGISTER: "register",
 LOGIN: "login",
 SEND_RESET_PASSWORD_EMAIL: "send_reset_password_email"
};
 
export default {
 [AUTHENTICATION_MODULE_ACTIONS.REGISTER]({ commit }, { email,name, password }) {
   return new Promise((resolve, reject) => {
     authService
       .register({ email, name, password })
       .then((response) => {
         commit(
           AUTHENTICATION_MODULE_MUTATIONS.SET_IS_AUTHENTICATED);
       })
       .catch((error) => reject(error));
   });
 },
 [AUTHENTICATION_MODULE_ACTIONS.LOGIN]({ commit }, { email, password }) {
   return new Promise((resolve, reject) => {
     authService
       .login({ email, password })
       .then((response) => {
         commit(
           AUTHENTICATION_MODULE_MUTATIONS.SET_IS_AUTHENTICATED);
       })
       .catch((error) => reject(error));
   });
 },
 
 [AUTHENTICATION_MODULE_ACTIONS.SEND_RESET_PASSWORD_EMAIL]({ commit }, { email }) {
   return new Promise((resolve, reject) => {
     authService
       .send_reset_password_email({ email })
       .then((response) => {
         console.log(response.data)
       })
       .catch((error) => reject(error));
   });
 },
 [AUTHENTICATION_MODULE_ACTIONS.RESET_PASSWORD]({ commit }, { email,password,password_confirmation,token }) {
   return new Promise((resolve, reject) => {
     authService
       .reset_password({ email,password,password_confirmation,token })
       .then((response) => {
         console.log(response.data)
       })
       .catch((error) => reject(error));
   });
 },
};
 
export const getAuthModule = "auth";
export const getAuthModuleComponent = (key) => `auth/${key}`;
 

getters.js with the code

export const AUTHENTICATION_MODULE_GETTERS = {
   ERRORS: "errors",
   IS_AUTHENTICATED: "isAuthenticated",
 };
  export default {
   errors: (state) => state.errors,
   isAuthenticated: (state) => state.isAuthenticated,
 };

mutaions.js with the code

export const AUTHENTICATION_MODULE_MUTATIONS = {
   SET_ERRORS: "setErrors",
   SET_IS_AUTHENTICATED: "setIsAuthenticated",
 };
  export default {
   [AUTHENTICATION_MODULE_MUTATIONS.SET_ERRORS](state) {
     state.errors = true;
   },
    [AUTHENTICATION_MODULE_MUTATIONS.SET_IS_AUTHENTICATED](state) {
     state.isAuthenticated = true;
   },
 
 };

state.js withe code

const inBrowser = typeof window !== "undefined";
 
const defaultState = { errors: null, isAuthenticated: false };
 
export default () =>
 inBrowser && window.__INITIAL_STATE__ ? window.__INITIAL_STATE__.page : defaultState;

In your store/index.js add the following code

import createLogger from "vuex/dist/logger";
import { getAuthModuleComponent } from "./auth/actions";
import { AUTHENTICATION_MODULE_MUTATIONS } from "./auth/mutations";
 
const debug = process.env.NODE_ENV !== "production";
 
export const plugins = debug ? [createLogger()] : [];
 
export const actions = {
 nuxtServerInit({ commit }, { req }) {
   if (req.session) {
     commit(getAuthModuleComponent(AUTHENTICATION_MODULE_MUTATIONS.SET_IS_AUTHENTICATED));
   }
 },
};

step 6

In your shared folder create a new folder named as services

In this create a new file api.service.js with the following code

import axios from 'axios'
 
const apiService = {
 init() {
   axios.defaults.withCredentials = true
 },
 
 setHeader() {
   axios.defaults.headers.common['X-Api-Client'] = `web`
 },
 
 query(resource, config) {
   this.setHeader()
   return axios.get(resource, config).catch((error) => {
     return Promise.reject(error)
   })
 },
 
 get(resource) {
   this.setHeader()
   return axios.get(`${resource}/`).catch((error) => {
     return Promise.reject(error)
   })
 },
 
 post(resource, data, config) {
   this.setHeader()
   return axios.post(`${resource}`, data, config).catch((error) => {
     return Promise.reject(error)
   })
 },
 
 patch(resource, data, config) {
   this.setHeader()
   return axios.patch(`${resource}`, data, config).catch((error) => {
     return Promise.reject(error)
   })
 },
 
 delete(resource) {
   this.setHeader()
   return axios.delete(resource).catch((error) => {
     return Promise.reject(error)
   })
 },
}
 
export default apiService

In the same services folder create a auth.service.js file with the following code:

import apiService from "./api.service";
import { API_URL } from "~/shared/config/api.config";
 
const authService = {
 async register({ email, name, password }) {
   return await apiService.post(`${API_URL}/register`, {
     name,
     email,
     password,
   });
 },
 async login({ email, password }) {
   return await apiService.post(`${API_URL}/login`, {
     email,
     password,
   });
 },
 async send_reset_password_email({ email }) {
   return await apiService.post(`${API_URL}/forgot-password`, {
     email,
   });
 },
 
 async reset_password({ email, password, password_confirmation, token }) {
   return await apiService.post(`${API_URL}/reset-password`, {
     email,
     password,
     password_confirmation,
     token
   });
 }
}
export default authService;

step 7

In your pages folder:

create a register.vue file

<template>
 <div class="bg-white py-6 sm:py-8 lg:py-12">
   <div class="max-w-screen-2xl px-4 md:px-8 mx-auto">
     <h2
       class="
         text-gray-800 text-2xl
         lg:text-3xl
         font-bold
         text-center
         mb-4
         md:mb-8
       "
     >
       Register
     </h2>
 
     <form class="max-w-lg border rounded-lg mx-auto">
       <div class="flex flex-col gap-4 p-4 md:p-8">
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Name</label
           >
           <input
             v-model="form.name"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Email</label
           >
           <input
             v-model="form.email"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
 
         <div>
           <label
             for="password"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Password</label
           >
           <input
             v-model="form.password"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <nuxt-link to="/login">
         <span
           class="bg-indigo-500 cursor-pointer flex justify-center text-white py-4 rounded-md"
           @click.prevent="submit"
           >Register</span
         >
         </nuxt-link>
       </div>
 
       <div class="flex justify-center items-center bg-gray-100 p-4">
         <p class="text-gray-500 text-sm text-center">
           Already have an account?
           <a
             href="#"
             class="
               text-indigo-500
               hover:text-indigo-600
               active:text-indigo-700
               transition
               duration-100
             "
             >Login</a
           >
         </p>
       </div>
     </form>
   </div>
 </div>
</template>
<script>
import {getAuthModuleComponent, AUTHENTICATION_MODULE_ACTIONS} from "../store/auth/actions"
export default {
 data() {
   return {
     form: {
       name: '',
       email: '',
       password: '',
     },
   }
 },
 methods: {
   async submit() {
     try {
           await this.$store.dispatch(
           getAuthModuleComponent(AUTHENTICATION_MODULE_ACTIONS.REGISTER),
           { email: this.form.email, password: this.form.password, name: this.form.name },
         );
 
       } catch (error) {
         return error;
       }
   },
 },
}
</script>

create a login.vue file

<template>
 <div class="bg-white py-6 sm:py-8 lg:py-12">
   <div class="max-w-screen-2xl px-4 md:px-8 mx-auto">
     <h2
       class="
         text-gray-800 text-2xl
         lg:text-3xl
         font-bold
         text-center
         mb-4
         md:mb-8
       "
     >
       Register
     </h2>
 
     <form class="max-w-lg border rounded-lg mx-auto">
       <div class="flex flex-col gap-4 p-4 md:p-8">
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Email</label
           >
           <input
             v-model="form.email"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
 
         <div>
           <label
             for="password"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Password</label
           >
           <input
             v-model="form.password"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <span
           class="bg-indigo-500 cursor-pointer flex justify-center text-white py-4 rounded-md"
           @click.prevent="submit"
           >Login</span
         >
       </div>
 
       <div class="flex justify-center items-center bg-gray-100 p-4">
         <p class="text-gray-500 text-sm text-center">
           Don't have an account?
           <a
             href="#"
             class="
               text-indigo-500
               hover:text-indigo-600
               active:text-indigo-700
               transition
               duration-100
             "
             >Register</a
           >
         </p>
       </div>
     </form>
   </div>
 </div>
</template>
<script>
import {getAuthModuleComponent, AUTHENTICATION_MODULE_ACTIONS} from "../store/auth/actions"
export default {
 data() {
   return {
     form: {
       email: '',
       password: '',
     },
   }
 },
 methods: {
   async submit() {
     try {
         const isNewUser = await this.$store.dispatch(
           getAuthModuleComponent(AUTHENTICATION_MODULE_ACTIONS.LOGIN),
           { email: this.form.email, password: this.form.password },
         );
 
         console.log(isNewUser);
       } catch (error) {
         return error;
       }
   },
 },
}
</script>

create a send-reset-password-email.vue file

<template>
 <div class="bg-white py-6 sm:py-8 lg:py-12">
   <div class="max-w-screen-2xl px-4 md:px-8 mx-auto">
     <h2
       class="
         text-gray-800 text-2xl
         lg:text-3xl
         font-bold
         text-center
         mb-4
         md:mb-8
       "
     >
       Send Email
     </h2>
 
     <form class="max-w-lg border rounded-lg mx-auto">
       <div class="flex flex-col gap-4 p-4 md:p-8">
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Email</label
           >
           <input
             v-model="form.email"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <span
           class="bg-indigo-500 cursor-pointer flex justify-center text-white py-4 rounded-md"
           @click.prevent="submit"
           >Send</span
         >
       </div>
 
   
     </form>
   </div>
 </div>
</template>
<script>
import {getAuthModuleComponent, AUTHENTICATION_MODULE_ACTIONS} from "../store/auth/actions"
export default {
 data() {
   return {
     form: {
       email: '',
       password: '',
     },
   }
 },
 methods: {
   async submit() {
     try {
         const isNewUser = await this.$store.dispatch(
           getAuthModuleComponent(AUTHENTICATION_MODULE_ACTIONS.SEND_RESET_PASSWORD_EMAIL),
           { email: this.form.email },
         );
 
         console.log(isNewUser);
       } catch (error) {
         return error;
       }
   },
 },
}
</script>

create a reset-password.vue file

<template>
 <div class="bg-white py-6 sm:py-8 lg:py-12">
   <div class="max-w-screen-2xl px-4 md:px-8 mx-auto">
     <h2
       class="
         text-gray-800 text-2xl
         lg:text-3xl
         font-bold
         text-center
         mb-4
         md:mb-8
       "
     >
       Reset password
     </h2>
 
     <form class="max-w-lg border rounded-lg mx-auto">
       <div class="flex flex-col gap-4 p-4 md:p-8">
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Email</label
           >
           <input
             v-model="form.email"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >New password</label
           >
           <input
             v-model="form.password"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <div>
           <label
             for="email"
             class="inline-block text-gray-800 text-sm sm:text-base mb-2"
             >Confirm password</label
           >
           <input
             v-model="form.password_confirmation"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         </div>
         <input
           v-model="form.token"
             type="hidden"
             class="
               w-full
               bg-gray-50
               text-gray-800
               border
               focus:ring
               ring-indigo-300
               rounded
               outline-none
               transition
               duration-100
               px-3
               py-2
             "
           />
         <span
           class="bg-indigo-500 cursor-pointer flex justify-center text-white py-4 rounded-md"
           @click.prevent="submit"
           >Send</span
         >
       </div>
 
   
     </form>
   </div>
 </div>
</template>
<script>
import {getAuthModuleComponent, AUTHENTICATION_MODULE_ACTIONS} from "../store/auth/actions"
export default {
 data() {
   return {
     form: {
       email: this.$route.query.email,
       password: '',
       token: this.$route.query.token,
       password_confirmation: ''
     },
   }
 },
 methods: {
   async submit() {
     try {
         const isNewUser = await this.$store.dispatch(
           getAuthModuleComponent(AUTHENTICATION_MODULE_ACTIONS.RESET_PASSWORD),
           { email: this.form.email, password: this.form.password, password_confirmation: this.form.password_confirmation, token: this.form.token },
         );
 
         console.log(isNewUser);
       } catch (error) {
         return error;
       }
   },
 },
}
</script>

Hope it helps you 😊