Courses

Vue.js 3 + Laravel 11 API + Vite: SPA CRUD

Login Cleanup and Laravel Sanctum

Summary of this lesson:
- Implementing Sanctum middleware protection
- Setting up frontend route guards
- Managing authentication state
- Configuring API route protection

Now that we have created an authentication page and can log in the user, we need to protect the routes, both on the front-end and back-end. So let's implement auth:sanctum Middleware.


Protect Back-end API Calls

We have installed Sanctum earlier with the install:api Artisan command. Now, we can update the API routes to use the auth:sanctum Middleware.

routes/api.php:

Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
 
Route::group(['middleware' => 'auth:sanctum'], function() {
Route::apiResource('posts', PostController::class);
Route::get('categories', [CategoryController::class, 'index']);
Route::get('/user', function (Request $request) {
return $request->user();
});
});

Next, we need to redirect the user to the login page, on the front-end. We need to intercept the response with Axios.

resources/js/bootstrap.js:

import axios from 'axios';
window.axios = axios;
 
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401 || error.response?.status === 419) {
if (JSON.parse(localStorage.getItem('loggedIn'))) {
localStorage.setItem('loggedIn', false)
location.assign('/login')
}
}
 
return Promise.reject(error)
}
)

Here, in case of an error, if the status is 401 (unauthorized) or 419 (session expired), then if we have loggedIn in the local storage, we set it to false and redirect the user to the login page.

Next, to make Laravel Sanctum work properly we need to change a few things. We must add the statefulApi Middleware.

bootstrap/app.php:

return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi();
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

Then for the Axios, we need another header withCredentials.

resources/js/bootstrap.js:

import axios from 'axios';
window.axios = axios;
 
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.withCredentials = true
window.axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401 || error.response?.status === 419) {
if (JSON.parse(localStorage.getItem('loggedIn'))) {
localStorage.setItem('loggedIn', false)
location.assign('/login')
}
}
 
return Promise.reject(error)
}
)

Notice: for your SPA to work correctly, don't forget to set the correct APP_URL in the .env file.


Protect Vue Routes

And lastly, we will protect the Vue.js routes. In the routes, we will add a function auth which will do the same thing: check the local storage if the user is logged in and redirect to the /login.

resources/js/routes/index.js:

import { createRouter, createWebHistory } from 'vue-router';
 
import AuthenticatedLayout from '@/layouts/Authenticated.vue';
import GuestLayout from '@/layouts/Guest.vue';
 
import PostsIndex from '@/components/Posts/Index.vue'
import PostsCreate from '@/components/Posts/Create.vue'
import PostsEdit from '@/components/Posts/Edit.vue'
import Login from '@/components/Auth/Login.vue';
 
function auth(to, from, next) {
if (JSON.parse(localStorage.getItem('loggedIn'))) {
next()
}
 
next('/login')
}
// ...

Then we need to add this auth function for the authenticated routes with the beforeEnter parameter.

resources/js/routes/index.js:

import { createRouter, createWebHistory } from 'vue-router';
 
import AuthenticatedLayout from '@/layouts/Authenticated.vue';
import GuestLayout from '@/layouts/Guest.vue';
 
import PostsIndex from '@/components/Posts/Index.vue'
import PostsCreate from '@/components/Posts/Create.vue'
import PostsEdit from '@/components/Posts/Edit.vue'
import Login from '@/components/Auth/Login.vue';
 
function auth(to, from, next) {
if (JSON.parse(localStorage.getItem('loggedIn'))) {
next()
}
 
next('/login')
}
 
const routes = [
{
path: '/',
component: GuestLayout,
children: [
{
path: '/login',
name: 'login',
component: Login
},
]
},
{
component: AuthenticatedLayout,
beforeEnter: auth,
children: [
{
path: '/posts',
name: 'posts.index',
component: PostsIndex,
meta: { title: 'Posts' }
},
{
path: '/posts/create',
name: 'posts.create',
component: PostsCreate,
meta: { title: 'Add new post' }
},
{
path: '/posts/edit/:id',
name: 'posts.edit',
component: PostsEdit,
meta: { title: 'Edit post' }
},
]
}
]
 
export default createRouter({
history: createWebHistory(),
routes
})

Homepage

And finally, let's take care of the home page which would automatically redirect to the login form. In the routes, we need to add the path and where to redirect to.

resources/js/routes/index.js:

// ...
const routes = [
{
path: '/',
redirect: { name: 'login' },
component: GuestLayout,
children: [
{
path: '/login',
name: 'login',
component: Login
},
]
},
{
component: AuthenticatedLayout,
beforeEnter: auth,
children: [
{
path: '/posts',
name: 'posts.index',
component: PostsIndex,
meta: { title: 'Posts' }
},
{
path: '/posts/create',
name: 'posts.create',
component: PostsCreate,
meta: { title: 'Add new post' }
},
{
path: '/posts/edit/:id',
name: 'posts.edit',
component: PostsEdit,
meta: { title: 'Edit post' }
},
]
}
]
// ...

And that's it. Now we have protected our routes from both front-end and back-end.

Previous: Login Form and First Authentication
avatar

Hi Povilas, Thank you for your great lessons. According to the vue-router doc, the next function should be called exactly once otherwise the hook will never be resolved or produce errors.

function auth(to, from, next) { if (JSON.parse(localStorage.getItem('loggedIn'))) { next() } else { next('/login') } }

[Navigation Guards | Vue Router]https://v3.router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards)

👍 2
avatar
You can use Markdown
avatar

Also, I think location.assign('/login') shoud be outside the if (JSON.parse(localStorage.getItem('loggedIn'))) scope as all unauthorized users should be moved to login page whether they have loggedIn status or not.

avatar
You can use Markdown
avatar

hi..i dont know if this bug or wat....when im in post create or edit.. i refresh page it redirect back to post.index..why this happen? i hv been try for few hours...

{ beforeEnter: auth, component: AuthLayout, children: [ { path: '/dashboard', name: 'dashboard', component: Dashboard, meta: { title: 'Dashboard' } }, { path: '/posts', component: { render: () => h('router-view') }, children: [ { path: '', name: 'post.index', component: PostsIndex, meta: { title: 'List of post' } }, { path: 'create', name: 'post.create', component: PostsCreate, meta: { title: 'Add new post' } }, ] }, ] }

or should i move post to another group instead?

avatar

finaly find the root cause...

lesson #23

const loginUser = (response) => { user.name = response.data.name user.email = response.data.email localStorage.setItem('loggedIn', JSON.stringify(true)) router.push({ name: 'posts.index' }) }

and

lesson #25

setup() { const { getUser } = useAuth() onMounted(getUser) }

so when reload...apps getUser detail and then store new data using loginUser....so it will keep push post.index page...so i move router.push outside and it work fine

avatar
You can use Markdown
avatar
You can use Markdown