This article is a guest post by Mohamad Shahbaz Allam, Developer Advocate at Magic as part of our partner ecosystem.
This tutorial will teach you how to use Strapi as a backend to our Nuxt.js to-do application, where Magic secures everything.
Strapi is the leading open-source headless CMS. It's 100% Javascript, fully customizable, and developer-first. It gives developers the power to easily create self-hosted, customizable, and performant content REST/GraphQL APIs.
Nuxt.js is a higher-level framework based on Vue.js to create production-ready modern web applications. Nuxt is inspired by Next.js, a framework of similar purpose based on React.js, our to-do application's frontend will be built in Nuxt, which would use Strapi APIs to manage the to-dos.
Magic empowers developers with tools that make passwordless auth simple, with the best user experience and long-term security built-in. With a few lines of code, no bloat, and zero unnecessary configs, your app can support various passwordless login methods. We will be using Magic's email passwordless to login a user and receive a token. We will later pass on that received token to Strapi in our authorization header when making API calls to get our protected resources.
There's much to learn in this tutorial, but don't worry, we will go one step at a time. But before we get started, below is the outline for this article:
Prerequisites
Setting up a Strapi Backend
1- Initializing a Strapi application
2- Creating Collections
3- Add Magic to Strapi
Get your Magic API Keys Extend default permission policy Edit the Authenticated Role Test with Postman
Setting up Nuxt.js Frontend
1- Initializing a Nuxt.js Project
Create a secured Nuxt.js application with Magic
2- Add Nuxt Todo backed by Strapi
Update store Add components to show our Todo layout.
Run Application
1- Backend
2- Frontend
3- Source Code
Done
I can't wait to see your reactions and feedback at the end of this tutorial, but first, let's see the prerequisites and prepare ourselves for fun learning.
Prerequisites
Setting up a Strapi Backend
Initializing a Strapi application
Let's start by scaffolding your strapi backend. Open your terminal and type.
yarn create strapi-app strapi-todo-api --quickstart //using yarn
npx create-strapi-app strapi-todo-api --quickstart //using npxYour package manager will create a strapi-todo-api directory and install Strapi in it.
If everything went smoothly, the strapi server would be started at [localhost:1337](http://localhost:1337/), and a browser window would open at localhost:1337/admin to set up your admin account.
Fill in your details, then click on the "LET'S START" button.
You will see the Strapi Dashboard, similar to this.
Creating Collections
Go to Content-Types Builder and click on the Create new collection type.
A modal window will appear, enter todo under the Display name input box, and click Continue.
A modal will appear. Select Text as the field for your collection type.
To enter the name of the field, type todoTitle, select Long Text for Type, then click on +Add another field.
From the pop-up modal window, select Boolean, enter completed in the Name field, and click Finish.
Our todo content type would look like this by now, click on Save to make the changes into effect.
That's it for our Backend, we have a todoTitle for our To-do's and completed for the status of our To-do's.
Let's go ahead and fill some To-do by clicking + Add New Todos
Fill in TodoTitle and Select Completed to OFF meaning, it's not completed. Then, click on Publish.
At this point, our Strapi backend is ready, and we can test it with Postman. But in Strapi, every resource at first is set to private, which will result in 403 Forbidden status. So, to see the newly created To-do's, we have to change the Roles and allow Permissions to Public roles, which is not what we want in this application because we want the To-do to be displayed to an authenticated user.
So, let's add Magic to our Strapi backend, after which we will be able to see the To-do's we created.
Add Magic to Strapi
Install strapi-plugin-magic by running the following command inside your strapi-todo-api directory (strapi project's root) and then rebuild your application.
Install the Plugin
// ~/strapi-todo-api
yarn add strapi-plugin-magic //using yarn
npm i strapi-plugin-magic //using npmRebuild your Strapi Admin
yarn build //using yarn
npm run build //using npmOnce the rebuild is done, you will see 🪄 Magic on the side-bar menu under PLUGINS, click on it.
Here, you need to insert SECRET_KEY from the Magic Dashboard.
Get your Magic API Keys
When you sign up for Magic, which is free for developers, a new application is created for you, named 'First App', or you can create a new one from the Dashboard.
You will need the following information:
1.PUBLISHABLE_KEY
2.SECRET_KEY
Copy the SECRET KEY from the dashboard and paste in the Strapi Magic input field and click Submit.
Extend default permission policy
Let's override default permission so that Magic can generate and manage the user for you.
Create a file /extensions/users-permissions/config/policies/permissions.js
Copy the "normal version" from here
This file allows you to customize the way the user gets logged in. You will need to replace line 26 with the following code to log in the user using a magic token:
Replace line 26 with the following:
/** With Magic Changes */
try{
await strapi.plugins['magic'].services['magic'].loginWithMagic(ctx)
} catch (err) {
return handleErrors(ctx, err, 'unauthorized');
}
/** END With Magic Changes */\Or simply copy and paste the below full code from here:
"use strict";
const _ = require("lodash");
module.exports = async (ctx, next) => {
let role;
if (ctx.state.user) {
// request is already authenticated in a different way
return next();
}
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
try {
const { id } = await strapi.plugins[
"users-permissions"
].services.jwt.getToken(ctx);
if (id === undefined) {
throw new Error("Invalid token: Token did not contain required fields");
}
// fetch authenticated user
ctx.state.user = await strapi.plugins[
"users-permissions"
].services.user.fetchAuthenticatedUser(id);
} catch (err) {
/** With Magic Changes */
try {
await strapi.plugins["magic"].services["magic"].loginWithMagic(ctx);
} catch (err) {
return handleErrors(ctx, err, "unauthorized");
}
/** END With Magic Changes */
}
if (!ctx.state.user) {
return handleErrors(ctx, "User Not Found", "unauthorized");
}
role = ctx.state.user.role;
if (role.type === "root") {
return await next();
}
const store = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions",
});
if (
_.get(await store.get({ key: "advanced" }), "email_confirmation") &&
!ctx.state.user.confirmed
) {
return handleErrors(
ctx,
"Your account email is not confirmed.",
"unauthorized"
);
}
if (ctx.state.user.blocked) {
return handleErrors(
ctx,
"Your account has been blocked by the administrator.",
"unauthorized"
);
}
}
// Retrieve `public` role.
if (!role) {
role = await strapi
.query("role", "users-permissions")
.findOne({ type: "public" }, []);
}
const route = ctx.request.route;
const permission = await strapi
.query("permission", "users-permissions")
.findOne(
{
role: role.id,
type: route.plugin || "application",
controller: route.controller,
action: route.action,
enabled: true,
},
[]
);
if (!permission) {
return handleErrors(ctx, undefined, "forbidden");
}
// Execute the policies.
if (permission.policy) {
return await strapi.plugins["users-permissions"].config.policies[
permission.policy
](ctx, next);
}
// Execute the action.
await next();
};
const handleErrors = (ctx, err = undefined, type) => {
throw strapi.errors[type](err);
};\Edit the Authenticated Role
Let's come back to the Strapi dashboard and click on Settings > Roles > Authenticated.
Under permissions, click on APPLICATION, then click on the select all checkbox.
Finally, Click on Save.
Now our Strapi Backend is secured with Magic.
Test with Postman
GET localhost:1337/todos will return 403 Forbidden Status, as our backend is protected with Magic, and it requires a DID Token in the Authorization Header.
What is a DID Token?\ Decentralized ID (DID) tokens are used as cryptographically-generated proofs used to manage user access to the application's resource server.
The DID token created by the Magic client-side SDK leverages the Ethereum blockchain and elliptic curve cryptography to generate verifiable proofs of identity and authorization. These proofs are encoded in a lightweight, digital signature that can be shared between client and server to manage permissions; protect routes and resources, or authenticate users.
To learn more, visit magic docs.
With an Authorization Header and selecting Bearer Token from dropdown, let's try again and type AnythingJustToFoolTheSystem inside the Token field.\
You will get a 401 Unauthorized status with an "Invalid Token" message.
Now, the question is, how to get a DID Token?
We will get that from the frontend application, which we are going to build in Nuxt.js.\ Our frontend application will use Magic Client SDK to authenticate a user and then get the DID Token from Magic, which we will trade with Strapi to access our resources.
But for now, I have fast-forwarded to show you how after getting a DID Token, we can call the endpoint with a valid DID Token, which returns 200 OK as status and the list of To-dos with all its fields.
First part of the tutorial is done.
Let's move on to create the frontend in Nuxt.js and use Magic.
Setting up Nuxt.js Frontend
Our finished frontend application will look something like this:
Initializing a Nuxt.js Project
We will be using the npx make-magic CLI tool developed by Magic to get you started with the Magic-powered Nuxt application.
Create a secured Nuxt.js application with Magic
Open a new terminal window and type the following:
npx make-magic --template nuxtAfter a few seconds, you will be prompted for a project name, this will also be the name of the folder that will be created for this project.
After putting in a project name, you will be prompted for your
Magic Publishable API Key, enabling user authentication with Magic.
To get your publishable API key, you'll need to sign up to Magic Dashboard. Once you've signed up, an app will be created upon your first login (you'll be able to create new apps later).
You'll now be able to see your Publishable API Key - copy and paste the key into your CLI prompt.
After hitting Enter, you'll be asked to select whether you'd like to use npm / yarn as the NPM client for your project.
After selecting your NPM client, the nuxt server will automatically start, and your application will be running on http://localhost:3000.
In this example app, you'll be prompted to sign up for a new account using an email address or login into an existing one. Magic secures the authentication process.
After clicking on your magic link email, you'll be successfully logged in and redirected to the profile page that displays your email, issuer, and public address.
We have our Nuxt application secured by Magic. Let's build our Todo application on top of this application and add Strapi support to store, update, delete our Todos.
Add Nuxt Todo backed by Strapi
First, we will add DID Token support to our frontend, which will authorize our requests at Strapi endpoints.
Update store
Open ~/store/index.js file and replace it with the following code:
import { magic } from '../plugins/magic'
export const state = () => ({
user: null,
didToken: null,
authenticated: false,
})
export const mutations = {
SET_USER_DATA(state, data) {
state.user = data.userData
state.didToken = data.didToken
state.authenticated = true
},
CLEAR_USER_DATA(state) {
state.user = null
state.didToken = null
state.authenticated = false
this.$router.push('/login')
},
}
export const actions = {
async login({ commit }, email) {
await magic.auth.loginWithMagicLink(email)
const userData = await magic.user.getMetadata()
const didToken = await magic.user.getIdToken({ lifespan: 7200 })
const data = { userData, didToken }
// didToken is valid for 7200 seconds, i.e 2 hours
// Read more https://docs.magic.link/client-sdk/web/api-reference#getidtoken
commit('SET_USER_DATA', data)
},
async logout({ commit }) {
await magic.user.logout()
commit('CLEAR_USER_DATA')
},
}Here we have added didToken: null to state, updated mutations to reflect the didToken state.
Also, updated login action to use didToken using Magic's getIdToken.
const didToken = await magic.user.getIdToken({ lifespan: 3600 })
Add components to show our Todo layout.
Create a ToDoList.vue file inside the components directory and fill it with the following code:
<template>
<div class="todo-container">
<div class="create-container">
<input
v-model="newToDo"
type="text"
class="todo-input"
placeholder="What needs to be done?"
v-on:keyup.enter="create"
/>
<button @click="create">Add</button>
</div>
<div class="todo-list-container">
<ToDoListItem v-for="todo in todos" :key="todo.id" :todo="todo" />
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
props: {
todos: {
default() {
return []
},
},
},
data() {
return {
newToDo: '',
}
},
methods: {
async create() {
try {
if (this.newToDo && this.newToDo.length > 0) {
const result = await axios.post(
'http://localhost:1337/todos',
{
todoTitle: this.newToDo,
completed: false,
},
{
headers: {
Authorization: `Bearer ${this.$store.state.didToken}`,
},
}
)
this.$router.push('/loading')
}
this.newToDo = ''
} catch (error) {
console.log('handle error')
}
},
},
}
</script>
<style>
.todo-container {
max-width: 550px;
margin: 50px auto;
border-radius: 10px;
box-shadow: 0 0 25px rgb(0 0 0 / 15%);
overflow: hidden;
}
.create-container {
display: flex;
}
.todo-input {
-webkit-box-flex: 1;
flex-grow: 1;
border: none;
padding: 16px;
font-size: 24px;
font-weight: 100;
color: rgb(77, 77, 77);
}
input:focus {
outline: none;
}
input::placeholder {
color: #6f8297;
font-style: italic;
}
button {
font-size: 22px;
color: #6e6e6e;
padding: 0 27px;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s;
}
button:disabled {
opacity: 0;
pointer-events: none;
}
.todo-list-container {
width: 550px;
margin: auto;
display: block;
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.1);
}
</style>The ToDoList component is to list all the Todos.
Now, Let's create a ToDoListItem.vue file inside the components directory and fill it with the following code:
<template>
<div class="to-do-item" :class="{ completed: todo.completed }">
<span>{{ todo.todoTitle }}</span>
<div class="controls">
<span @click="deleteItem(todo)">❌</span>
<span @click="completeItem(todo)">✅</span>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
props: {
todo: {
default() {
return {}
},
},
},
methods: {
async completeItem(todo) {
try {
if (todo.completed) {
await axios.put(
'http://localhost:1337/todos/' + todo.id,
{
completed: false,
},
{
headers: {
Authorization: `Bearer ${this.$store.state.didToken}`,
},
}
)
} else {
await axios.put(
'http://localhost:1337/todos/' + todo.id,
{
completed: true,
},
{
headers: {
Authorization: `Bearer ${this.$store.state.didToken}`,
},
}
)
}
this.$router.push('/loading')
} catch (error) {
console.log('handle error')
}
},
async deleteItem(todo) {
try {
await axios.delete('http://localhost:1337/todos/' + todo.id, {
headers: {
Authorization: `Bearer ${this.$store.state.didToken}`,
},
})
this.$router.push('/loading')
} catch (error) {
console.log('handle error')
}
},
},
}
</script>
<style>
.to-do-item {
width: 100%;
display: block;
height: 50px;
border-top: solid 1px #eeeeee;
font-size: 24px;
}
.completed {
opacity: 0.4;
}
.to-do-item span {
height: 50px;
padding-left: 20px;
line-height: 50px;
width: 450px;
text-align: left;
display: inline-block;
}
.to-do-item .controls {
display: inline-block;
height: 50px;
line-height: 50px;
float: right;
}
.to-do-item .controls span {
line-height: 50px;
height: 50px;
display: inline-block;
width: 45px;
text-align: center;
padding: 0;
cursor: pointer;
}
</style>ToDoListItem is to display individual todo.
At last for our Todo Layout, we need to update our index.vue page.
Open the ~/pages/index.vue file and replace it with the following code:
<template>
<div class="container">
<div class="nuxt-todo">Nuxt Todo Application</div>
<ToDoList :todos="todos" />
</div>
</template>
<script>
import axios from 'axios'
export default {
middleware: 'magicauth',
data() {
return {
todos: '',
}
},
async asyncData({ store }) {
try {
const result = await axios.get('http://localhost:1337/todos', {
headers: {
Authorization: `Bearer ${store.state.didToken}`,
},
})
return {
todos: result.data,
}
} catch (error) {
console.log('handle error')
}
},
}
</script>
<style scoped>
.nuxt-todo {
font-size: 24px;
text-align: center;
margin-top: 10px;
padding-top: 20px;
color: #6851ff;
}
</style>
```\
Here, we have added the `ToDoList` component and used `asyncData()` to fetch the todos. We have also used DID Token in our Authorization header to authorize our API request.
Let's add loading page support to our frontend, which will give a nice animation each time we update, create or delete a todo. But before that, let's install `epic-spinners` to our nuxt project.
```bash
// ~/hello-nuxt
npm i epic-spinners // npm
yarn add epic-spinners // yarnNow, create a loading.vue file inside the ~/pages directory and fill it with the following code:
<template>
<div id="app">
<atom-spinner
id="inner-center"
:animation-duration="1000"
:size="300"
:color="'#6851ff'"
/>
</div>
</template>
<style scoped>
#app {
width: 100%;
text-align: center;
margin: 200px auto;
}
#inner-center {
display: inline-block;
margin: 0 auto;
padding: 3px;
}
</style>
<script>
import { AtomSpinner } from 'epic-spinners'
export default {
components: {
AtomSpinner,
},
mounted() {
if (this.$store.state.didToken) {
this.$router.push('/')
} else {
this.$router.push('/login')
}
},
}
</script>Your application would be live on http://localhost:3000 displaying the list of todos. Play around and add your todos.
Note: If no todos are shown, try running your backend and frontend again.
Run Application
Backend
Start the Strapi Backend server, if not already running.
// ~/strapi-todo-api
yarn develop //using yarn
// OR
npm run develop //using npmStrapi server will be running on http://localhost:1337
Frontend
Start the Nuxt Frontend Application.
// ~/hello-nuxt
yarn dev //using yarn
// OR
npm run dev //using npmYou should have your Nuxt application running on https://localhost:3000, displaying the list of todos. Try updating, creating, or deleting a to-do.
Source Code
Done
Congratulations!!!🎉 You have successfully built a Nuxt.js todo App using Strapi as a backend, and Magic secures everything.
I hope this tutorial has given you an insight into how to use Strapi, Nuxt, and Magic. There are endless possibilities to add more functionalities to this application. Feel free to make a PR on both of the repo listed above.
If you have any questions, feedback, or suggestions, please comment here and reach out to me on Twitter or open a GitHub issue to any one of the above repositories.
Cheers!
Mohammad is a Developer Advocate and Full Stack Developer, Mozilla Representative and the organizer of GDG Ranchi is a community-ran meetup for developers interested in resources and technology from Google.