Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building a Simple Real estate App With Laravel and Vuejs 3 Part4

Building a Simple Real estate App With Laravel and Vuejs 3

In this part we continue handling authenticating users in frontend, at first we will setup Vuex store management library, and then calling and connecting with backend Apis.

 

 

Installing Vuex

We will install the vuex (the Vue state management) library for Vue. We will use it to store authentication data and later to store properties search page.

npm install vuex@next --save

Next create a new store/ directory inside of resources/js/ directory. In the store/ directory create new file types.js which will contain the mutation types as constants we will use throughout our store.

For now i declared two constants for the auth store like so:

resources/js/store/types.js

export const SET_AUTHENTICATED = 'SET_AUTHENTICATED';
export const SET_USER = 'SET_USER';

Then to store the authentication data of the current user, create a new file auth.js . This file is a vuex module with below contents:

resources/js/store/auth.js

import {SET_SIGNED_IN, SET_USER} from "./types";

export default {
    namespaced: true,
    state: () => ({
        signedIn: false,
        user: null
    }),
    getters: {
        signedIn(state) {
            return state.signedIn;
        },
        user(state) {
            return state.user;
        }
    },
    mutations: {
        [SET_SIGNED_IN](state, payload) {
            state.signedIn = payload;
        },
        [SET_USER](state, payload) {
            state.user = payload;
        }
    },
    actions: {
        login({commit}, payload) {
            commit(SET_SIGNED_IN, payload.signedIn);
            commit(SET_USER, payload.user);
        },
        logout({commit}) {
            commit(SET_SIGNED_IN, false);
            commit(SET_USER, null);
        }
    }
}

In the above code i am storing two keys into the store (signedIn and user). signedIn is a boolean to indicates that a user is login successfully and have a default value of false, and the user holds the user object after successful login.

Our store not yet completed, we have to to create a store instance using vuex createStore() function, i have done this in a separate file index.js

resources/js/store/index.js

import {createStore} from "vuex";
import auth from "./auth";

export default createStore({
   modules: {
       auth
   }
});

Then import this store and add it to the vue app like so:

resources/js/app.js

require('./bootstrap');
import { createApp } from 'vue';
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
import App from './components/App.vue';
import router from "./routes";
import store from "./store";


const app = createApp(App);
app.use(router);
app.use(Toast);
app.use(store);

app.mount('#app');

Tip: In order to ease development when working with the vuex store i recommend that you install the Vue devtools extension available for chrome and firefox. This extension has many features, among of these is to view the vuex store object and track the live updates as in this figure.Building a Simple Real estate App With Laravel and Vuejs - vuex devtools

Now let’s call the login and register apis and update the store. But first open bootstrap.js file and add these lines:

resources/js/bootstrap.js

window.axios.defaults.baseURL = window._base_url;
window.axios.defaults.withCredentials = true;
window.sanctumEndpoint = "/sanctum/csrf-cookie";

Here i set the withCredentials=true and the sanctum endpoint. Also i set the global axios base url to an environment variable, typically defined in app.blade.php

 

Updating Register Page

Update resources/js/components/pages/Register.vue:

<template>
    <div class="page-head">
        <div class="container">
            <div class="row">
                <div class="page-head-content">
                    <h1 class="page-title">Home New account / Sign in </h1>
                </div>
            </div>
        </div>
    </div>

    <div class="register-area" style="background-color: rgb(249, 249, 249);">
        <div class="container">

            <div class="col-md-6">
                <div class="box-for overflow">
                    <div class="col-md-12 col-xs-12 register-blocks">
                        <h2>New account : </h2>
                        <form action="#" method="post" @submit.prevent="handleRegister">
                            <div class="form-group">
                                <label>Name</label>
                                <input type="text" class="form-control" name="name" v-model="registerForm.name" />
                            </div>
                            <div class="form-group">
                                <label>Email</label>
                                <input type="text" class="form-control" name="email" v-model="registerForm.email" />
                            </div>
                            <div class="form-group">
                                <label>Password</label>
                                <input type="password" class="form-control" name="password" v-model="registerForm.password" />
                            </div>
                            <div class="form-group">
                                <label>Confirm Password</label>
                                <input type="password" class="form-control" name="password_confirmation" v-model="registerForm.password_confirmation" />
                            </div>
                            <div class="text-center">
                                <button type="submit" class="btn btn-default" :disabled="!registerForm.name || !registerForm.email || !registerForm.password || !registerForm.password_confirmation">Register</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>

            <div class="col-md-6">
                <div class="box-for overflow">
                    <div class="col-md-12 col-xs-12 login-blocks">
                        <h2>Login : </h2>
                        <form action="#" method="post" @submit.prevent="handleLogin">
                            <div class="form-group">
                                <label>Email</label>
                                <input type="text" class="form-control" name="email" v-model="loginForm.email" />
                            </div>
                            <div class="form-group">
                                <label>Password</label>
                                <input type="password" class="form-control" name="password" v-model="loginForm.password" />
                            </div>
                            <div class="text-center">
                                <button type="submit" class="btn btn-default" :disabled="!loginForm.email || !loginForm.password"> Log in</button>
                            </div>
                        </form>
                    </div>

                </div>
            </div>

        </div>
    </div>
</template>

<script>

import {useLogin} from "../../composables/useLogin";
import {useRegister} from "../../composables/useRegister";

export default {
    name: "Login",
    setup() {
        const {loginForm, handleLogin} = useLogin();
        const {registerForm, handleRegister} = useRegister();

        return {
            loginForm,
            registerForm,
            handleLogin,
            handleRegister
        }
    }
}
</script>

Here i am Vue 3 composition api using the new setup() function as you see i this code. The new composition api as per vue docs helps for better reuse-ability by extracting shared code into separate files called composable files.

setup() function rules:

  • setup() replaces the options api by writing all the code inside the setup() function.
  • You can’t use the “this” keyword.
  • The function must return any properties or methods that should be output in the template, as in this code i returned the loginForm, registerForm objects and handleLogin and handleRegister handlers.
  • Lifecycle hooks like mounted or created and computed can also be accessed inside setup() using the new onMounted(), onCreated(), computed() functions, etc.
  • When working with two way model binding you have to use reactive properties and not direct objects. You can declare reactive properties using either ref() or reactive() functions.
  • You can still use the options Api alongside the composition Api.

I already created two composable files that contain login and register functionality. Per conventions the file names of the composable files should start with the “use” keyword, for example useLogin.js and userRegister.js.

Inside the composable function you should export a function with the same name as the filename and must return the data that will be consumed in template.

Now after you understood the basics of composition api, let’s see the contents of both files as shown below:

resources/js/composables/useRegister.js

import {reactive} from "vue";
import {useToastError} from "./useToastError";

export const useRegister = () => {
    const {displayErrors, toast} = useToastError();

    const registerForm = reactive({email: "", name: "", password: "", password_confirmation: ""});

    const handleRegister = () => {
        window.axios.post("/api/register", registerForm).then(registerResponse => {
            toast.success("Registered successfully! Now you can sign in");
            resetRegisterForm();
        }).catch(error => {
            displayErrors(error.response);
        });
    }

    const resetRegisterForm = () => {
        registerForm.name = "";
        registerForm.email = "";
        registerForm.password = "";
        registerForm.password_confirmation = "";
    }

    return {
        registerForm,
        handleRegister
    }
}

resources/js/composables/useLogin.js

import {reactive} from "vue";
import {useStore} from "vuex";
import {useRouter} from "vue-router";
import {useToastError} from "./useToastError";

export const useLogin = () => {
    const store = useStore();
    const router = useRouter();
    const {displayErrors, toast} = useToastError();

    const loginForm = reactive({email: "", password: ""});

    const handleLogin = () => {
        window.axios.get(window.sanctumEndpoint).then(response => {
            window.axios.post("/api/login", loginForm).then(loginResponse => {
                toast.success("Signed in successfully!");
                store.dispatch("auth/login", {signedIn: true, user: loginResponse.data.user});
                router.push("/");
                resetLoginForm();
            }).catch(error => {
                displayErrors(error.response);
            });
        });
    }

    const handleLogout = () => {
        window.axios.get(window.sanctumEndpoint).then(response => {
            window.axios.post("/api/logout").then(logoutResponse => {
                store.dispatch("auth/logout");
                router.push("/");
            });
        });
    }

    const resetLoginForm = () => {
        loginForm.email = "";
        loginForm.password = "";
    }

    return {
        loginForm,
        handleLogin,
        handleLogout
    }
}

In the useRegister.js i declared a function useRegister() which return the registerForm reactive property and handleRegister handler. The handleRegister() handler makes a post request to /api/register endpoint. On successful response we show a success message to the user. On failure we display error messages using the toast displayErrors() function which defined in another composable file, we will see it below.

The useLogin.js works the same way. The handleLogin() function sends a post request to /api/login endpoint. On successful response we show a success message to the user and update the vuex store.

You can access the vuex store in the composition api using useStore() function. Also the new version of vue-router supports the composition api using the useRouter() function.

resources/js/composables/useToastError.js

import {useToast} from "vue-toastification";

export const useToastError = () => {
    const toast = useToast();

    const displayErrors = (response) => {
        if(response.data.message) {
            toast.error(response.data.message);
        }
        if(response.data.errors) {
            let errorArr = [];
            for(let field in response.data.errors) {
                errorArr.push(response.data.errors[field][0]);
            }
            toast.error(errorArr.join("\n"));
        }
    }

    return {
        toast,
        displayErrors
    }
}

Update Menu.vue to reflect the current signed-in user

resources/js/components/partials/Menu.vue

<template>
    <nav class="navbar navbar-default ">
        <div class="container">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <router-link to="/" class="navbar-brand"><img src="/template/assets/img/logo.png" alt=""></router-link>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse yamm" id="navigation">
                <ul class="nav navbar-nav navbar-right">
                    <li><button class="navbar-btn nav-button wow bounceInRight login" @click="$router.push('/register')" data-wow-delay="0.4s" v-if="!$store.state.auth.signedIn">Login</button></li>

                        <li class="dropdown" v-if="$store.state.auth.signedIn" data-wow-delay="0.4s">
                            <a href="#" class="dropdown-toggle navbar-btn" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Hi {{$store.state.auth.user.name}} <span class="caret"></span></a>
                            <ul class="dropdown-menu" style="right: auto !important;">
                                <li><router-link to="/user/edit-profile">Edit Profile</router-link></li>
                                <li><router-link to="/user/my-properties">My Properties</router-link></li>
                                <li><router-link to="/user/fav">Favorites</router-link></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#" @click="handleLogout()">Logout</a></li>
                            </ul>
                        </li>

                    <li><button class="navbar-btn nav-button wow fadeInRight" @click="$router.push('/submit-property')" data-wow-delay="0.5s">Submit</button></li>
                </ul>

                <ul class="main-nav nav navbar-nav navbar-right">
                    <li class="ymm-sw " data-wow-delay="0.1s">
                        <router-link to="/">Home</router-link>
                    </li>

                    <li class="wow fadeInDown" data-wow-delay="0.1s">
                        <router-link to="/properties">Properties</router-link>
                    </li>

                    <li class="wow fadeInDown" data-wow-delay="0.4s">
                        <router-link to="/contact">Contact</router-link>
                    </li>
                </ul>
            </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>
</template>

<script>
import {useLogin} from "../../composables/useLogin";

export default {
    name: "Menu",
    setup() {
        const {handleLogout} = useLogin();

        return {
            handleLogout
        }
    }
}
</script>

Let’s test this by running:

npm run watch

Launch the server with:

php artisan serve

Go to http://localhost:8000 and click the register button. Create a new account then login you should see a message “Signed in successfully” and you will see your name in the header instead of the register button.

At this point we finished the login functionality but still a single problem exist. What happens if we refresh the page, typically the auth data will be lost. In the next section we will find a solution for this.

 

Persisting Authentication & Protecting Routes

There are two problems we need to think about. The first is to persist the authentication data in the Vuex store whenever the user reloads the page. The second issue is to protect specific routes for authenticated users only like edit profile, add property, etc.

To persist the authentication details there are many options, you can store the login response in browser localstorage or cookie when the user signs in successfully. Some people can use some packages like vue-persist.

For the sake of this tutorial i will declare a variable in the app.blade.php which contains the auth data as json and add it to the Window object.

Open app.blade.php and add this code right before the closing </head> tag:

@if(auth()->check())
        @php
            $authUser = [
                'signedIn' => true,
                'user' =>  auth()->user()
            ];
        @endphp
    @else
        @php
            $authUser = [
                'signedIn' => false,
                'user' => null
            ];
        @endphp
    @endif

    <script>
        window._authUser = JSON.parse(atob('{{ base64_encode(json_encode($authUser)) }}'));
    </script>

Next update the auth store to initialize the state data using the declared window variable above:

resources/js/store/auth.js

import {SET_SIGNED_IN, SET_USER} from "./types";

const authData = window._authUser;

export default {
    namespaced: true,
    state: () => ({
        signedIn: authData.signedIn,
        user: authData.user
    }),
    getters: {
        signedIn(state) {
            return state.signedIn;
        },
        user(state) {
            return state.user;
        }
    },
    mutations: {
        [SET_SIGNED_IN](state, payload) {
            state.signedIn = payload;
        },
        [SET_USER](state, payload) {
            state.user = payload;
        }
    },
    actions: {
        login({commit}, payload) {
            commit(SET_SIGNED_IN, payload.signedIn);
            commit(SET_USER, payload.user);
        },
        logout({commit}) {
            commit(SET_SIGNED_IN, false);
            commit(SET_USER, null);
        }
    }
}

Now let’s check this solution by signing in and reloading the page, you will notice the store is already loaded and persisted the auth data.

Based on this solution we can now handle the second problem which is to protect the user specific routes.

We can do this globally in the router using router.beforeEach() callback function which fires before every route the user is navigated to.

resources/js/routes.js

import {createRouter, createWebHistory} from "vue-router";

import Home from "./components/pages/Home";
import Properties from "./components/pages/Properties";
import Contact from "./components/pages/Contact";
import PropertySingle from "./components/pages/PropertySingle";
import Register from "./components/pages/Register";
import SubmitProperty from "./components/pages/SubmitProperty";
import EditProfile from "./components/pages/user/EditProfile";
import ChangePassword from "./components/pages/user/ChangePassword";
import MyProperties from "./components/pages/user/MyProperties";
import Favorites from "./components/pages/user/Favorites";
import EditProperty from "./components/pages/user/EditProperty";

import store from "./store";

const routes = [
    {path: '/', name: 'home', component: Home},
    {path: '/properties', component: Properties},
    {path: '/register', name: 'register', component: Register},
    {path: '/property/:id/:slug', component: PropertySingle},
    {path: '/submit-property', component: SubmitProperty, meta: {
        requiresAuth: true,
    }},
    {path: '/contact', component: Contact},
    {path: '/user/edit-profile', component: EditProfile, meta: {
            requiresAuth: true,
        }
    },
    {path: '/user/change-password', component: ChangePassword, meta: {
            requiresAuth: true,
    }},
    {path: '/user/my-properties', component: MyProperties, meta: {
            requiresAuth: true,
    }},
    {path: '/user/edit-property/:id', component: EditProperty, meta: {
            requiresAuth: true,
    }},
    {path: '/user/fav', component: Favorites, meta: {
            requiresAuth: true,
    }},
];

const router = createRouter({
    history: createWebHistory(),
    routes
});

router.beforeEach(async (to, from) => {
    const authenticated = store.getters['auth/signedIn'];
    const user = store.getters['auth/user'];

    if (to.matched.some((record) => record.meta.requiresAuth)) {

        if (!authenticated || !user) {
            return { name: 'register' };
        }
    }

    if(to.name === 'register' && authenticated && user) {
       return { name: 'home' };
    }

    return true;
});

export default router;

In this code i modified some routes by adding a meta key with requiresAuth: true which marks the route as protected. We will use this key to check for in the beforeEach() callback.

In the beforeEach() callback i got access to the store instance to retrieve the authentication status of the user. Then we check for the incoming route using the “to” param.

If the incoming have a meta of requiresAuth and the user is not authenticated then we return {name: ‘register’}. This tells vue-router to cancel and redirect to the register route. Otherwise we return true so that the user can proceed and view the page.

The final check is to determine if the user is already authenticated and tries to go to the register page, the router will redirect him to the homepage.

After this setup if you try to go to some pages like “/user/edit-profile” you will automatically redirected to register page.

 

Continue to part 5: Helper Components>>>

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments