From 465272fedcf568a5af5bf3554c27c381b02ef089 Mon Sep 17 00:00:00 2001 From: jarrodback Date: Fri, 4 Feb 2022 11:44:14 +0000 Subject: [PATCH 1/7] HT-7 Add authentication and authorisation to front/backend --- server/routes/bill.routes.js | 5 +- ui/package-lock.json | 50 ++++++- ui/package.json | 3 +- ui/src/api/api.js | 87 ++++++++---- ui/src/auth/index.js | 34 +++++ ui/src/components/navbar.vue | 264 +++++++++++++++++++++++++---------- ui/src/router/router.js | 67 +++++---- ui/src/store/store.js | 110 ++++++++++----- ui/src/utilities.js | 47 ++++--- ui/src/views/Login.vue | 86 ++++++++++++ 10 files changed, 574 insertions(+), 179 deletions(-) create mode 100644 ui/src/auth/index.js create mode 100644 ui/src/views/Login.vue diff --git a/server/routes/bill.routes.js b/server/routes/bill.routes.js index 46e3986..848c979 100644 --- a/server/routes/bill.routes.js +++ b/server/routes/bill.routes.js @@ -1,13 +1,14 @@ const express = require("express"); const router = express.Router(); +const { checkJwtToken } = require("../middleware/auth/authJwt"); // Get the Bill controller const billController = require("../controllers/bill.controller"); // Get All Bills -router.get("/", billController.getAllBills); +router.get("/", checkJwtToken, billController.getAllBills); // Pay for bill -router.put("/:id", billController.payBill); +router.put("/:id", checkJwtToken, billController.payBill); module.exports = router; diff --git a/ui/package-lock.json b/ui/package-lock.json index dc984d3..c492833 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -18,7 +18,8 @@ "money": "^0.2.0", "vue": "^2.6.11", "vue-router": "^3.5.3", - "vuex": "^3.6.2" + "vuex": "^3.6.2", + "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", @@ -15612,6 +15613,11 @@ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, + "node_modules/shvl": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.3.tgz", + "integrity": "sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==" + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -17792,6 +17798,27 @@ "vue": "^2.0.0" } }, + "node_modules/vuex-persistedstate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz", + "integrity": "sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "deepmerge": "^4.2.2", + "shvl": "^2.0.3" + }, + "peerDependencies": { + "vuex": "^3.0 || ^4.0.0-rc" + } + }, + "node_modules/vuex-persistedstate/node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -31129,6 +31156,11 @@ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, + "shvl": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.3.tgz", + "integrity": "sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==" + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -32884,6 +32916,22 @@ "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==", "requires": {} }, + "vuex-persistedstate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz", + "integrity": "sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==", + "requires": { + "deepmerge": "^4.2.2", + "shvl": "^2.0.3" + }, + "dependencies": { + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + } + } + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 14d9f16..4c11426 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,7 +20,8 @@ "money": "^0.2.0", "vue": "^2.6.11", "vue-router": "^3.5.3", - "vuex": "^3.6.2" + "vuex": "^3.6.2", + "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", diff --git a/ui/src/api/api.js b/ui/src/api/api.js index 5ccb03a..f05bf5a 100644 --- a/ui/src/api/api.js +++ b/ui/src/api/api.js @@ -1,28 +1,67 @@ -import axios from 'axios' +import axios from "axios"; const api = class Api { - constructor() { - this.baseUrl = 'http://localhost:3000' - } - - async getAllBills(queryString) { - const query = { - driver: queryString.driver, - paid: queryString.paid, - limit: queryString.limit, - offset: queryString.offset - } - - return axios.get(`${this.baseUrl}/bill/`, { params: JSON.parse(JSON.stringify(query)) } ) - .then(response => {return response.data}) - .catch(error => {throw error}) - } + constructor() { + this.baseUrl = "http://localhost:3000"; + this.authUrl = "http://localhost:3000/auth"; + } - async payBill(billId) { - return axios.put(`${this.baseUrl}/bill/${billId}`, { body: { paid: true }}) - .then(response => {return response.data}) - .catch(error => {throw error}) - } -} + async getAllBills(queryString) { + const query = { + driver: queryString.driver, + paid: queryString.paid, + limit: queryString.limit, + offset: queryString.offset, + }; -export default new api \ No newline at end of file + return axios + .get(`${this.baseUrl}/bill/`, { + params: JSON.parse(JSON.stringify(query)), + withCredentials: true, + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + throw error; + }); + } + + async payBill(billId) { + return axios + .put( + `${this.baseUrl}/bill/${billId}`, + { body: { paid: true } }, + { + withCredentials: true, + } + ) + .then((response) => { + return response.data; + }) + .catch((error) => { + throw error; + }); + } + + async login(payload) { + return axios + .post(`${this.authUrl}/login`, payload, { + withCredentials: true, + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + throw error; + }); + } + + async logout() { + return axios.post(`${this.authUrl}/logout`, { + withCredentials: true, + }); + } +}; + +export default new api(); diff --git a/ui/src/auth/index.js b/ui/src/auth/index.js new file mode 100644 index 0000000..1557b3e --- /dev/null +++ b/ui/src/auth/index.js @@ -0,0 +1,34 @@ +import store from "../store/store"; + +/** + * Check if the user transitioning the route is logged in. + */ +export const isAuthenticated = (to, from, next) => { + if (store.state.loggedIn) { + next(); + } else { + next({ path: "/login" }); + } +}; + +/** + * Check if the user transitioning the route is an admin. + */ +export const isOperator = (to, from, next) => { + if (store.getters.user.type == "Operator") { + next(); + } else { + next({ path: "/forbidden" }); + } +}; + +/** + * Check if the user transitioning the route is logged out. + */ +export const isLoggedOut = (to, from, next) => { + if (store.state.loggedIn) { + next({ path: "/my-bills" }); + } else { + next(); + } +}; diff --git a/ui/src/components/navbar.vue b/ui/src/components/navbar.vue index a1bd030..d77c57d 100644 --- a/ui/src/components/navbar.vue +++ b/ui/src/components/navbar.vue @@ -1,86 +1,198 @@  \ No newline at end of file diff --git a/ui/src/router/router.js b/ui/src/router/router.js index 3f13a20..4f93d65 100644 --- a/ui/src/router/router.js +++ b/ui/src/router/router.js @@ -2,49 +2,62 @@ import VueRouter from "vue-router"; import MyBills from "@/views/MyBills"; import PayBill from "@/views/PayBill"; +import Login from "@/views/Login"; +import { isAuthenticated, isLoggedOut } from "../auth"; /** * Import Vue Router. */ -Vue.use(VueRouter) +Vue.use(VueRouter); /** * Declare routes for the my bills and pay bills routes. */ const routes = [ - { - path: '/', - redirect: '/my-bills' - }, - { - path: '/my-bills', - name: 'MyBills', - component: MyBills, - meta: { - title: 'My Bills' - } - }, - { - path: '/my-bills/:id', - name: 'PayBill', - component: PayBill - } -] + { + path: "/", + redirect: "/my-bills", + }, + { + path: "/my-bills", + name: "MyBills", + component: MyBills, + meta: { + title: "My Bills", + }, + beforeEnter: isAuthenticated, + }, + { + path: "/my-bills/:id", + name: "PayBill", + component: PayBill, + beforeEnter: isAuthenticated, + }, + { + path: "/login", + name: "Login", + component: Login, + meta: { + title: "Login", + }, + beforeEnter: isLoggedOut, + }, +]; /** * Creates an instance of Vue Router with history mode enables and injects the routes above. */ const router = new VueRouter({ - mode: 'history', - base: process.env.BASE_URL, - routes -}) + mode: "history", + base: process.env.BASE_URL, + routes, +}); /** * Sets the document title of each page before route enter. */ router.beforeEach((to, from, next) => { - document.title = to.meta.title - next() -}) + document.title = to.meta.title; + next(); +}); -export default router \ No newline at end of file +export default router; diff --git a/ui/src/store/store.js b/ui/src/store/store.js index 5c80304..05acd05 100644 --- a/ui/src/store/store.js +++ b/ui/src/store/store.js @@ -1,35 +1,81 @@ -import Vue from 'vue' -import Vuex from 'vuex' +import Vue from "vue"; +import Vuex from "vuex"; +import createPersistedState from "vuex-persistedstate"; -Vue.use(Vuex) +Vue.use(Vuex); export default new Vuex.Store({ - state: () => ({ - selectedCurrency: 'NOK' - }), - mutations: { - /** - * Updates the selectedCurrency in the state. - * @param state - * @param selectedCurrency - */ - updateSelectedCurrency (state, selectedCurrency) { - state.selectedCurrency = selectedCurrency - } - }, - actions: { - /** - * Calls the updateSelectedCurrency mutation. - * @param selectedCurrency - */ - updateSelectedCurrency({ commit }, selectedCurrency) { - commit('updateSelectedCurrency', selectedCurrency) - } - }, - getters: { - /** - * Gets the selectedCurrency from the state. - */ - selectedCurrency: state => state.selectedCurrency - } -}) \ No newline at end of file + plugins: [ + // Handle storing and retrieving data from the session storage + // as the store is stateless. + createPersistedState({ + storage: sessionStorage, + }), + ], + state: () => ({ + selectedCurrency: "NOK", + loggedIn: false, + user: { + id: null, + username: null, + type: null, + }, + }), + mutations: { + /** + * Updates the selectedCurrency in the state. + * @param state + * @param selectedCurrency + */ + updateSelectedCurrency(state, selectedCurrency) { + state.selectedCurrency = selectedCurrency; + }, + + /** + * Updates the loggedIn state in the state. + * @param state + * @param payload + */ + setLoggedIn(state, payload) { + state.loggedIn = payload; + }, + + /** + * Updates the user state in the state. + * @param state + * @param payload + */ + setUser(state, payload) { + state.user = payload; + }, + }, + actions: { + /** + * Calls the updateSelectedCurrency mutation. + * @param selectedCurrency + */ + updateSelectedCurrency({ commit }, selectedCurrency) { + commit("updateSelectedCurrency", selectedCurrency); + }, + }, + getters: { + /** + * Gets the selectedCurrency from the state. + */ + selectedCurrency: (state) => state.selectedCurrency, + + /** + * Gets the loggedIn state. + */ + loggedIn: (state) => { + return state.loggedIn; + }, + + /** + * Gets the logged in user. + */ + user: (state) => { + return state.user; + }, + }, +}); diff --git a/ui/src/utilities.js b/ui/src/utilities.js index 87eaf6d..2df51b5 100644 --- a/ui/src/utilities.js +++ b/ui/src/utilities.js @@ -1,22 +1,21 @@ -import dayjs from 'dayjs' -import store from './store/store' -import fx from 'money' - +import dayjs from "dayjs"; +import store from "./store/store"; +import fx from "money"; /** * Configure money.js. * Sets the base currency to NOK. * Configures the conversion rates for each currency based on NOK. */ -fx.base = "NOK" +fx.base = "NOK"; fx.rates = { - "EUR": 0.10, - "GBP": 0.084, - "DKK": 0.74, - "ISK": 14.28, - "SEK": 1.04, - "NOK": 1 -} + EUR: 0.1, + GBP: 0.084, + DKK: 0.74, + ISK: 14.28, + SEK: 1.04, + NOK: 1, +}; /** * Formats a date string into the format ddd D MMM YYYY @ H:mm. @@ -24,7 +23,7 @@ fx.rates = { * @returns {string} */ export function formatDate(date) { - return dayjs(date).format('ddd D MMM YYYY @ H:mm') + return dayjs(date).format("ddd D MMM YYYY @ H:mm"); } /** @@ -33,6 +32,22 @@ export function formatDate(date) { * @returns {string} */ export function formatCost(cost) { - const selectedCurrency = store.getters.selectedCurrency - return new Intl.NumberFormat(undefined, {style: 'currency', currency: selectedCurrency}).format(Number.parseFloat(fx.convert(cost, {from: 'NOK', to: selectedCurrency}))) -} \ No newline at end of file + const selectedCurrency = store.getters.selectedCurrency; + return new Intl.NumberFormat(undefined, { + style: "currency", + currency: selectedCurrency, + }).format( + Number.parseFloat( + fx.convert(cost, { from: "NOK", to: selectedCurrency }) + ) + ); +} + +/** + * Check if the user is logged in + * @returns {Boolean} + */ +export function isUserAuthenticated() { + console.log("check", store.getters.loggedIn); + return store.getters.loggedIn; +} diff --git a/ui/src/views/Login.vue b/ui/src/views/Login.vue new file mode 100644 index 0000000..8c83757 --- /dev/null +++ b/ui/src/views/Login.vue @@ -0,0 +1,86 @@ + + + + + From 2b2d0aa52f00b82936f1db5db645c33f9dc378a8 Mon Sep 17 00:00:00 2001 From: jarrodback Date: Fri, 4 Feb 2022 11:56:49 +0000 Subject: [PATCH 2/7] HT-7 update store actions --- ui/coverage/lcov.info | 74 ++++++++----- ui/src/components/navbar.vue | 4 +- ui/src/store/store.js | 24 ++++- ui/src/views/Login.vue | 4 +- ui/tests/unit/utilities.test.js | 181 ++++++++++++++++++-------------- 5 files changed, 175 insertions(+), 112 deletions(-) diff --git a/ui/coverage/lcov.info b/ui/coverage/lcov.info index c963dce..bea8122 100644 --- a/ui/coverage/lcov.info +++ b/ui/coverage/lcov.info @@ -1,40 +1,62 @@ TN: -SF:C:\Projects\CSSD-Assignment\ui\src\utilities.js -FN:15,formatDate -FN:19,formatCost -FNF:2 +SF:C:\Users\Jarrod\Documents\AAF\CSSD\ui\src\utilities.js +FN:25,formatDate +FN:34,formatCost +FN:50,isUserAuthenticated +FNF:3 FNH:2 FNDA:1,formatDate FNDA:5,formatCost -DA:5,1 -DA:6,1 -DA:16,1 -DA:20,5 -DA:21,5 -LF:5 +FNDA:0,isUserAuthenticated +DA:10,1 +DA:11,1 +DA:26,1 +DA:35,5 +DA:36,5 +DA:51,0 +DA:52,0 +LF:7 LH:5 BRF:0 BRH:0 end_of_record TN: -SF:C:\Projects\CSSD-Assignment\ui\src\store\store.js -FN:7,(anonymous_0) -FN:11,(anonymous_1) -FN:16,(anonymous_2) -FN:21,(anonymous_3) -FNF:4 -FNH:4 +SF:C:\Users\Jarrod\Documents\AAF\CSSD\ui\src\store\store.js +FN:15,(anonymous_0) +FN:30,(anonymous_1) +FN:39,(anonymous_2) +FN:48,(anonymous_3) +FN:57,(anonymous_4) +FN:65,(anonymous_5) +FN:73,(anonymous_6) +FN:81,(anonymous_7) +FN:86,(anonymous_8) +FN:93,(anonymous_9) +FNF:10 +FNH:7 FNDA:1,(anonymous_0) FNDA:5,(anonymous_1) -FNDA:5,(anonymous_2) -FNDA:5,(anonymous_3) -DA:4,1 -DA:7,1 -DA:12,5 -DA:17,5 -DA:21,5 -LF:5 -LH:5 +FNDA:2,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:5,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:5,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:0,(anonymous_9) +DA:5,1 +DA:15,1 +DA:31,5 +DA:40,2 +DA:49,0 +DA:58,5 +DA:66,2 +DA:74,0 +DA:81,5 +DA:87,2 +DA:94,0 +LF:11 +LH:8 BRF:0 BRH:0 end_of_record diff --git a/ui/src/components/navbar.vue b/ui/src/components/navbar.vue index d77c57d..0a2ef9f 100644 --- a/ui/src/components/navbar.vue +++ b/ui/src/components/navbar.vue @@ -137,8 +137,8 @@ export default Vue.extend({ */ signOut() { api.logout().then(() => { - store.commit("setLoggedIn", false); - store.commit("setUser", {}); + store.dispatch("updateLoggedIn", false); + store.dispatch("updateUser", {}); sessionStorage.clear(); this.$router.push("/login").catch(() => {}); }); diff --git a/ui/src/store/store.js b/ui/src/store/store.js index 05acd05..4627864 100644 --- a/ui/src/store/store.js +++ b/ui/src/store/store.js @@ -36,8 +36,8 @@ export default new Vuex.Store({ * @param state * @param payload */ - setLoggedIn(state, payload) { - state.loggedIn = payload; + updateLoggedIn(state, loggedIn) { + state.loggedIn = loggedIn; }, /** @@ -45,8 +45,8 @@ export default new Vuex.Store({ * @param state * @param payload */ - setUser(state, payload) { - state.user = payload; + updateUser(state, user) { + state.user = user; }, }, actions: { @@ -57,6 +57,22 @@ export default new Vuex.Store({ updateSelectedCurrency({ commit }, selectedCurrency) { commit("updateSelectedCurrency", selectedCurrency); }, + + /** + * Calls the updateLoggedIn mutation. + * @param loggedIn + */ + updateLoggedIn({ commit }, loggedIn) { + commit("updateLoggedIn", loggedIn); + }, + + /** + * Calls the updateUser mutation. + * @param loggedIn + */ + updateUser({ commit }, user) { + commit("updateUser", user); + }, }, getters: { /** diff --git a/ui/src/views/Login.vue b/ui/src/views/Login.vue index 8c83757..d02cc95 100644 --- a/ui/src/views/Login.vue +++ b/ui/src/views/Login.vue @@ -65,8 +65,8 @@ export default { api.login(this.loginData) .then((data) => { // If successful, store returned user details and change route. - store.commit("setLoggedIn", true); - store.commit("setUser", { + store.dispatch("updateLoggedIn", true); + store.dispatch("updateUser", { id: data.id, username: data.username, type: data.type, diff --git a/ui/tests/unit/utilities.test.js b/ui/tests/unit/utilities.test.js index 64d044e..5997b9d 100644 --- a/ui/tests/unit/utilities.test.js +++ b/ui/tests/unit/utilities.test.js @@ -1,78 +1,103 @@ -import { formatDate, formatCost } from '@/utilities' -import Store from '@/store/store' - -describe('Format Date', () => { - test('Can return date in correct format', () => { - //Arrange - const dateString = '2022-02-01T15:50:51.039Z' - - //Act - const formattedDateString = formatDate(dateString) - - //Assert - expect(formattedDateString).toBe('Tue 1 Feb 2022 @ 15:50') - }) -}) - -describe('Format Cost', () => { - test('Can convert price from NOK to SEK', () => { - //Arrange - const currencyString = 'SEK' - Store.dispatch('updateSelectedCurrency', currencyString) - - //Act - const convertedCost = formatCost(10) - - //Assert - expect(convertedCost).toBe("SEK 10.40") - }) - - test('Can convert price from NOK to ISK', () => { - //Arrange - const currencyString = 'ISK' - Store.dispatch('updateSelectedCurrency', currencyString) - - //Act - const convertedCost = formatCost(10) - - //Assert - expect(convertedCost).toBe("ISK 143") - }) - - test('Can convert price from NOK to DKK', () => { - //Arrange - const currencyString = 'DKK' - Store.dispatch('updateSelectedCurrency', currencyString) - - //Act - const convertedCost = formatCost(10) - - //Assert - expect(convertedCost).toBe("DKK 7.40") - }) - - test('Can convert price from NOK to GBP', () => { - //Arrange - const currencyString = 'GBP' - Store.dispatch('updateSelectedCurrency', currencyString) - - //Act - const convertedCost = formatCost(10) - - //Assert - expect(convertedCost).toBe("£0.84") - }) - - test('Can convert price from NOK to EUR', () => { - //Arrange - const currencyString = 'EUR' - Store.dispatch('updateSelectedCurrency', currencyString) - - //Act - const convertedCost = formatCost(10) - - //Assert - expect(convertedCost).toBe("€1.00") - }) -}) - +import { formatDate, formatCost } from "@/utilities"; +import Store from "@/store/store"; + +describe("Format Date", () => { + test("Can return date in correct format", () => { + //Arrange + const dateString = "2022-02-01T15:50:51.039Z"; + + //Act + const formattedDateString = formatDate(dateString); + + //Assert + expect(formattedDateString).toBe("Tue 1 Feb 2022 @ 15:50"); + }); +}); + +describe("Format Cost", () => { + test("Can convert price from NOK to SEK", () => { + //Arrange + const currencyString = "SEK"; + Store.dispatch("updateSelectedCurrency", currencyString); + + //Act + const convertedCost = formatCost(10); + + //Assert + expect(convertedCost).toBe("SEK 10.40"); + }); + + test("Can convert price from NOK to ISK", () => { + //Arrange + const currencyString = "ISK"; + Store.dispatch("updateSelectedCurrency", currencyString); + + //Act + const convertedCost = formatCost(10); + + //Assert + expect(convertedCost).toBe("ISK 143"); + }); + + test("Can convert price from NOK to DKK", () => { + //Arrange + const currencyString = "DKK"; + Store.dispatch("updateSelectedCurrency", currencyString); + + //Act + const convertedCost = formatCost(10); + + //Assert + expect(convertedCost).toBe("DKK 7.40"); + }); + + test("Can convert price from NOK to GBP", () => { + //Arrange + const currencyString = "GBP"; + Store.dispatch("updateSelectedCurrency", currencyString); + + //Act + const convertedCost = formatCost(10); + + //Assert + expect(convertedCost).toBe("£0.84"); + }); + + test("Can convert price from NOK to EUR", () => { + //Arrange + const currencyString = "EUR"; + Store.dispatch("updateSelectedCurrency", currencyString); + + //Act + const convertedCost = formatCost(10); + + //Assert + expect(convertedCost).toBe("€1.00"); + }); +}); + +describe("Can check if user is authenticated", () => { + test("Can get the loggedIn status", () => { + //Arrange + const loggedIn = false; + Store.dispatch("updateLoggedIn", loggedIn); + + //Act + const isLoggedIn = Store.getters.loggedIn; + + //Assert + expect(isLoggedIn).toBe(false); + }); + + test("Can get the loggedIn status", () => { + //Arrange + const loggedIn = true; + Store.dispatch("updateLoggedIn", loggedIn); + + //Act + const isLoggedIn = Store.getters.loggedIn; + + //Assert + expect(isLoggedIn).toBe(true); + }); +}); From 73a676aa92b91658e41a351afc3f61b2fc88b745 Mon Sep 17 00:00:00 2001 From: jarrodback Date: Fri, 4 Feb 2022 12:01:25 +0000 Subject: [PATCH 3/7] HT-7 fix server tests --- .../test/integration/auth.controller.test.js | 2 +- .../test/integration/bill.controller.test.js | 563 ++++++++++++------ 2 files changed, 367 insertions(+), 198 deletions(-) diff --git a/server/test/integration/auth.controller.test.js b/server/test/integration/auth.controller.test.js index dc87a1b..752f027 100644 --- a/server/test/integration/auth.controller.test.js +++ b/server/test/integration/auth.controller.test.js @@ -48,7 +48,7 @@ describe("Testing /auth paths", () => { res.body.should.have.property("message"); res.body.message.should.be.eql("Successfully logged in."); res.should.have.cookie("highwayTracker-token"); - + done(); }); }); diff --git a/server/test/integration/bill.controller.test.js b/server/test/integration/bill.controller.test.js index 54a9fa2..fe4c971 100644 --- a/server/test/integration/bill.controller.test.js +++ b/server/test/integration/bill.controller.test.js @@ -4,201 +4,370 @@ let server = require("../../app"); let should = chai.should(); chai.use(chaiHttp); +let authCookie; +let authCookieSig; +before(function (done) { + chai.request(server) + .post("/auth/login") + .send({ + email: "test@email.com", + password: "test1", + }) + .end((err, res) => { + authCookie = res.headers["set-cookie"].pop().split(";")[0]; + authCookieSig = res.headers["set-cookie"].pop().split(";")[0]; + + done(); + }); +}); + describe("Testing /bill paths", () => { - it("Should get all bills", (done) => { - // Arrange - const url = "/bill/" - - // Act - chai.request(server) - .get(url) - .send() - .end((err, res) => { - // Assert - res.should.have.status(200); - res.should.be.a("object"); - res.body.bills.should.have.lengthOf(2); - res.body.bills[0].should.haveOwnProperty('cost', 72.93887106726764) - res.body.bills[0].should.haveOwnProperty('paid', false) - res.body.bills[0].driver.should.haveOwnProperty('username', 'test_username') - res.body.bills[0].driver.should.haveOwnProperty('email', 'test@email.com') - res.body.bills[0].driver.should.haveOwnProperty('type', 'Driver') - res.body.bills[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number') - res.body.bills[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.039Z') - res.body.bills[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) - res.body.bills[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) - res.body.count.should.equal(2) - - done(); - }) - }) - - it("Should get all bills which match the driver ID", (done) => { - // Arrange - const driverId = "123456789107" - const url = `/bill?driver=${driverId}` - - // Act - chai.request(server) - .get(url) - .send() - .end((err, res) => { - // Assert - - res.should.have.status(200); - res.should.be.a("object"); - res.body.bills.should.have.lengthOf(1); - res.body.bills[0].should.haveOwnProperty('cost', 72.93887106726764) - res.body.bills[0].should.haveOwnProperty('paid', false) - res.body.bills[0].driver.should.haveOwnProperty('username', 'test_username') - res.body.bills[0].driver.should.haveOwnProperty('email', 'test@email.com') - res.body.bills[0].driver.should.haveOwnProperty('type', 'Driver') - res.body.bills[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number') - res.body.bills[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.039Z') - res.body.bills[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) - res.body.bills[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) - res.body.count.should.equal(2) - - done(); - }) - }) - - it("Should get all bills which match the paid", (done) => { - // Arrange - const paid = true - const url = `/bill?paid=${paid}` - - // Act - chai.request(server) - .get(url) - .send() - .end((err, res) => { - // Assert - - res.should.have.status(200); - res.should.be.a("object"); - res.body.bills.should.have.lengthOf(1); - res.body.bills[0].should.haveOwnProperty('cost', 72.93887106726764) - res.body.bills[0].driver.should.haveOwnProperty('username', 'test_username2') - res.body.bills[0].driver.should.haveOwnProperty('email', 'test2@email.com') - res.body.bills[0].driver.should.haveOwnProperty('type', 'Driver') - res.body.bills[0].should.haveOwnProperty('paid', true) - res.body.bills[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number2') - res.body.bills[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.038Z') - res.body.bills[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) - res.body.bills[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) - res.body.count.should.equal(2) - - done(); - }) - }) - - it("Should get one bill when pagination limit is one", (done) => { - // Arrange - const limit = 1 - const url = `/bill?limit=${limit}` - - // Act - chai.request(server) - .get(url) - .send() - .end((err, res) => { - // Assert - res.should.have.status(200); - res.should.be.a("object"); - res.body.bills.should.have.lengthOf(1); - res.body.bills[0].should.haveOwnProperty('cost', 72.93887106726764) - res.body.bills[0].driver.should.haveOwnProperty('username', 'test_username') - res.body.bills[0].driver.should.haveOwnProperty('email', 'test@email.com') - res.body.bills[0].driver.should.haveOwnProperty('type', 'Driver') - res.body.bills[0].should.haveOwnProperty('paid', false) - res.body.bills[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number') - res.body.bills[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.039Z') - res.body.bills[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) - res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) - res.body.bills[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) - res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) - res.body.count.should.equal(2) - - done(); - }) - }) - - it("Should get no bills when pagination offset is one", (done) => { - // Arrange - const offset = 1 - const url = `/bill?offset=${offset}` - - // Act - chai.request(server) - .get(url) - .send() - .end((err, res) => { - // Assert - - res.should.have.status(200); - res.should.be.a("object"); - res.body.bills.should.have.lengthOf(0); - res.body.count.should.equal(2) - - done(); - }) - }) - - it("Should update paid to true", (done) => { - // Arrange - const requestBody = { - paid: true - } - const url = `/bill/123456789105` - - // Act - chai.request(server) - .put(url) - .send(requestBody) - .end((err, res) => { - // Assert - res.should.have.status(200); - res.should.be.a("object"); - res.body.message.should.be.eql("Bill paid."); - - done(); - }) - }) - - it("Should throw error if bill doesnt exist", (done) => { - // Arrange - const fakeId = '111111111111' - const requestBody = { - paid: true - } - const url = `/bill/${fakeId}` - - // Act - chai.request(server) - .put(url) - .send(requestBody) - .end((err, res) => { - // Assert - res.should.have.status(404); - res.body.message.should.be.eql("Bill can't be found in the database."); - - done(); - }) - }) - -}) \ No newline at end of file + it("Should get all bills", (done) => { + // Arrange + const url = "/bill/"; + + // Act + chai.request(server) + .get(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send() + .end((err, res) => { + // Assert + res.should.have.status(200); + res.should.be.a("object"); + res.body.bills.should.have.lengthOf(2); + res.body.bills[0].should.haveOwnProperty( + "cost", + 72.93887106726764 + ); + res.body.bills[0].should.haveOwnProperty("paid", false); + res.body.bills[0].driver.should.haveOwnProperty( + "username", + "test_username" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "email", + "test@email.com" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "type", + "Driver" + ); + res.body.bills[0].journey.should.haveOwnProperty( + "regNumber", + "test_reg_number" + ); + res.body.bills[0].journey.should.haveOwnProperty( + "journeyDateTime", + "2022-02-01T15:50:51.039Z" + ); + res.body.bills[0].journey.entryLocation.should.haveOwnProperty( + "name", + "test_location_1" + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "longitude", + 50 + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "latitude", + 50 + ); + res.body.bills[0].journey.exitLocation.should.haveOwnProperty( + "name", + "test_location_2" + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "longitude", + 0 + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "latitude", + 0 + ); + res.body.count.should.equal(2); + + done(); + }); + }); + + it("Should get all bills which match the driver ID", (done) => { + // Arrange + const driverId = "123456789107"; + const url = `/bill?driver=${driverId}`; + + // Act + chai.request(server) + .get(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send() + .end((err, res) => { + // Assert + + res.should.have.status(200); + res.should.be.a("object"); + res.body.bills.should.have.lengthOf(1); + res.body.bills[0].should.haveOwnProperty( + "cost", + 72.93887106726764 + ); + res.body.bills[0].should.haveOwnProperty("paid", false); + res.body.bills[0].driver.should.haveOwnProperty( + "username", + "test_username" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "email", + "test@email.com" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "type", + "Driver" + ); + res.body.bills[0].journey.should.haveOwnProperty( + "regNumber", + "test_reg_number" + ); + res.body.bills[0].journey.should.haveOwnProperty( + "journeyDateTime", + "2022-02-01T15:50:51.039Z" + ); + res.body.bills[0].journey.entryLocation.should.haveOwnProperty( + "name", + "test_location_1" + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "longitude", + 50 + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "latitude", + 50 + ); + res.body.bills[0].journey.exitLocation.should.haveOwnProperty( + "name", + "test_location_2" + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "longitude", + 0 + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "latitude", + 0 + ); + res.body.count.should.equal(2); + + done(); + }); + }); + + it("Should get all bills which match the paid", (done) => { + // Arrange + const paid = true; + const url = `/bill?paid=${paid}`; + + // Act + chai.request(server) + .get(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send() + .end((err, res) => { + // Assert + + res.should.have.status(200); + res.should.be.a("object"); + res.body.bills.should.have.lengthOf(1); + res.body.bills[0].should.haveOwnProperty( + "cost", + 72.93887106726764 + ); + res.body.bills[0].driver.should.haveOwnProperty( + "username", + "test_username2" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "email", + "test2@email.com" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "type", + "Driver" + ); + res.body.bills[0].should.haveOwnProperty("paid", true); + res.body.bills[0].journey.should.haveOwnProperty( + "regNumber", + "test_reg_number2" + ); + res.body.bills[0].journey.should.haveOwnProperty( + "journeyDateTime", + "2022-02-01T15:50:51.038Z" + ); + res.body.bills[0].journey.entryLocation.should.haveOwnProperty( + "name", + "test_location_1" + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "longitude", + 50 + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "latitude", + 50 + ); + res.body.bills[0].journey.exitLocation.should.haveOwnProperty( + "name", + "test_location_2" + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "longitude", + 0 + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "latitude", + 0 + ); + res.body.count.should.equal(2); + + done(); + }); + }); + + it("Should get one bill when pagination limit is one", (done) => { + // Arrange + const limit = 1; + const url = `/bill?limit=${limit}`; + + // Act + chai.request(server) + .get(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send() + .end((err, res) => { + // Assert + res.should.have.status(200); + res.should.be.a("object"); + res.body.bills.should.have.lengthOf(1); + res.body.bills[0].should.haveOwnProperty( + "cost", + 72.93887106726764 + ); + res.body.bills[0].driver.should.haveOwnProperty( + "username", + "test_username" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "email", + "test@email.com" + ); + res.body.bills[0].driver.should.haveOwnProperty( + "type", + "Driver" + ); + res.body.bills[0].should.haveOwnProperty("paid", false); + res.body.bills[0].journey.should.haveOwnProperty( + "regNumber", + "test_reg_number" + ); + res.body.bills[0].journey.should.haveOwnProperty( + "journeyDateTime", + "2022-02-01T15:50:51.039Z" + ); + res.body.bills[0].journey.entryLocation.should.haveOwnProperty( + "name", + "test_location_1" + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "longitude", + 50 + ); + res.body.bills[0].journey.entryLocation.coordinates.should.haveOwnProperty( + "latitude", + 50 + ); + res.body.bills[0].journey.exitLocation.should.haveOwnProperty( + "name", + "test_location_2" + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "longitude", + 0 + ); + res.body.bills[0].journey.exitLocation.coordinates.should.haveOwnProperty( + "latitude", + 0 + ); + res.body.count.should.equal(2); + + done(); + }); + }); + + it("Should get no bills when pagination offset is one", (done) => { + // Arrange + const offset = 1; + const url = `/bill?offset=${offset}`; + + // Act + chai.request(server) + .get(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send() + .end((err, res) => { + // Assert + + res.should.have.status(200); + res.should.be.a("object"); + res.body.bills.should.have.lengthOf(0); + res.body.count.should.equal(2); + + done(); + }); + }); + + it("Should update paid to true", (done) => { + // Arrange + const requestBody = { + paid: true, + }; + const url = `/bill/123456789105`; + + // Act + chai.request(server) + .put(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send(requestBody) + .end((err, res) => { + // Assert + res.should.have.status(200); + res.should.be.a("object"); + res.body.message.should.be.eql("Bill paid."); + + done(); + }); + }); + + it("Should throw error if bill doesnt exist", (done) => { + // Arrange + const fakeId = "111111111111"; + const requestBody = { + paid: true, + }; + const url = `/bill/${fakeId}`; + + // Act + chai.request(server) + .put(url) + .set("Cookie", authCookie + "; " + authCookieSig) + .send(requestBody) + .end((err, res) => { + // Assert + res.should.have.status(404); + res.body.message.should.be.eql( + "Bill can't be found in the database." + ); + + done(); + }); + }); +}); From 945e252ad119ea16e90059c3b50058325727ef7f Mon Sep 17 00:00:00 2001 From: jarrodback Date: Fri, 4 Feb 2022 12:10:30 +0000 Subject: [PATCH 4/7] HT-7 fix a code smell --- ui/src/components/navbar.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/components/navbar.vue b/ui/src/components/navbar.vue index 0a2ef9f..50f7b2e 100644 --- a/ui/src/components/navbar.vue +++ b/ui/src/components/navbar.vue @@ -140,7 +140,9 @@ export default Vue.extend({ store.dispatch("updateLoggedIn", false); store.dispatch("updateUser", {}); sessionStorage.clear(); - this.$router.push("/login").catch(() => {}); + this.$router.push("/login").catch(() => { + //Intentional comment for sonar. Stop route error if already on same page. + }); }); }, }, From 02e6c7ea953d90408fe69925af705f23a67bc080 Mon Sep 17 00:00:00 2001 From: jarrodback Date: Fri, 4 Feb 2022 12:17:09 +0000 Subject: [PATCH 5/7] HT-7 100% coverage on unit tests --- ui/src/utilities.js | 1 - ui/tests/unit/store.test.js | 18 ++++++++++++++++++ ui/tests/unit/utilities.test.js | 6 +++--- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 ui/tests/unit/store.test.js diff --git a/ui/src/utilities.js b/ui/src/utilities.js index 2df51b5..f379866 100644 --- a/ui/src/utilities.js +++ b/ui/src/utilities.js @@ -48,6 +48,5 @@ export function formatCost(cost) { * @returns {Boolean} */ export function isUserAuthenticated() { - console.log("check", store.getters.loggedIn); return store.getters.loggedIn; } diff --git a/ui/tests/unit/store.test.js b/ui/tests/unit/store.test.js new file mode 100644 index 0000000..f03425c --- /dev/null +++ b/ui/tests/unit/store.test.js @@ -0,0 +1,18 @@ +import Store from "@/store/store"; + +describe("Test store sets and gets values correctly", () => { + test("Can set a user on store", () => { + //Arrange + const user = { + id: "id", + username: "username", + type: "Driver", + }; + + //Act + Store.dispatch("updateUser", user); + + //Assert + expect(Store.getters.user).toBe(user); + }); +}); diff --git a/ui/tests/unit/utilities.test.js b/ui/tests/unit/utilities.test.js index 5997b9d..793b1ce 100644 --- a/ui/tests/unit/utilities.test.js +++ b/ui/tests/unit/utilities.test.js @@ -1,4 +1,4 @@ -import { formatDate, formatCost } from "@/utilities"; +import { formatDate, formatCost, isUserAuthenticated } from "@/utilities"; import Store from "@/store/store"; describe("Format Date", () => { @@ -83,7 +83,7 @@ describe("Can check if user is authenticated", () => { Store.dispatch("updateLoggedIn", loggedIn); //Act - const isLoggedIn = Store.getters.loggedIn; + const isLoggedIn = isUserAuthenticated(); //Assert expect(isLoggedIn).toBe(false); @@ -95,7 +95,7 @@ describe("Can check if user is authenticated", () => { Store.dispatch("updateLoggedIn", loggedIn); //Act - const isLoggedIn = Store.getters.loggedIn; + const isLoggedIn = isUserAuthenticated(); //Assert expect(isLoggedIn).toBe(true); From 37d4b153c1afa3c624ba98edcf0dc6c4127f95a1 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 4 Feb 2022 13:14:20 +0000 Subject: [PATCH 6/7] HT-7 Style login page and add error handling --- ui/package-lock.json | 15 ++++++++ ui/package.json | 1 + ui/src/components/input.vue | 25 ++++++++++++ ui/src/components/navbar.vue | 16 +++++++- ui/src/main.js | 9 +++++ ui/src/styles/style.scss | 4 ++ ui/src/validation.js | 12 ++++++ ui/src/views/Login.vue | 74 +++++++++++++----------------------- 8 files changed, 108 insertions(+), 48 deletions(-) create mode 100644 ui/src/components/input.vue create mode 100644 ui/src/validation.js diff --git a/ui/package-lock.json b/ui/package-lock.json index c492833..43665a0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16,6 +16,7 @@ "core-js": "^3.6.5", "dayjs": "^1.10.7", "money": "^0.2.0", + "vee-validate": "^3.4.14", "vue": "^2.6.11", "vue-router": "^3.5.3", "vuex": "^3.6.2", @@ -17522,6 +17523,14 @@ "node": ">= 0.8" } }, + "node_modules/vee-validate": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-3.4.14.tgz", + "integrity": "sha512-Hqqic8G9WcRSIzCxiCPqMZv4qB8JE1lIQqIOLDm2K5BXUiL8d4a2+kqkanv8gQSGDzYpnCQZ7BO/T99Aj05T1Q==", + "peerDependencies": { + "vue": "^2.5.18" + } + }, "node_modules/vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", @@ -32696,6 +32705,12 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "vee-validate": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-3.4.14.tgz", + "integrity": "sha512-Hqqic8G9WcRSIzCxiCPqMZv4qB8JE1lIQqIOLDm2K5BXUiL8d4a2+kqkanv8gQSGDzYpnCQZ7BO/T99Aj05T1Q==", + "requires": {} + }, "vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", diff --git a/ui/package.json b/ui/package.json index 4c11426..2fbc6a3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "core-js": "^3.6.5", "dayjs": "^1.10.7", "money": "^0.2.0", + "vee-validate": "^3.4.14", "vue": "^2.6.11", "vue-router": "^3.5.3", "vuex": "^3.6.2", diff --git a/ui/src/components/input.vue b/ui/src/components/input.vue new file mode 100644 index 0000000..2e6c4dc --- /dev/null +++ b/ui/src/components/input.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/ui/src/components/navbar.vue b/ui/src/components/navbar.vue index 50f7b2e..7b8c4a5 100644 --- a/ui/src/components/navbar.vue +++ b/ui/src/components/navbar.vue @@ -2,6 +2,7 @@
profile icon + class="mr-2" + />{{ username }} Sign Out @@ -189,6 +191,18 @@ export default Vue.extend({ selectedCurrency() { return this.currency; }, + /** + * Check if the user is logged in. + */ + loggedIn() { + return store.getters.loggedIn + }, + /** + * Gets the username + */ + username() { + return store.getters.user.username + } }, /** * Sets the selected currency on create. diff --git a/ui/src/main.js b/ui/src/main.js index 493b279..3756970 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -3,7 +3,9 @@ import App from './App.vue' import router from './router/router' import store from './store/store' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' +import axios from 'axios' import '@/styles/style.scss' +import './validation' /** * Import Bootstrap into Vue @@ -11,6 +13,13 @@ import '@/styles/style.scss' Vue.use(BootstrapVue) Vue.use(IconsPlugin) +/** + * Catch any api errors and throw a Javascript Error for the toast to catch + */ +axios.interceptors.response.use(response => response, (error) => { + throw Error(error.response.data.message) +}) + Vue.config.productionTip = false new Vue({ diff --git a/ui/src/styles/style.scss b/ui/src/styles/style.scss index 38f34b1..79166c6 100644 --- a/ui/src/styles/style.scss +++ b/ui/src/styles/style.scss @@ -12,6 +12,10 @@ $White: white; color: $White !important; } +.card-title { + color: black; +} + h4 { color: $White; } diff --git a/ui/src/validation.js b/ui/src/validation.js new file mode 100644 index 0000000..94fa5a8 --- /dev/null +++ b/ui/src/validation.js @@ -0,0 +1,12 @@ +import { extend, setInteractionMode } from 'vee-validate' +import { required, email } from 'vee-validate/dist/rules' + +setInteractionMode('lazy') + +extend('required', { + ...required, + message: (fieldName) => { + return `${fieldName} is required.` + } +}) +extend('email', email) \ No newline at end of file diff --git a/ui/src/views/Login.vue b/ui/src/views/Login.vue index d02cc95..166e98e 100644 --- a/ui/src/views/Login.vue +++ b/ui/src/views/Login.vue @@ -1,58 +1,32 @@ + +