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:
Get your Magic API Keys Extend default permission policy Edit the Authenticated Role Test with Postman
Create a secured Nuxt.js application with Magic
Update store Add components to show our Todo layout.
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.
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 npx
Your 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.
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.
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
1// ~/strapi-todo-api
2
3yarn add strapi-plugin-magic //using yarn
4
5npm i strapi-plugin-magic //using npm
Rebuild your Strapi Admin
yarn build //using yarn
npm run build //using npm
Once 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.
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
.
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:
1 /** With Magic Changes */
2
3 try{
4
5 await strapi.plugins['magic'].services['magic'].loginWithMagic(ctx)
6
7 } catch (err) {
8
9 return handleErrors(ctx, err, 'unauthorized');
10
11 }
12
13 /** END With Magic Changes */\
Or simply copy and paste the below full code from here:
1"use strict";
2
3const _ = require("lodash");
4
5module.exports = async (ctx, next) => {
6
7 let role;
8
9 if (ctx.state.user) {
10
11 // request is already authenticated in a different way
12
13 return next();
14
15 }
16
17 if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
18
19 try {
20
21 const { id } = await strapi.plugins[
22
23 "users-permissions"
24
25 ].services.jwt.getToken(ctx);
26
27 if (id === undefined) {
28
29 throw new Error("Invalid token: Token did not contain required fields");
30
31 }
32
33 // fetch authenticated user
34
35 ctx.state.user = await strapi.plugins[
36
37 "users-permissions"
38
39 ].services.user.fetchAuthenticatedUser(id);
40
41 } catch (err) {
42
43 /** With Magic Changes */
44
45 try {
46
47 await strapi.plugins["magic"].services["magic"].loginWithMagic(ctx);
48
49 } catch (err) {
50
51 return handleErrors(ctx, err, "unauthorized");
52
53 }
54
55 /** END With Magic Changes */
56
57 }
58
59 if (!ctx.state.user) {
60
61 return handleErrors(ctx, "User Not Found", "unauthorized");
62
63 }
64
65 role = ctx.state.user.role;
66
67 if (role.type === "root") {
68
69 return await next();
70
71 }
72
73 const store = await strapi.store({
74
75 environment: "",
76
77 type: "plugin",
78
79 name: "users-permissions",
80
81 });
82
83 if (
84
85 _.get(await store.get({ key: "advanced" }), "email_confirmation") &&
86
87 !ctx.state.user.confirmed
88
89 ) {
90
91 return handleErrors(
92
93 ctx,
94
95 "Your account email is not confirmed.",
96
97 "unauthorized"
98
99 );
100
101 }
102
103 if (ctx.state.user.blocked) {
104
105 return handleErrors(
106
107 ctx,
108
109 "Your account has been blocked by the administrator.",
110
111 "unauthorized"
112
113 );
114
115 }
116
117 }
118
119 // Retrieve `public` role.
120
121 if (!role) {
122
123 role = await strapi
124
125 .query("role", "users-permissions")
126
127 .findOne({ type: "public" }, []);
128
129 }
130
131 const route = ctx.request.route;
132
133 const permission = await strapi
134
135 .query("permission", "users-permissions")
136
137 .findOne(
138
139 {
140
141 role: role.id,
142
143 type: route.plugin || "application",
144
145 controller: route.controller,
146
147 action: route.action,
148
149 enabled: true,
150
151 },
152
153 []
154
155 );
156
157 if (!permission) {
158
159 return handleErrors(ctx, undefined, "forbidden");
160
161 }
162
163 // Execute the policies.
164
165 if (permission.policy) {
166
167 return await strapi.plugins["users-permissions"].config.policies[
168
169 permission.policy
170
171 ](ctx, next);
172
173 }
174
175 // Execute the action.
176
177 await next();
178
179};
180
181const handleErrors = (ctx, err = undefined, type) => {
182
183 throw strapi.errors[type](err);
184
185};\
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.
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.
Our finished frontend application will look something like this:
We will be using the npx make-magic
CLI tool developed by Magic to get you started with the Magic-powered Nuxt application.
Open a new terminal window and type the following:
npx make-magic --template nuxt
After 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.
First, we will add DID Token
support to our frontend, which will authorize our requests at Strapi endpoints.
Open ~/store/index.js
file and replace it with the following code:
1import { magic } from '../plugins/magic'
2
3export const state = () => ({
4
5 user: null,
6
7 didToken: null,
8
9 authenticated: false,
10
11})
12
13export const mutations = {
14
15 SET_USER_DATA(state, data) {
16
17 state.user = data.userData
18
19 state.didToken = data.didToken
20
21 state.authenticated = true
22
23 },
24
25 CLEAR_USER_DATA(state) {
26
27 state.user = null
28
29 state.didToken = null
30
31 state.authenticated = false
32
33 this.$router.push('/login')
34
35 },
36
37}
38
39export const actions = {
40
41 async login({ commit }, email) {
42
43 await magic.auth.loginWithMagicLink(email)
44
45 const userData = await magic.user.getMetadata()
46
47 const didToken = await magic.user.getIdToken({ lifespan: 7200 })
48
49 const data = { userData, didToken }
50
51 // didToken is valid for 7200 seconds, i.e 2 hours
52
53 // Read more https://docs.magic.link/client-sdk/web/api-reference#getidtoken
54
55 commit('SET_USER_DATA', data)
56
57 },
58
59 async logout({ commit }) {
60
61 await magic.user.logout()
62
63 commit('CLEAR_USER_DATA')
64
65 },
66
67}
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 })
Create a ToDoList.vue
file inside the components directory and fill it with the following code:
1<template>
2
3 <div class="todo-container">
4
5 <div class="create-container">
6
7 <input
8
9 v-model="newToDo"
10
11 type="text"
12
13 class="todo-input"
14
15 placeholder="What needs to be done?"
16
17 v-on:keyup.enter="create"
18
19 />
20
21 <button @click="create">Add</button>
22
23 </div>
24
25 <div class="todo-list-container">
26
27 <ToDoListItem v-for="todo in todos" :key="todo.id" :todo="todo" />
28
29 </div>
30
31 </div>
32
33</template>
34
35<script>
36
37import axios from 'axios'
38
39export default {
40
41 props: {
42
43 todos: {
44
45 default() {
46
47 return []
48
49 },
50
51 },
52
53 },
54
55 data() {
56
57 return {
58
59 newToDo: '',
60
61 }
62
63 },
64
65 methods: {
66
67 async create() {
68
69 try {
70
71 if (this.newToDo && this.newToDo.length > 0) {
72
73 const result = await axios.post(
74
75 'http://localhost:1337/todos',
76
77 {
78
79 todoTitle: this.newToDo,
80
81 completed: false,
82
83 },
84
85 {
86
87 headers: {
88
89 Authorization: `Bearer ${this.$store.state.didToken}`,
90
91 },
92
93 }
94
95 )
96
97 this.$router.push('/loading')
98
99 }
100
101 this.newToDo = ''
102
103 } catch (error) {
104
105 console.log('handle error')
106
107 }
108
109 },
110
111 },
112
113}
114
115</script>
116
117<style>
118
119.todo-container {
120
121 max-width: 550px;
122
123 margin: 50px auto;
124
125 border-radius: 10px;
126
127 box-shadow: 0 0 25px rgb(0 0 0 / 15%);
128
129 overflow: hidden;
130
131}
132
133.create-container {
134
135 display: flex;
136
137}
138
139.todo-input {
140
141 -webkit-box-flex: 1;
142
143 flex-grow: 1;
144
145 border: none;
146
147 padding: 16px;
148
149 font-size: 24px;
150
151 font-weight: 100;
152
153 color: rgb(77, 77, 77);
154
155}
156
157input:focus {
158
159 outline: none;
160
161}
162
163input::placeholder {
164
165 color: #6f8297;
166
167 font-style: italic;
168
169}
170
171button {
172
173 font-size: 22px;
174
175 color: #6e6e6e;
176
177 padding: 0 27px;
178
179 background: transparent;
180
181 border: none;
182
183 cursor: pointer;
184
185 transition: all 0.2s;
186
187}
188
189button:disabled {
190
191 opacity: 0;
192
193 pointer-events: none;
194
195}
196
197.todo-list-container {
198
199 width: 550px;
200
201 margin: auto;
202
203 display: block;
204
205 box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.1);
206
207}
208
209</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:
1<template>
2
3 <div class="to-do-item" :class="{ completed: todo.completed }">
4
5 <span>{{ todo.todoTitle }}</span>
6
7 <div class="controls">
8
9 <span @click="deleteItem(todo)">❌</span>
10
11 <span @click="completeItem(todo)">✅</span>
12
13 </div>
14
15 </div>
16
17</template>
18
19<script>
20
21import axios from 'axios'
22
23export default {
24
25 props: {
26
27 todo: {
28
29 default() {
30
31 return {}
32
33 },
34
35 },
36
37 },
38
39 methods: {
40
41 async completeItem(todo) {
42
43 try {
44
45 if (todo.completed) {
46
47 await axios.put(
48
49 'http://localhost:1337/todos/' + todo.id,
50
51 {
52
53 completed: false,
54
55 },
56
57 {
58
59 headers: {
60
61 Authorization: `Bearer ${this.$store.state.didToken}`,
62
63 },
64
65 }
66
67 )
68
69 } else {
70
71 await axios.put(
72
73 'http://localhost:1337/todos/' + todo.id,
74
75 {
76
77 completed: true,
78
79 },
80
81 {
82
83 headers: {
84
85 Authorization: `Bearer ${this.$store.state.didToken}`,
86
87 },
88
89 }
90
91 )
92
93 }
94
95 this.$router.push('/loading')
96
97 } catch (error) {
98
99 console.log('handle error')
100
101 }
102
103 },
104
105 async deleteItem(todo) {
106
107 try {
108
109 await axios.delete('http://localhost:1337/todos/' + todo.id, {
110
111 headers: {
112
113 Authorization: `Bearer ${this.$store.state.didToken}`,
114
115 },
116
117 })
118
119 this.$router.push('/loading')
120
121 } catch (error) {
122
123 console.log('handle error')
124
125 }
126
127 },
128
129 },
130
131}
132
133</script>
134
135<style>
136
137.to-do-item {
138
139 width: 100%;
140
141 display: block;
142
143 height: 50px;
144
145 border-top: solid 1px #eeeeee;
146
147 font-size: 24px;
148
149}
150
151.completed {
152
153 opacity: 0.4;
154
155}
156
157.to-do-item span {
158
159 height: 50px;
160
161 padding-left: 20px;
162
163 line-height: 50px;
164
165 width: 450px;
166
167 text-align: left;
168
169 display: inline-block;
170
171}
172
173.to-do-item .controls {
174
175 display: inline-block;
176
177 height: 50px;
178
179 line-height: 50px;
180
181 float: right;
182
183}
184
185.to-do-item .controls span {
186
187 line-height: 50px;
188
189 height: 50px;
190
191 display: inline-block;
192
193 width: 45px;
194
195 text-align: center;
196
197 padding: 0;
198
199 cursor: pointer;
200
201}
202
203</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:
1<template>
2
3 <div class="container">
4
5 <div class="nuxt-todo">Nuxt Todo Application</div>
6
7 <ToDoList :todos="todos" />
8
9 </div>
10
11</template>
12
13<script>
14
15import axios from 'axios'
16
17export default {
18
19 middleware: 'magicauth',
20
21 data() {
22
23 return {
24
25 todos: '',
26
27 }
28
29 },
30
31 async asyncData({ store }) {
32
33 try {
34
35 const result = await axios.get('http://localhost:1337/todos', {
36
37 headers: {
38
39 Authorization: `Bearer ${store.state.didToken}`,
40
41 },
42
43 })
44
45 return {
46
47 todos: result.data,
48
49 }
50
51 } catch (error) {
52
53 console.log('handle error')
54
55 }
56
57 },
58
59}
60
61</script>
62
63<style scoped>
64
65.nuxt-todo {
66
67 font-size: 24px;
68
69 text-align: center;
70
71 margin-top: 10px;
72
73 padding-top: 20px;
74
75 color: #6851ff;
76
77}
78
79</style>
80
81```\
82Here, 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.
83
84Let'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.
85
86```bash
87
88// ~/hello-nuxt
89
90npm i epic-spinners // npm
91
92yarn add epic-spinners // yarn
Now, create a loading.vue
file inside the ~/pages
directory and fill it with the following code:
1<template>
2
3 <div id="app">
4
5 <atom-spinner
6
7 id="inner-center"
8
9 :animation-duration="1000"
10
11 :size="300"
12
13 :color="'#6851ff'"
14
15 />
16
17 </div>
18
19</template>
20
21<style scoped>
22
23#app {
24
25 width: 100%;
26
27 text-align: center;
28
29 margin: 200px auto;
30
31}
32
33#inner-center {
34
35 display: inline-block;
36
37 margin: 0 auto;
38
39 padding: 3px;
40
41}
42
43</style>
44
45<script>
46
47import { AtomSpinner } from 'epic-spinners'
48
49export default {
50
51 components: {
52
53 AtomSpinner,
54
55 },
56
57 mounted() {
58
59 if (this.$store.state.didToken) {
60
61 this.$router.push('/')
62
63 } else {
64
65 this.$router.push('/login')
66
67 }
68
69 },
70
71}
72
73</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.
Start the Strapi Backend server, if not already running.
// ~/strapi-todo-api
yarn develop //using yarn
// OR
npm run develop //using npm
Strapi server will be running on http://localhost:1337
Start the Nuxt Frontend Application.
// ~/hello-nuxt
yarn dev //using yarn
// OR
npm run dev //using npm
You should have your Nuxt application running on https://localhost:3000, displaying the list of todos. Try updating, creating, or deleting a to-do.
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.