The cleanest way of handling user authentication with Nuxt.js
By Mike Street
I am currently in the process of making a small side-project which features a user login and register function. The backend is built using Lumen, a lightweight framework from the makers of Laravel. The API is 100% JSON based and features login and register endpoints.
As I developed the API first, each endpoint features server side validation, ensuring the data is correct before putting into the database.
Because of this, I didn't want to duplicate the validation rules - especially as I am trying to create this beta prototype with a "fail fast" methodology, in that I want to try and get it in front of users as quickly as possible, so if I decide to alter my validation rules (either enhancing or adding more fields, or changing the tone of voice) I don't want to have to deploy edits to both the server and client side code (or risk them not matching).
Another reason for not wanting to do validation client side is that more JavaScript is more code for the user to download, which slows down the site performance.
For this, I am using Nuxt, with Axios and nuxt/auth. There are some great tutorials (and Stack Overflow) questions for initial set up of the auth, but I struggled to find out what happens if your API returns anything but a 200?
The 422 HTTP response code stands for Unprocessable Entity
, which is apt for a failed login attempt, be it wrong username/password or missing data. With my custom API, when returning the 422
, I was also returning the error ands what happened - I wanted to display this on the frontend.
The Code
Sorry for the lengthy preamble, on with the code (which is equally lengthy...).
API Response
In order to make sense of some of my code, each of my API responses is laid out as such:
{
"meta": {
"success": true, // Was the request successful?
"code": 200, // The HTTP code the response was returned with
"message": "" // A summary message as to what happened
},
"data": [] // The actual data
}
If a request fails, the message will be something like "An error occurred when logging in", while the data array would contain specific details about fields "Please enter a valid email address" or similar
Nuxt Config
Nuxt Auth
First off, this is what my auth
block in my nuxt.config.js
looks like. This outlines where to login and logout and how nuxt can get details about the user. You'll notice data.token
in the token section, this tells Nuxt where to find the bearer token data on the login request.
auth: {
strategies: {
local: {
token: {
property: 'data.token',
required: true,
type: 'Bearer'
},
user: {
property: false,
autoFetch: true
},
endpoints: {
login: { url: '/user/login', method: 'post'},
logout: { url: '/user/logout', method: 'post' },
user: { url: '/user', method: 'get'}
}
}
}
},
Axios
There is also an axios
block that accompanies this specifying the baseURL
that the login
, logout
and user
endpoints are relative too.
axios: {
baseURL: 'http://api.com/api'
},
Middleware
As my app will be 100% behind a login (except the login/register pages) I have set every page to require authorisation to be access. This can then be disabled on a page by page basis.
This is done by adding the following to our nuxt.config.js
:
router: {
middleware: ['auth']
}
On each page you wish to disable this, you can put auth: false
if you wish for anyone to see the page or auth: 'guest'
if you only want non-authorised people to see (e.g. you don't want people who are logged in to ever be faced with a login page themselves).
login.vue
With the login
endpoint defined, we can tackle creating a login page which captures the data and displays errors if necessary.
Template
This is a barebones template - you see there is an email/password fields with v-models
attached, a Notification
element for displaying general messages, plus field specific error blocks below each field. This allows us to display things like This email was not accepted or similar below the email field.
<template>
<section class="section">
<h2 class="title has-text-centered">Welcome back!</h2>
<Notification :message="message" v-if="message"/>
<form method="post" @submit.prevent="login">
<div class="field">
<label class="label">Email</label>
<div class="control">
<input type="email" class="input" name="email" v-model="email"/>
</div>
<div class="error" v-if="errors.email">
<span v-for="error in errors.email"></span>
</div>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control">
<input type="password" class="input" name="password" v-model="password" />
<div class="error" v-if="errors.password">
<span v-for="error in errors.password"></span>
</div>
</div>
</div>
<div class="control">
<button type="submit" class="button is-dark is-fullwidth">Log In</button>
</div>
</form>
</section>
</template>
Script
The script part of the page captures the data and tries to login with it. If the attempt fails, the errors and message will be set alerting the user as to why it failed. If successful, they will be redirected to the profile page.
<script>
import Notification from '~/components/Notification' // Import the notification component
export default {
auth: 'guest', // Only make is available for non-auth'd people
components: {
Notification,
},
data() {
return {
// Create empty data points for the login & error information
email: '',
password: '',
message: null,
errors: []
}
},
methods: {
async login() {
// Reset the errors, so if they are trying again, it is obvious the messages change
this.message = null;
this.errors = [];
// Place in a try/catch in case the API errors out
try {
await this.$auth.loginWith('local', {
data: {
email: this.email,
password: this.password
}
})
.catch(error => {
// The actual data returned from the API is in `error.response.data`
let response = error.response.data;
// Set the messages & errors to display
this.message = response.meta.message;
this.errors = response.data;
})
// Redirect to profile if successful
this.$router.push('/profile');
} catch (e) {
// Display generic error if API has failed
this.message = e.response.data.message;
}
}
}
}
</script>
The main thing that caught me out (excuse the pun) when trying to solve this was the catch
on the await
function and digging deep through the error
response to find the data returned from the API.
This component could then be enhanced by adding simple validation, e.g. checking password and email are filled in before trying to submit to the API, but again that creates duplication error messages.
Hope this helps capturing data returned from a custom API and displaying the included error messages!