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/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(); + }); + }); +}); 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/package-lock.json b/ui/package-lock.json index dc984d3..43665a0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16,9 +16,11 @@ "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" + "vuex": "^3.6.2", + "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", @@ -15612,6 +15614,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", @@ -17516,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", @@ -17792,6 +17807,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 +31165,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", @@ -32664,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", @@ -32884,6 +32931,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..2fbc6a3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,9 +18,11 @@ "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" + "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..4db511d 100644 --- a/ui/src/api/api.js +++ b/ui/src/api/api.js @@ -1,28 +1,80 @@ -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}) - } - - async payBill(billId) { - return axios.put(`${this.baseUrl}/bill/${billId}`, { body: { paid: true }}) - .then(response => {return response.data}) - .catch(error => {throw error}) - } -} - -export default new api \ No newline at end of file + constructor() { + this.baseUrl = "http://localhost:3000"; + this.authUrl = "http://localhost:3000/auth"; + } + + 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)), + 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 register(payload) { + return axios + .post(`${this.authUrl}/register`, 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/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 e2a4da1..0a0270c 100644 --- a/ui/src/components/navbar.vue +++ b/ui/src/components/navbar.vue @@ -1,86 +1,199 @@  \ No newline at end of file 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/router/router.js b/ui/src/router/router.js index 6d6a1ee..b5cc9e4 100644 --- a/ui/src/router/router.js +++ b/ui/src/router/router.js @@ -2,61 +2,82 @@ import VueRouter from "vue-router"; import MyBills from "@/views/MyBills"; import PayBill from "@/views/PayBill"; +import Login from "@/views/Login"; +import Register from "@/views/Register"; +import { isAuthenticated, isLoggedOut } from "../auth"; import Help from "@/views/Help"; + /** * Import Vue Router. */ -Vue.use(VueRouter) +Vue.use(VueRouter); /** * Declare routes for the my bills, pay bills, help 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, - meta: { - title: 'Pay Bill' - } - }, - { - path: '/help', - name: 'Help', - component: Help, - meta: { - title: 'Frequently Asked Questions (FAQ)' + { + 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, + }, + { + path: "/register", + name: "Register", + component: Register, + meta: { + title: "Register", + }, + beforeEnter: isLoggedOut, + }, + { + path: '/help', + name: 'Help', + component: Help, + meta: { + title: 'Frequently Asked Questions (FAQ)' + } } - } -] +]; /** * 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..4627864 100644 --- a/ui/src/store/store.js +++ b/ui/src/store/store.js @@ -1,35 +1,97 @@ -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 + */ + updateLoggedIn(state, loggedIn) { + state.loggedIn = loggedIn; + }, + + /** + * Updates the user state in the state. + * @param state + * @param payload + */ + updateUser(state, user) { + state.user = user; + }, + }, + actions: { + /** + * Calls the updateSelectedCurrency mutation. + * @param selectedCurrency + */ + 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: { + /** + * 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/styles/style.scss b/ui/src/styles/style.scss index 2339df2..733818a 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/utilities.js b/ui/src/utilities.js index 87eaf6d..f379866 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,21 @@ 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() { + return store.getters.loggedIn; +} 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 new file mode 100644 index 0000000..166e98e --- /dev/null +++ b/ui/src/views/Login.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/ui/src/views/Register.vue b/ui/src/views/Register.vue new file mode 100644 index 0000000..cb244e8 --- /dev/null +++ b/ui/src/views/Register.vue @@ -0,0 +1,108 @@ + + + + + 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 64d044e..793b1ce 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, isUserAuthenticated } 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 = isUserAuthenticated(); + + //Assert + expect(isLoggedIn).toBe(false); + }); + + test("Can get the loggedIn status", () => { + //Arrange + const loggedIn = true; + Store.dispatch("updateLoggedIn", loggedIn); + + //Act + const isLoggedIn = isUserAuthenticated(); + + //Assert + expect(isLoggedIn).toBe(true); + }); +});