BlogHome
Adding a Lock Screen to Your Protected User Pages

Adding a Lock Screen to Your Protected User Pages

Apr 18, 2021Updated on Sep 22, 2021

If you would like to employ a more secure way of protecting your protected user pages beyond account authentication this brief tutorial might just be for you.

On this tutorial we are going to add this extra bit of security by implementing a screen lock on top of these secured user pages.

This feature can be quiet useful in a working environment where people have immediate access to workstations of others, hence instead of signing out each time one needs to excuse themselves from their workstations they can just lock it from access by others with a click of a button while still remaining logged in.

On this tutorial we are going to utilize the capabilities of Vue.js to accomplish this. A little knowledge of vuex is also needed as we are going to use it to manage our data.

The anatomy of our "Screen Lock" consists of the lock wall, lock state and a password. When the screen lock is activated we'll check if a password has been set, if not, the user is prompted to set one, afterwards we will have the lock state set to true which will trigger the lock wall to obstruct the user portal and its HTML code from view.

As an additional security practice the user will be required to set a new password daily.

For the lock to function efficiently we will need to have our password and lock status data persistent on page reloads, to do this we'll use the vuex-persistedstate plugin.

Let's install vuex-persistedstate.

npm i vuex-persistedstate 

We are also going to encrypt our password with bcrypt, so let's install it too.

npm i bcrypt

By the end of this tutorial we will have a lock screen that works as demonstrated below.

Lock Screen

Creating the Screen Lock

First let's set up the data that we'll be using to operate our Screen Lock and the respective setters and getters inside the Vuex Store.

import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";

Vue.use(Vuex);

export default new Vuex.Store({
  state:{
    lock: {
      password: 0, status: false
    },
    dateToday: '',
  },
  getters:{
    getLockPassword: state => state.lock.password,
    getLockStatus: state => state.lock.status,
  },
  mutations:{
    changeLockStatus(state, payload){
      state.lock.status = payload.status
    },
    setLockPassword(state, payload){
      state.lock.password = payload.password
    },
    setTheDate(state, payload){
      state.dateToday = payload.date
    },
  },
  actions: {
    NEW_DAY_CHECK({state, commit}, payload){
      if(state.dateToday === ''){ // first day
          commit( 'setTheDate', payload);
      }else{
        if(state.dateToday !== payload.date){
          commit( 'setTheDate', payload);
          commit( 'setLockPassword', {password: ''});
          commit( 'changeLockStatus', {status: false});
        }
      }
    },
    TRIGGER_LOCK({commit}, payload){
      commit( 'changeLockStatus', payload);
    },
    UPDATE_PASSWORD({commit}, payload){
      commit( 'setLockPassword', payload);
    }
  },
  plugins: [createPersistedState()]
});

Since we'll be using the vuex store's data on different components lets set up a mixin to help us do just that.

Create a new file and name it LockScreenMixin.js then place it inside a /mixins directory.

import { mapGetters, mapActions } from 'vuex'
let LockScreenMixin = {
  computed:{
    ...mapGetters({
      lockPassword: 'getLockPassword',
      lockStatus: 'getLockStatus'
    })
  },
  methods: {
    ...mapActions(['NEW_DAY_CHECK', 'TRIGGER_LOCK', 'UPDATE_PASSWORD'])
  },
}

export default LockScreenMixin

After setting up the mixin, we are going to easily use and modify our store data without code repetitions.

Next we'll be setting up our lock screen and the user portal's app layout. Here we are going to construct three components, the app layout component (AppLayout.vue) which will be displaying our lock screen wall and the rest of the user portal, the navbar (AppNavbar.vue) component that is going to host the button or anchor that will enable the user to trigger the screen locking event and the lock screen itself (LockScreen.vue) which is going to provide an input to enable the user to unlock the screen lock.

Starting with the app layout, hide or modify authenticated pages at router-view level.

<template>
  <div id="app">
    <app-navbar v-if="!lockStatus"></app-navbar>
    <router-view  v-if="!lockStatus"></router-view>
    <lock-screen :show-lock="lockStatus"></lock-screen>
  </div>
</template>

<script>
  import AppNavbar from "./layout/AppNavbar.vue";
  import LockScreen from "./components/LockScreen.vue";
  import LockScreenMixin from "./mixins/LockScreenMixin";
  export default {
    name: "Administration",
    components: {LockScreen, AppNavbar},
    mixins:[LockScreenMixin],
    mounted(){
      let date = new Date()
      let today = `${date.getDate()} ${(date.getMonth()+1)} ${date.getFullYear()}`;
      this.NEW_DAY_CHECK({date: today});
    }
  }
</script>

Whenever the app layout component is mounted we'll be calling the NEW_DAY_CHECK vuex store action to check if the day has changed and updating our lock screen data accordingly.

Next, on the navbar component where we are going to trigger our lock screen add the following code.

<template>
  <nav class="navbar">
    <div class="container">
      <div class="navbar-menu">
        <div class="navbar-end">
          <div class="navbar-item has-dropdown is-hoverable">
            <div class="navbar-dropdown">
              <a class="navbar-item" href="#" @click="lockUser()"> LOCK </a>
              <a class="navbar-item" href="/log-user-out">Logout</a>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="modal" :class="{'is-active' : showSetLockPasswordModal}">
      <div class="modal-background"></div>
      <div class="modal-card">
        <header class="modal-card-head">
          <p class="modal-card-title">Set Lock Password</p>
          <button @click="showSetLockPasswordModal = false" class="delete" aria-label="close"></button>
        </header>
        <section class="modal-card-body">
          <div class="field is-horizontal">
            <div class="field-label">
              <label class="label" for="password">Password</label>
            </div>
            <div class="field-body">
              <div class="field">
                <p class="control">
                  <input class="input is-large" id="password" type="password" v-model="password" autofocus>
                </p>
              </div>
            </div>
          </div>
          <div class="field is-horizontal">
            <div class="field-label">
              <label class="label" for="repeat-password">Repeat Password</label>
            </div>
            <div class="field-body">
              <div class="field">
                <p class="control">
                  <input class="input is-large" id="repeat-password" type="password" v-model="repeat_password" autofocus>
                </p>
              </div>
            </div>
          </div>
        </section>
        <footer class="modal-card-foot">
          <button class="button is-success" @click="setLockPass()">Save Password</button>
        </footer>
      </div>
    </div>
  </nav>
</template>

<script>
  import LockScreenMixin from './../mixins/LockScreenMixin'
  const bcrypt = require('bcryptjs')
  const salt = bcrypt.genSaltSync(10)
  export default {
    name: "AppNavbar",
    mixins: [LockScreenMixin],
    data() {
      return {
        showSetLockPasswordModal: false,
        password: '',
        repeat_password: ''
      }
    },
    methods:{
      lockUser(){
        // set lock password if it's not set
        if(this.lockPassword === ''){
          this.showSetLockPasswordModal = true;
        } else {
          this.TRIGGER_LOCK({ status: true });
        }
      },
      setLockPass(){
        if ((this.password === '') || (this.repeat_password === '')){
          console.log('Password can not be empty!');
        }
        else if (this.password !== this.repeat_password){
          console.log('Passwords don\'t match!');
        } else {
          this.UPDATE_PASSWORD({
            password: bcrypt.hashSync(this.password, salt)
          });
          this.showSetLockPasswordModal = false;
          this.lockUser();
        }
      }
    }
  }
</script>

Above, when the user clicks on the lock anchor to trigger the lock screen we'll check if a password is set, if not we'll prompt the user to set one before triggering the screen lock wall.

The last component we'll be dealing with is the lock screen itself.

<template>
  <transition name="drop">
    <div v-if="showLock" class="lock-screen section">
      <div class="container-fluid">
        <div class="level lock-icon">
          <div class="level-item" :style="`background-image: url(/storage/default-photos/${(lockStatus ? 'lock' : 'unlock')}.png)`">
          </div>
        </div>
        <div class="level">
          <div class="level-item unlock-password">
            <div class="field is-horizontal">
              <div class="field-body">
                <div class="field">
                  <p class="control">
                    <input class="input" placeholder="Password" id="email" type="password" v-model="password"  required autofocus>
                  </p>

                  <p v-if="password_error" class="help is-danger has-text-centered">
                    Wrong Password!
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div class="watch">
        [[ watchTime ]]
      </div>
    </div>
  </transition>
</template>

<script>
  import LockScreenMixin from "./../mixins/LockScreenMixin";
  const bcrypt = require('bcryptjs');
  export default {
    name: "LockScreen",
    props: {showLock: Boolean},
    mixins:[LockScreenMixin],
    data(){
      return {
        password_error: false,
        password: '',
        watchTime: "",
        intervalId: ""
      }
    },
    delimiters: ["[[", "]]"],
    watch:{
    password () {
        if(this.password_error){
          this.password_error = false;
        }else{
          setTimeout(()=>{
            if(bcrypt.compareSync(this.password, this.lockPassword)){
              this.locked = false;
              this.TRIGGER_LOCK({ status: false });
              this.password = '';
              this.password_error = false;
            }else{
              this.password_error = true;
            }
          },2000);
        }
      },
      lockStatus(){ // watch simulator
        if(this.lockStatus){ // start interval
          this.startInterval();
        } else { // end interval
          if(this.intervalId) clearInterval(this.intervalId);
        }
      }
    },
    mounted() {
      this.startInterval();
    },
    methods: {
      startInterval(){
        this.intervalId = setInterval(() => {
          let today = new Date();
          const doubleUp = (val) => { return `${val.toString().length === 1 ? ('0'+val) : val}` };
          this.watchTime = `${doubleUp(today.getHours())} : ${doubleUp(today.getMinutes())} : ${doubleUp(today.getUTCSeconds())}`;
        }, 1000);
      }
    }
  }
</script>

<style scoped>
  .level-item > img{
    height: 200px;
    width: auto;
  }
  div.level.lock-icon{
      margin-top: 15% !important;
    }
  .lock-icon > .level-item{
    height: 200px;
    width: 100%;
    background-size: contain;
    background-repeat: no-repeat;
    background-position: top;
  }
  .lock-screen{
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: 0;
    border: 0;
    padding: 40px;
    background: #141e30; /* fallback for old browsers */
    background: -webkit-linear-gradient(to right, #141e30, #243b55); /* Chrome 10-25, Safari 5.1-6 */
    background: linear-gradient(to right, #141e30, #243b55); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
    z-index: 10;
  }
  .watch{
    font-size: 100px;
    font-weight: lighter;
    color: white;
    position: absolute;
    left: 50px;
    right: 0;
    bottom: 30px;
    text-align: left;
  }
  /* width */
  ::-webkit-scrollbar {
    width: 0 !important;
  }
  .drop-enter-active {
    animation: drop-in .5s;
    -webkit-transform:translateY(-200%);
    -moz-transform:translateY(-200%);
    transform:translateY(-100%);
    -webkit-transition: all 0.5s ease-in-out;
    -moz-transition: all 0.5s ease-in-out;
    transition: all 0.5s ease-in-out;
  }
  .drop-leave-active {
    animation: drop-in .5s reverse;
    -webkit-transform:translateY(-200%);
    -moz-transform:translateY(-200%);
    transform:translateY(-100%);
    -webkit-transition: all 0.5s ease-in-out;
    -moz-transition: all 0.5s ease-in-out;
    transition: all 0.5s ease-in-out;
  }
  @keyframes drop-in {
    0% {
      -webkit-transform:translateY(-200%);
      -moz-transform:translateY(-200%);
      transform:translateY(-100%);
      -webkit-transition: all 0.5s ease-in-out;
      -moz-transition: all 0.5s ease-in-out;
      transition: all 0.5s ease-in-out;
    }
    100% {
      -webkit-transform:translateY(0px);
      -moz-transform:translateY(0px);
      transform:translateY(0px);
      -webkit-transition: all 0.5s ease-in-out;
      -moz-transition: all 0.5s ease-in-out;
      transition: all 0.5s ease-in-out;
    }
  }
</style>

The lock screen components above transitions the lock wall into view when the lockStatus is true, it also listens to the password inputs and removes the lock wall from view when the password inserted is the correct one, lastly it simulates an analog watch on the lock screen.

Use this as a starting point, get creative and create more robust screen locks.

If you have questions regarding this screen lock don't hesitate to ask, and if you like the content much so that you'd like to support the content creation process, you can go ahead and do just that.

Go ahead and secure the web portals.