The Strapi Users and Permissions plugin allows you to add an authentication layer to your app using JSON Web Tokens. If a user is successfully authenticated, they are provided with a JWT token. When making authenticated requests, this token is passed in an Authorization header that Strapi checks to determine whether the user is allowed to access the resource they requested. The Users and Permissions plugin also bundles an ACL strategy. With it, you can authorize different groups of users to access resources and set permissions for them.
This tutorial is a continuation to “Build an Angular quiz app with Strapi”. That tutorial explained how to create a Strapi app with two content types: Quiz and Question. The app included an extra route to score a user’s answers to a quiz. The initial Strapi app was built using Strapi v3. The front-end of the app was built with Angular 11. It had three pages: one to list all the available quizzes, one where a user takes the quiz, and the last one to show scores.
In this tutorial, you will continue building the Angular app. However, you will have to create a new Strapi app for the backend because Strapi has moved to a new major version, v4. Since Strapi v4 and v3 are not backwards compatible and you cannot currently upgrade from v3 to v4, you will have to build the Strapi backend app from scratch. After adding the Question and Quiz content types, you will add Score and Answer types to track the user’s quiz attempts. Next, you will modify multiple Score controllers to score the quiz answers. Then you will change some API plugin permissions to allow actions like fetching and creating content for both public and authenticated roles. Lastly, you will add sample quizzes and questions that will be displayed on the Angular app, which you will build out in part 2.
By the end of this tutorial, you should have:
create
, find
, and findOne
Score controllers.To follow along with this tutorial, you will need Node.js installed. Strapi requires either Node.js v12 or v14; however, v14 is recommended. If you use a different version of Node.js from those two, you will run into problems generating the Strapi app. You can find pre-built installers for Node.js on the Node.js website downloads page..
As mentioned earlier, Strapi released a new major version of their headless CMS, v4. It’s currently impossible to upgrade the initial Strapi app built with v3 to v4. So you’ll have to create the backend from scratch. You will generate a new v4 Strapi app, add four content types, and modify routes to score quiz answers.
Call the backend strapi-quiz-app
. To generate it, run this command on your terminal:
npx create-strapi-app@latest strapi-quiz-app --quickstart
Running this command will generate the Strapi app, install its dependencies, and start it. The --quickstart
flag initiates app creation with the quickstart system where the database used is SQLite. The app will be made available at http://localhost:1337 on your browser. Once the app has started, you will be redirected to http://localhost:1337/admin/auth/register-admin, where you will be prompted to register an admin account. Below is a screenshot of what this should look like.
Once you’ve signed up, you’ll be redirected to the Strapi Dashboard. Here’s where you will create content and manage plugin settings. The Strapi dashboard looks like the screenshot below:
The Strapi app will have four content types:
You’ll create the content types using the Strapi CLI. The Strapi app may crash as you do this. If it is still running from the last step, stop it on the terminal. You can restart it once you finish creating the content types.
The Question content type will collect questions, the available options, and the correct answers. It will have five attributes:
text
: the body of the question. It is a text
type.a
: the A choice. It is a text
type.b
: the B choice. It is a text
type.c
: the C choice. It is a text
type.d
: the D choice. It is a text
type.answer
: the answer (a
, b
, c
, or d
). It is an enumeration
type.To create this content type, run the command below on your terminal at the app’s root directory:
npm run strapi generate
The above command launches the interactive Strapi API generator. You will be guided through a series of prompts. Answer these prompts using the inputs shown in the screenshot below. It will guide you on how to create the Question content type.
After creating the Question content type using the API generator, its schema, controller, routes, and service files will be generated for it in the /api/questions folder. This new content type will also appear on the Strapi dashboard.
The last thing to do in this step is make all the attributes for the Question content type required
. You can do this by replacing the content of src/api/question/content-types/question/schema.json with this below:
1 {
2 "kind": "collectionType",
3 "collectionName": "questions",
4 "info": {
5 "singularName": "question",
6 "pluralName": "questions",
7 "displayName": "Question",
8 "description": ""
9 },
10 "options": {
11 "draftAndPublish": false,
12 "comment": ""
13 },
14 "attributes": {
15 "text": {"type": "text","required": true},
16 "a": {"type": "text","required": true},
17 "b": {"type": "text","required": true},
18 "c": {"type": "text","required": true},
19 "d": {"type": "text","required": true},
20 "answer": {"type": "enumeration","required": true,"enum": ["a","b","c","d"]}
21 }
22 }
The highlighted portions are the changes that were made to the schema. For each of the attributes, add the "required": true property as shown in the code snippet above.
In this step, you created the Question content type. In the next one, you will create the Quiz content type.
The Quiz content type collects quizzes, their titles, descriptions, and questions. It will have three attributes:
title
: the name of the quiz. It is a string
type.description
: what the quiz is about. It is a text
type.questions
: all the questions in the quiz. It is a relation
type.To generate the Quiz content type, run the command below on your terminal.
npm run strapi generate
Use the screenshot below to guide you on what to input as responses to the API generator prompts.
Its schema, controllers, services, and routes are added to the /api/quiz folder. The API generator doesn’t allow you to add relation attributes with it. The questions
attribute will have to be added to the Quiz schema file. You will also make the title
and the description
attributes required
. Copy the code below to src/api/quiz/content-types/quiz/schema.json.
1 {
2 "kind": "collectionType",
3 "collectionName": "quizzes",
4 "info": {
5 "singularName": "quiz",
6 "pluralName": "quizzes",
7 "displayName": "Quiz",
8 "description": ""
9 },
10 "options": {
11 "draftAndPublish": false,
12 "comment": ""
13 },
14 "attributes": {
15 "title": {"type": "string","required": true},
16 "description": {"type": "text","required": true},
17 "questions": {"type": "relation","relation": "oneToMany","target": "api::question.question"}
18 }
19 }
The highlighted portions are the changes that were made to the schema. The questions
attribute is a oneToMany
relation
, where a quiz has many questions. In this step, you created the Quiz content type. In the next one, you will create the Answer content type.
The Answer content type collects the users’ answers. It will have three attributes:
question
: the question the user answered. It is a relation
type. value
: the actual value of the answer. It is a string
type.score
: the quiz attempt that the answer is a part of. It is a relation
type. The score
content type does not exist currently, so you will omit this field and add it later. To generate the Answer content type, run the command below on your terminal.
npm run strapi generate
Use the screenshot below to guide you on what to input as responses give to the API generation prompts.
Its schema, controllers, services, and routes are added to the /api/answer folder. As mentioned earlier, the API generator doesn’t allow you to add relation attributes. The question
attribute should be added to the Answer schema file. Also, make the value
attribute required
. Copy the code below to src/api/answer/content-types/answer/schema.json.
1 {
2 "kind": "collectionType",
3 "collectionName": "answers",
4 "info": {
5 "singularName": "answer",
6 "pluralName": "answers",
7 "displayName": "Answer",
8 "description": ""
9 },
10 "options": {
11 "draftAndPublish": false,
12 "comment": ""
13 },
14 "attributes": {
15 "value": {"string": "text","required": true},
16 "question": {"type": "relation","relation": "oneToOne","target": "api::question.question"}
17 }
18 }
The highlighted portions are the changes that were made to the schema. The question
attribute is a oneToOne
relation
, where an answer only has one question. In this step, we created the Answer content type.
In this step, you created the Answer content type. In the next one, you will create the Score content type.
The Score content type collects all the answers a user has given for a quiz. It has three attributes:
quiz
: the quiz that the score is created from. It is a relation
type. user
: the owner of the score. It is a relation
type. answers
: all the answers the user gave for the quiz. It is a relation type
. To generate the Score content type, run the command below on your terminal.
npm run strapi generate
Use the screenshot below to guide you on what to input as responses to the API generation prompts.
Its schema, controllers, services, and routes are added to the /api/score folder. Since all the attributes are relations, you won’t be able to add any of them using the API generator as it doesn’t allow it. The quiz
, user
, and answers
attributes should be added to the Score schema file manually. Copy the code below to src/api/answer/content-types/score/schema.json.
1 {
2 "kind": "collectionType",
3 "collectionName": "scores",
4 "info": {
5 "singularName": "score",
6 "pluralName": "scores",
7 "displayName": "Score",
8 "description": ""
9 },
10 "options": {
11 "draftAndPublish": false,
12 "comment": ""
13 },
14 "attributes": {
15 "quiz": {"type": "relation","relation": "oneToOne","target": "api::quiz.quiz"},
16 "user": {"type": "relation","relation": "oneToOne","target": "plugin::users-permissions.user"},
17 "answers": {"type": "relation","relation": "oneToMany","target": "api::answer.answer","mappedBy": "score"}
18 }
19 }
The highlighted portions are the changes that were made to the schema. The Quiz
attribute is a oneToOne
relation
, where a score has one quiz. The User
attribute is also a oneToOne
relation
, where a score has one user. The Answer
attribute is a oneToMany
relation
, where a score belongs to many answers.
In this step, you created the Score content type. In the next one, you will update the Answer content type to have a score
attribute.
score
attribute to the Answer content type. Do this by copying the code below to src/api/answer/content-types/answer/schema.json. 1 {
2 "kind": "collectionType",
3 "collectionName": "answers",
4 "info": {
5 "singularName": "answer",
6 "pluralName": "answers",
7 "displayName": "Answer",
8 "description": ""
9 },
10 "options": {
11 "draftAndPublish": false,
12 "comment": ""
13 },
14 "attributes": {
15 "value": {"type": "text","required": true},
16 "question": {"type": "relation","relation": "oneToOne","target": "api::question.question"},
17 "score": {"type": "relation","relation": "manyToOne","target": "api::score.score","inversedBy": "answers"}
18 }
19 }
The highlighted portions are the changes that were made to the schema. The score
attribute is a manyToOne
relation
, where a score has many answers.
In this step, you created four content types: Question, Quiz, Answer, and Score. In the Next step, you will, let’s extend the Score controllers to make them calculate scores based on the answers of the score.
In this step, you will modify the create
, find
, and findOne
Score controllers. Typically, the Score controllers perform the basic CRUD operations. However, the create
controller, in addition to creating a score, also needs to create user’s answers when a score is created. The find
and findOne
controllers need to fetch the user’s answers, calculate the score total based on the answers, and send the score total as a response.
To begin, change the content of src/api/score/controllers/score.js to:
1 'use strict';
2 /**
3 * score controller
4 */
5 const { createCoreController } = require('@strapi/strapi').factories;
6
7 module.exports = createCoreController('api::score.score', ({ strapi }) => ({
8 // place modified controllers here
9 }));
Here, you are adding a second argument to the createCoreController
factory that returns the modified controllers. You’ll place the extended create
, find
, and findOne
controllers here. You will also add a helper function that calculates the score total above the exports.
In the create
Score controller, the user’s answers are added to the database and a score is created to mark a quiz attempt. The score total is then calculated based on the answers given and returned as a response. Add the code below in the src/api/score/controllers/score.js file, where the // place modified controllers here
comment is.
1 async create(ctx) {
2 let { answers } = ctx.request.body;
3 const quizId = ctx.request.body.quiz.id;
4
5 try {
6 let quiz = await strapi.service('api::quiz.quiz').findOne(quizId, { populate: ['questions'] });
7 '
8 let score = await strapi.service('api::score.score').create({
9 data: {
10 quiz: quizId,
11 user: ctx.state.user.id
12 }
13 });
14
15 let question, userAnswer;
16 let scoreTotal = 0;
17
18 score.answers = [];
19
20 for (let i = 0; i < answers.length; i++) {
21 userAnswer = answers[i];
22
23 question = quiz.questions.find((qst) => qst.id === userAnswer.question.id);
24 if (question) {
25 await strapi.service('api::answer.answer').create({
26 data: {
27 question: question.id,
28 score: score.id,
29 value: userAnswer.value
30 }
31 });
32
33 if (question.answer === userAnswer.value) {
34 userAnswer.correct = true;
35 scoreTotal += 1;
36 } else {
37 userAnswer.correct = false;
38 }
39 userAnswer.correctValue = question.answer;
40 }
41 score.answers.push(userAnswer);
42 }
43 const questionCount = quiz.questions.length;
44 delete quiz.questions;
45
46 return { questionCount, scoreTotal, quiz, score };
47 } catch {
48 ctx.response.status = 500;
49 return { error: { message: 'There was a problem scoring your answers'}};
50 }
51 },
To begin, we will retrieve the answers
and quizId
from the request body, ctx.request.body
. Using the quizId
, we’ll fetch the attempted quiz
with its associated questions populated. Then, a new score
is created with the current user and quiz. For each of the answers
a user has provided, a corresponding question
is identified. Each answer, userAnswer
, is then saved to the database with its value and associated score and question. Next, a check is performed to verify whether the value for the userAnswer
matches the correct answer for the question
. If it does, a correct
property is added to it, indicating that it is correct and the scoreTotal
is incremented. A correctValue
property is also added, so that if the user got it wrong, they can see the correct answer. Lastly, all the correct answers are added to the score. Then the score
, questionCount
, quiz
, and scoreTotal
are returned. In an error occurs, a 500
status and message are returned.
In this section, you modified the create
Score controller. In the next one, you will add a helper function to score user answers when saved scores are fetched.
In this section, you will add a function called calculateScore
, which calculates scores for the find
and findOne
Score controllers. When a score is retrieved, the score total is calculated from the answers by the calculateScore
function before it is returned. In the src/api/score/controllers/score.js file, add this code before the exports.
1 function calculateScore(score) {
2 let question;
3 let scoreTotal = 0;
4
5 score.answers.map((userAnswer) => {
6 question = userAnswer.question;
7
8 if (question.answer == userAnswer.value) {
9 userAnswer.correct = true;
10 scoreTotal += 1;
11 } else {
12 userAnswer.correct = false;
13 }
14
15 userAnswer.correctValue = question.answer;
16 delete userAnswer.question;
17
18 return userAnswer;
19 });
20
21 const questionCount = score.quiz.questions.length;
22 delete score.quiz.questions;
23
24 let resp = {
25 questionCount,
26 scoreTotal,
27 quiz: score.quiz
28 };
29
30 delete score.quiz;
31
32 return { ...resp, score };
33 }
This function takes a score
parameter. For each of the answers, similar to what happens in the create
Score controller, a check is performed against its value to see if it is correct. If it is, it is marked as correct and the scoreTotal
is incremented. The correct answer to the question is also added to the scored answer. Lastly, the quiz
, questionCount
, scoreTotal
, and score
are returned as one object. In this section, you added a calculateScore
function. In the next one, you will use this function in the find
and findOne
Score controllers.
We need to modify the findOne
Score controller, so that the score total is generated and added to the score. In the src/api/score/controllers/score.js file, add the following code below the create
controller.
1 async findOne(ctx) {
2 const { id } = ctx.params;
3
4 try {
5 let score = await strapi.service('api::score.score').findOne(id, {
6 filters: { user: ctx.state.user.id },
7 populate: {
8 answers: { populate: ['question'] },
9 quiz: { populate: ['questions']}
10 }
11 });
12
13 return calculateScore(score);
14 }
15 catch {
16 ctx.response.status = 500;
17 return { error: { message: 'There was a problem fetching your score.' }};
18 }
19 },
You’ll begin by fetching the score id
from the request route parameters. Then, using this id
, a score
is fetched with the user’s answers and the quiz associated with it is populated. The score total for the score
is then calculated and returned. If there is an error, a 500
status and message are returned instead. In this section, you modified the findOne
Score controller to calculate score totals. In the next section, you will modify the find
Score controller to do the same.
We will modify the find
Score controller to calculate the total for each of the fetched scores. In the src/api/score/controllers/score.js file, add the following code below the findOne
controller.
1 async find(ctx) {
2 try {
3 const { query } = ctx;
4
5 let scores = await strapi.service('api::score.score').find({
6 filters: { user: ctx.state.user.id },
7 populate: {
8 answers: { populate: ['question'] },
9 quiz: { populate: ['questions'] }
10 },
11 ...query
12 });
13
14 return scores.results.map(calculateScore);
15 } catch {
16 ctx.response.status = 500;
17 return { error: { message: 'There was a problem fetching your scores.' }};
18 }
19 }
Start by getting the query
from the context which returns a parsed version of the request query-string. Then, you’ll fetch all the scores
that belong to the user with the answers and quiz populated. The scores are filtered using any values provided in the query
. You’ll then calculate the score total for each of the fetched scores
and return them as a response. If any problem occurs, an error message and a 500
status are returned instead.
In this step, you extended the create
, find
, and findOne
Score controllers to calculate scores. In the next step, you will change the roles settings for the Users and Permissions plugin to make various API routes accessible.
Change the allowed actions for various API plugins for the authenticated and public roles in the Users and Permissions ***plugin settings. Modify the API plugin permissions for the authenticated user role so that they can access some Score and Answer routes. Then, change the API plugin permissions for an unauthenticated/public user so that they can access some Question and Quiz* routes.
If you haven’t started your Strapi app since step 2, you can run it from the terminal within its root directory using this command:
npm run develop
On the Public Role settings page for the Users and Permissions plugin, locate the Quiz and Questions API plugin settings under the Permissions section. Select the find and findOne checkboxes to make these three allowed actions for both the Quiz and Question API plugins.
On the Authenticated Role settings page for the Users and Permissions plugin, locate the Answer and Score API plugin settings under the Permissions section. Select the create, find, and findOne checkboxes to make these three allowed actions for both the Answer and Score API plugins. The Answer and Score actions need to be authenticated because you’ll need the user’s information to create and fetch answers and scores.
In this step, you modified the allowed actions for the Question, Quiz, Answer, and Score API plugins for both authenticated and public roles. In the following step, you will add sample Question and Quiz content.
In this step, you will add sample question and quiz content. These will be displayed on the frontend. If you have alternative ideas for questions and quizzes, you can use those instead. On the Question Content Manager “Create an entry” page, add these four questions with the data shown in the screenshots. Once you have added the content, click the bright blue Save button located at the top right corner of the page.
On the Quiz Content Manager “Create an entry” page, add these three quizzes with the data shown in the screenshots. Make sure that all the four available questions are added to each of the quizzes in the“Questions” Relation section. Once you’re finished inputting the content, click the bright blue Save button located at the top right corner of the page to create them.
You may also need to add a user who will take the tests. You can do this now on the dashboard or later on the frontend on the signup page. If you opt to do this now, add a user on the User Content Manager “Create an entry” page. In this step, you added sample questions and quizzes to be displayed on the frontend.
In this tutorial, you created a Strapi quiz app. You made four content types: Question, Quiz, Answer, and Score content types. Next, you extended the create
, find
, and findOne
Score controllers so that they can calculate score totals. To make routes for these content types accessible, you added allowed actions to their API plugins for public and authenticated roles on the User and Permissions plugin. Lastly, you created sample question and quiz content to be displayed on the frontend. In part 2, you will modify the Angular 13 frontend to allow for user signup and login and to track their scores.
To learn more about Strapi and ways you can customize it, checkout its website and its documentation.
Zara is a software developer and technical writer, using React Native, React, Rails, Node, Golang, Angular and many more technologies.