Authentication is an integral part of application development, as it helps to secure user data and authorization.
In this tutorial, we'll be learning how to integrate authentication into our Strapi Application, and we'll be building a simple Recipe Application with Strapi backend and Vue.js frontend. Users will search for recipes in this Application and will pull results from the Edamam recipe API. Users will be able to register, log in to our Application and also perform password recovery.
What'll you need for this tutorial:
Here's what the final version of our Application will look like
You can find the GitHub repository for the vue application here. Also, see the assets used in this project: Background image & Other assets
I hope you're very excited; let's get started with our Strapi Backend setup:
The Strapi documentation says that "Strapi is an open-source headless CMS that gives developers the freedom to choose their favorite tools and frameworks while also allowing editors to manage and distribute their content using their application's admin panel."
By making the admin panel and API extensible through a plugin system, Strapi enables the world's largest companies to accelerate content delivery while building beautiful digital experiences.
Strapi is fantastic; I'm still stunned by what Strapi can do.
The documentation will walk you through installing Strapi from the CLI, the minimum requirements for running Strapi, and how to create a quickstart project.
The Quickstart project uses SQLite as the default database, but feel free to use whatever database you like.
yarn create strapi-app my-project //using yarn
npx create-strapi-app my-project --quickstart //using npm
Replace my-project
with the name you wish to call your application directory. Your package manager will create a directory with the name and will install Strapi.
If you have followed the instructions correctly, you should have Strapi installed on your machine. Run the following command:
yarn develop //using yarn
npm run develop //using npm
To start our development server, Strapi starts our app on http://localhost:1337/admin
.
Next, we are going to create the Bookmarks Collection Type. Follow these steps below to create your first Collection Types.
bookmark
and then click Continue.Next, we are going to choose all the fields on the Notes Collection Type. Follow the step below to choose your types.
Text
, name the field label
, leave the type selection as Short Text
, and add another field.Text
, name the field source
, leave the type selection as Short Text
, and click on add another field.Text
, name the field image
, leave the type selection as Short Text
, and click on add another field.Text
, name the field url
, leave the type selection as Short Text
, and next, add another field.Number
, name the field yield
, set the Number format selection as Integer
, and next, add another field.Number
, name the field totalTime
, set the Number format selection as decimal
, and next, add another field.JSON
, name the field ingredientLines
, then click on add another field.Relations
, and then click on the dropdown on the right side of the popup window, select User (from: users-permissions-user)
, then click on Users
have many bookmarks. It should look like the image below.If you follow the steps above correctly, the final bookmarks collection type schema should look like the image below.
Now that we have successfully created our Bookmark Content Types, let's add and assign a permission level on the bookmarks Collection-Type for an authenticated user by following the steps below.
GENERAL
in the side menuCreate
, findOne
, and find
checkboxes.Next, we will create and assign permissions on notes collection-type for our public users by following the steps below.
On the side menu bar, under settings,
Users & Permission plugin
, click on Advanced settings
.Reset password page
input with the following url, http://localhost:8080/resetpassword
.To obtain Edamam Recipe API credentials, follow the steps below:
1. Visit https://www.edamam.com/.
2. Under Recipe search API
, click More info
.
3. Under Developer, click Get Started
.
4. Enter your credentials to sign up.
5. Next Sign in to the Edamam APIs
.
6. Click Go to Dashboard
.
7. Click Create a new Application
.
8. Select Recipe search API
.
9. On the next page, give the Application a name and a description.
10. Click Create Application
.
11. The next page that's rendered should contain your Application ID
and Application keys
.
Now, we're done with both our backend setup, and we have our API credentials. We can proceed with installing Vue.js and building the Front-end of our Application.
Next, we will install and configure Vue.Js to work with our Strapi backend.
To install Vue.js, using the @vue/cli package visit the Vue CLI docs or run one of these commands to get started. We used Vue 2 for this project.
npm install -g @vue/cli
# OR
yarn global add @vue/cli
Once the vue CLI is installed on your local machine, run the following commands to create a Vue.js project.
vue create my-project
Replace my-project
with the name you wish to call your project.
The above command should start a command-line application that walks you through creating a Vue.js project. Select whatever options you like, but select Router
, Vuex
, and linter/formatter
because the first two are essential in our Application, then the last one is to format our code nicely.
After vue CLI is done creating your project, run the following command.
cd my-project
yarn serve //using yarn
npm serve //using npm
Finally, visit the following URL: \[http://localhost:8080\](http://localhost:8080/)
to open your Vue.js Application in your browser.
We are going to use TailwindCSS as our CSS framework. Let's see how we can integrate TailwindCSS into our Vue.js Application.
npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
or
yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
postcss.config.js
and fill it up with the following lines.1 module.exports = {
2 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
5 }
6 }
tailwindcss.config.js
and fill it up with the following lines.1 module.exports = {
2 purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
3 darkMode: false, // or 'media' or 'class'
4 theme: {
5 extend: {
6 fontFamily: {
7 'pacifico': ['Pacifico'],
8 'montserrat': ['Montserrat'],
9 'roboto': ['Roboto'],
10 'righteous': ['Righteous'],
11 'lato': ['Lato'],
12 'raleway': ['Raleway'],
13 }
14 },
15 },
16 variants: {
17 extend: {},
18 },
19 plugins: [],
20 }
We've extended the components of the font by adding some fonts which we will use. These fonts have to be installed in your local machine to work appropriately but feel free to use whatever fonts you like.
Finally, create a index.css
file in your src
folder and add the following lines
1 /* ./src/main.css */
2 @tailwind base;
3 @tailwind components;
4 @tailwind utilities;
Font-awesome is a package that we’ll use for getting and rendering icons in our application. Execute the following commands to install vue-fontawesome on your machine
npm i --save @fortawesome/fontawesome-svg-core
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/vue-fontawesome
or
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/vue-fontawesome
yarn add @fortawesome/vue-fontawesome
We need a package for making API calls to our Strapi backend, and we'll be using the Vue-Axios package for that purpose.
Run the following command to install Vue-Axios in your machine:
npm install --save axios vue-axios vue-router vuex
or
yarn add axios vue-axios vue-router vuex
Next, we need a way to show our users that data is being fetched from an API. We'll do that using the Vue-progress-path package.
Execute the following commands to install Vue-progress-path in your machine
npm i -S vue-progress-path
yarn add vue-progress-path
What've we've done above is to install the packages. Vue.js has no idea what to do with the installed packages, so we're going to tell Vue.js what to do with the packages.
main.js
file that is located in the src
folder and replace the contents of the file with the following code1 import Vue from 'vue'
2 import App from './App.vue'
3 import router from './router'
4 import store from './store'
5 import axios from 'axios'
6 import vueAxios from 'vue-axios'
7 import './index.css'
8 import { library } from '@fortawesome/fontawesome-svg-core'
9 import { faArrowRight, faArrowLeft, faSearch, faBookmark, faShare, faClock, faCheck, faUserCircle, faTrash, faBars, faTimes } from '@fortawesome/free-solid-svg-icons'
10 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
11 import 'vue-progress-path/dist/vue-progress-path.css'
12 import VueProgress from 'vue-progress-path'
13
14 library.add(faArrowRight, faArrowLeft, faSearch, faBookmark, faShare, faClock, faCheck, faUserCircle, faTrash, faBars, faTimes)
15 Vue.component('font-awesome-icon', FontAwesomeIcon)
16 Vue.use(vueAxios, axios)
17 Vue.use(VueProgress, {
18 // defaultShape: 'circle',
19 })
20 Vue.config.productionTip = false
21 new Vue({
22 router,
23 store,
24 render: h => h(App)
25 }).$mount('#app')
App.vue
file in src
and replace it with the following codes.1 <template>
2 <div id="app">
3 <router-view />
4 </div>
5 </template>
6 <script>
7 export default {
8 name: "App",
9 };
10 </script>
Let’s begin building the front-end of our application.
To build the homepage, create an Home.vue
file located in the src/views
folder, and add the following lines of code to the file.
1 <template>
2 <div class="overflow-x-hidden">
3 <Nav class="z-20" />
4
5 <!-- Hero section -->
6 <HeroSection />
7 <!-- featured section -->
8 <FeaturedSection />
9 </div>
10 </template>
11 <script>
12 // @ is an alias to /src
13 import Nav from '@/components/Nav.vue'
14 import HeroSection from '@/components/HeroSection.vue'
15 import FeaturedSection from '@/components/FeaturedSection.vue'
16 export default {
17 name: 'Home',
18 components: {
19 Nav,
20 HeroSection,
21 FeaturedSection
22 }
23 }
24 </script>
To build a component, follow the steps below:
Nav.vue
file in the components folder cd components
touch Nav.vue
Nav.vue
file and fill it up with the following lines of code.1 <template>
2 <div class="w-full bg-white fixed top-0 shadow-lg">
3
4 <div class="w-11/12 mx-auto flex justify-between justify-center items-center px-5 py-7">
5 <div class="text-black sm:text-left text-center text-4xl font-bold font-pacifico">
6 <h1>Recipee</h1>
7 </div>
8 <div @click="toggleMobileMenu" class="md:hidden">
9 <font-awesome-icon v-if='!mobileMenu' class="text-xl" :icon="['fas', 'bars']" />
10 <font-awesome-icon v-if='mobileMenu' class="text-xl" :icon="['fas', 'times']" />
11 </div>
12 <!-- desktop view -->
13 <div class="flex bg-white space-x-12 hidden sm:block text-black-200 font-raleway tracking-wide items-center">
14 <router-link to="/">HOME</router-link>
15 <router-link to="/explore">SEARCH RECIPES</router-link>
16 <router-link to="/register" v-if="!user">SIGN UP</router-link>
17 <router-link to="/login" v-if="!user">LOGIN</router-link>
18 <router-link to="/bookmarks" v-if="user">
19 <font-awesome-icon class="text-xl" :icon="['fas', 'bookmark']" /> BOOKMARKS
20 </router-link>
21 <router-link to="" v-if="user">
22 <font-awesome-icon class="text-xl" :icon="['fas', 'user-circle']" /> {{ user.username }}
23 </router-link>
24 <span @click="logout">
25 <router-link to="" v-if="user">LOGOUT</router-link>
26 </span>
27
28 </div>
29 </div>
30 <!-- mobile view -->
31 <div v-if="mobileMenu" class="h-screen md:hidden text-2xl text-left font-raleway p-10">
32 <router-link to="/" class="block my-7">HOME</router-link>
33 <hr>
34 <router-link to="/explore" class="block my-7">SEARCH RECIPES</router-link>
35 <hr>
36 <router-link to="/register" v-if="!user" class="block my-7">SIGN UP</router-link>
37 <hr>
38 <router-link to="/login" v-if="!user" class="block my-7">LOGIN</router-link>
39 <hr>
40 <router-link to="/bookmarks" v-if="user" class="block my-7">
41 <font-awesome-icon class="text-xl" :icon="['fas', 'bookmark']" /> BOOKMARKS
42 </router-link>
43 <hr>
44 <router-link to="" v-if="user" class="block my-7">
45 <font-awesome-icon class="text-xl" :icon="['fas', 'user-circle']" /> {{ user.username }}
46 </router-link>
47 <hr>
48 <span @click="logout" class="block my-7">
49 <router-link to="" v-if="user">LOGOUT</router-link>
50 </span>
51 </div>
52 </div>
53 </template>
54 <script>
55 export default {
56 name: 'Nav',
57 data() {
58 return {
59 user: {},
60 mobileMenu: false
61 }
62 },
63 mounted() {
64 this.user = JSON.parse(window.localStorage.getItem('userData'))
65 },
66 methods: {
67 logout() {
68 window.localStorage.removeItem('jwt')
69 window.localStorage.removeItem('userData')
70 window.localStorage.removeItem('bookmarks')
71 this.$router.push('/login')
72 },
73 toggleMobileMenu() {
74 this.mobileMenu = !this.mobileMenu
75 }
76 }
77 }
78 </script>
79 <style scoped>
80 </style>
Below are the steps to build this component:
HeroSection.vue
file in the components folder cd components
touch HeroSection.vue
HeroSection.vue
file and fill it up with the following lines of code. <template>
<div>
<section>
<div class=" h-screen bg-cover" style="background: url(newFood.png)">
<div class="bg-blue-800 bg-opacity-50">
<div class="mx-auto h-screen flex text-white justify-left sm:w-4/5 items-center">
<div class="text-left font-montserrat mx-5 z-10">
<h1 class="text-6xl font-black my-10 z-10">
FIND THE
<br>
WORLD'S BEST
<br>
RECIPES ONLINE
</h1>
<router-link to='/explore' class="py-5 px-10 text-xl bg-green-600 z-10">
Search Recipes
<font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
</router-link>
</div>
<div class="text-8xl absolute right-0 font-lato hidden sm:block font-bold overflow-y-hidden w-1/2">
<img src="../assets/undraw_breakfast-removebg-preview.png" alt="" class="w-full">
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'HeroSection'
}
</script>
<style scoped>
</style>
To build the FeaturedSection component:
FeaturedSection.vue
file in the components folder cd components
touch FeaturedSection.vue
FeaturedSection.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <section>
4 <div class="relative">
5 <div class="sm:flex block mx-auto my-20 justify-center items-center">
6 <div class="z-10">
7 <div class="mx-auto mb-5 sm:mb-0 w-4/5 bg-pink-300 p-20">
8 <img src="../assets/burger.png" alt="" class="">
9 </div>
10 </div>
11
12 <div class="absolute top-0 right-0">
13 <img src="../assets/watercolor_stain.png" alt="" class="opacity-40 sm:opacity-70">
14 </div>
15 <div class="z-10">
16 <div class="mx-auto w-4/5 text-left font-raleway z-10">
17 <h1 class="font-bold text-black text-6xl mb-10">
18 THE BEST MEALS
19 <br>
20 IN THE UNIVERSE AWAITS
21 </h1>
22 <p class="text-sm tracking-wide font-montserrat mb-10">
23 Lorem ipsum dolor sit amet consectetur adipisicing elit.
24 <br>
25 Facilis ex iure rem vero voluptate, sint praesentium quidem,
26 <br>
27 eius sequi, officia itaque? Eveniet quaerat eos qui sunt suscipit nisi sequi? Soluta.
28 </p>
29 <p class="text-xl text-black font-bold font-raleway">
30 EXPLORE
31 <font-awesome-icon class="" :icon="['fas', 'arrow-right']" />
32 </p>
33 </div>
34 </div>
35 </div>
36 </div>
37 <div class="relative">
38 <div class="block sm:flex my-20 justify-center items-center">
39 <div class="absolute top-0 left-0">
40 <img src="../assets/watercolor_drops.png" alt="" class="opacity-70">
41 </div>
42
43 <div class="z-10">
44 <div class="w-4/5 mx-auto text-left font-raleway z-10">
45 <h1 class="font-bold text-black text-6xl mb-10">
46 LEARN HOW
47 <br>
48 TO PREPARE MEALS YOU LOVE
49 </h1>
50 <p class="text-sm tracking-wide font-montserrat mb-10">
51 Lorem ipsum dolor sit amet consectetur adipisicing elit.
52 <br>
53 Facilis ex iure rem vero voluptate, sint praesentium quidem,
54 <br>
55 eius sequi, officia itaque? Eveniet quaerat eos qui sunt suscipit nisi sequi? Soluta.
56 </p>
57 <p class="text-xl mb-5 sm:mb-0 text-black font-bold font-raleway">
58 EXPLORE
59 <font-awesome-icon class="" :icon="['fas', 'arrow-right']" />
60 </p>
61 </div>
62 </div>
63 <div class="">
64 <div class="mx-auto w-4/5 bg-green-300 p-20">
65 <img src="../assets/barbercue.png" alt="" class="">
66 </div>
67 </div>
68 </div>
69 </div>
70 </section>
71 </div>
72 </template>
73 <script>
74 export default {
75 name: 'FeaturedSection'
76 }
77 </script>
78 <style scoped>
79 </style>
We need routing functionality in our Application. Luckily for us, we installed the Vue-router package when creating our project.
Create a router/index.js
file, and fill it up with the following lines of codes
1 import Vue from 'vue'
2 import VueRouter from 'vue-router'
3 import Home from '../views/Home.vue'
4 import Register from '../views/Register.vue'
5 import Login from '../views/Login.vue'
6 import Explore from '../views/Explore.vue'
7 import Recipe from '../views/Recipe.vue'
8 import Bookmarks from '../views/Bookmarks.vue'
9 import BookmarkId from '../views/BookmarkId.vue'
10 import ForgotPassword from '../views/ForgottenPassword.vue'
11 import ResetPassword from '../views/ResetPassword.vue'
12 Vue.use(VueRouter)
13 const routes = [
14 {
15 path: '/',
16 name: 'Home',
17 component: Home
18 },
19 {
20 path: '/register',
21 name: 'Register',
22 component: Register
23 },
24 {
25 path: '/login',
26 name: 'Login',
27 component: Login
28 },
29 {
30 path: '/explore',
31 name: 'Explore',
32 component: Explore
33 },
34 {
35 path: '/recipe/:id',
36 name: 'Recipe',
37 component: Recipe
38 },
39 {
40 path: '/bookmarks',
41 name: 'Bookmarks',
42 component: Bookmarks
43 },
44 {
45 path: '/bookmark/:id',
46 name: 'BookmarkId',
47 component: BookmarkId
48 },
49 {
50 path: '/forgotpassword',
51 name: 'ForgotPassword',
52 component: ForgotPassword
53 },
54 {
55 path: '/resetpassword',
56 name: 'ResetPassword',
57 component: ResetPassword
58 }
59 ]
60 const router = new VueRouter({
61 mode: 'history',
62 base: process.env.BASE_URL,
63 routes
64 })
65 export default router
Now we have router functionalities in our application, next we’ll set up our vuex
store.
store
folder in src
folder, and create a new index.js
file with the following code.1 import Vue from "vue";
2 import Vuex from "vuex";
3 import Results from "./results.js";
4 Vue.use(Vuex);
5 export default new Vuex.Store({
6 modules: {
7 Results
8 }
9 });
results.js
file in the src/store
directory cd store
touch results.js
results.js
file and fill it up with the following code.1 import Vue from "vue";
2 const state = {
3 searchParam: '',
4 searchResults: [],
5 bookmarks: JSON.parse(window.localStorage.getItem('bookmarks'))
6 }
7 const getters = {
8 getSearchResults: state => state.searchResults,
9 getSearchParam: state => state.searchParam,
10 getBookmarks: state => {
11 return state.bookmarks
12 }
13 }
14 const actions = {
15 async fetchSearchResult ({ commit }, searchItem) {
16
17 const res = await Vue.axios.get(`https://api.edamam.com/search?q=${searchItem}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=20`)
18 const results = res.data.hits
19 commit('updateSearchResults', results)
20 },
21 async fetchSearchItem ({ commit }, item) {
22 commit('updateSearchItem', item)
23 }
24 }
25 const mutations = {
26 updateSearchResults: (state, results) => {
27 state.searchResults = results
28 },
29 updateSearchItem: (state, item) => {
30 state.searchParam = item
31 }
32 }
33 export default {
34 state,
35 getters,
36 actions,
37 mutations
38 }
Here we've created our store. On line 17, we make an API call to the Edamam recipe API
using the Vue-Axios
package we installed earlier, and then we commit the results to the store. Replace ${APP_ID}
and ${APP_KEY}
with your Edamam Application ID
and Application key
, respectively.
Let’s build the other routes of our application.
To create this page:
Explore.vue
file in the views folder cd views
touch Explore.vue
Explore.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <Nav class="z-20" />
4 <section>
5 <div class="h-sreen w-full bg-cover" style="background: url(newFood.png)">
6 <div class="bg-blue-800 w-full bg-opacity-50">
7 <div class="mx-auto flex h-screen w-full justify-center items-center">
8 <div class="font-montserrat w-full text-white mx-5 z-10">
9 <h1 class="font-pacifico hidden sm:block text-6xl mb-10">Recipee</h1>
10 <!-- <h1 class="text-4xl mb-10 font-raleway">Search for whatsoever recipe you want</h1> -->
11 <form @submit="getRecipes">
12 <input type="text" name="search" v-model="search" placeholder="Search Recipe" class="p-10 focus:outline-none w-4/5 sm:w-3/5 text-black">
13 <button class="p-5 cursor-pointer bg-green-400">
14 <font-awesome-icon class="text-2xl" :icon="['fas', 'search']" />
15 </button>
16 </form>
17 </div>
18
19 <!-- <div v-if="loading" class="rounded-full absolute bottom-20 bg-blue-300 w-10 h-10">
20 </div> -->
21 <loading-progress class="absolute bottom-20" v-if="loading"
22 :progress="50"
23 :indeterminate='true'
24 :counter-clockwise="true"
25 :hide-background="false"
26 size="50"
27 rotate
28 fillDuration="2"
29 rotationDuration="1"
30 />
31 </div>
32
33 </div>
34 </div>
35
36 </section>
37 <SearchResults />
38 </div>
39 </template>
40 <script>
41 import Nav from '@/components/Nav.vue'
42 import SearchResults from '@/components/SearchResults.vue'
43 import { mapActions } from 'vuex'
44 export default {
45 components: {
46 Nav,
47 SearchResults
48 },
49
50 data() {
51 return {
52 data : [],
53 search: '',
54 loading: false
55 }
56 },
57 methods: {
58 ...mapActions(['fetchSearchResult']),
59 async getRecipes(e) {
60 this.loading = true
61 e.preventDefault()
62 this.fetchSearchResult(this.search).then(result => {
63 result;
64 this.loading = false
65 })
66 }
67 },
68 filters: {
69 capitalize(word) {
70 return word.toUpperCase()
71 }
72 },
73 async mounted() {}
74 }
75 </script>
76 <style scoped>
77 </style>
Follow the steps below to build the component.
SearchResults.vue
file in the components
folder cd components
touch SearchResults.vue
SearchResults.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <section>
4 <div v-if="getSearchResults.length > 1">
5 <h1 class="my-10 font-montserrat font-bold text-4xl">RESULTS</h1>
6 <div class="sm:grid sm:grid-cols-3 gap-5 w-4/5 sm:w-3/5 my-5 mx-auto">
7 <div class="mb-5 cursor-pointer" v-for="(item, i) in getSearchResults" :key="i">
8 <router-link :to='`/recipe/${item.recipe.label}`'>
9 <img :src='`${item.recipe.image}`' class="w-full" alt="">
10 </router-link>
11
12
13 <div class="p-5 shadow-lg">
14 <div class="flex space-x-4">
15 <button @click="addItemToBookmark(item.recipe)" class="click:text-yellow-400 rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
16 <font-awesome-icon class="text-xl hover:text-yellow-400" :icon="['fas', 'bookmark']" />
17 </button>
18 <div class="rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
19 <font-awesome-icon class="text-xl" :icon="['fas', 'share']" />
20 </div>
21 </div>
22 <router-link :to='`/recipe/${item.recipe.label}`'>
23 <h1 class="text-2xl font-bold font-montserrat mb-5">
24 {{ item.recipe.label }}
25 </h1>
26 </router-link>
27 <div class="text-md font-raleway tracking-wide">
28 <p>
29 {{ item.recipe.yield }} Servings | {{ item.recipe.ingredientLines.length }} Ingredients
30 </p>
31 <p v-if="item.recipe.totalTime > 0">
32 <font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ item.recipe.totalTime }} Minutes
33 </p>
34 </div>
35 </div>
36
37 </div>
38 </div>
39 </div>
40 </section>
41 </div>
42 </template>
43 <script>
44 import { mapGetters } from 'vuex'
45 export default {
46 name: 'searchResult',
47 data() {
48 return {
49 bookmarks: JSON.parse(window.localStorage.getItem('bookmarks'))
50 }
51 },
52 methods: {
53 // ...mapActions(['addBookmark']),
54 async addItemToBookmark(item) {
55
56 if(window.localStorage.getItem('userData')) {
57 const { label, ingredientLines, totalTime, image, source, url } = item
58 let bookmarkItem
59 if(this.bookmarks.findIndex(recipe => recipe.label === item.label) === -1){
60 bookmarkItem = {
61 label,
62 ingredientLines,
63 totalTime,
64 image,
65 url,
66 source,
67 yield: item.yield,
68 users_permissions_user: JSON.parse(window.localStorage.getItem('userData')).id
69 }
70 this.bookmarks.push(bookmarkItem)
71 //set to localstorage
72 window.localStorage.setItem('bookmarks', JSON.stringify(this.bookmarks))
73 await this.axios.post(`http://localhost:1337/api/bookmarks`, {
74 data: {...bookmarkItem},
75 },
76 {
77 headers: {
78 Authorization: `Bearer ${window.localStorage.getItem('jwt')}`,
79 },
80 })
81 const res = await this.axios.get(`http://localhost:1337/api/users/${bookmarkItem.users_permissions_user}/?populate=*`, {
82 headers: {
83 Authorization: `Bearer ${window.localStorage.getItem('jwt')}`,
84 }
85 })
86 const user = res.data
87 window.localStorage.setItem('userData', JSON.stringify(user))
88 window.localStorage.setItem('bookmarks', JSON.stringify(user.bookmarks))
89 }
90 }
91 }
92
93 },
94 computed: {
95 ...mapGetters(['getSearchResults', 'getBookmarks'])
96 }
97 }
98 </script>
99 <style scoped>
100 </style>
In this component, we display the User's search results and give the User the ability to create bookmarks.
Recipe.vue
file in the views folder cd views
touch Recipe.vue
Recipe.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <Nav class="relative" />
4 <div class="w-4/5 sm:w-3/5 mx-auto mt-10 text-left">
5 <div class="sm:grid grid-cols-2 gap-2">
6 <div>
7 <img class="mb-10" :src="`${curRecipe.image}`" alt="">
8 </div>
9 <h1 class="text-4xl sm:text-8xl font-bold font-montserrat">{{ name }}</h1>
10 </div>
11
12 <div class="text-xl mt-5 sm:mt-0 font-raleway tracking-wide flex space-x-5">
13 <p>
14 {{ curRecipe.yield }} Servings
15 </p>
16 <p> | </p>
17 <p v-if="curRecipe.totalTime > 0">
18 <font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ curRecipe.totalTime }} Minutes
19 </p>
20 </div>
21 <div class="mt-10">
22 <h1 class="text-2xl sm:text-4xl font-montserrat font-bold mb-10">
23 {{ curRecipe.ingredientLines.length }} Ingredients
24 </h1>
25 <div class="w-4/5 sm:grid font-raleway grid-cols-2 gap-2">
26 <div class="mb-5 mr-5" v-for="(Ingredients, i) in curRecipe.ingredientLines" :key="i">
27 <font-awesome-icon class="text-xl ml-3 text-green-300" :icon="['fas', 'check']" />
28 {{ Ingredients }}
29 </div>
30 </div>
31 </div>
32 <div class="mb-10 font-raleway">
33 <p class="mb-10"> Courtsey of <span class="text-2xl">{{ curRecipe.source }} </span></p>
34 <p >
35
36 <a class="py-5 px-10 text-xl bg-green-600 z-10 text-left text-white" target="blank" :href='`${curRecipe.url}`'>
37 Preparation Steps <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
38 </a>
39 </p>
40 </div>
41 </div>
42
43 </div>
44 </template>
45 <script>
46 import Nav from '@/components/Nav.vue'
47 import { mapGetters } from 'vuex'
48 export default {
49 components: {
50 Nav
51 },
52 data() {
53 return {
54 name: this.$route.params.id,
55 curRecipe: {}
56 }
57 },
58 computed: {
59 ...mapGetters(['getSearchResults'])
60 },
61 created() {
62 const recipeItem = this.getSearchResults.find(item => item.recipe.label === this.name)
63 this.curRecipe = recipeItem.recipe
64 }
65 }
66 </script>
67 <style scoped>
68 </style>
Here we just created the view for individual recipes, and this page displays the ingredients, name, and a link to the procedures for preparing the meal.
To build the Bookmarks page:
Bookmarks.vue
file in the views folder cd views
touch Bookmarks.vue
Bookmarks.vue
file and fill it up with the following lines of code. <template>
<div>
<Nav />
<section>
<div v-if="bookmarks.length > 0">
<h1 class="mt-32 mb-4 font-montserrat font-bold text-4xl">Bookmarks</h1>
<div class="sm:grid sm:grid-cols-3 gap-5 w-4/5 sm:w-3/5 my-5 mx-auto">
<div class="mb-5 cursor-pointer" v-for="(item, i) in bookmarks" :key="i">
<router-link :to='`/bookmark/${item.label}`'>
<img :src='`${item.image}`' class="w-full" alt="">
</router-link>
<div class="p-5 shadow-lg">
<div class="flex space-x-4">
<button @click="removeItemFromBookmarks(item)" class="click:text-yellow-400 rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
<font-awesome-icon class="text-xl hover:text-yellow-400" :icon="['fas', 'trash']" />
</button>
<div class="rounded-full mb-5 h-10 bg-white w-10 flex justify-center items-center shadow-lg">
<font-awesome-icon class="text-xl" :icon="['fas', 'share']" />
</div>
</div>
<router-link :to='`/bookmark/${item.label}`'>
<h1 class="text-2xl font-bold font-montserrat mb-5">
{{ item.label }}
</h1>
</router-link>
<div class="text-md font-raleway tracking-wide">
<p>
{{ item.yield }} Servings | {{ item.ingredientLines.length }} Ingredients
</p>
<p v-if="item.totalTime > 0">
<font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ item.totalTime }} Minutes
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
// import { mapGetters } from 'vuex';
import Nav from '@/components/Nav.vue'
export default {
name: 'BookmarkPage',
components: {
Nav
},
data() {
return {
bookmarks: []
}
},
methods: {
async removeItemFromBookmarks(item) {
const itemIndex = this.bookmarks.findIndex(bookmarkItem => bookmarkItem.label === item.label)
this.bookmarks.splice(itemIndex, 1)
window.localStorage.setItem('bookmarks', JSON.stringify(this.bookmarks))
await this.axios.delete(`http://localhost:1337/api/bookmarks/${item.id}`, {
headers: {
Authorization: `Bearer ${window.localStorage.getItem('jwt')}`,
},
})
}
},
created() {
this.bookmarks = JSON.parse(window.localStorage.getItem('bookmarks'))
}
}
</script>
<style scoped>
</style>
BookmarkId.vue
file in the views folder cd views
touch BookmarkId.vue
BookmarkId.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <Nav class="relative" />
4 <div class="w-4/5 sm:w-3/5 mx-auto mt-10 text-left">
5 <div class="sm:grid grid-cols-2 gap-2">
6 <div>
7 <img class="mb-10" :src="`${curRecipe.image}`" alt="">
8 </div>
9 <h1 class="text-4xl sm:text-8xl font-bold font-montserrat">{{ name }}</h1>
10 </div>
11
12 <div class="text-xl mt-5 sm:mt-0 font-raleway tracking-wide flex space-x-5">
13 <p>
14 {{ curRecipe.yield }} Servings
15 </p>
16 <p> | </p>
17 <p v-if="curRecipe.totalTime > 0">
18 <font-awesome-icon class="text-lg" :icon="['fas', 'clock']" /> {{ curRecipe.totalTime }} Minutes
19 </p>
20 </div>
21 <div class="mt-10">
22 <h1 class="text-2xl sm:text-4xl font-montserrat font-bold mb-10">
23 {{ curRecipe.ingredientLines.length }} Ingredients
24 </h1>
25 <div class="w-4/5 sm:grid font-raleway grid-cols-2 gap-2">
26 <div class="mb-5 mr-5" v-for="(Ingredients, i) in curRecipe.ingredientLines" :key="i">
27 <font-awesome-icon class="text-xl ml-3 text-green-300" :icon="['fas', 'check']" />
28 {{ Ingredients }}
29 </div>
30 </div>
31 </div>
32 <div class="mb-10 font-raleway">
33 <p class="mb-10"> Courtsey of <span class="text-2xl">{{ curRecipe.source }} </span></p>
34 <p >
35 <a class="py-5 px-10 text-xl bg-green-600 z-10 text-left text-white" target="blank" :href='`${curRecipe.url}`'>
36 Preparation Steps <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
37 </a>
38 </p>
39 </div>
40 </div>
41
42 </div>
43 </template>
44 <script>
45 import Nav from '@/components/Nav.vue'
46 export default {
47 name: 'BookmarkId',
48 components: {
49 Nav
50 },
51 data() {
52 return {
53 name: this.$route.params.id,
54 curRecipe: {},
55 bookmarkRecipes: JSON.parse(window.localStorage.getItem('bookmarks'))
56 }
57 },
58
59 created() {
60 const recipeItem = this.bookmarkRecipes.find(item => item.label === this.name)
61 this.curRecipe = recipeItem
62 }
63 }
64 </script>
65 <style scoped>
66 </style>
This page displays individual bookmarks. Users have the ability to delete bookmarked items.
Let's see how we can add user registration to our site. Once users register, then they can create bookmarks.
Register.vue
file in the views folder cd views
touch Register.vue
Register.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <div class="flex items-center justify-center h-screen">
4 <div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
5 <div class="bg-blue-800 w-full h-screen bg-opacity-20">
6 </div>
7 </div>
8 <div class="sm:w-1/2">
9 <div class="p-5 w-4/5 mx-auto text-left font-raleway">
10 <div class="text-left mb-7">
11 <router-link to="/">
12 <font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> HOME
13 </router-link>
14 </div>
15 <h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-7">
16 Sign Up. To. Join Recipee
17 </h1>
18 <p v-show="error" class="text-sm text-red-500">{{ errorMsg }}</p>
19 <form @submit="register">
20 <div class="my-4">
21 <h1 class="text-left font-bold mb-2 font-montserrat">Name</h1>
22 <input type="text" v-model="name" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
23 </div>
24 <div class="my-4">
25 <h1 class="text-left font-bold mb-2 font-montserrat">Email</h1>
26 <input type="email" v-model="email" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
27 </div>
28 <div class="my-4">
29 <h1 class="text-left font-bold mb-2 font-montserrat">Password</h1>
30 <input type="password" v-model="password" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
31 </div>
32 <div class="my-4">
33 <h1 class="text-left font-bold mb-2 font-montserrat">Username</h1>
34 <input type="text" v-model="username" class="text-sm outline-none pb-2 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
35 </div>
36
37 <button type="submit" :disabled="name.length < 6 || password.length < 6 || username.length < 3" class="bg-green-400 p-5 text-white">
38 Sign Up <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
39 </button>
40 </form>
41 </div>
42 </div>
43 </div>
44 </div>
45 </template>
46 <script>
47 export default {
48 name: 'RegisterPage',
49 data() {
50 return {
51 name: '',
52 email: '',
53 password: '',
54 username: '',
55 error: false,
56 errorMsg: `An Error occurred, please try again`
57 }
58 },
59 methods: {
60 async register(e) {
61 try {
62 e.preventDefault()
63 await this.axios.post(`http://localhost:1337/api/auth/local/register`, {
64 name: this.name,
65 password: this.password,
66 email: this.email,
67 username: this.username
68 })
69 this.$router.push('login')
70 } catch(e) {
71 this.error = true
72 this.email = ''
73 }
74 }
75 }
76 }
77 </script>
78 <style scoped>
79 </style>
In the code block above, we're integrating user signup and redirecting the users to the login page on successful registration.
Login.vue
file in the views folder cd views
touch Login.vue
Login.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <div class="flex items-center justify-center h-screen">
4 <div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
5 <div class="bg-blue-800 w-full h-screen bg-opacity-20">
6 </div>
7 </div>
8 <div class="sm:w-1/2">
9 <div class="p-5 w-4/5 mx-auto text-left font-raleway">
10 <div class="text-left mb-10">
11 <router-link to="/">
12 <font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> HOME
13 </router-link>
14 </div>
15
16 <h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-10">
17 Login. To. Recipee
18 </h1>
19 <p v-show="error" class="text-sm text-red-500">{{ errorMsg }}</p>
20 <form @submit="login">
21 <div class="my-5">
22 <h1 class="text-left font-bold mb-5 font-montserrat">Email</h1>
23 <input type="email" v-model="email" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
24 </div>
25 <div class="my-5">
26 <h1 class="text-left font-bold mb-5 font-montserrat">Password</h1>
27 <input type="password" v-model="password" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
28 </div>
29
30 <button type="submit" :disabled="password.length < 3" class="bg-green-400 p-5 text-white">
31 Login <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
32 </button>
33 <p class="my-2">
34 <router-link to="/forgotpassword" >Forgot Password?</router-link>
35 </p>
36
37 </form>
38 </div>
39 </div>
40 </div>
41 </div>
42 </template>
43 <script>
44
45 export default {
46 name: 'LoginPage',
47
48 data() {
49 return {
50 email: '',
51 password: '',
52 error: false,
53 errorMsg: `An error occurred, please try again`
54 }
55 },
56 methods: {
57 async login(e) {
58 e.preventDefault()
59
60 try {
61 const res = await this.axios.post(`http://localhost:1337/api/auth/local`, {
62 identifier: this.email,
63 password: this.password
64 });
65 const { jwt, user } = res.data
66 window.localStorage.setItem('jwt', jwt)
67 window.localStorage.setItem('userData', JSON.stringify(user))
68 const res2 = await this.axios.get(`http://localhost:1337/api/users/${user.id}?populate=*`, {
69 headers: {
70 Authorization: `Bearer ${jwt}`,
71 }
72 })
73 window.localStorage.setItem('bookmarks', JSON.stringify(res2?.data?.bookmarks || []))
74 this.$router.push('/')
75 } catch(error) {
76 this.error = true
77 this.password = ''
78 }
79 },
80 }
81 }
82 </script>
83 <style scoped>
84 </style>
In the code block above, we're integrating user login and redirecting the users to the Homepage
on successful Login. We're also storing user details
and JWT
in localStorage.
To create a channels for users to recover their forgotten passwords:
ForgottenPassword.vue
file in the views folder cd views
touch ForgottenPassword.vue
ForgottenPassword.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <div class="flex items-center justify-center h-screen">
4 <div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
5 <div class="bg-blue-800 w-full h-screen bg-opacity-20">
6 </div>
7 </div>
8 <div class="sm:w-1/2">
9 <div class="p-5 w-4/5 mx-auto text-left font-raleway">
10 <div class="text-left mb-10">
11 <router-link to="/login">
12 <font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> Login
13 </router-link>
14 </div>
15
16 <h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-10">
17 Recover Your. Recipee. Password
18 </h1>
19 <p v-show="done" class="text-sm text-green-500">Password reset link has been sent to {{ email }}</p>
20 <p v-show="error" class="text-sm text-red-500">An error occurred</p>
21 <form @submit="forgotPassword">
22 <div class="my-5">
23 <h1 class="text-left font-bold mb-5 font-montserrat">Email</h1>
24 <input type="email" v-model="email" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
25 </div>
26
27 <button type="submit" class="bg-green-400 p-5 text-white">
28 Send Email link <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
29 </button>
30 </form>
31 </div>
32 </div>
33 </div>
34 </div>
35 </template>
36 <script>
37 export default {
38 name: 'ForgotPassword',
39
40 data() {
41 return {
42 email: '',
43 done: false,
44 error: false,
45 }
46 },
47 methods: {
48 async forgotPassword(e) {
49 e.preventDefault()
50 this.done = false;
51 this.error = false;
52 this.axios.post(`http://localhost:1337/api/auth/forgot-password`, {
53 email: this.email
54 })
55 .then(() => {
56 this.done = true
57 })
58 .catch(e => {
59 e;
60 this.error = true
61 })
62 }
63 }
64 }
65 </script>
66 <style scoped>
67 </style>
Here, users can request a password reset, and Strapi will send a message to the email address that the User enters on the page. The sent mail will possess a link that resembles the following:
http://localhost:8080/resetpassword?code=9d99862a974907c375988ed4727173d56983dbcfb7c400f006ca47958e07089f950de8979d0ae3a8fab684f1b73b55910b04fe448b77c92178cabf4b3c58e77f
We'll be using the Strapi-provider-email-nodemailer package to configure and send emails.
Strapi-provider-email-nodemailer
.Using yarn, run:
yarn add @strapi/provider-email-nodemailer
If you'd prefer to use npm, run:
npm install @strapi/provider-email-nodemailer --save
config
directory and create a plugins.js
file and fill it up with the following code1 module.exports = ({ env }) => ({
2 email: {
3 config: {
4 provider: 'nodemailer',
5 providerOptions: {
6 host: env('SMTP_HOST', 'smtp.gmail.com'),
7 port: env('SMTP_PORT', 465),
8 auth: {
9 user: env('GMAIL_USER'),
10 pass: env('GMAIL_PASSWORD'),
11 },
12 // ... any custom nodemailer options
13 },
14 settings: {
15 defaultFrom: 'noreply@recipee.com',
16 defaultReplyTo: 'noreply@recipee.com',
17 },
18 },
19 },
20 });
.env
file and add the following line of code1 SMTP_HOST = smtp.gmail.com
2 SMTP_PORT = 465
3 GMAIL_USER = YOUR_GMAIL_ADDRESS
4 GMAIL_PASS = YOUR_GMAIL_PASSWORD
Now we have email services configured, and we can finally create our reset password page and logic.
ResetPassword.vue
file in the views folder cd views
touch ResetPassword.vue
ResetPassword.vue
file and fill it up with the following lines of code.1 <template>
2 <div>
3 <div class="flex items-center justify-center h-screen">
4 <div class="hidden sm:block w-1/2 bg-cover h-screen" style='background: url(newFood.png)'>
5 <div class="bg-blue-800 w-full h-screen bg-opacity-20">
6 </div>
7 </div>
8 <div class="sm:w-1/2">
9 <div class="p-5 w-4/5 mx-auto text-left font-raleway">
10 <div class="text-left mb-10">
11 <router-link to="/login">
12 <font-awesome-icon class="mr-5" :icon="['fas', 'arrow-left']" /> Login
13 </router-link>
14 </div>
15
16 <h1 class="font-bold text-left font-montserrat text-4xl sm:text-6xl mb-10">
17 Recover Your. Recipee. Password
18 </h1>
19 <p v-show="error" class="text-sm text-red-500">An Error Occurred, Please Try Again</p>
20 <form @submit="resetPassword">
21 <div class="my-5">
22 <h1 class="text-left font-bold mb-5 font-montserrat">Password</h1>
23 <input type="password" v-model="password" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
24 </div>
25 <div class="my-5">
26 <h1 class="text-left font-bold mb-5 font-montserrat">Confirm Password</h1>
27 <input type="password" v-model="confirmPassword" class="text-sm outline-none pb-5 w-4/5 bg-transparent border-b hover:border-blue-700 focus:border-blue-700">
28 </div>
29
30 <button type="submit" :disabled="password.length < 3 || password !== confirmPassword" class="bg-green-400 p-5 text-white">
31 Reset Password <font-awesome-icon class="ml-3" :icon="['fas', 'arrow-right']" />
32 </button>
33 </form>
34 </div>
35 </div>
36 </div>
37 </div>
38 </template>
39 <script>
40
41 export default {
42 name: 'ResetPassword',
43 data() {
44 return {
45 password: '',
46 confirmPassword: '',
47 done: false,
48 error: false,
49 }
50 },
51 methods: {
52 async resetPassword(e) {
53 e.preventDefault()
54 this.axios.post(`http://localhost:1337/api/auth/reset-password`, {
55 code: this.$route.query.code,
56 password: this.password,
57 passwordConfirmation: this.confirmPassword
58 })
59 .then(() => {
60 this.done = true
61 this.$router.push("login")
62 })
63 .catch(e => {
64 e;
65 this.error = true
66 })
67 }
68 },
69 }
70 </script>
71 <style scoped>
72 </style>
Now, users can input a new password that will be used to access their accounts, after which they are redirected to the login page. Users who lost their passwords can now resume using our Application.
That's all for this article, and I hope you're well equipped to integrate user authentication into your Strapi Application.
You can find the GitHub repository for the vue application here. Also, see the assets used in this project: Background image & Other assets
Alexander Godwin is a Software Developer and writer that likes to write code and build things. Learning by doing is the best way and it's how Alex helps others learn. Follow him on Twitter (@oviecodes)