How to use the @nuxtjs/strapi Module to add Authentication to a Nuxt Application

How to use the @nuxtjs/strapi Module to add Authentication to a Nuxt Application

What do you need for this tutorial?

  • Basic Knowledge of Vue.j
  • Knowledge of JavaScript, and
  • Node.js (v14 recommended for strapi).

Table of Contents

Installing Strapi

yarn create strapi-app my-project //using yarn
npx create-strapi-app@latest my-project //using npx
Replace `my-project` with the name you wish to call your application directory. Your package manager will create a directory with the specified name and install Strapi.
yarn develop //using yarn
npm run develop //using npm

Building the API with Strapi

  1. To Create the Article Content Type
  • Click on content-type builder in the side menu.
  • Under Collection-types, click create new collection type.
  • Add new content-type named article.
  • Create fields under article content-type. — Name as short text — Description as short text — content as rich text — Image as a single type.
  1. Add User Relationship
  • Create a relation field under article.
  • Select User (from users-permissions-user), and click on “user has many articles” relation.
  • Save the article content type.
  1. Create User and Enable User Permission and Roles
  • Strapi provides a Users collection type by default. Head to settings on the side menu, and select Roles under Users and Permissions Plugin.
  • Click on Authenticated and check all permissions.
  • Save your changes, then go back and click on public.
  • Check only the find and findOne permissions.
  • Click save to save changes.
  • Create a user called author with whatever credentials you’d like, but select the authenticated role and enable email confirmation.
  • Create an article and select Users_permissions_user as author. This means that the user author created the article.
  • Save the article and proceed.

Installing Nuxt.js

  • To install Nuxt.js, run the following commands:
yarn create nuxt-app <project-name> //using yarn
npx create-nuxt-app <project-name> //using npx
npm init nuxt-app <project-name> //using npm
yarn dev //using yarn
npm run dev //using npm

Installing @nuxtjs/strapi

  • Run the command below:
yarn add @nuxtjs/strapi@^0.3.4 //using yarn
npm install @nuxtjs/strapi@^0.3.4 //using npm
  • Open the nuxt.config.js file and add the following code to the file.
modules: [
// ...other modules
'@nuxtjs/strapi',
]

strapi: {
url: process.env.STRAPI_URL || `http:localhost:1337/api`,
entities: ['articles'],
}
  • We’ll be using @nuxtjs/strapi in two ways:
this.$strapi() //from properties such as methods, data, computed$strapi() //from nuxtjs lifecycle methods

Installing @nuxtjs/markdownit

  • Run the command below.
yarn add @nuxtjs/markdownit //using yarn
npm install @nuxtjs/markdownit //using npm
  • Add the following lines of code to your nuxt.config.js file.
modules: [
//...other modules
'@nuxtjs/markdownit'
],

markdownit: {
preset: 'default',
linkify: true,
breaks: true,
injected: true,
// use: ['markdown-it-div', 'markdown-it-attrs'],
},

Building the Frontend with NuxtJs

  • Execute the following lines of code to create a signup.vue file in the pages directory.
cd pages
touch signup.vue
  • Fill signup.vue with the following lines of code.
  • <template> <div class="w-4/5 mx-auto md:w-1/2 text-center my-12"> <div v-show="error !== ''" class="p-3 border"> <p>{{ error }}</p> </div> <h1 class="font-bold text-2xl md:text-4xl mt-5">Signup</h1> <form @submit="createUser"> <div> <input v-model="email" class="p-3 my-5 border w-full" type="email" placeholder="email" /> </div> <div> <input v-model="username" class="p-3 my-5 border w-full" type="text" placeholder="username" /> </div> <div> <input v-model="password" class="p-3 my-5 border w-full" type="password" placeholder="password" /> </div> <div> <button class="button--green" :disabled="email === '' || password === '' || username === ''" type="submit" > Signup </button> </div> </form> </div> </template> <script> export default { data() { return { email: '', username: '', password: '', error: '', } }, methods: { async createUser(e) { e.preventDefault() try { const newUser = await this.$strapi.register({ email: this.email, username: this.username, password: this.password, }) console.log(newUser) if (newUser !== null) { this.error = '' this.$nuxt.$router.push('/articles') } } catch (error) { this.error = error.message } }, }, middleware: 'authenticated', } </script> <style></style>
We just built our signup logic; when users provide their email, username and password, and click the signup button, we invoke the `createUser` method. All we’re doing in this method is registering a new user using the `@nuxtjs/strapi` module i.e `this.$strapi.register()` method. Then, we redirect the user to the `/articles` route. If the email belongs to an existing user, an error message is displayed at the top of the page. Finally, we’re using `nuxtjs middleware` feature to invoke a custom-made `middleware` that we’re going to create.**To Build the Login Page**- Execute the following lines of code to create a `login.vue` file in the pages directory.```vue
touch login.vue
  • Fill up login.vue with the following lines of code.
<template>
<div class="w-4/5 mx-auto md:w-1/2 text-center my-12">
<div v-show="error !== ''" class="p-3 border">
<p>{{ error }}</p>
</div>
<h1 class="font-bold text-2xl md:text-4xl mt-5">Login</h1>
<form @submit="loginUser">
<div>
<input
v-model="identifier"
class="p-3 my-5 border w-full"
type="email"
placeholder="email"
/>
</div>
<div>
<input
v-model="password"
class="p-3 my-5 border w-full"
type="password"
placeholder="password"
/>
</div>
<div>
<button
:disabled="identifier === '' || password === ''"
class="button--green"
type="submit"
>
Login
</button>
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
identifier: '',
password: '',
error: '',
}
},
methods: {
async loginUser(e) {
e.preventDefault()
try {
const user = await this.$strapi.login({
identifier: this.identifier,
password: this.password,
})
console.log(user)
if (user !== null) {
this.error = ''
this.$nuxt.$router.push('/articles')
}
} catch (error) {
this.error = 'Error in login credentials'
}
},
},
middleware: 'authenticated',
}
</script>
<style></style>
  • Execute the following lines of code to create an authenticated.js file in the middleware directory.
cd middleware
touch authenticated.js
  • Fill up authenticated.js with the following code.
export default function ({ $strapi, redirect }) {
if ($strapi.user) {
redirect('/articles')
}
}
  • Execute the following lines of code to create a Nav.vue file in the components directory.
cd components
touch Nav.vue
  • Fill up the file with the following code.
<template>
<div
class="flex space-x-5 items-center justify-center bg-black text-white py-3 sm:py-5"
>
<NuxtLink to="/articles">Articles</NuxtLink>
<div v-if="$strapi.user === null">
<NuxtLink class="border-r px-3" to="/login">Login</NuxtLink>
<NuxtLink class="border-r px-3" to="/signup">Signup</NuxtLink>
</div>
<div v-if="$strapi.user !== null">
<span class="border-r px-3">{{ $strapi.user.username }}</span>
<NuxtLink class="border-r px-3" to="/new">Create Post</NuxtLink>
<button class="pl-3" @click="logout">Logout</button>
</div>
</div>
</template>
<script>
export default {
name: 'Nav',
methods: {
async logout() {
await this.$strapi.logout()
this.$nuxt.$router.push('/')
},
},
}
</script>
<style></style>
$strapi.user //returns the loggedin user or null
  • Execute the following lines of code to create an index.vue file in the pages directory.
cd pages
code index.vue
  • Fill up the index.vue file with the following code.
<template>
<div class="container">
<div>
<h1 class="title">Welcome To The BlogApp</h1>
<div class="links">
<NuxtLink to="/login" class="button--green"> Login </NuxtLink>
<NuxtLink to="/articles" class="button--grey"> Continue Free </NuxtLink>
</div>
</div>
</div>
</template>
<script>
export default {
middleware: 'authenticated',
}
</script>
<style>
/* Sample `apply` at-rules with Tailwind CSS
.container {
@apply min-h-screen flex justify-center items-center text-center mx-auto;
}
*/
.container {
margin: 0 auto;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.title {
font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: block;
font-weight: 300;
font-size: 80px;
color: #35495e;
letter-spacing: 1px;
}
.subtitle {
font-weight: 300;
font-size: 42px;
color: #526488;
word-spacing: 5px;
padding-bottom: 15px;
}
.links {
padding-top: 15px;
}
</style>
  • Execute the following lines of code to create a articles.vue file in the pages directory.
cd pages
touch articles.vue
  • Fill it up with the following code.
<template>
<div>
<Nav class="mx-auto sticky top-0" />
<h1 class="text-center my-5">All our articles</h1>
<div
v-show="error !== ''"
class="sticky z-100 border p-5 m-3 top-0 bg-black text-white text-center mx-auto w-4/5 sm:w-4/5 md:w-4/5 lg:w-1/2"
>
<p class="m-1 sm:m-3">{{ error }}</p>
<button class="button--grey" @click="resetError()">Ok</button>
</div>
<div
v-for="(article, i) in data.data"
:key="i"
class="sm:flex sm:space-x-5 my-5 shadow-lg mx-auto w-4/5 sm:w-4/5 md:w-4/5 lg:w-1/2"
>
<img
:src="`http://localhost:1337${article.attributes.Image.data.attributes.formats.small.url}`"
class="max-h-screen sm:h-48"
/>
<div class="px-2 sm:pr-2 sm:text-left text-center">
<h3 class="font-bold my-3">{{ article.attributes.name }}</h3>
<p class="my-3">{{ article.attributes.description }}</p>
<button class="button--green mb-4 sm:mb-0" @click="readPost(article)">
Read more
</button>
</div>
</div>
</div>
</template>
<script>
export default {
async asyncData({ $strapi, $md }) {
const data = await $strapi.$articles.find({ populate: '*' })
return { data }
},
data() {
return {
error: '',
}
},
methods: {
readPost(article) {
if (this.$strapi.user) {
this.error = ''
this.$nuxt.$router.push(`/article/${article.id}`)
} else {
this.error = 'Please Login to read articles'
}
},
resetError() {
this.error = ''
},
},
}
</script>
<style></style>
  • Execute the following lines of code to create a _id.vue file in the pages directory.
mkdir article
touch _id.vue
  • Fill the _id.vue file with the following code.
<template>
<div>
<Nav class="mx-auto sticky top-0" />
<div class="w-4/5 sm:w-1/2 mx-auto my-5">
<h3 class="my-5 font-bold text-4xl">
{{ article.name }}
</h3>
<img
:src="`http://localhost:1337${article.Image.url}`"
class="max-h-screen"
/>
<p class="mt-5 font-bold">
written by {{ article.users_permissions_user.username }}
</p>
<div class="my-5" v-html="$md.render(article.content)"></div>
<button
v-if="
$strapi.user && article.users_permissions_user.id === $strapi.user.id
"
class="button--grey"
@click="deletePost(article.id)"
>
Delete
</button>
</div>
</div>
</template>
<script>
export default {
async asyncData({ $strapi, route }) {
const id = route.params.id
const article = await $strapi.$articles.findOne(id, {
populate: '*',
})
return { article }
},
methods: {
async deletePost(id) {
await this.$strapi.$articles.delete(id)
this.$nuxt.$router.push('/articles')
},
},
middleware({ $strapi, redirect }) {
if ($strapi.user === null) {
redirect('/articles')
}
},
}
</script>
<style scoped>
h1 {
font-weight: 700;
font-size: 2rem;
margin: 0.5em 0;
}
</style>
  • Navigate to the src/api/controllers folder.
  • Click on the article.js file.
  • Fill it up with the following code.
'use strict';
/**
* article controller
*/
const { createCoreController } = require('@strapi/strapi').factories;


module.exports = createCoreController('api::article.article', ({ strapi }) => ({
async findOne(ctx) {
console.log(ctx.request.params.id)
const data = await strapi.service('api::article.article').findOne(ctx.request.params.id, {
populate: ['Image', 'users_permissions_user']
})
delete data.users_permissions_user.password
return data
}
}));
  • Execute the following lines of code to create a New.vue file in the pages directory.
touch New.vue
  • Fill up the New.vue file with the following lines of code
<template>
<div class="w-4/5 mx-auto md:w-1/2 text-center my-12 overflow-hidden">
<form ref="form" @submit="createPost">
<h2 class="font-bold text-2xl md:text-4xl mt-5">Create a new post</h2>
<div>
<input
v-model="form.name"
name="Title"
type="text"
placeholder="title"
class="p-3 my-3 border w-full"
/>
</div>
<div>
<input
v-model="form.description"
name="description"
type="text"
placeholder="description"
class="p-3 my-3 border w-full"
/>
</div>
<div>
<textarea
v-model="form.content"
name="Content"
cols="30"
rows="10"
class="p-3 my-3 border w-full"
></textarea>
</div>
<div>
<input
type="file"
name="Image"
class="p-3 my-3 border w-full"
@change="assignFileInput()"
/>
</div>
<div>
<button
class="button--green"
:disabled="
form.name === '' ||
form.description === '' ||
form.content === '' ||
fileInput === ''
"
type="submit"
>
Create
</button>
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
form: {
name: '',
description: '',
content: '',
users_permissions_user: this.$strapi.user.id,
},
fileInput: '',
}
},
methods: {
async createPost(e) {
const formData = new FormData()
let file
const formElements = this.$refs.form.elements
formElements.forEach((el, i) => {
if (el.type === 'file') {
file = el.files[0]
}
})
formData.append(`files.Image`, file, file.name)
formData.append('data', JSON.stringify(this.form))
e.preventDefault()
await this.$strapi.$articles.create(formData)
this.$nuxt.$router.push('/articles')
},
assignFileInput() {
const formElements = this.$refs.form.elements
formElements.forEach((el, i) => {
if (el.type === 'file') {
this.fileInput = el.files[0] !== undefined ? el.files[0].name : ''
}
})
},
},
middleware({ $strapi, redirect }) {
if (!$strapi.user) {
redirect('/articles')
}
},
}
</script>
<style></style>
  1. Using the v-model directive, we linked up the fields with their respective data property; file inputs do not support the v-model directive, so we’ve built a workaround.
  2. What we’ve done is create an assignInput()` method that is invoked when the field input with file type changes.
  3. When a change occurs, we check if the type of the form element that changed is a file. If it is, we assign the name of the selected file as the value of fileInput.
  1. Using FormData we append the form object from the page’s data property in string form to FormData with a data property.
  2. We do the same thing for file input but we append it to FormData with a files.image property. This is because, for multipart data, Strapi requires that the property be preceded by files i.e files.${fieldname} and our fieldname from the article content-type is image.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Strapi

Strapi

The open source Headless CMS Front-End Developers love.