Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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
2
3
4
5
// ~/strapi-todo-api
yarn add strapi-plugin-magic //using yarn
npm 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
2
3
4
5
6
7
8
9
10
11
12
13
/** 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"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);
};\
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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 })
Create a ToDoList.vue
file inside the components directory and fill it with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
<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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<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 // yarn
Now, create a loading.vue
file inside the ~/pages
directory and fill it with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<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.
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.