Guide

How to add Magic Link to a Vue.js Application

How to add Magic Link to a Vue.js Application

16 February 2021

This tutorial demonstrates how to add passwordless login to a Vue.js application using Magic Link.

Prerequisites

Live Demo

Demo: https://vue-magic.vercel.app

Quickstart

git clone https://github.com/magiclabs/example-vue.git
cd example-vue
mv .env.example .env
npm install
npm run serve

Get your Magic Publishable Key

Sign Up with Magic and get your VUE_APP_MAGIC_KEY

Dashboard Image

Deploy your own

Want to see how this would look in production? Click the below button and deploy your own Vue.js application on Vercel secured by Magic Link.

Deploy with Vercel

Create a Sample Application

note

The following tutorial creates a new Vue application using the Vue CLI, and presents some common ways to build Vue applications, in terms of its structure and naming conventions. If you are using this guide to integrate Magic SDK into your Vue application, you may need to adjust some of the steps to suit your needs.

If you don’t already have an existing application, you can create one using the Vue CLI tool. Using the terminal, find a location on your drive where you want to create the app and run the following commands:

# Install the Vue CLI
npm install -g @vue/cli

# Create the application using the Vue CLI.
# When asked to pick a preset,
# use your arrow key to highlight Manually select features
# and press enter
vue create magic-vue

Vue CLI

We will be selecting the Vue Router, Vuex and other tools necessary for the project setup. Use spacebar to select Vuex and Router and press enter

Vue Router and Vuex

Choose 2.x for the Vue.js version for this project

Vue 2.x

Type Y for selecting the history mode for the router.

The DOM Window object provides access to the browser's session history through the history object. It exposes useful methods and properties that let you navigate back and forth through the user's history, and manipulate the contents of the history stack.

To learn more, read here.

Vue History Mode

Select ESLint + Prettier, but it’s optional and up to the developer’s preferences.

Vue ESLint + Prettier

Select Lint on save

Vue Lint on save

And select In dedicated config files for config for Babel, ESlint, etc.

Vue In dedicated config files

And at last, select Y or N based on your preferences for saving the presets.

It will take some time to download. Once downloaded, you will see the following screen:

Vue Install Finish

Start the application

Let’s move into the project directory and start the Vue application

cd magic-vue
npm run serve

Vue Serve

Vue Init

We have added Vue Router and Vuex for this Vue.js application. And as a result, you can see an About route added, but to see Vuex in action. Let’s wait for the moment before we go ahead and explain and add Magic for our passwordless authentication to see it in action.

Add Routes

Let’s add two more routes, one for the profile and other for the login.

Create a Profile.vue file under the src/views folder.

<template>
 <div class="profile">
   <img alt="Vue logo" src="../assets/logo.png" />
   <h1>Welcome Shahbaz!</h1>
 </div>
</template>
<script>
export default {}
</script>
<style scoped></style>

Also, create a Login.vue file under src/views folder

<template>
 <div class="container">
   <div class="login">
     <form @submit.prevent="login">
       <h3>Login</h3>
       <label>
         <input
           v-model="email"
           type="email"
           name="email"
           placeholder="Email"
           required
           class="email-input email-extra"
           value
         />
       </label>
       <div class="submit">
         <button type="submit" name="button">
           Send Magic Link
         </button>
       </div>
     </form>
   </div>
 </div>
</template>
<script>
export default {
 data() {
   return {
     email: '',
   }
 },
 methods: {
   login() {
     console.log(this.email)
   },
 },
}
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300&display=swap');
*,
*::before,
*::after {
 box-sizing: border-box;
 font-family: 'Inter', sans-serif;
 outline: none;
}
body {
 margin: 0;
 color: #333;
 background-color: #fff;
 min-height: 100%;
}
form,
label {
 display: flex;
 flex-flow: column;
 text-align: center;
}
.email-input {
 padding: 10px;
 margin: 1rem auto;
 border: 1px solid #ccc;
 border-radius: 50px;
 outline: none;
 transition: 0.5s;
 width: 80%;
}
.email-input:focus {
 border: 1px solid #42b983;
}
.submit {
 display: flex;
 justify-content: flex-end;
 align-items: center;
 justify-content: space-between;
}
.submit > a {
 text-decoration: none;
}
.submit > button {
 padding: 0.6rem 1rem;
 cursor: pointer;
 background: #fff;
 border: 1px solid #ccc;
 border-radius: 50px;
 width: 80%;
 outline: none;
 transition: 0.3s;
 margin: 0 auto;
 font-size: 14px;
}
.submit > button:hover {
 border-color: #42b983;
}
.error {
 color: brown;
 margin: 1rem 0 0;
}
.container {
 max-width: 42rem;
 margin: 0 auto;
 padding: 2rem 1.25rem;
 overflow: auto;
}
.login {
 max-width: 20rem;
 margin: 0px auto;
 padding: 1rem;
 border: 1px solid rgb(204, 204, 204);
 border-radius: 4px;
 text-align: center;
}
</style>

We have added two routes, the login route displays a login form which we will use to sign-in with Magic’s passwordless feature and the profile route will display the user data received from Magic after a successful login. But for now, it says, Welcome Shahbaz!

Feel free to change this name ☺️

To see these two routes in action, let’s update the route file and App.vue to see the changes on the homepage.

Update the src/router/index.js file to include the following objects into the routes array:

{
   path: '/profile',
   name: 'Profile',
   component: () =>
     import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
 },
 {
   path: '/login',
   name: 'Login',
   component: () =>
     import(/* webpackChunkName: "login" */ '../views/Login.vue'),
 },

Update App.vue file to include new routes for /profile and /login and a little bit more css to nav a:

<template>
 <div id="app">
   <div id="nav">
     <router-link to="/">Home</router-link>
     <router-link to="/about">About</router-link>
     <router-link to="/profile">Profile</router-link>
     <router-link to="/login">Login</router-link>
   </div>
   <router-view />
 </div>
</template>

<style>
/*  */

#nav a {
 font-weight: bold;
 padding: 5px;
 text-decoration: none;
 color: #2c3e50;
}

</style>

Your application would look like this by now

Vue Home

Vue Profile

Vue Login

Install Magic SDK

After creating a new Vue application and updating the routes, install the Magic SDK for passwordless authentication.

npm install --save magic-sdk

As of now, our login page looks like this, the email entered will be shown in the console.

Vue Console

Let’s add Magic to this application to allow user authentication.

note

Magic could be implemented in a single route single Vue application, but that’s not how the real world example would look like, so we decided to work with Vuex, a state management library, to store the user’s state and fetch those data in different routes.

To do so, we will use Vuex, a state management library for Vue.js applications. It serves as a centralized store for all the components in an application.

To learn more about Vuex, visit the official documentation page

Update Login.vue file

Change the tag with the following code.

<script>
import { SDKError, RPCError, ExtensionError } from 'magic-sdk'

export default {
 data() {
   return {
     email: '',
   }
 },
 methods: {
   login() {
     this.$store
       .dispatch('login', {
         email: this.email,
       })
       .then(() => {
         this.$router.push({ name: 'Profile' })
       })
       .catch((err) => {
         if (err instanceof SDKError) {
           console.log(err)
         }
         if (err instanceof RPCError) {
           console.log(err)
         }
         if (err instanceof ExtensionError) {
           console.log(err)
         }
       })
   },
 },
}
</script>

We are using Vuex store to dispatch a login action with email as a data object. Upon successful login, it will be routed to the profile page. We are also using Magic’s built-in error classes to log errors.

To learn more about Errors & Warnings, read here

Create login action

Open store/index.js file and replace it with the following code

import Vue from 'vue';
import Vuex from 'vuex';
import { Magic } from 'magic-sdk';

Vue.use(Vuex);

const m = new Magic(process.env.VUE_APP_MAGIC_KEY);

export default new Vuex.Store({
  state: {
    user: null,
  },
  mutations: {
    SET_USER_DATA(state, userData) {
      state.user = userData;
      localStorage.setItem('user', JSON.stringify(userData));
    },
  },
  actions: {
    async login({ commit }, email) {
      await m.auth.loginWithMagicLink(email);
      const data = await m.user.getMetadata();
      commit('SET_USER_DATA', data);
    },
  },
  modules: {},
});

Login action is committing SET_USER_DATA mutation which changes the user state.

We are also using localStorage to store our user object, which we will use later in this guide to auto-login our users.

Create a .env file

Create a .env file in the root of your project.

VUE_APP_MAGIC_KEY=pk_test_CC6XXXXXXX576

To get the VUE_APP_MAGIC_KEY visit Magic dashboard, create a new app and grab its Test Publishable Key.

note

You might have to stop the server and restart to use the .env file’s data.

Login with Magic

Visit the login page, enter your email address and click on Send Magic Link

You will see something like this with your email address on the pop-up modal

Magic pop-up with check email

Magic don't close modal

At this time, check your email. And click on Log in to {Application Name}.

Magic email with button

note

the link will expire in 10 minutes if not used.

After successful login, you will see the following screen:

Magic Success

Let’s ReVue

After a successful login, you will be redirected to the profile page. And it looks the same as it did before because there was no change in the UI.

note

To get the most out of Vue development, please install Vue’s official devtools extension

To see the state changes, open your browser developer tool and select Vue from the tab panel, and click on Vuex.

Vue Vuex using Devtool

As you can see, our Log In action was able to commit SET_USER_DATA. This means we have successfully logged in using Magic.

note

when you refresh the page, your state object is lost, we will fix this later in the guide using created() lifecycle hook

Update Profile.vue file

Since we are storing our user in state, we can use this state object to bring the user object into the Profile page.

Replace Profile.vue with the below code

<template>
 <div class="profile">
   <img alt="Vue logo" src="../assets/logo.png" />
   <h1>Welcome {{ email }}</h1>
   <h3>
     Your Public Address is: <i>{{ publicAddress }}</i>
   </h3>
 </div>
</template>
<script>
import { mapState } from 'vuex'

export default {
 computed: mapState({
   email: (state) => state.user.email,
   issuer: (state) => state.user.issuer,
   publicAddress: (state) => state.user.publicAddress,
 }),
}
</script>
<style scoped>
h3 {
 margin: 20px 0 0;
}
</style>

Our profile page is displaying the user information.

Profile page shows user's information upon successful login with Magic

Everything is working fine, but as you can see, we still see the login link on our navbar.

Let’s fix this by adding a logout button and hiding the profile link when the user is not logged in.

Replace App.vue to add Logout

Replace the App.vue file with the below code.

<template>
 <div id="app">
   <div id="nav">
     <router-link to="/">Home</router-link>
     <router-link to="/about">About</router-link>
     <router-link v-if="this.$store.state.user" to="/profile"
       >Profile</router-link
     >
     <router-link v-if="!this.$store.state.user" to="/login"
       >Login</router-link
     >
     <button v-else type="button" class="button" @click="logout">
       Logout
     </button>
   </div>
   <router-view />
 </div>
</template>
<script>
export default {
 methods: {
   logout() {
     this.$store.dispatch('logout')
   },
 },
}
</script>
<style>
#app {
 font-family: Avenir, Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
}

#nav {
 padding: 30px;
}

#nav a {
 font-weight: bold;
 padding: 5px;
 text-decoration: none;
 color: #2c3e50;
}

#nav a.router-link-exact-active {
 color: #42b983;
}

#nav > button {
 padding: 0.6rem 1rem;
 cursor: pointer;
 background: #fff;
 border: 1px solid #ccc;
 border-radius: 50px;
 width: 5rem;
 outline: none;
 transition: 0.3s;
 margin: 0 auto;
 font-size: 14px;
 font-weight: bold;
}

#nav > button:hover {
 background-color: #42b983;
 color: white;
}
</style>

The above code adds a logout button, when clicked dispatches a logout action. Uses user state to show the routes

Note: the profile is still unguarded, which means anyone can view it. To fix this, we will use beforeEach guard and Route Meta Field later in this guide.

Create logout action

Open store/index.js and replace it with

import Vue from 'vue';
import Vuex from 'vuex';
import { Magic } from 'magic-sdk';

Vue.use(Vuex);

const m = new Magic(process.env.VUE_APP_MAGIC_KEY);

export default new Vuex.Store({
  state: {
    user: null,
  },
  mutations: {
    SET_USER_DATA(state, userData) {
      state.user = userData;
      localStorage.setItem('user', JSON.stringify(userData));
    },
    CLEAR_USER_DATA() {
      localStorage.removeItem('user');
      location.reload();
    },
  },
  actions: {
    async login({ commit }, email) {
      await m.auth.loginWithMagicLink(email);
      const data = await m.user.getMetadata();
      commit('SET_USER_DATA', data);
    },
    async logout({ commit }) {
      await m.user.logout();
      commit('CLEAR_USER_DATA');
    },
  },
  modules: {},
});

logout action calls Magic’s logout function and commits CLEAR_USER_DATA mutation CLEAR_USER_DATA mutation removes the user object from the localStorage and reloads the page, which refreshes the Vue application. Clicking logout doesn’t redirect the application to the home page. To fix this, let’s add Navigation Guard and a Route Meta Field

beforeEach Navigation Guard and Route Meta Field

Replace the router/index.js file with the below code:

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue'),
  },
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user');

  if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
    next('/');
  }
  next();
});

export default router;

Vue router supports meta fields, which we can include when defining a route. Let’s update the /profile route object to include meta: { requiresAuth: true }:

{
  path: '/profile',
  name: 'Profile',
  component: Profile,
  meta: { requiresAuth: true }
}

Just keep in mind, meta property expects an object and we can define requireAuth: true. With the meta fields we can create advanced route login for our needs. For instance, to prevent an action based on permission or by requiring authentication.

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user');

  if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
    next('/');
  }
  next();
});

We also added the navigation guard with router.beforeEach(), which gets the user’s logged in state from the localStorage and checks for a record in a route object for a meta field of requireAuth.

As the name suggests, the navigation guards provided by vue-router are primarily used to guard navigations either by redirecting it or canceling it.

To learn more about beforeEach guard, read here.

Auto-login using created() lifecycle hook

Open your main.js file and replace it with the below code:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  created() {
    const userString = localStorage.getItem('user');
    if (userString) {
      const userData = JSON.parse(userString);
      this.$store.commit('SET_USER_DATA', userData);
    }
  },
  render: h => h(App),
}).$mount('#app');

Now, when you refresh the page, your state object won’t be lost!

Done

Our application is ready now,

Let’s ReVue:

  • We have used Vue CLI to generate a Vue.js Application
  • We have used Vue Router and Vuex to manage routes and states.
  • We have used Magic SDK to add passwordless authentication to a Vue.js application
  • Learned about the beforeEach Navigation Guard and Route Meta Field
  • Used created() lifecycle hook to allow auto-login upon page refresh using localStorage.

Deploy to Vercel

Deploy with Vercel

Let's make some magic!