From 015d67cf738e4ad6d397824dc09a44d85d643b75 Mon Sep 17 00:00:00 2001
From: Mateja <mail@matejamaric.com>
Date: Thu, 29 Jul 2021 02:46:33 +0200
Subject: Fully implement client-side login system...

---
 client/src/App.vue            | 27 ++++++++++++++++++++++++--
 client/src/main.js            |  7 +++++++
 client/src/router/index.js    | 27 ++++++++++++++++++++++++--
 client/src/store/index.js     | 44 ++++++++++++++++++++++++++++++++++++++++++-
 client/src/views/Checkout.vue |  8 +++++++-
 client/src/views/Login.vue    | 29 +++++++++++++++++++++++-----
 client/src/views/Register.vue | 32 ++++++++++++++++++++++++++-----
 server/routes/api.js          |  4 ++--
 8 files changed, 160 insertions(+), 18 deletions(-)

diff --git a/client/src/App.vue b/client/src/App.vue
index f2bef74..f61f236 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -17,7 +17,7 @@
             </router-link>
           </li>
         </ul>
-        <ul class="navbar-nav mb-2 mb-lg-0">
+        <ul class="navbar-nav mb-2 mb-lg-0" v-if="!isLoggedIn">
           <li class="nav-item">
             <router-link class="nav-link" active-class="active" to="/register">Register</router-link>
           </li>
@@ -25,6 +25,11 @@
             <router-link class="nav-link" active-class="active" to="/login">Login</router-link>
           </li>
         </ul>
+        <ul class="navbar-nav mb-2 mb-lg-0" v-else>
+          <li class="nav-item">
+            <span class="nav-link" @click="logout">Logout</span>
+          </li>
+        </ul>
       </div>
     </div>
   </nav>
@@ -34,11 +39,29 @@
 </template>
 
 <script>
+import axios from 'axios';
+
 export default {
-  name: 'Checkout',
+  name: 'App',
+  created() {
+    axios.interceptors.response.use(undefined, error => {
+      if (error.status === 401) {
+        this.$store.commit('auth_clean');
+      }
+      return Promise.reject(error);
+    });
+  },
   computed: {
     cartSize() {
       return this.$store.getters.getCartSize;
+    },
+    isLoggedIn() {
+      return this.$store.getters.isLoggedIn;
+    }
+  },
+  methods: {
+    logout() {
+      this.$store.commit('auth_clean');
     }
   }
 }
diff --git a/client/src/main.js b/client/src/main.js
index c210589..5ab733a 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -1,4 +1,11 @@
 import {createApp} from 'vue';
+import axios from 'axios';
+
+const tokenInStorage = localStorage.getItem('token');
+if (tokenInStorage) {
+  axios.defaults.headers.common['Authorization'] = `Bearer ${tokenInStorage}`;
+}
+
 import App from './App.vue';
 import router from './router';
 import store from './store';
diff --git a/client/src/router/index.js b/client/src/router/index.js
index eee610c..ee78676 100644
--- a/client/src/router/index.js
+++ b/client/src/router/index.js
@@ -5,6 +5,8 @@ import Checkout from '@/views/Checkout.vue';
 import Login from '@/views/Login.vue';
 import Register from '@/views/Register.vue';
 
+import store from '@/store/index';
+
 const routes = [
   {
     path: '/',
@@ -24,12 +26,18 @@ const routes = [
   {
     path: '/login',
     name: 'Login',
-    component: Login
+    component: Login,
+    meta: {
+      guest: true
+    }
   },
   {
     path: '/register',
     name: 'Register',
-    component: Register
+    component: Register,
+    meta: {
+      guest: true
+    }
   }
 ];
 
@@ -38,4 +46,19 @@ const router = createRouter({
   routes
 });
 
+router.beforeEach((to, from) => {
+  if (to.matched.some(record => record.meta.guest) && store.getters.isLoggedIn)
+    return false;
+
+  if (to.matched.some(record => record.meta.admin) && !store.getters.isAdmin)
+    return false;
+
+  if (to.matched.some(record => record.meta.auth) && !store.getters.isLoggedIn) {
+    from.params.nextUrl = to.fullPath;
+    return '/login';
+  }
+
+  return true;
+});
+
 export default router;
diff --git a/client/src/store/index.js b/client/src/store/index.js
index ff4648f..47f8f47 100644
--- a/client/src/store/index.js
+++ b/client/src/store/index.js
@@ -5,7 +5,9 @@ export default createStore({
   state: {
     products: [],
     currentProduct: {},
-    cart: []
+    cart: [],
+    token: localStorage.getItem('token') || '',
+    isAdmin: localStorage.getItem('isAdmin') === 'true'
   },
   getters: {
     getProducts(state) {
@@ -37,6 +39,12 @@ export default createStore({
           amount = x.quantity;
       });
       return amount;
+    },
+    isLoggedIn(state) {
+      return !!state.token;
+    },
+    isAdmin(state) {
+      return state.isAdmin;
     }
   },
   mutations: {
@@ -68,6 +76,20 @@ export default createStore({
     },
     clearCart(state) {
       state.cart.length = 0;
+    },
+    auth_set(state, token, isAdmin) {
+      state.token = token;
+      state.isAdmin = isAdmin;
+      localStorage.setItem('token', token);
+      localStorage.setItem('isAdmin', isAdmin);
+      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
+    },
+    auth_clean(state) {
+      state.token = null;
+      state.isAdmin = false;
+      localStorage.removeItem('token');
+      localStorage.removeItem('isAdmin');
+      delete axios.defaults.headers.common['Authorization'];
     }
   },
   actions: {
@@ -96,6 +118,26 @@ export default createStore({
         .post(`${process.env.VUE_APP_ROOT_API}/transactions/capture`, {orderId})
         .then(() => true)
         .catch(err => console.error(err));
+    },
+    login(context, loginData) {
+      return new Promise((resolve, reject) => {
+        axios.post(`${process.env.VUE_APP_ROOT_API}/login`, loginData)
+          .then(response => {
+            context.commit('auth_set', response.data.token, response.data.isAdmin);
+            resolve(response);
+          })
+          .catch(error => reject(error));
+      });
+    },
+    register(context, registerData) {
+      return new Promise((resolve, reject) => {
+        axios.post(`${process.env.VUE_APP_ROOT_API}/register`, registerData)
+          .then(response => {
+            context.commit('auth_set', response.data.token, response.data.isAdmin);
+            resolve(response);
+          })
+          .catch(error => reject(error));
+      });
     }
   },
   modules: {
diff --git a/client/src/views/Checkout.vue b/client/src/views/Checkout.vue
index 52da138..7004b1f 100644
--- a/client/src/views/Checkout.vue
+++ b/client/src/views/Checkout.vue
@@ -16,7 +16,10 @@
         <p class="text-center my-3 fw-bold">You can buy {{ cartSize }} items for ${{ cartPrice }}.</p>
       </div>
       <div class="col-md-4 card p-2" v-if="cartSize">
-        <div ref="paypal"></div>
+        <div ref="paypal" v-if="isLoggedIn"></div>
+        <p v-else class="fw-bold text-center my-auto">
+          You need to be logged in to make a purchase.
+        </p>
       </div>
     </div>
   </div>
@@ -55,6 +58,9 @@ export default {
     },
     cartPrice() {
       return this.$store.getters.getCartPrice;
+    },
+    isLoggedIn() {
+      return this.$store.getters.isLoggedIn;
     }
   },
   methods: {
diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue
index 53badcb..f0a1759 100644
--- a/client/src/views/Login.vue
+++ b/client/src/views/Login.vue
@@ -32,7 +32,7 @@
       </div>
     </div>
   </div>
-  <Modal :title="modalTitle" v-if="showModal" @close="showModal = false">
+  <Modal :title="modalTitle" v-if="showModal" @close="closeBtnAction">
     <p v-text="modalText"></p>
   </Modal>
 </template>
@@ -55,7 +55,8 @@ export default {
       passwordBlured:false,
       showModal: false,
       modalTitle: '',
-      modalText: ''
+      modalText: '',
+      closeBtnAction: null
     }
   },
   methods: {
@@ -75,15 +76,33 @@ export default {
 
     login() {
       this.validate();
-      if (this.valid) {
+
+      const successMsg = () => {
         this.modalTitle = 'Success!';
         this.modalText = 'You successfully logged in!';
         this.showModal = true;
-      }
-      else {
+        this.closeBtnAction = () => this.$router.push('/');
+      };
+
+      const failureMsg = () => {
         this.modalTitle = 'Failure!';
         this.modalText = 'You failed to login.';
         this.showModal = true;
+        this.closeBtnAction = () => this.showModal = false;
+      };
+
+      if (this.valid) {
+        const requestData = {
+          email: this.email,
+          password: this.password
+        };
+
+        this.$store.dispatch('login', requestData)
+          .then(() => successMsg())
+          .catch(() => failureMsg());
+      }
+      else {
+        failureMsg();
       }
     }
   }
diff --git a/client/src/views/Register.vue b/client/src/views/Register.vue
index e26c751..238b5df 100644
--- a/client/src/views/Register.vue
+++ b/client/src/views/Register.vue
@@ -58,7 +58,7 @@
       </div>
     </div>
   </div>
-  <Modal :title="modalTitle" v-if="showModal" @close="showModal = false">
+  <Modal :title="modalTitle" v-if="showModal" @close="closeBtnAction">
     <p v-text="modalText"></p>
   </Modal>
 </template>
@@ -87,7 +87,8 @@ export default {
       passwordConfirmBlured:false,
       showModal: false,
       modalTitle: '',
-      modalText: ''
+      modalText: '',
+      closeBtnAction: null
     }
   },
   methods: {
@@ -114,15 +115,36 @@ export default {
 
     submit() {
       this.validate();
-      if (this.valid) {
+
+      const successMsg = () => {
         this.modalTitle = 'Success!';
         this.modalText = 'You successfully registered!';
         this.showModal = true;
-      }
-      else {
+        this.closeBtnAction = () => this.$router.push('/');
+      };
+
+      const failureMsg = () => {
         this.modalTitle = 'Failure!';
         this.modalText = 'You failed to register.';
         this.showModal = true;
+        this.closeBtnAction = () => this.showModal = false;
+      };
+
+      if (this.valid) {
+        const requestData = {
+          firstname: this.firstname,
+          lastname: this.lastname,
+          email: this.email,
+          password: this.password,
+          confirmPassword: this.passwordConfirm,
+        };
+
+        this.$store.dispatch('register', requestData)
+          .then(() => successMsg())
+          .catch(() => failureMsg());
+      }
+      else {
+        failureMsg();
       }
     }
   }
diff --git a/server/routes/api.js b/server/routes/api.js
index b680b70..0c73ec0 100644
--- a/server/routes/api.js
+++ b/server/routes/api.js
@@ -18,7 +18,7 @@ router.patch('/products/:id', isAuth, isAdmin, upload.single('image'), productsC
 router.delete('/products/:id', isAuth, isAdmin, productsController.destroy);
 
 router.get('/transactions/paid', isAuth, isAdmin, transactionController.showPaid);
-router.post('/transactions/setup', transactionController.setup);
-router.post('/transactions/capture', transactionController.capture);
+router.post('/transactions/setup', isAuth, transactionController.setup);
+router.post('/transactions/capture', isAuth, transactionController.capture);
 
 module.exports = router;
-- 
cgit v1.2.3