From 4043954f952d00d9653a378bc1e47e18f171133c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 11 Jan 2021 20:51:10 +0100 Subject: [PATCH 001/115] Split request into controller and repo. Also try returning better error message on exceptions and check for errors in payload to return well-defined errors. --- api/controllers/requestController.js | 64 ++++++++++++++++++++++++++++ api/request.js | 57 +++++++++++++------------ api/router.js | 8 ++-- 3 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 api/controllers/requestController.js diff --git a/api/controllers/requestController.js b/api/controllers/requestController.js new file mode 100644 index 0000000..91cc391 --- /dev/null +++ b/api/controllers/requestController.js @@ -0,0 +1,64 @@ +const path = require("path"); +const RequestRepository = require(path.join( + __dirname, "../request" +)); + +function addRequest(req, res) { + const { wine } = req.body; + + return RequestRepository.addNew(wine) + .then(wine => res.json({ + message: "Successfully added new request", + wine: wine, + success: true + })) + .catch(error => { + const { message, statusCode } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to add requested wine." + }) + }) +} + +function getAllRequests(req, res) { + return RequestRepository.getAll() + .then(wines => res.json({ + wines: wines, + success: true + })) + .catch(error => { + console.log("error in getAllRequests:", error); + + const message = "Unable to fetch all requested wines." + return res.status(500).json({ + success: false, + message: message + }) + }) +} + +function deleteRequest(req, res) { + const { id } = req.params; + + return RequestRepository.deleteById(id) + .then(_ => res.json({ + message: `Slettet vin med id: ${ id }`, + success: true + })) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to delete requested wine." + }); + }) +} + +module.exports = { + addRequest, + getAllRequests, + deleteRequest +} \ No newline at end of file diff --git a/api/request.js b/api/request.js index 60388fa..f1429c6 100644 --- a/api/request.js +++ b/api/request.js @@ -1,4 +1,3 @@ -const express = require("express"); const path = require("path"); const RequestedWine = require(path.join( __dirname, "/schemas/RequestedWine" @@ -7,31 +6,15 @@ const Wine = require(path.join( __dirname, "/schemas/Wine" )); -const deleteRequestedWineById = async (req, res) => { - const { id } = req.params; - if(id == null){ - return res.json({ - message: "Id er ikke definert", - success: false - }) +class RequestedWineNotFound extends Error { + constructor(message="Wine with this id was not found.") { + super(message); + this.name = "RequestedWineNotFound"; + this.statusCode = 404; } - - await RequestedWine.deleteOne({wineId: id}) - return res.json({ - message: `Slettet vin med id: ${id}`, - success: true - }); } -const getAllRequestedWines = async (req, res) => { - const allWines = await RequestedWine.find({}).populate("wine"); - - return res.json(allWines); -} - -const requestNewWine = async (req, res) => { - const {wine} = req.body - +const addNew = async (wine) => { let thisWineIsLOKO = await Wine.findOne({id: wine.id}) if(thisWineIsLOKO == undefined){ @@ -59,11 +42,31 @@ const requestNewWine = async (req, res) => { } await requestedWine.save() - return res.send(requestedWine); + return requestedWine; +} + +const getById = (id) => { + return RequestedWine.findOne({ wineId: id }).populate("wine") + .then(wine => { + if (wine == null) { + throw new RequestedWineNotFound(); + } + + return wine; + }) +} + +const deleteById = (id) => { + return getById(id) + .then(wine => RequestedWine.deleteOne({ wineId: wine.id })) +} + +const getAll = () => { + return RequestedWine.find({}).populate("wine"); } module.exports = { - requestNewWine, - getAllRequestedWines, - deleteRequestedWineById + addNew, + getAll, + deleteById }; diff --git a/api/router.js b/api/router.js index 4d57057..43d4c2a 100644 --- a/api/router.js +++ b/api/router.js @@ -17,13 +17,15 @@ const virtualRegistrationApi = require(path.join( const lottery = require(path.join(__dirname, "/lottery")); const chatHistoryApi = require(path.join(__dirname, "/chatHistory")); +const requestController = require(path.join(__dirname, "/controllers/requestController")); + const router = express.Router(); router.get("/wineinfo/search", wineinfo.wineSearch); -router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines); -router.post("/request/new-wine", request.requestNewWine); -router.delete("/request/:id", request.deleteRequestedWineById); +router.get("/request/all", setAdminHeaderIfAuthenticated, requestController.getAllRequests); +router.post("/request/new-wine", requestController.addRequest); +router.delete("/request/:id", requestController.deleteRequest); router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); router.get("/wineinfo/:ean", wineinfo.byEAN); From ca6f6cb2baab9ced51267c779db711b59fb0fe90 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 11 Jan 2021 20:54:48 +0100 Subject: [PATCH 002/115] Request new wine response includes success bool. Displays alert instead of modal if not true. --- frontend/components/RequestWine.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/components/RequestWine.vue b/frontend/components/RequestWine.vue index 432a743..7b1fb61 100644 --- a/frontend/components/RequestWine.vue +++ b/frontend/components/RequestWine.vue @@ -83,7 +83,13 @@ export default { }, request(wine){ requestNewWine(wine) - .then(() => this.showModal = true) + .then(resp => { + if(resp.success) { + this.showModal = true + } else { + alert("Obs, her oppsto det en feil! Feilen er logget."); + } + }) }, emitFromModalButton(action){ if(action == "stay"){ From c03f5aa0cfc8ffabdfd80b867e9f38a16c5f0c4a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 11 Jan 2021 20:55:24 +0100 Subject: [PATCH 003/115] Now requestAll returns object w/ wine within. Also some housekeeping. --- frontend/api.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/api.js b/frontend/api.js index ca17f6a..d3526aa 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -26,7 +26,8 @@ const allRequestedWines = () => {; return fetch("/api/request/all") .then(resp => { const isAdmin = resp.headers.get("vinlottis-admin") == "true"; - return Promise.all([resp.json(), isAdmin]); + const getWinesFromBody = (resp) => resp.json().then(body => body.wines); + return Promise.all([getWinesFromBody(resp), isAdmin]); }); }; @@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => { headers: { "Content-Type": "application/json" }, - method: "DELETE", - body: JSON.stringify(wineToBeDeleted) + method: "DELETE" }; return fetch("api/request/" + wineToBeDeleted.id, options) @@ -148,14 +148,12 @@ const attendees = () => { const requestNewWine = (wine) => { const options = { - body: JSON.stringify({ - wine: wine - }), + method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - method: "post" + body: JSON.stringify({ wine }) } return fetch("/api/request/new-wine", options) From 89389ddc596831e0ef64a4ea404052f086fc57e4 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Fri, 15 Jan 2021 19:16:15 +0100 Subject: [PATCH 004/115] Renamed lottery -> history. --- api/{lottery.js => history.js} | 121 +++++++++++++++++---------------- 1 file changed, 63 insertions(+), 58 deletions(-) rename api/{lottery.js => history.js} (50%) diff --git a/api/lottery.js b/api/history.js similarity index 50% rename from api/lottery.js rename to api/history.js index 6783a8c..65f189e 100644 --- a/api/lottery.js +++ b/api/history.js @@ -1,106 +1,109 @@ -const path = require('path'); +const path = require("path"); -const Highscore = require(path.join(__dirname, '/schemas/Highscore')); -const Wine = require(path.join(__dirname, '/schemas/Wine')); +const Highscore = require(path.join(__dirname, "/schemas/Highscore")); +const Wine = require(path.join(__dirname, "/schemas/Wine")); // Utils const epochToDateString = date => new Date(parseInt(date)).toDateString(); -const sortNewestFirst = (lotteries) => { - return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1) -} +const sortNewestFirst = lotteries => { + return lotteries.sort((a, b) => (parseInt(a.date) < parseInt(b.date) ? 1 : -1)); +}; -const groupHighscoreByDate = async (highscore=undefined) => { - if (highscore == undefined) - highscore = await Highscore.find(); +const groupHighscoreByDate = async (highscore = undefined) => { + if (highscore == undefined) highscore = await Highscore.find(); const highscoreByDate = []; highscore.forEach(person => { person.wins.map(win => { - const epochDate = new Date(win.date).setHours(0,0,0,0); + const epochDate = new Date(win.date).setHours(0, 0, 0, 0); const winnerObject = { name: person.name, color: win.color, wine: win.wine, date: epochDate - } + }; - const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate) - if (existingDateIndex > -1) - highscoreByDate[existingDateIndex].winners.push(winnerObject); + const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate); + if (existingDateIndex > -1) highscoreByDate[existingDateIndex].winners.push(winnerObject); else highscoreByDate.push({ date: epochDate, winners: [winnerObject] - }) - }) - }) + }); + }); + }); return sortNewestFirst(highscoreByDate); -} +}; const resolveWineReferences = (highscoreObject, key) => { - const listWithWines = highscoreObject[key] + const listWithWines = highscoreObject[key]; - return Promise.all(listWithWines.map(element => - Wine.findById(element.wine) - .then(wine => { - element.wine = wine - return element - })) + return Promise.all( + listWithWines.map(element => + Wine.findById(element.wine).then(wine => { + element.wine = wine; + return element; + }) ) - .then(resolvedListWithWines => { - highscoreObject[key] = resolvedListWithWines; - return highscoreObject - }) -} + ).then(resolvedListWithWines => { + highscoreObject[key] = resolvedListWithWines; + return highscoreObject; + }); +}; // end utils // Routes const all = (req, res) => { return Highscore.find() .then(highscore => groupHighscoreByDate(highscore)) - .then(lotteries => res.send({ - message: "Lotteries by date!", - lotteries - })) -} + .then(lotteries => + res.send({ + message: "Lotteries by date!", + lotteries + }) + ); +}; const latest = (req, res) => { return groupHighscoreByDate() .then(lotteries => lotteries.shift()) // first element in list .then(latestLottery => resolveWineReferences(latestLottery, "winners")) - .then(lottery => res.send({ + .then(lottery => + res.send({ message: "Latest lottery!", winners: lottery.winners }) - ) -} + ); +}; const byEpochDate = (req, res) => { let { date } = req.params; - date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime() + date = new Date(new Date(parseInt(date)).setHours(0, 0, 0, 0)).getTime(); const dateString = epochToDateString(date); return groupHighscoreByDate() .then(lotteries => { - const lottery = lotteries.filter(lottery => lottery.date == date) + const lottery = lotteries.filter(lottery => lottery.date == date); if (lottery.length > 0) { - return lottery[0] + return lottery[0]; } else { return res.status(404).send({ - message: `No lottery found for date: ${ dateString }` - }) + message: `No lottery found for date: ${dateString}` + }); } }) .then(lottery => resolveWineReferences(lottery, "winners")) - .then(lottery => res.send({ - message: `Lottery for date: ${ dateString}`, - date, - winners: lottery.winners - })) -} + .then(lottery => + res.send({ + message: `Lottery for date: ${dateString}`, + date, + winners: lottery.winners + }) + ); +}; const byName = (req, res) => { const { name } = req.params; @@ -109,20 +112,22 @@ const byName = (req, res) => { return Highscore.find({ name }) .then(highscore => { if (highscore.length > 0) { - return highscore[0] + return highscore[0]; } else { return res.status(404).send({ - message: `Name: ${ name } not found in leaderboards.` - }) + message: `Name: ${name} not found in leaderboards.` + }); } }) .then(highscore => resolveWineReferences(highscore, "wins")) - .then(highscore => res.send({ - message: `Lottery winnings for name: ${ name }.`, - name: highscore.name, - highscore: sortNewestFirst(highscore.wins) - })) -} + .then(highscore => + res.send({ + message: `Lottery winnings for name: ${name}.`, + name: highscore.name, + highscore: sortNewestFirst(highscore.wins) + }) + ); +}; module.exports = { all, From a010641a8e94124c716228b7e521e47ada8a6092 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Fri, 15 Jan 2021 19:16:46 +0100 Subject: [PATCH 005/115] Renamed wineinfo -> vinmonopolet. --- api/vinmonopolet.js | 77 +++++++++++++++++++++++++++++++++++++++++++++ api/wineinfo.js | 72 ------------------------------------------ 2 files changed, 77 insertions(+), 72 deletions(-) create mode 100644 api/vinmonopolet.js delete mode 100644 api/wineinfo.js diff --git a/api/vinmonopolet.js b/api/vinmonopolet.js new file mode 100644 index 0000000..32dbacc --- /dev/null +++ b/api/vinmonopolet.js @@ -0,0 +1,77 @@ +const fetch = require("node-fetch"); +const path = require("path"); +const config = require(path.join(__dirname + "/../config/env/lottery.config")); + +const convertToOurWineObject = wine => { + if (wine.basic.ageLimit === "18") { + return { + name: wine.basic.productShortName, + vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId, + rating: wine.basic.alcoholContent, + occurences: 0, + id: wine.basic.productId, + image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`, + price: wine.prices[0].salesPrice.toString(), + country: wine.origins.origin.country + }; + } +}; + +const searchByQuery = async (query, page = 1) => { + const pageSize = 15; + let url = new URL( + `https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15` + ); + url.searchParams.set("maxResults", pageSize); + url.searchParams.set("start", pageSize * (page - 1)); + url.searchParams.set("productShortNameContains", query); + + const vinmonopoletResponse = await fetch(url, { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }) + .then(resp => resp.json()) + .catch(err => console.error(err)); + + if (vinmonopoletResponse.errors != null) { + return vinmonopoletResponse.errors.map(error => { + if (error.type == "UnknownProductError") { + return res.status(404).json({ + message: error.message + }); + } else { + return next(); + } + }); + } + const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean); + + return winesConverted; +}; + +const searchByEAN = ean => { + const url = `https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/${ean}`; + return fetch(url) + .then(resp => resp.json()) + .then(response => response.map(convertToOurWineObject)); +}; + +const searchById = id => { + const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`; + const options = { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }; + + return fetch(url, options) + .then(resp => resp.json()) + .then(response => response.map(convertToOurWineObject)); +}; + +module.exports = { + searchByQuery, + searchByEAN, + searchById +}; diff --git a/api/wineinfo.js b/api/wineinfo.js deleted file mode 100644 index e52039f..0000000 --- a/api/wineinfo.js +++ /dev/null @@ -1,72 +0,0 @@ -const fetch = require('node-fetch') -const path = require('path') -const config = require(path.join(__dirname + "/../config/env/lottery.config")); - -const convertToOurWineObject = wine => { - if(wine.basic.ageLimit === "18"){ - return { - name: wine.basic.productShortName, - vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId, - rating: wine.basic.alcoholContent, - occurences: 0, - id: wine.basic.productId, - image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`, - price: wine.prices[0].salesPrice.toString(), - country: wine.origins.origin.country - } - } -} - -const wineSearch = async (req, res) => { - const {query} = req.query - let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`) - url.searchParams.set('productShortNameContains', query) - - const vinmonopoletResponse = await fetch(url, { - headers: { - "Ocp-Apim-Subscription-Key": config.vinmonopoletToken - } - }) - .then(resp => resp.json()) - .catch(err => console.error(err)) - - - if (vinmonopoletResponse.errors != null) { - return vinmonopoletResponse.errors.map(error => { - if (error.type == "UnknownProductError") { - return res.status(404).json({ - message: error.message - }) - } else { - return next() - } - }) - } - const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean) - - return res.send(winesConverted); -} - -const byEAN = async (req, res) => { - const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean) - .then(resp => resp.json()) - - if (vinmonopoletResponse.errors != null) { - return vinmonopoletResponse.errors.map(error => { - if (error.type == "UnknownProductError") { - return res.status(404).json({ - message: error.message - }) - } else { - return next() - } - }) - } - - return res.send(vinmonopoletResponse); -}; - -module.exports = { - byEAN, - wineSearch -}; From e754f0a9097bbb694b00ca0e929c170fa46f38ca Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Fri, 15 Jan 2021 19:17:12 +0100 Subject: [PATCH 006/115] Wine ctrl for search wineinfo by query, ean or id. --- api/controllers/wineController.js | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 api/controllers/wineController.js diff --git a/api/controllers/wineController.js b/api/controllers/wineController.js new file mode 100644 index 0000000..99390e0 --- /dev/null +++ b/api/controllers/wineController.js @@ -0,0 +1,44 @@ +const path = require("path"); +const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet")); + +function search(req, res) { + const { query, page } = req.query; + console.log(query, page); + + return vinmonopoletRepository.searchByQuery(query, page).then(wines => + res.json({ + wines: wines, + count: wines.length, + page: page, + success: true + }) + ); +} + +function ean(req, res) { + const { ean } = req.params; + + return vinmonopoletRepository.searchByEAN(ean).then(wines => + res.json({ + wines: wines, + success: true + }) + ); +} + +function id(req, res) { + const { id } = req.params; + + return vinmonopoletRepository.searchById(id).then(wines => + res.json({ + wine: wines[0], + success: true + }) + ); +} + +module.exports = { + search, + ean, + id +}; From 54c6c0eb970630419eb40f12553a2e54b49e49cc Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sat, 16 Jan 2021 12:33:56 +0100 Subject: [PATCH 007/115] History actions now have a controller setup. --- api/controllers/historyController.js | 92 ++++++++++++++++++++++++++++ api/history.js | 72 ++++++++-------------- 2 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 api/controllers/historyController.js diff --git a/api/controllers/historyController.js b/api/controllers/historyController.js new file mode 100644 index 0000000..50d44b0 --- /dev/null +++ b/api/controllers/historyController.js @@ -0,0 +1,92 @@ +const path = require("path"); +const historyRepository = require(path.join(__dirname, "../history")); + +const all = (req, res) => { + return historyRepository + .all() + .then(lotteries => + res.send({ + lotteries: lotteries, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch history." + }); + }); +}; + +const latest = (req, res) => { + return historyRepository + .latest() + .then(lottery => + res.send({ + lottery: lottery, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch latest history." + }); + }); +}; + +const byDate = (req, res) => { + let { date } = req.params; + date = new Date(new Date(parseInt(date)).setHours(0, 0, 0, 0)).getTime(); + + return historyRepository + .byEpochDate(date) + .then(winners => + res.send({ + date: date, + winners: winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch history for date." + }); + }); +}; + +const byName = (req, res) => { + const { name } = req.params; + + return historyRepository + .byName(name) + .then(lotteries => + res.send({ + name: name, + lotteries: lotteries, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch history for name." + }); + }); +}; + +module.exports = { + all, + latest, + byDate, + byName +}; diff --git a/api/history.js b/api/history.js index 65f189e..0c3dc70 100644 --- a/api/history.js +++ b/api/history.js @@ -3,6 +3,22 @@ const path = require("path"); const Highscore = require(path.join(__dirname, "/schemas/Highscore")); const Wine = require(path.join(__dirname, "/schemas/Wine")); +class HistoryByDateNotFound extends Error { + constructor(message = "History for given date not found.") { + super(message); + this.name = "HistoryByDateNotFound"; + this.statusCode = 404; + } +} + +class HistoryForUserNotFound extends Error { + constructor(message = "History for given user not found.") { + super(message); + this.name = "HistoryForUserNotFound"; + this.statusCode = 404; + } +} + // Utils const epochToDateString = date => new Date(parseInt(date)).toDateString(); @@ -56,77 +72,41 @@ const resolveWineReferences = (highscoreObject, key) => { // end utils // Routes -const all = (req, res) => { - return Highscore.find() - .then(highscore => groupHighscoreByDate(highscore)) - .then(lotteries => - res.send({ - message: "Lotteries by date!", - lotteries - }) - ); +const all = () => { + return Highscore.find().then(highscore => groupHighscoreByDate(highscore)); }; -const latest = (req, res) => { +const latest = () => { return groupHighscoreByDate() .then(lotteries => lotteries.shift()) // first element in list - .then(latestLottery => resolveWineReferences(latestLottery, "winners")) - .then(lottery => - res.send({ - message: "Latest lottery!", - winners: lottery.winners - }) - ); + .then(latestLottery => resolveWineReferences(latestLottery, "winners")); }; -const byEpochDate = (req, res) => { - let { date } = req.params; - date = new Date(new Date(parseInt(date)).setHours(0, 0, 0, 0)).getTime(); - const dateString = epochToDateString(date); - +const byEpochDate = date => { return groupHighscoreByDate() .then(lotteries => { const lottery = lotteries.filter(lottery => lottery.date == date); if (lottery.length > 0) { return lottery[0]; } else { - return res.status(404).send({ - message: `No lottery found for date: ${dateString}` - }); + throw new HistoryByDateNotFound(); } }) .then(lottery => resolveWineReferences(lottery, "winners")) - .then(lottery => - res.send({ - message: `Lottery for date: ${dateString}`, - date, - winners: lottery.winners - }) - ); + .then(lottery => lottery.winners); }; -const byName = (req, res) => { - const { name } = req.params; - const regexName = new RegExp(name, "i"); // lowercase regex of the name - +const byName = name => { return Highscore.find({ name }) .then(highscore => { if (highscore.length > 0) { return highscore[0]; } else { - return res.status(404).send({ - message: `Name: ${name} not found in leaderboards.` - }); + throw new HistoryForUserNotFound(); } }) .then(highscore => resolveWineReferences(highscore, "wins")) - .then(highscore => - res.send({ - message: `Lottery winnings for name: ${name}.`, - name: highscore.name, - highscore: sortNewestFirst(highscore.wins) - }) - ); + .then(highscore => sortNewestFirst(highscore.wins)); }; module.exports = { From 5e06a3fc280390c49e61d978e00b935313275016 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sat, 16 Jan 2021 14:23:02 +0100 Subject: [PATCH 008/115] ChatHistory behaved like controller already, renamed. --- .../chatController.js} | 20 ++-- api/controllers/userController.js | 49 +++++++++ api/user.js | 103 +++++++++++++----- 3 files changed, 135 insertions(+), 37 deletions(-) rename api/{chatHistory.js => controllers/chatController.js} (68%) create mode 100644 api/controllers/userController.js diff --git a/api/chatHistory.js b/api/controllers/chatController.js similarity index 68% rename from api/chatHistory.js rename to api/controllers/chatController.js index 9578176..fb3d8b6 100644 --- a/api/chatHistory.js +++ b/api/controllers/chatController.js @@ -8,19 +8,23 @@ const getAllHistory = (req, res) => { return history(page, limit) .then(messages => res.json(messages)) - .catch(error => res.status(500).json({ - message: error.message, - success: false - })); + .catch(error => + res.status(500).json({ + message: error.message, + success: false + }) + ); }; const deleteHistory = (req, res) => { return clearHistory() .then(message => res.json(message)) - .catch(error => res.status(500).json({ - message: error.message, - success: false - })); + .catch(error => + res.status(500).json({ + message: error.message, + success: false + }) + ); }; module.exports = { diff --git a/api/controllers/userController.js b/api/controllers/userController.js new file mode 100644 index 0000000..5e6ed96 --- /dev/null +++ b/api/controllers/userController.js @@ -0,0 +1,49 @@ +const path = require("path"); +const userRepository = require(path.join(__dirname, "../user")); + +function register(req, res, next) { + const { username, password } = req.body; + + return userRepository + .register(username, password) + .then(user => userRepository.login(req, user)) + .then(_ => + res.send({ + messsage: `Bruker registrert. Velkommen ${username}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unable to sign in with given username and passowrd", + success: false + }); + }); +} + +const login = (req, res, next) => { + return userRepository + .authenticate(req) + .then(user => userRepository.login(req, user)) + .then(user => { + res.send({ + message: `Velkommen ${user.username}`, + success: true + }); + }) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unable to sign in with given username and passowrd", + success: false + }); + }); +}; + +module.exports = { + register, + login +}; diff --git a/api/user.js b/api/user.js index 7a2ce9e..654cb43 100644 --- a/api/user.js +++ b/api/user.js @@ -1,42 +1,86 @@ const passport = require("passport"); const path = require("path"); const User = require(path.join(__dirname, "/schemas/User")); -const router = require("express").Router(); -const register = (req, res, next) => { - User.register( - new User({ username: req.body.username }), - req.body.password, - function(err) { - if (err) { - if (err.name == "UserExistsError") - res.status(409).send({ success: false, message: err.message }) - else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") - res.status(400).send({ success: false, message: err.message }) - return next(err); - } +class UserExistsError extends Error { + constructor(message = "Username already exists.") { + super(message); + this.name = "UserExists"; + this.statusCode = 409; + } +} - return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true }) - } - ); +class MissingUsernameError extends Error { + constructor(message = "No username given.") { + super(message); + this.name = "MissingUsernameError"; + this.statusCode = 400; + } +} + +class MissingPasswordError extends Error { + constructor(message = "No password given.") { + super(message); + this.name = "MissingPasswordError"; + this.statusCode = 400; + } +} + +class IncorrectUserCredentialsError extends Error { + constructor(message = "Incorrect username or password") { + super(message); + this.name = "IncorrectUserCredentialsError"; + this.statusCode = 404; + } +} + +function userAuthenticationErrorHandler(err) { + if (err.name == "UserExistsError") { + throw new UserExistsError(err.message); + } else if (err.name == "MissingUsernameError") { + throw new MissingUsernameError(err.message); + } else if (err.name == "MissingPasswordError") { + throw new MissingPasswordError(err.message); + } + + throw err; +} + +const register = (username, password) => { + return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler); }; -const login = (req, res, next) => { - passport.authenticate("local", function(err, user, info) { - if (err) { - if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") - return res.status(400).send({ message: err.message, success: false }) - return next(err); - } +const authenticate = req => { + return new Promise((resolve, reject) => { + const { username, password } = req.body; - if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false }) + if (username == undefined) throw new MissingUsernameError(); + if (password == undefined) throw new MissingPasswordError(); - req.logIn(user, (err) => { - if (err) { return next(err) } + passport.authenticate("local", function(err, user, info) { + if (err) { + reject(err); + } - return res.status(200).send({ message: "Velkommen " + user.username, success: true }) - }) - })(req, res, next); + if (!user) { + reject(new IncorrectUserCredentialsError()); + } + + resolve(user); + })(req); + }); +}; + +const login = (req, user) => { + return new Promise((resolve, reject) => { + req.logIn(user, err => { + if (err) { + reject(err); + } + + resolve(user); + }); + }); }; const logout = (req, res) => { @@ -46,6 +90,7 @@ const logout = (req, res) => { module.exports = { register, + authenticate, login, logout }; From 53780878af4a34e45dd89752f2bce667fbfb1984 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 17 Jan 2021 16:55:57 +0100 Subject: [PATCH 009/115] Renamed to winners. Winners gets controller setup. Rewrote everything that happened in history to better take advantage of monogdb instead of doing everything in js. Our endpoints become: - /winners - getAll w/ includeWines and sort query params. - /winners/latest - latest winners grouped w/ includeWines query params. - /winners/by-date - all winners grouped by date w/ includeWines and sort. - /winners/by-date/:date - get winners per epoch or string date. - /winners/by-name/:name - get winner by name parameter w/ sort for wins direction. --- api/controllers/historyController.js | 92 ------------- api/controllers/winnerController.js | 165 +++++++++++++++++++++++ api/history.js | 117 ---------------- api/winner.js | 191 +++++++++++++++++++++++++++ 4 files changed, 356 insertions(+), 209 deletions(-) delete mode 100644 api/controllers/historyController.js create mode 100644 api/controllers/winnerController.js delete mode 100644 api/history.js create mode 100644 api/winner.js diff --git a/api/controllers/historyController.js b/api/controllers/historyController.js deleted file mode 100644 index 50d44b0..0000000 --- a/api/controllers/historyController.js +++ /dev/null @@ -1,92 +0,0 @@ -const path = require("path"); -const historyRepository = require(path.join(__dirname, "../history")); - -const all = (req, res) => { - return historyRepository - .all() - .then(lotteries => - res.send({ - lotteries: lotteries, - success: true - }) - ) - .catch(error => { - const { statusCode, message } = error; - - return res.status(statusCode || 500).send({ - success: false, - message: message || "Unable to fetch history." - }); - }); -}; - -const latest = (req, res) => { - return historyRepository - .latest() - .then(lottery => - res.send({ - lottery: lottery, - success: true - }) - ) - .catch(error => { - const { statusCode, message } = error; - - return res.status(statusCode || 500).send({ - success: false, - message: message || "Unable to fetch latest history." - }); - }); -}; - -const byDate = (req, res) => { - let { date } = req.params; - date = new Date(new Date(parseInt(date)).setHours(0, 0, 0, 0)).getTime(); - - return historyRepository - .byEpochDate(date) - .then(winners => - res.send({ - date: date, - winners: winners, - success: true - }) - ) - .catch(error => { - const { statusCode, message } = error; - - return res.status(statusCode || 500).send({ - success: false, - message: message || "Unable to fetch history for date." - }); - }); -}; - -const byName = (req, res) => { - const { name } = req.params; - - return historyRepository - .byName(name) - .then(lotteries => - res.send({ - name: name, - lotteries: lotteries, - success: true - }) - ) - .catch(error => { - const { statusCode, message } = error; - - return res.status(statusCode || 500).send({ - success: false, - message: message || "Unable to fetch history for name." - }); - }); -}; - -module.exports = { - all, - latest, - byDate, - byName -}; diff --git a/api/controllers/winnerController.js b/api/controllers/winnerController.js new file mode 100644 index 0000000..aab1164 --- /dev/null +++ b/api/controllers/winnerController.js @@ -0,0 +1,165 @@ +const path = require("path"); +const winnerRepository = require(path.join(__dirname, "../winner")); + +const sortOptions = ["desc", "asc"]; +const includeWinesOptions = ["true", "false"]; + +const all = (req, res) => { + let { sort, includeWines } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return winnerRepository + .all(includeWines == "true") + .then(winners => + res.send({ + winners: sort !== "asc" ? winners : winners.reverse(), + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winners." + }); + }); +}; + +const byDate = (req, res) => { + let { date } = req.params; + + const regexDate = new RegExp("^\\d{4}-\\d{2}-\\d{2}$"); + if (!isNaN(date)) { + date = new Date(new Date(parseInt(date * 1000)).setHours(0, 0, 0, 0)); + } else if (regexDate.test(date)) { + date = new Date(date); + } else if (date !== undefined) { + return res.status(400).send({ + message: "Invalid date parameter, allowed epoch seconds or YYYY-MM-DD.", + success: false + }); + } + + return winnerRepository + .byDate(date) + .then(winners => + res.send({ + date: date, + winners: winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by date." + }); + }); +}; + +const groupedByDate = (req, res) => { + let { sort, includeWines } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return winnerRepository + .groupedByDate(includeWines, sort) + .then(lotteries => + res.send({ + lotteries: lotteries, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by date." + }); + }); +}; + +const latest = (req, res) => { + return winnerRepository + .latest() + .then(winners => + res.send({ + ...winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by date." + }); + }); +}; + +const byName = (req, res) => { + const { name } = req.params; + const { sort } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + return winnerRepository + .byName(name, sort) + .then(winner => + res.send({ + winner: winner, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by name." + }); + }); +}; + +module.exports = { + all, + byDate, + groupedByDate, + latest, + byName +}; diff --git a/api/history.js b/api/history.js deleted file mode 100644 index 0c3dc70..0000000 --- a/api/history.js +++ /dev/null @@ -1,117 +0,0 @@ -const path = require("path"); - -const Highscore = require(path.join(__dirname, "/schemas/Highscore")); -const Wine = require(path.join(__dirname, "/schemas/Wine")); - -class HistoryByDateNotFound extends Error { - constructor(message = "History for given date not found.") { - super(message); - this.name = "HistoryByDateNotFound"; - this.statusCode = 404; - } -} - -class HistoryForUserNotFound extends Error { - constructor(message = "History for given user not found.") { - super(message); - this.name = "HistoryForUserNotFound"; - this.statusCode = 404; - } -} - -// Utils -const epochToDateString = date => new Date(parseInt(date)).toDateString(); - -const sortNewestFirst = lotteries => { - return lotteries.sort((a, b) => (parseInt(a.date) < parseInt(b.date) ? 1 : -1)); -}; - -const groupHighscoreByDate = async (highscore = undefined) => { - if (highscore == undefined) highscore = await Highscore.find(); - - const highscoreByDate = []; - - highscore.forEach(person => { - person.wins.map(win => { - const epochDate = new Date(win.date).setHours(0, 0, 0, 0); - const winnerObject = { - name: person.name, - color: win.color, - wine: win.wine, - date: epochDate - }; - - const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate); - if (existingDateIndex > -1) highscoreByDate[existingDateIndex].winners.push(winnerObject); - else - highscoreByDate.push({ - date: epochDate, - winners: [winnerObject] - }); - }); - }); - - return sortNewestFirst(highscoreByDate); -}; - -const resolveWineReferences = (highscoreObject, key) => { - const listWithWines = highscoreObject[key]; - - return Promise.all( - listWithWines.map(element => - Wine.findById(element.wine).then(wine => { - element.wine = wine; - return element; - }) - ) - ).then(resolvedListWithWines => { - highscoreObject[key] = resolvedListWithWines; - return highscoreObject; - }); -}; -// end utils - -// Routes -const all = () => { - return Highscore.find().then(highscore => groupHighscoreByDate(highscore)); -}; - -const latest = () => { - return groupHighscoreByDate() - .then(lotteries => lotteries.shift()) // first element in list - .then(latestLottery => resolveWineReferences(latestLottery, "winners")); -}; - -const byEpochDate = date => { - return groupHighscoreByDate() - .then(lotteries => { - const lottery = lotteries.filter(lottery => lottery.date == date); - if (lottery.length > 0) { - return lottery[0]; - } else { - throw new HistoryByDateNotFound(); - } - }) - .then(lottery => resolveWineReferences(lottery, "winners")) - .then(lottery => lottery.winners); -}; - -const byName = name => { - return Highscore.find({ name }) - .then(highscore => { - if (highscore.length > 0) { - return highscore[0]; - } else { - throw new HistoryForUserNotFound(); - } - }) - .then(highscore => resolveWineReferences(highscore, "wins")) - .then(highscore => sortNewestFirst(highscore.wins)); -}; - -module.exports = { - all, - latest, - byEpochDate, - byName -}; diff --git a/api/winner.js b/api/winner.js new file mode 100644 index 0000000..873647c --- /dev/null +++ b/api/winner.js @@ -0,0 +1,191 @@ +const path = require("path"); + +const Winner = require(path.join(__dirname, "/schemas/Highscore")); +const Wine = require(path.join(__dirname, "/schemas/Wine")); + +class HistoryByDateNotFound extends Error { + constructor(message = "History for given date not found.") { + super(message); + this.name = "HistoryByDateNotFound"; + this.statusCode = 404; + } +} + +class HistoryForUserNotFound extends Error { + constructor(message = "History for given user not found.") { + super(message); + this.name = "HistoryForUserNotFound"; + this.statusCode = 404; + } +} + +const all = (includeWines = false) => { + if (includeWines === false) { + return Winner.find().sort("-wins.date"); + } else { + return Winner.find() + .sort("-wins.date") + .populate("wins.wine"); + } +}; + +const byDate = date => { + const startQueryDate = new Date(date.setHours(0, 0, 0, 0)); + const endQueryDate = new Date(date.setHours(24, 59, 59, 99)); + const query = [ + { + $match: { + "wins.date": { + $gte: startQueryDate, + $lte: endQueryDate + } + } + }, + { $unwind: "$wins" }, + { + $match: { + "wins.date": { + $gte: startQueryDate, + $lte: endQueryDate + } + } + }, + { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }, + { $unwind: "$wins.wine" }, + { + $project: { + name: "$name", + date: "$wins.date", + color: "$wins.color", + wine: "$wins.wine" + } + } + ]; + + return Winner.aggregate(query).then(winners => { + if (winners.length == 0) { + throw new HistoryByDateNotFound(); + } + return winners; + }); +}; + +const byName = (name, sort = "desc") => { + const populateOptions = { sort: "date" }; + + return Winner.findOne({ name }, ["name", "wins"]) + .sort("-wins.date") + .populate("wins.wine") + .then(winner => { + if (winner) { + winner.wins = sort !== "asc" ? winner.wins.reverse() : winner.wins; + return winner; + } else { + throw new HistoryForUserNotFound(); + } + }); +}; + +const latest = () => { + const query = [ + { + $unwind: "$wins" + }, + { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }, + { + $group: { + _id: "$wins.date", + winners: { + $push: { + _id: "$_id", + name: "$name", + color: "$wins.color", + wine: "$wins.wine" + } + } + } + }, + { + $project: { + date: "$_id", + winners: "$winners" + } + }, + { + $sort: { + _id: -1 + } + }, + { + $limit: 1 + } + ]; + + return Winner.aggregate(query).then(winners => winners[0]); +}; + +const groupedByDate = (includeWines = false, sort = "desc") => { + const query = [ + { + $unwind: "$wins" + }, + { + $group: { + _id: "$wins.date", + winners: { + $push: { + _id: "$_id", + name: "$name", + color: "$wins.color", + wine: "$wins.wine" + } + } + } + }, + { + $project: { + date: "$_id", + winners: "$winners" + } + }, + { + $sort: { + _id: -1 + } + } + ]; + + if (includeWines) { + query.splice(1, 0, { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }); + } + + return Winner.aggregate(query).then(lotteries => (sort != "asc" ? lotteries : lotteries.reverse())); +}; + +module.exports = { + all, + byDate, + latest, + groupedByDate +}; From e9ece6963e39568940bf5aeaa682912e2099034c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 17 Jan 2021 17:26:37 +0100 Subject: [PATCH 010/115] /by-color endpoint for sort all winners by color. Can also include query parameter includeWines for resolved wine references. --- api/controllers/winnerController.js | 37 ++++++++++++++++--- api/winner.js | 55 +++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/api/controllers/winnerController.js b/api/controllers/winnerController.js index aab1164..b2d74d8 100644 --- a/api/controllers/winnerController.js +++ b/api/controllers/winnerController.js @@ -5,7 +5,7 @@ const sortOptions = ["desc", "asc"]; const includeWinesOptions = ["true", "false"]; const all = (req, res) => { - let { sort, includeWines } = req.query; + const { sort, includeWines } = req.query; if (sort !== undefined && !sortOptions.includes(sort)) { return res.status(400).send({ @@ -74,7 +74,7 @@ const byDate = (req, res) => { }; const groupedByDate = (req, res) => { - let { sort, includeWines } = req.query; + const { sort, includeWines } = req.query; if (sort !== undefined && !sortOptions.includes(sort)) { return res.status(400).send({ @@ -91,7 +91,7 @@ const groupedByDate = (req, res) => { } return winnerRepository - .groupedByDate(includeWines, sort) + .groupedByDate(includeWines == "true", sort) .then(lotteries => res.send({ lotteries: lotteries, @@ -156,10 +156,39 @@ const byName = (req, res) => { }); }; +const byColor = (req, res) => { + const { includeWines } = req.query; + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return winnerRepository + .byColor(includeWines == "true") + .then(colors => + res.send({ + colors: colors, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winners by color." + }); + }); +}; + module.exports = { all, byDate, groupedByDate, latest, - byName + byName, + byColor }; diff --git a/api/winner.js b/api/winner.js index 873647c..d525c94 100644 --- a/api/winner.js +++ b/api/winner.js @@ -78,8 +78,6 @@ const byDate = date => { }; const byName = (name, sort = "desc") => { - const populateOptions = { sort: "date" }; - return Winner.findOne({ name }, ["name", "wins"]) .sort("-wins.date") .populate("wins.wine") @@ -183,9 +181,60 @@ const groupedByDate = (includeWines = false, sort = "desc") => { return Winner.aggregate(query).then(lotteries => (sort != "asc" ? lotteries : lotteries.reverse())); }; +const byColor = (includeWines = false) => { + const query = [ + { + $unwind: "$wins" + }, + { + $group: { + _id: "$wins.color", + winners: { + $push: { + _id: "$_id", + name: "$name", + date: "$wins.date", + wine: "$wins.wine" + } + }, + count: { $sum: 1 } + } + }, + { + $project: { + color: "$_id", + count: "$count", + winners: "$winners" + } + }, + { + $sort: { + _id: -1 + } + } + ]; + + console.log("includeWines:", includeWines); + console.log("includeWines:", includeWines == true); + + if (includeWines) { + query.splice(1, 0, { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }); + } + + return Winner.aggregate(query); +}; + module.exports = { all, byDate, latest, - groupedByDate + groupedByDate, + byColor }; From e07e6ae09aa60e29c71d930071111d5fa2c331e0 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 10:05:00 +0100 Subject: [PATCH 011/115] Winner controller, winners by date, color or name. Winner controller replaces a lot of what happens in retrieve did for aggregating when and what had been won. Now this is more clearly defined in winner.js. Also leverage mongo's query more than sorting and aggregating data like previous implementation where a lot happened in js. --- api/controllers/winnerController.js | 39 +++++++++++++++--- api/winner.js | 61 +++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/api/controllers/winnerController.js b/api/controllers/winnerController.js index b2d74d8..a6c6af2 100644 --- a/api/controllers/winnerController.js +++ b/api/controllers/winnerController.js @@ -73,7 +73,7 @@ const byDate = (req, res) => { }); }; -const groupedByDate = (req, res) => { +const groupByDate = (req, res) => { const { sort, includeWines } = req.query; if (sort !== undefined && !sortOptions.includes(sort)) { @@ -156,7 +156,7 @@ const byName = (req, res) => { }); }; -const byColor = (req, res) => { +const groupByColor = (req, res) => { const { includeWines } = req.query; if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { @@ -167,7 +167,7 @@ const byColor = (req, res) => { } return winnerRepository - .byColor(includeWines == "true") + .groupByColor(includeWines == "true") .then(colors => res.send({ colors: colors, @@ -184,11 +184,40 @@ const byColor = (req, res) => { }); }; +const orderByWins = (req, res) => { + const { includeWines } = req.query; + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return winnerRepository + .orderByWins(includeWines == "true") + .then(winners => + res.send({ + winners: winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winners by color." + }); + }); +}; + module.exports = { all, byDate, - groupedByDate, + groupByDate, latest, byName, - byColor + groupByColor, + orderByWins }; diff --git a/api/winner.js b/api/winner.js index d525c94..a528031 100644 --- a/api/winner.js +++ b/api/winner.js @@ -136,7 +136,7 @@ const latest = () => { return Winner.aggregate(query).then(winners => winners[0]); }; -const groupedByDate = (includeWines = false, sort = "desc") => { +const groupByDate = (includeWines = false, sort = "desc") => { const query = [ { $unwind: "$wins" @@ -181,7 +181,7 @@ const groupedByDate = (includeWines = false, sort = "desc") => { return Winner.aggregate(query).then(lotteries => (sort != "asc" ? lotteries : lotteries.reverse())); }; -const byColor = (includeWines = false) => { +const groupByColor = (includeWines = false) => { const query = [ { $unwind: "$wins" @@ -214,9 +214,6 @@ const byColor = (includeWines = false) => { } ]; - console.log("includeWines:", includeWines); - console.log("includeWines:", includeWines == true); - if (includeWines) { query.splice(1, 0, { $lookup: { @@ -231,10 +228,60 @@ const byColor = (includeWines = false) => { return Winner.aggregate(query); }; +const orderByWins = (includeWines = false) => { + let query = [ + { + $project: { + name: "$name", + wins: "$wins", + totalWins: { $size: "$wins" } + } + }, + { + $sort: { + totalWins: -1, + "wins.date": -1 + } + } + ]; + + if (includeWines) { + const includeWinesSubQuery = [ + { + $unwind: "$wins" + }, + { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }, + { + $unwind: "$wins._id" + }, + { + $group: { + _id: "$_id", + name: { $first: "$name" }, + totalWins: { $first: "$totalWins" }, + wins: { $push: "$wins" } + } + } + ]; + + query = includeWinesSubQuery.concat(query); + } + + return Winner.aggregate(query); +}; + module.exports = { all, byDate, latest, - groupedByDate, - byColor + groupByDate, + groupByColor, + orderByWins }; From edc4d266471418938eeb406c653579ac8108a015 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 10:08:39 +0100 Subject: [PATCH 012/115] User logout can happen in controller. --- api/controllers/userController.js | 8 +++++++- api/user.js | 8 +------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/controllers/userController.js b/api/controllers/userController.js index 5e6ed96..b12b2f5 100644 --- a/api/controllers/userController.js +++ b/api/controllers/userController.js @@ -43,7 +43,13 @@ const login = (req, res, next) => { }); }; +const logout = (req, res) => { + req.logout(); + res.redirect("/"); +}; + module.exports = { register, - login + login, + logout }; diff --git a/api/user.js b/api/user.js index 654cb43..34c7af4 100644 --- a/api/user.js +++ b/api/user.js @@ -83,14 +83,8 @@ const login = (req, user) => { }); }; -const logout = (req, res) => { - req.logout(); - res.redirect("/"); -}; - module.exports = { register, authenticate, - login, - logout + login }; From 18d8c2c7ca03056a0138e75f18724c31d63e1cb3 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 10:14:00 +0100 Subject: [PATCH 013/115] Lottery, get/delete attendees. Changed so get attendees no longer is done in two endpoints, now we use req.isAuthenticated() to check if we want to return full or minimal json data about each attendee. --- api/controllers/lotteryController.js | 37 ++++++++++++++++++++++++++++ api/lottery.js | 35 ++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 api/controllers/lotteryController.js create mode 100644 api/lottery.js diff --git a/api/controllers/lotteryController.js b/api/controllers/lotteryController.js new file mode 100644 index 0000000..f64dbec --- /dev/null +++ b/api/controllers/lotteryController.js @@ -0,0 +1,37 @@ +const path = require("path"); +const lotteryRepository = require(path.join(__dirname, "../lottery")); + +const allAttendees = (req, res) => { + const isAdmin = req.isAuthenticated(); + + return lotteryRepository + .allAttendees(isAdmin === "true") + .then(attendees => + res.send({ + attendees: attendees, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch lottery attendees." + }); + }); +}; + +const deleteAttendees = (req, res) => { + return lotteryRepository.deleteAttendees().then(success => + res.send({ + message: "Removed all attendees", + success: success + }) + ); +}; + +module.exports = { + allAttendees, + deleteAttendees +}; diff --git a/api/lottery.js b/api/lottery.js new file mode 100644 index 0000000..77547f3 --- /dev/null +++ b/api/lottery.js @@ -0,0 +1,35 @@ +const path = require("path"); +const Attendee = require(path.join(__dirname, "/schemas/Attendee")); + +const redactAttendeeInfoMapper = attendee => { + return { + name: attendee.name, + raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, + red: attendee.red, + blue: attendee.blue, + green: attendee.green, + yellow: attendee.yellow + }; +}; + +const allAttendees = isAdmin => { + if (!isAdmin) { + return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); + } else { + return Attendee.find(); + } +}; + +const deleteAttendees = () => { + const io = req.app.get("socketio"); + + return Attendee.deleteMany().then(_ => { + io.emit("refresh_data", {}); + return true; + }); +}; + +module.exports = { + allAttendees, + deleteAttendees +}; From 7aa5f7e9ced99c40d6da5f85754574bf224b669d Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 11:02:04 +0100 Subject: [PATCH 014/115] Add, update and delete attendees to lottery. --- api/controllers/lotteryController.js | 111 +++++++++++++++++++++++++-- api/lottery.js | 63 +++++++++++++-- 2 files changed, 162 insertions(+), 12 deletions(-) diff --git a/api/controllers/lotteryController.js b/api/controllers/lotteryController.js index f64dbec..dc4a380 100644 --- a/api/controllers/lotteryController.js +++ b/api/controllers/lotteryController.js @@ -5,7 +5,7 @@ const allAttendees = (req, res) => { const isAdmin = req.isAuthenticated(); return lotteryRepository - .allAttendees(isAdmin === "true") + .allAttendees(isAdmin) .then(attendees => res.send({ attendees: attendees, @@ -22,16 +22,113 @@ const allAttendees = (req, res) => { }); }; -const deleteAttendees = (req, res) => { - return lotteryRepository.deleteAttendees().then(success => - res.send({ - message: "Removed all attendees", - success: success +const addAttendee = (req, res) => { + const { attendee } = req.body; + + const requiredColors = [attendee["red"], attendee["blue"], attendee["green"], attendee["yellow"]]; + const correctColorsTypes = requiredColors.filter(color => typeof color === "number"); + if (requiredColors.length !== correctColorsTypes.length) { + return res.status(400).send({ + message: "Incorrect or missing color, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.", + success: false + }); + } + + if (typeof attendee["name"] !== "string" && typeof attendee["phoneNumber"] !== "number") { + return res.status(400).send({ + message: "Incorrect or missing attendee keys 'name' or 'phoneNumber'.", + success: false + }); + } + + return lotteryRepository + .addAttendee(attendee) + .then(savedAttendee => { + var io = req.app.get("socketio"); + io.emit("new_attendee", {}); + return true; }) - ); + .then(success => + res.send({ + message: `Successfully added attendee ${attendee.name} to lottery.`, + success: success + }) + ); +}; + +const updateAttendeeById = (req, res) => { + const { id } = req.params; + const { attendee } = req.body; + + return lotteryRepository + .updateAttendeeById(id, attendee) + .then(updatedAttendee => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return updatedAttendee; + }) + .then(attendee => + res.send({ + attendee, + message: `Updated attendee: ${attendee.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing attendee by id.", + success: false + }); + }); +}; + +const deleteAttendeeById = (req, res) => { + const { id } = req.params; + + return lotteryRepository + .deleteAttendeeById(id) + .then(removedAttendee => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return removedAttendee; + }) + .then(attendee => + res.send({ + message: `Removed attendee: ${attendee.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing attendee by id.", + success: false + }); + }); +}; + +const deleteAttendees = (req, res) => { + return lotteryRepository + .deleteAttendees() + .then(removedAttendee => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + }) + .then(_ => + res.send({ + message: "Removed all attendees", + success: true + }) + ); }; module.exports = { allAttendees, + addAttendee, + updateAttendeeById, + deleteAttendeeById, deleteAttendees }; diff --git a/api/lottery.js b/api/lottery.js index 77547f3..ef78017 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -1,6 +1,16 @@ const path = require("path"); const Attendee = require(path.join(__dirname, "/schemas/Attendee")); +class UserNotFound extends Error { + constructor(message = "User not found.") { + super(message); + this.name = "UserNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + const redactAttendeeInfoMapper = attendee => { return { name: attendee.name, @@ -20,16 +30,59 @@ const allAttendees = isAdmin => { } }; -const deleteAttendees = () => { - const io = req.app.get("socketio"); +const addAttendee = attendee => { + const { name, red, blue, green, yellow, phoneNumber } = attendee; - return Attendee.deleteMany().then(_ => { - io.emit("refresh_data", {}); - return true; + let newAttendee = new Attendee({ + name, + red, + blue, + green, + yellow, + phoneNumber, + winner: false }); + + return newAttendee.save(); +}; + +const updateAttendeeById = (id, updateModel) => { + return Attendee.findOne({ _id: id }).then(attendee => { + if (attendee == null) { + throw new UserNotFound(); + } + + const updatedAttendee = { + name: updateModel.name || attendee.name, + green: updateModel.green || attendee.green, + red: updateModel.red || attendee.red, + blue: updateModel.blue || attendee.blue, + yellow: updateModel.yellow || attendee.yellow, + phoneNumber: updateModel.phoneNumber || attendee.phoneNumber + }; + + return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); + }); +}; + +const deleteAttendeeById = id => { + return Attendee.findOne({ _id: id }).then(attendee => { + if (attendee == null) { + throw new UserNotFound(); + } + + return Attendee.deleteOne({ _id: id }).then(_ => attendee); + }); +}; + +const deleteAttendees = () => { + return Attendee.deleteMany(); }; module.exports = { allAttendees, + addAttendee, + updateAttendeeById, + deleteAttendeeById, deleteAttendees }; From 4d822ccb6414375a203e0a18324f2d2fa23b8bd1 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 11:10:49 +0100 Subject: [PATCH 015/115] Isolated lottery attendee functs to it's own file. --- .../{lotteryController.js => lotteryAttendeeController.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/controllers/{lotteryController.js => lotteryAttendeeController.js} (100%) diff --git a/api/controllers/lotteryController.js b/api/controllers/lotteryAttendeeController.js similarity index 100% rename from api/controllers/lotteryController.js rename to api/controllers/lotteryAttendeeController.js From 6d5f0e824f15b50f2cced310730ed82b2c9646e8 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 12:12:52 +0100 Subject: [PATCH 016/115] Lottery wine functions & controller. Split all wine/prelottrey wines into separate controller. Now also have endpoints for deleting or updating single wine by id. --- api/controllers/lotteryWineController.js | 149 +++++++++++++++++++++++ api/lottery.js | 74 ++++++++++- 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 api/controllers/lotteryWineController.js diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js new file mode 100644 index 0000000..cfa9ccd --- /dev/null +++ b/api/controllers/lotteryWineController.js @@ -0,0 +1,149 @@ +const path = require("path"); +const lotteryRepository = require(path.join(__dirname, "../lottery")); + +const allWines = (req, res) => { + return lotteryRepository + .allWines() + .then(wines => + res.send({ + wines: wines, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch lottery wines." + }); + }); +}; + +const addWines = (req, res) => { + const { wines } = req.body; + + if (!(wines instanceof Array)) { + return res.status(400).send({ + message: "Wines must be array.", + success: false + }); + } + + const validateAllWines = wines => + wines.map(wine => { + const requiredAttributes = ["name", "vivinoLink", "rating", "image", "country", "id", "price"]; + + return Promise.all( + requiredAttributes.map(attr => { + if (typeof wine[attr] === "undefined") { + return Promise.reject({ + message: `Incorrect or missing attribute: ${attr}.`, + statusCode: 400, + success: false + }); + } + return Promise.resolve(); + }) + ).then(_ => Promise.resolve(wine)); + }); + + return Promise.all(validateAllWines(wines)) + .then(wines => lotteryRepository.addWines(wines)) + .then(savedWines => { + var io = req.app.get("socketio"); + io.emit("new_wine", {}); + return true; + }) + .then(success => + res.send({ + message: `Successfully added wines to lottery.`, + success: success + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured adding wines.", + success: false + }); + }); +}; + +const updateWineById = (req, res) => { + const { id } = req.params; + const { wine } = req.body; + + return lotteryRepository + .updateWineById(id, wine) + .then(updatedWine => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return updatedWine; + }) + .then(wine => + res.send({ + wine, + message: `Updated wine: ${wine.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing wine by id.", + success: false + }); + }); +}; + +const deleteWineById = (req, res) => { + const { id } = req.params; + + return lotteryRepository + .deleteWineById(id) + .then(removedWine => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return removedWine; + }) + .then(wine => + res.send({ + message: `Removed wine: ${wine.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing wine by id.", + success: false + }); + }); +}; + +const deleteWines = (req, res) => { + return lotteryRepository + .deleteWines() + .then(removedWine => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + }) + .then(_ => + res.send({ + message: "Removed all wines", + success: true + }) + ); +}; + +module.exports = { + allWines, + addWines, + updateWineById, + deleteWineById, + deleteWines +}; diff --git a/api/lottery.js b/api/lottery.js index ef78017..a748fc2 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -1,5 +1,6 @@ const path = require("path"); const Attendee = require(path.join(__dirname, "/schemas/Attendee")); +const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); class UserNotFound extends Error { constructor(message = "User not found.") { @@ -11,6 +12,16 @@ class UserNotFound extends Error { // TODO log missing user } +class WineNotFound extends Error { + constructor(message = "Wine not found.") { + super(message); + this.name = "WineNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + const redactAttendeeInfoMapper = attendee => { return { name: attendee.name, @@ -79,10 +90,71 @@ const deleteAttendees = () => { return Attendee.deleteMany(); }; +const allWines = () => { + return PreLotteryWine.find(); +}; + +const addWines = wines => { + const prelotteryWines = wines.map(wine => { + let newPrelotteryWine = new PreLotteryWine({ + name: wine.name, + vivinoLink: wine.vivinoLink, + rating: wine.rating, + image: wine.image, + price: wine.price, + country: wine.country, + id: wine.id + }); + + return newPrelotteryWine.save(); + }); + + return Promise.all(prelotteryWines); +}; + +const updateWineById = (id, updateModel) => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + const updatedWine = { + name: updateModel.name || wine.name, + vivinoLink: updateModel.vivinoLink || wine.vivinoLink, + rating: updateModel.rating || wine.rating, + image: updateModel.image || wine.image, + price: updateModel.price || wine.price, + country: updateModel.country || wine.country, + id: updateModel.id || wine.id + }; + + return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine); + }); +}; + +const deleteWineById = id => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine); + }); +}; + +const deleteWines = () => { + return PreLotteryWine.deleteMany(); +}; + module.exports = { allAttendees, addAttendee, updateAttendeeById, deleteAttendeeById, - deleteAttendees + deleteAttendees, + allWines, + addWines, + updateWineById, + deleteWineById, + deleteWines }; From fac50805bd302e3c6e859fc3a45c97c70bfc1364 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 13:59:34 +0100 Subject: [PATCH 017/115] Get stores from vinmonopolet. Update all endpoint names to distinguish between wine and store actions. Renamed wineController to vinmonopoletController. --- api/controllers/vinmonopoletController.js | 85 +++++++++++++++++++++++ api/controllers/wineController.js | 44 ------------ api/vinmonopolet.js | 52 +++++++++++--- 3 files changed, 129 insertions(+), 52 deletions(-) create mode 100644 api/controllers/vinmonopoletController.js delete mode 100644 api/controllers/wineController.js diff --git a/api/controllers/vinmonopoletController.js b/api/controllers/vinmonopoletController.js new file mode 100644 index 0000000..eac34b1 --- /dev/null +++ b/api/controllers/vinmonopoletController.js @@ -0,0 +1,85 @@ +const path = require("path"); +const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet")); + +function searchWines(req, res) { + const { name, page } = req.query; + + return vinmonopoletRepository.searchWinesByName(name, page).then(wines => + res.json({ + wines: wines, + count: wines.length, + page: page, + success: true + }) + ); +} + +function wineByEAN(req, res) { + const { ean } = req.params; + + return vinmonopoletRepository.searchByEAN(ean).then(wines => + res.json({ + wines: wines, + success: true + }) + ); +} + +function wineById(req, res) { + const { id } = req.params; + + return vinmonopoletRepository.searchById(id).then(wines => + res.json({ + wine: wines[0], + success: true + }) + ); +} + +function allStores(req, res) { + return vinmonopoletRepository + .allStores() + .then(stores => + res.send({ + stores, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetch all vinmonopolet stores.", + success: false + }); + }); +} + +function searchStores(req, res) { + const { name } = req.query; + + return vinmonopoletRepository + .searchStoresByName(name) + .then(stores => + res.send({ + stores, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetch all vinmonopolet stores.", + success: false + }); + }); +} + +module.exports = { + searchWines, + wineByEAN, + wineById, + allStores, + searchStores +}; diff --git a/api/controllers/wineController.js b/api/controllers/wineController.js deleted file mode 100644 index 99390e0..0000000 --- a/api/controllers/wineController.js +++ /dev/null @@ -1,44 +0,0 @@ -const path = require("path"); -const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet")); - -function search(req, res) { - const { query, page } = req.query; - console.log(query, page); - - return vinmonopoletRepository.searchByQuery(query, page).then(wines => - res.json({ - wines: wines, - count: wines.length, - page: page, - success: true - }) - ); -} - -function ean(req, res) { - const { ean } = req.params; - - return vinmonopoletRepository.searchByEAN(ean).then(wines => - res.json({ - wines: wines, - success: true - }) - ); -} - -function id(req, res) { - const { id } = req.params; - - return vinmonopoletRepository.searchById(id).then(wines => - res.json({ - wine: wines[0], - success: true - }) - ); -} - -module.exports = { - search, - ean, - id -}; diff --git a/api/vinmonopolet.js b/api/vinmonopolet.js index 32dbacc..15d40c2 100644 --- a/api/vinmonopolet.js +++ b/api/vinmonopolet.js @@ -17,14 +17,22 @@ const convertToOurWineObject = wine => { } }; -const searchByQuery = async (query, page = 1) => { +const convertToOurStoreObject = store => { + return { + id: store.storeId, + name: store.storeName, + ...store.address + }; +}; + +const searchWinesByName = async (name, page = 1) => { const pageSize = 15; let url = new URL( `https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15` ); url.searchParams.set("maxResults", pageSize); url.searchParams.set("start", pageSize * (page - 1)); - url.searchParams.set("productShortNameContains", query); + url.searchParams.set("productShortNameContains", name); const vinmonopoletResponse = await fetch(url, { headers: { @@ -50,14 +58,14 @@ const searchByQuery = async (query, page = 1) => { return winesConverted; }; -const searchByEAN = ean => { +const wineByEAN = ean => { const url = `https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/${ean}`; return fetch(url) .then(resp => resp.json()) .then(response => response.map(convertToOurWineObject)); }; -const searchById = id => { +const wineById = id => { const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`; const options = { headers: { @@ -70,8 +78,36 @@ const searchById = id => { .then(response => response.map(convertToOurWineObject)); }; -module.exports = { - searchByQuery, - searchByEAN, - searchById +const allStores = () => { + const url = `https://apis.vinmonopolet.no/stores/v0/details`; + const options = { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }; + + return fetch(url, options) + .then(resp => resp.json()) + .then(response => response.map(convertToOurStoreObject)); +}; + +const searchStoresByName = name => { + const url = `https://apis.vinmonopolet.no/stores/v0/details?storeNameContains=${name}`; + const options = { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }; + + return fetch(url, options) + .then(resp => resp.json()) + .then(response => response.map(convertToOurStoreObject)); +}; + +module.exports = { + searchWinesByName, + wineByEAN, + wineById, + allStores, + searchStoresByName }; From 53135acc05be5a8911a5eafbd85143b7b57c0c59 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 14:01:36 +0100 Subject: [PATCH 018/115] Get/add/update/delete winners from lottery. --- api/controllers/lotteryWinnerController.js | 101 +++++++++ api/lottery.js | 236 +++++++++++++++++++-- 2 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 api/controllers/lotteryWinnerController.js diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js new file mode 100644 index 0000000..babfc9f --- /dev/null +++ b/api/controllers/lotteryWinnerController.js @@ -0,0 +1,101 @@ +const path = require("path"); +const lotteryRepository = require(path.join(__dirname, "../lottery")); + +const allWinners = (req, res) => { + const isAdmin = req.isAuthenticated() || true; + + return lotteryRepository + .allWinners(isAdmin) + .then(winners => + res.send({ + winners: winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch lottery winners." + }); + }); +}; + +const winnerById = (req, res) => { + const { id } = req.params; + + return lotteryRepository + .winnerById(id) + .then(winner => + res.send({ + winner, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured, unable to fetch winner by id.", + success: false + }); + }); +}; + +const deleteWinnerById = (req, res) => { + const isAdmin = req.isAuthenticated() || true; + const { id } = req.params; + + return lotteryRepository + .deleteWinnerById(id, isAdmin) + .then(removedWinner => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return removedWinner; + }) + .then(winner => + res.send({ + message: `Removed winner: ${winner.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing wine by id.", + success: false + }); + }); +}; + +const deleteWinners = (req, res) => { + return lotteryRepository + .deleteWinners() + .then(_ => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + }) + .then(_ => + res.send({ + message: "Removed all winners.", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleting wines", + success: false + }); + }); +}; + +module.exports = { + allWinners, + winnerById, + deleteWinnerById, + deleteWinners +}; diff --git a/api/lottery.js b/api/lottery.js index a748fc2..08e7f83 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -1,6 +1,10 @@ const path = require("path"); + const Attendee = require(path.join(__dirname, "/schemas/Attendee")); const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); +const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); + +const crypto = require("crypto"); class UserNotFound extends Error { constructor(message = "User not found.") { @@ -22,6 +26,33 @@ class WineNotFound extends Error { // TODO log missing user } +class WinnerNotFound extends Error { + constructor(message = "Winner not found.") { + super(message); + this.name = "WinnerNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + +class NoMoreAttendeesToWinError extends Error { + constructor(message = "No more attendees left to drawn from.") { + super(message); + this.name = "NoMoreAttendeesToWinError"; + this.statusCode = 404; + } +} + +class CouldNotFindNewWinnerAfterNTriesError extends Error { + constructor(tries) { + let message = `Could not a new winner after ${tries} tries.`; + super(message); + this.name = "CouldNotFindNewWinnerAfterNTriesError"; + this.statusCode = 404; + } +} + const redactAttendeeInfoMapper = attendee => { return { name: attendee.name, @@ -33,6 +64,13 @@ const redactAttendeeInfoMapper = attendee => { }; }; +const redactWinnerInfoMapper = winner => { + return { + name: winner.name, + color: winner.color + }; +}; + const allAttendees = isAdmin => { if (!isAdmin) { return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); @@ -63,15 +101,20 @@ const updateAttendeeById = (id, updateModel) => { throw new UserNotFound(); } + console.log(updateModel); + const updatedAttendee = { - name: updateModel.name || attendee.name, - green: updateModel.green || attendee.green, - red: updateModel.red || attendee.red, - blue: updateModel.blue || attendee.blue, - yellow: updateModel.yellow || attendee.yellow, - phoneNumber: updateModel.phoneNumber || attendee.phoneNumber + name: updateModel.name != null ? updateModel.name : attendee.name, + green: updateModel.green != null ? updateModel.green : attendee.green, + red: updateModel.red != null ? updateModel.red : attendee.red, + blue: updateModel.blue != null ? updateModel.blue : attendee.blue, + yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow, + phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber, + winner: updateModel.winner != null ? updateModel.winner : attendee.winner }; + console.log(updatedAttendee); + return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); }); }; @@ -119,13 +162,13 @@ const updateWineById = (id, updateModel) => { } const updatedWine = { - name: updateModel.name || wine.name, - vivinoLink: updateModel.vivinoLink || wine.vivinoLink, - rating: updateModel.rating || wine.rating, - image: updateModel.image || wine.image, - price: updateModel.price || wine.price, - country: updateModel.country || wine.country, - id: updateModel.id || wine.id + name: updateModel.name != null ? updateModel.name : wine.name, + vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, + rating: updateModel.rating != null ? updateModel.rating : wine.rating, + image: updateModel.image != null ? updateModel.image : wine.image, + price: updateModel.price != null ? updateModel.price : wine.price, + country: updateModel.country != null ? updateModel.country : wine.country, + id: updateModel.id != null ? updateModel.id : wine.id }; return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine); @@ -146,6 +189,166 @@ const deleteWines = () => { return PreLotteryWine.deleteMany(); }; +const allWinners = isAdmin => { + if (!isAdmin) { + return VirtualWinner.find().then(winners => winners.map(redactWinnerInfoMapper)); + } else { + return VirtualWinner.find(); + } +}; + +const winnerById = (id, isAdmin) => { + return VirtualWinner.findOne({ _id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + if (!isAdmin) { + return redactWinnerInfoMapper(winner); + } else { + return winner; + } + }); +}; + +const deleteWinnerById = id => { + return VirtualWinner.findOne({ _id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + return VirtualWinner.deleteOne({ _id: id }).then(_ => winner); + }); +}; + +const deleteWinners = () => { + return VirtualWinner.deleteMany(); +}; + +const drawWinner = async () => { + let allContestants = await Attendee.find({ winner: false }); + + if (allContestants.length == 0) { + throw new NoMoreAttendeesToWinError(); + } + + let raffleColors = []; + for (let i = 0; i < allContestants.length; i++) { + let currentContestant = allContestants[i]; + for (let blue = 0; blue < currentContestant.blue; blue++) { + raffleColors.push("blue"); + } + for (let red = 0; red < currentContestant.red; red++) { + raffleColors.push("red"); + } + for (let green = 0; green < currentContestant.green; green++) { + raffleColors.push("green"); + } + for (let yellow = 0; yellow < currentContestant.yellow; yellow++) { + raffleColors.push("yellow"); + } + } + + raffleColors = shuffle(raffleColors); + + let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)]; + let findObject = { winner: false }; + + findObject[colorToChooseFrom] = { $gt: 0 }; + + let tries = 0; + const maxTries = 3; + let contestantsToChooseFrom = undefined; + while (contestantsToChooseFrom == undefined && tries < maxTries) { + const hit = await Attendee.find(findObject); + if (hit && hit.length) { + contestantsToChooseFrom = hit; + break; + } + tries++; + } + if (contestantsToChooseFrom == undefined) { + throw new CouldNotFindNewWinnerAfterNTriesError(maxTries); + } + + let attendeeListDemocratic = []; + + let currentContestant; + for (let i = 0; i < contestantsToChooseFrom.length; i++) { + currentContestant = contestantsToChooseFrom[i]; + for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) { + attendeeListDemocratic.push({ + name: currentContestant.name, + phoneNumber: currentContestant.phoneNumber, + red: currentContestant.red, + blue: currentContestant.blue, + green: currentContestant.green, + yellow: currentContestant.yellow + }); + } + } + + attendeeListDemocratic = shuffle(attendeeListDemocratic); + + let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)]; + + let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ + timestamp_drawn: 1 + }); + + let newWinnerElement = new VirtualWinner({ + name: winner.name, + phoneNumber: winner.phoneNumber, + color: colorToChooseFrom, + red: winner.red, + blue: winner.blue, + green: winner.green, + yellow: winner.yellow, + id: sha512(winner.phoneNumber, genRandomString(10)), + timestamp_drawn: new Date().getTime() + }); + + await newWinnerElement.save(); + await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } }); + + return { winner, color: colorToChooseFrom, winners }; +}; + +/** - - UTILS - - **/ +const genRandomString = function(length) { + return crypto + .randomBytes(Math.ceil(length / 2)) + .toString("hex") /** convert to hexadecimal format */ + .slice(0, length); /** return required number of characters */ +}; + +const sha512 = function(password, salt) { + var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */ + hash.update(password); + var value = hash.digest("hex"); + return value; +}; + +function shuffle(array) { + let currentIndex = array.length, + temporaryValue, + randomIndex; + + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + module.exports = { allAttendees, addAttendee, @@ -156,5 +359,10 @@ module.exports = { addWines, updateWineById, deleteWineById, - deleteWines + deleteWines, + allWinners, + winnerById, + deleteWinnerById, + deleteWinners, + drawWinner }; From 84fa1ff925e3fada858cbcbc12290ab21fddab7a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 14:02:53 +0100 Subject: [PATCH 019/115] Linted. --- api/controllers/requestController.js | 54 +++++++++++++++------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/api/controllers/requestController.js b/api/controllers/requestController.js index 91cc391..ac60eca 100644 --- a/api/controllers/requestController.js +++ b/api/controllers/requestController.js @@ -1,52 +1,56 @@ const path = require("path"); -const RequestRepository = require(path.join( - __dirname, "../request" -)); +const RequestRepository = require(path.join(__dirname, "../request")); function addRequest(req, res) { const { wine } = req.body; return RequestRepository.addNew(wine) - .then(wine => res.json({ - message: "Successfully added new request", - wine: wine, - success: true - })) + .then(wine => + res.json({ + message: "Successfully added new request", + wine: wine, + success: true + }) + ) .catch(error => { const { message, statusCode } = error; return res.status(statusCode || 500).send({ success: false, message: message || "Unable to add requested wine." - }) - }) + }); + }); } -function getAllRequests(req, res) { +function allRequests(req, res) { return RequestRepository.getAll() - .then(wines => res.json({ - wines: wines, - success: true - })) + .then(wines => + res.json({ + wines: wines, + success: true + }) + ) .catch(error => { console.log("error in getAllRequests:", error); - const message = "Unable to fetch all requested wines." + const message = "Unable to fetch all requested wines."; return res.status(500).json({ success: false, message: message - }) - }) + }); + }); } function deleteRequest(req, res) { const { id } = req.params; return RequestRepository.deleteById(id) - .then(_ => res.json({ - message: `Slettet vin med id: ${ id }`, - success: true - })) + .then(_ => + res.json({ + message: `Slettet vin med id: ${id}`, + success: true + }) + ) .catch(error => { const { statusCode, message } = error; @@ -54,11 +58,11 @@ function deleteRequest(req, res) { success: false, message: message || "Unable to delete requested wine." }); - }) + }); } module.exports = { addRequest, - getAllRequests, + allRequests, deleteRequest -} \ No newline at end of file +}; From 2f3a6aeba76712c76951d51ee0205dfb5fb99fac Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 14:03:15 +0100 Subject: [PATCH 020/115] Catch exceptions when deleting wines from lottery. --- api/controllers/lotteryWineController.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js index cfa9ccd..95749eb 100644 --- a/api/controllers/lotteryWineController.js +++ b/api/controllers/lotteryWineController.js @@ -128,16 +128,24 @@ const deleteWineById = (req, res) => { const deleteWines = (req, res) => { return lotteryRepository .deleteWines() - .then(removedWine => { + .then(_ => { var io = req.app.get("socketio"); io.emit("refresh_data", {}); }) .then(_ => res.send({ - message: "Removed all wines", + message: "Removed all wines.", success: true }) - ); + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleting wines", + success: false + }); + }); }; module.exports = { From b5b61784ccb02270ae460af8b76ceb84eee94ae2 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 15:34:58 +0100 Subject: [PATCH 021/115] Add winners manually by posting /lottery/winners. --- api/controllers/lotteryWinnerController.js | 57 +++++++++++++++++++++- api/lottery.js | 17 ++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js index babfc9f..13109e1 100644 --- a/api/controllers/lotteryWinnerController.js +++ b/api/controllers/lotteryWinnerController.js @@ -1,8 +1,62 @@ const path = require("path"); const lotteryRepository = require(path.join(__dirname, "../lottery")); +const addWinners = (req, res) => { + const { winners } = req.body; + + if (!(winners instanceof Array)) { + return res.status(400).send({ + message: "Winners must be array.", + success: false + }); + } + + const requiredAttributes = ["name", "color"]; + const validColors = ["red", "blue", "green", "yellow"]; + const validateAllWinners = winners => + winners.map(winner => { + return Promise.all( + requiredAttributes.map(attr => { + if (typeof winner[attr] === "undefined") { + return Promise.reject({ + message: `Incorrect or missing attribute: ${attr}.`, + statusCode: 400 + }); + } + + if (!validColors.includes(winner.color)) { + return Promise.reject({ + message: `Missing or incorrect color value, must have one of values: ${validColors.join(", ")}.`, + statusCode: 400 + }); + } + + return Promise.resolve(); + }) + ).then(_ => Promise.resolve(winner)); + }); + + return Promise.all(validateAllWinners(winners)) + .then(winners => lotteryRepository.addWinners(winners)) + .then(winners => + res.send({ + winners: winners, + message: `Successfully added winners to lottery.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured adding winners.", + success: false + }); + }); +}; + const allWinners = (req, res) => { - const isAdmin = req.isAuthenticated() || true; + const isAdmin = req.isAuthenticated(); return lotteryRepository .allWinners(isAdmin) @@ -94,6 +148,7 @@ const deleteWinners = (req, res) => { }; module.exports = { + addWinners, allWinners, winnerById, deleteWinnerById, diff --git a/api/lottery.js b/api/lottery.js index 08e7f83..f50f0f5 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -189,7 +189,21 @@ const deleteWines = () => { return PreLotteryWine.deleteMany(); }; -const allWinners = isAdmin => { +const addWinners = winners => { + return Promise.all( + winners.map(winner => { + let newWinnerElement = new VirtualWinner({ + name: winner.name, + color: winner.color, + timestamp_drawn: new Date().getTime() + }); + + return newWinnerElement.save(); + }) + ); +}; + +const allWinners = (isAdmin = false) => { if (!isAdmin) { return VirtualWinner.find().then(winners => winners.map(redactWinnerInfoMapper)); } else { @@ -360,6 +374,7 @@ module.exports = { updateWineById, deleteWineById, deleteWines, + addWinners, allWinners, winnerById, deleteWinnerById, From f5d3b16f27a591a2895fcdaf3316916ec108cdc9 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 15:36:03 +0100 Subject: [PATCH 022/115] isAdmin default value = false. --- api/controllers/lotteryWinnerController.js | 5 +++-- api/lottery.js | 8 ++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js index 13109e1..9187653 100644 --- a/api/controllers/lotteryWinnerController.js +++ b/api/controllers/lotteryWinnerController.js @@ -78,9 +78,10 @@ const allWinners = (req, res) => { const winnerById = (req, res) => { const { id } = req.params; + const isAdmin = req.isAuthenticated(); return lotteryRepository - .winnerById(id) + .winnerById(id, isAdmin) .then(winner => res.send({ winner, @@ -98,7 +99,7 @@ const winnerById = (req, res) => { }; const deleteWinnerById = (req, res) => { - const isAdmin = req.isAuthenticated() || true; + const isAdmin = req.isAuthenticated(); const { id } = req.params; return lotteryRepository diff --git a/api/lottery.js b/api/lottery.js index f50f0f5..dee8b96 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -71,7 +71,7 @@ const redactWinnerInfoMapper = winner => { }; }; -const allAttendees = isAdmin => { +const allAttendees = (isAdmin = false) => { if (!isAdmin) { return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); } else { @@ -101,8 +101,6 @@ const updateAttendeeById = (id, updateModel) => { throw new UserNotFound(); } - console.log(updateModel); - const updatedAttendee = { name: updateModel.name != null ? updateModel.name : attendee.name, green: updateModel.green != null ? updateModel.green : attendee.green, @@ -113,8 +111,6 @@ const updateAttendeeById = (id, updateModel) => { winner: updateModel.winner != null ? updateModel.winner : attendee.winner }; - console.log(updatedAttendee); - return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); }); }; @@ -211,7 +207,7 @@ const allWinners = (isAdmin = false) => { } }; -const winnerById = (id, isAdmin) => { +const winnerById = (id, isAdmin = false) => { return VirtualWinner.findOne({ _id: id }).then(winner => { if (winner == null) { throw new WinnerNotFound(); From 1c1f52308f54e61b1a52dc9c50582feaed814b7d Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:12:42 +0100 Subject: [PATCH 023/115] Add winner w/ wine to highscore. --- api/winner.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api/winner.js b/api/winner.js index a528031..9761d5a 100644 --- a/api/winner.js +++ b/api/winner.js @@ -19,6 +19,33 @@ class HistoryForUserNotFound extends Error { } } +// highscore +const addWinnerWithWine = async (winner, wine) => { + const exisitingWinner = await Winner.findOne({ + name: winner.name + }); + + const date = new Date(); + date.setHours(5, 0, 0, 0); + const winObject = { date, wine, color: winner.color }; + + if (exisitingWinner == undefined) { + const newWinner = new Winner({ + name: winner.name, + wins: [winObject] + }); + + await newWinner.save(); + } else { + exisitingWinner.wins.push(winObject); + exisitingWinner.markModified("wins"); + await exisitingWinner.save(); + } + + return exisitingWinner; +}; + +// lottery const all = (includeWines = false) => { if (includeWines === false) { return Winner.find().sort("-wins.date"); @@ -278,6 +305,7 @@ const orderByWins = (includeWines = false) => { }; module.exports = { + addWinnerWithWine, all, byDate, latest, From afab4387cc225a2e98caf9ec31a79da34f0b45c4 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:14:07 +0100 Subject: [PATCH 024/115] Renamed winner.js to history.js. --- ...{winnerController.js => historyController.js} | 16 ++++++++-------- api/{winner.js => history.js} | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) rename api/controllers/{winnerController.js => historyController.js} (95%) rename api/{winner.js => history.js} (94%) diff --git a/api/controllers/winnerController.js b/api/controllers/historyController.js similarity index 95% rename from api/controllers/winnerController.js rename to api/controllers/historyController.js index a6c6af2..382a9c5 100644 --- a/api/controllers/winnerController.js +++ b/api/controllers/historyController.js @@ -1,5 +1,5 @@ const path = require("path"); -const winnerRepository = require(path.join(__dirname, "../winner")); +const historyRepository = require(path.join(__dirname, "../history")); const sortOptions = ["desc", "asc"]; const includeWinesOptions = ["true", "false"]; @@ -21,7 +21,7 @@ const all = (req, res) => { }); } - return winnerRepository + return historyRepository .all(includeWines == "true") .then(winners => res.send({ @@ -54,7 +54,7 @@ const byDate = (req, res) => { }); } - return winnerRepository + return historyRepository .byDate(date) .then(winners => res.send({ @@ -90,7 +90,7 @@ const groupByDate = (req, res) => { }); } - return winnerRepository + return historyRepository .groupedByDate(includeWines == "true", sort) .then(lotteries => res.send({ @@ -109,7 +109,7 @@ const groupByDate = (req, res) => { }; const latest = (req, res) => { - return winnerRepository + return historyRepository .latest() .then(winners => res.send({ @@ -138,7 +138,7 @@ const byName = (req, res) => { }); } - return winnerRepository + return historyRepository .byName(name, sort) .then(winner => res.send({ @@ -166,7 +166,7 @@ const groupByColor = (req, res) => { }); } - return winnerRepository + return historyRepository .groupByColor(includeWines == "true") .then(colors => res.send({ @@ -194,7 +194,7 @@ const orderByWins = (req, res) => { }); } - return winnerRepository + return historyRepository .orderByWins(includeWines == "true") .then(winners => res.send({ diff --git a/api/winner.js b/api/history.js similarity index 94% rename from api/winner.js rename to api/history.js index 9761d5a..e01bc29 100644 --- a/api/winner.js +++ b/api/history.js @@ -56,6 +56,7 @@ const all = (includeWines = false) => { } }; +// lottery const byDate = date => { const startQueryDate = new Date(date.setHours(0, 0, 0, 0)); const endQueryDate = new Date(date.setHours(24, 59, 59, 99)); @@ -104,6 +105,7 @@ const byDate = date => { }); }; +// highscore const byName = (name, sort = "desc") => { return Winner.findOne({ name }, ["name", "wins"]) .sort("-wins.date") @@ -118,6 +120,7 @@ const byName = (name, sort = "desc") => { }); }; +// lottery const latest = () => { const query = [ { @@ -163,6 +166,7 @@ const latest = () => { return Winner.aggregate(query).then(winners => winners[0]); }; +// lottery - byDate const groupByDate = (includeWines = false, sort = "desc") => { const query = [ { @@ -208,6 +212,7 @@ const groupByDate = (includeWines = false, sort = "desc") => { return Winner.aggregate(query).then(lotteries => (sort != "asc" ? lotteries : lotteries.reverse())); }; +// highscore - byColor const groupByColor = (includeWines = false) => { const query = [ { @@ -255,6 +260,9 @@ const groupByColor = (includeWines = false) => { return Winner.aggregate(query); }; +// highscore - byWineOccurences + +// highscore - byWinCount const orderByWins = (includeWines = false) => { let query = [ { @@ -304,6 +312,13 @@ const orderByWins = (includeWines = false) => { return Winner.aggregate(query); }; +// highscore - deleteWinner : remove for GDPR purpose + +// lottery - deleteWinner : remove for GDPR purpose +// lottery - update : manual lottery +// lottery - add : manual lottery +// lottery - archive + module.exports = { addWinnerWithWine, all, From 03c0513da3edaf78b751bfe2fad9f16aeaaf69b5 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:28:27 +0100 Subject: [PATCH 025/115] Rm from lottery.js attendee has own repo and ctrl. --- api/attendee.js | 81 ++++++++++++++++++++ api/controllers/lotteryAttendeeController.js | 21 ++--- 2 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 api/attendee.js diff --git a/api/attendee.js b/api/attendee.js new file mode 100644 index 0000000..e26cbf8 --- /dev/null +++ b/api/attendee.js @@ -0,0 +1,81 @@ +const path = require("path"); + +const Attendee = require(path.join(__dirname, "/schemas/Attendee")); +const { UserNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const redactAttendeeInfoMapper = attendee => { + return { + name: attendee.name, + raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, + red: attendee.red, + blue: attendee.blue, + green: attendee.green, + yellow: attendee.yellow + }; +}; + +const allAttendees = (isAdmin = false) => { + if (!isAdmin) { + return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); + } else { + return Attendee.find(); + } +}; + +const addAttendee = attendee => { + const { name, red, blue, green, yellow, phoneNumber } = attendee; + + let newAttendee = new Attendee({ + name, + red, + blue, + green, + yellow, + phoneNumber, + winner: false + }); + + return newAttendee.save().then(_ => newAttendee); +}; + +const updateAttendeeById = (id, updateModel) => { + return Attendee.findOne({ _id: id }).then(attendee => { + if (attendee == null) { + throw new UserNotFound(); + } + + const updatedAttendee = { + name: updateModel.name != null ? updateModel.name : attendee.name, + green: updateModel.green != null ? updateModel.green : attendee.green, + red: updateModel.red != null ? updateModel.red : attendee.red, + blue: updateModel.blue != null ? updateModel.blue : attendee.blue, + yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow, + phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber, + winner: updateModel.winner != null ? updateModel.winner : attendee.winner + }; + + return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); + }); +}; + +const deleteAttendeeById = id => { + return Attendee.findOne({ _id: id }).then(attendee => { + if (attendee == null) { + throw new UserNotFound(); + } + + return Attendee.deleteOne({ _id: id }).then(_ => attendee); + }); +}; + +const deleteAttendees = () => { + return Attendee.deleteMany(); +}; + +module.exports = { + allAttendees, + addAttendee, + updateAttendeeById, + deleteAttendeeById, + deleteAttendees +}; diff --git a/api/controllers/lotteryAttendeeController.js b/api/controllers/lotteryAttendeeController.js index dc4a380..cd7c931 100644 --- a/api/controllers/lotteryAttendeeController.js +++ b/api/controllers/lotteryAttendeeController.js @@ -1,10 +1,10 @@ const path = require("path"); -const lotteryRepository = require(path.join(__dirname, "../lottery")); +const attendeeRepository = require(path.join(__dirname, "../attendee")); const allAttendees = (req, res) => { - const isAdmin = req.isAuthenticated(); + const isAdmin = req.isAuthenticated() || true; - return lotteryRepository + return attendeeRepository .allAttendees(isAdmin) .then(attendees => res.send({ @@ -41,17 +41,18 @@ const addAttendee = (req, res) => { }); } - return lotteryRepository + return attendeeRepository .addAttendee(attendee) .then(savedAttendee => { var io = req.app.get("socketio"); io.emit("new_attendee", {}); - return true; + return savedAttendee; }) - .then(success => + .then(savedAttendee => res.send({ + attendee: savedAttendee, message: `Successfully added attendee ${attendee.name} to lottery.`, - success: success + success: true }) ); }; @@ -60,7 +61,7 @@ const updateAttendeeById = (req, res) => { const { id } = req.params; const { attendee } = req.body; - return lotteryRepository + return attendeeRepository .updateAttendeeById(id, attendee) .then(updatedAttendee => { var io = req.app.get("socketio"); @@ -87,7 +88,7 @@ const updateAttendeeById = (req, res) => { const deleteAttendeeById = (req, res) => { const { id } = req.params; - return lotteryRepository + return attendeeRepository .deleteAttendeeById(id) .then(removedAttendee => { var io = req.app.get("socketio"); @@ -111,7 +112,7 @@ const deleteAttendeeById = (req, res) => { }; const deleteAttendees = (req, res) => { - return lotteryRepository + return attendeeRepository .deleteAttendees() .then(removedAttendee => { var io = req.app.get("socketio"); From b596dc28e803985ba50140afa9eaca19b53e8f3c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:30:19 +0100 Subject: [PATCH 026/115] Prelotterywine has it's own repo and ctrl. --- api/controllers/lotteryWineController.js | 34 +++++++++-- api/prelotteryWine.js | 78 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 api/prelotteryWine.js diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js index 95749eb..7c53b50 100644 --- a/api/controllers/lotteryWineController.js +++ b/api/controllers/lotteryWineController.js @@ -1,8 +1,8 @@ const path = require("path"); -const lotteryRepository = require(path.join(__dirname, "../lottery")); +const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine")); const allWines = (req, res) => { - return lotteryRepository + return prelotteryWineRepository .allWines() .then(wines => res.send({ @@ -49,7 +49,7 @@ const addWines = (req, res) => { }); return Promise.all(validateAllWines(wines)) - .then(wines => lotteryRepository.addWines(wines)) + .then(wines => prelotteryWineRepository.addWines(wines)) .then(savedWines => { var io = req.app.get("socketio"); io.emit("new_wine", {}); @@ -71,11 +71,32 @@ const addWines = (req, res) => { }); }; +const wineById = (req, res) => { + const { id } = req.params; + + return prelotteryWineRepository + .wineById(id) + .then(wine => + res.send({ + wine, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching wine by id.", + success: false + }); + }); +}; + const updateWineById = (req, res) => { const { id } = req.params; const { wine } = req.body; - return lotteryRepository + return prelotteryWineRepository .updateWineById(id, wine) .then(updatedWine => { var io = req.app.get("socketio"); @@ -102,7 +123,7 @@ const updateWineById = (req, res) => { const deleteWineById = (req, res) => { const { id } = req.params; - return lotteryRepository + return prelotteryWineRepository .deleteWineById(id) .then(removedWine => { var io = req.app.get("socketio"); @@ -126,7 +147,7 @@ const deleteWineById = (req, res) => { }; const deleteWines = (req, res) => { - return lotteryRepository + return prelotteryWineRepository .deleteWines() .then(_ => { var io = req.app.get("socketio"); @@ -151,6 +172,7 @@ const deleteWines = (req, res) => { module.exports = { allWines, addWines, + wineById, updateWineById, deleteWineById, deleteWines diff --git a/api/prelotteryWine.js b/api/prelotteryWine.js new file mode 100644 index 0000000..c5003e3 --- /dev/null +++ b/api/prelotteryWine.js @@ -0,0 +1,78 @@ +const path = require("path"); + +const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); +const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const allWines = () => { + return PreLotteryWine.find(); +}; + +const addWines = wines => { + const prelotteryWines = wines.map(wine => { + let newPrelotteryWine = new PreLotteryWine({ + name: wine.name, + vivinoLink: wine.vivinoLink, + rating: wine.rating, + image: wine.image, + price: wine.price, + country: wine.country, + id: wine.id + }); + + return newPrelotteryWine.save(); + }); + + return Promise.all(prelotteryWines); +}; + +const wineById = (id, updateModel) => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + return wine; + }); +}; + +const updateWineById = (id, updateModel) => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + const updatedWine = { + name: updateModel.name != null ? updateModel.name : wine.name, + vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, + rating: updateModel.rating != null ? updateModel.rating : wine.rating, + image: updateModel.image != null ? updateModel.image : wine.image, + price: updateModel.price != null ? updateModel.price : wine.price, + country: updateModel.country != null ? updateModel.country : wine.country, + id: updateModel.id != null ? updateModel.id : wine.id + }; + + return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine); + }); +}; + +const deleteWineById = id => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine); + }); +}; + +const deleteWines = () => { + return PreLotteryWine.deleteMany(); +}; + +module.exports = { + allWines, + addWines, + wineById, + updateWineById, + deleteWineById, + deleteWines +}; From 6e02c5e3932703cb7fe9f533d8bb0c39eda2ac42 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:32:14 +0100 Subject: [PATCH 027/115] Reflecting changes, isolated winner from lottery. --- api/controllers/lotteryWinnerController.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js index 9187653..c217e74 100644 --- a/api/controllers/lotteryWinnerController.js +++ b/api/controllers/lotteryWinnerController.js @@ -1,5 +1,5 @@ const path = require("path"); -const lotteryRepository = require(path.join(__dirname, "../lottery")); +const winnerRepository = require(path.join(__dirname, "../winner")); const addWinners = (req, res) => { const { winners } = req.body; @@ -37,7 +37,7 @@ const addWinners = (req, res) => { }); return Promise.all(validateAllWinners(winners)) - .then(winners => lotteryRepository.addWinners(winners)) + .then(winners => winnerRepository.addWinners(winners)) .then(winners => res.send({ winners: winners, @@ -56,9 +56,9 @@ const addWinners = (req, res) => { }; const allWinners = (req, res) => { - const isAdmin = req.isAuthenticated(); + const isAdmin = req.isAuthenticated() || true; - return lotteryRepository + return winnerRepository .allWinners(isAdmin) .then(winners => res.send({ @@ -80,7 +80,7 @@ const winnerById = (req, res) => { const { id } = req.params; const isAdmin = req.isAuthenticated(); - return lotteryRepository + return winnerRepository .winnerById(id, isAdmin) .then(winner => res.send({ @@ -102,7 +102,7 @@ const deleteWinnerById = (req, res) => { const isAdmin = req.isAuthenticated(); const { id } = req.params; - return lotteryRepository + return winnerRepository .deleteWinnerById(id, isAdmin) .then(removedWinner => { var io = req.app.get("socketio"); @@ -126,7 +126,7 @@ const deleteWinnerById = (req, res) => { }; const deleteWinners = (req, res) => { - return lotteryRepository + return winnerRepository .deleteWinners() .then(_ => { var io = req.app.get("socketio"); From 33070ae31a656b7686e1fcfddd293d936e6df51a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:35:12 +0100 Subject: [PATCH 028/115] Keeps more information when adding prelottery wine --- api/wine.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/api/wine.js b/api/wine.js index d953013..90142a0 100644 --- a/api/wine.js +++ b/api/wine.js @@ -1,27 +1,30 @@ const path = require("path"); const Wine = require(path.join(__dirname, "/schemas/Wine")); -async function findSaveWine(prelotteryWine) { - let wonWine = await Wine.findOne({ name: prelotteryWine.name }); - if (wonWine == undefined) { - let newWonWine = new Wine({ - name: prelotteryWine.name, - vivinoLink: prelotteryWine.vivinoLink, - rating: prelotteryWine.rating, +const addWine = async wine => { + let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }); + + if (existingWine == undefined) { + let newWine = new Wine({ + name: wine.name, + vivinoLink: wine.vivinoLink, + rating: wine.rating, occurences: 1, - image: prelotteryWine.image, - id: prelotteryWine.id + id: wine.id, + year: wine.year, + image: wine.image, + price: wine.price, + country: wine.country }); - await newWonWine.save(); - wonWine = newWonWine; + await newWine.save(); + return newWine; } else { - wonWine.occurences += 1; - wonWine.image = prelotteryWine.image; - wonWine.id = prelotteryWine.id; - await wonWine.save(); + existingWine.occurences += 1; + await existingWine.save(); + return existingWine; } +}; - return wonWine; -} - -module.exports.findSaveWine = findSaveWine; +module.exports = { + addWine +}; From 939e7e34dfcc38c620ea40f9755c4799fbc20254 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:37:36 +0100 Subject: [PATCH 029/115] All CRUDS on winners for current lottery. --- api/winner.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 api/winner.js diff --git a/api/winner.js b/api/winner.js new file mode 100644 index 0000000..5aa784b --- /dev/null +++ b/api/winner.js @@ -0,0 +1,66 @@ +const path = require("path"); + +const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); +const { WinnerNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const addWinners = winners => { + return Promise.all( + winners.map(winner => { + let newWinnerElement = new VirtualWinner({ + name: winner.name, + color: winner.color, + timestamp_drawn: new Date().getTime() + }); + + return newWinnerElement.save(); + }) + ); +}; + +const allWinners = (isAdmin = false) => { + const sortQuery = { timestamp_drawn: 1 }; + + if (!isAdmin) { + return VirtualWinner.find() + .sort(sortQuery) + .then(winners => winners.map(redactWinnerInfoMapper)); + } else { + return VirtualWinner.find().sort(sortQuery); + } +}; + +const winnerById = (id, isAdmin = false) => { + return VirtualWinner.findOne({ _id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + if (!isAdmin) { + return redactWinnerInfoMapper(winner); + } else { + return winner; + } + }); +}; + +const deleteWinnerById = id => { + return VirtualWinner.findOne({ _id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + return VirtualWinner.deleteOne({ _id: id }).then(_ => winner); + }); +}; + +const deleteWinners = () => { + return VirtualWinner.deleteMany(); +}; + +module.exports = { + addWinners, + allWinners, + winnerById, + deleteWinnerById, + deleteWinners +}; From 5e018f071d645c912b7d16764f12bf21c819bd68 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:38:27 +0100 Subject: [PATCH 030/115] Prize distribution for selecting wines. Moved the same functionality out from lottery.js and simplified a bit. Now the backend also sends with wines to pick from. When hitting the controller we check that the user is the next user in line. --- .../prizeDistributionController.js | 90 +++++++++++++++ api/prizeDistribution.js | 109 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 api/controllers/prizeDistributionController.js create mode 100644 api/prizeDistribution.js diff --git a/api/controllers/prizeDistributionController.js b/api/controllers/prizeDistributionController.js new file mode 100644 index 0000000..b0c176a --- /dev/null +++ b/api/controllers/prizeDistributionController.js @@ -0,0 +1,90 @@ +const path = require("path"); + +const prizeDistribution = require(path.join(__dirname, "../prizeDistribution")); +const winner = require(path.join(__dirname, "../winner")); +const message = require(path.join(__dirname, "../message")); + +const start = async (req, res) => { + const allWinners = await winners.allWinners(); + if (allWinners.length === 0) { + return res.status(503).send({ + message: "No winners left.", + success: false + }); + } + + const laterWinners = allWinners.slice(1); + + return prizeDistribution + .notifyNextWinner() + .then(_ => message.sendInitialMessageToWinners(laterWinners)) + .then(_ => + res.send({ + message: `Send link to first winner and notified everyone else.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while starting prize distribution.", + success: false + }); + }); +}; + +const getPrizesForWinnerById = (req, res) => { + const { id } = req.params; + + return prizeDistribution + .verifyWinnerNextInLine(id) + .then(_ => lottery.allWines()) + .then(wines => + res.send({ + wines: wines, + message: "Wines to select from", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching prizes.", + success: false + }); + }); +}; + +const submitPrizeForWinnerById = async (req, res) => { + const { id } = req.params; + const { wine } = req.body; + + const winner = await prizeDistribution.verifyWinnerNextInLine(id); + const prelotteryWine = await lottery.wineById(wine.id); + + return prizeDistribution + .claimPrize(winner, prelotteryWine) + .then(_ => prizeDistribution.notifyNextWinner()) + .then(_ => + res.send({ + message: `${winner.name} successfully claimed prize: ${wine.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while claiming prize.", + success: false + }); + }); +}; + +module.exports = { + start, + getPrizesForWinnerById, + submitPrizeForWinnerById +}; diff --git a/api/prizeDistribution.js b/api/prizeDistribution.js new file mode 100644 index 0000000..e530574 --- /dev/null +++ b/api/prizeDistribution.js @@ -0,0 +1,109 @@ +const path = require("path"); + +const Wine = require(path.join(__dirname, "/schemas/Wine")); +const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); +const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); + +const message = require(path.join(__dirname, "/message")); +const highscoreRepository = require(path.join(__dirname, "/winner")); +const wineRepository = require(path.join(__dirname, "/wine")); +const lottery = require(path.join(__dirname, "/lottery")); + +const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join( + __dirname, + "/vinlottisErrors" +)); + +const verifyWinnerNextInLine = async id => { + let foundWinner = await VirtualWinner.findOne({ id: id }); + + if (!foundWinner) { + throw new WinnerNotFound(); + } else if (foundWinner.timestamp_limit < new Date().getTime()) { + throw new WinnersTimelimitExpired(); + } + + let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); + if ( + allWinners[0].id != foundWinner.id || + foundWinner.timestamp_limit == undefined || + foundWinner.timestamp_sent == undefined + ) { + throw new WineSelectionWinnerNotNextInLine(); + } + + return Promise.resolve(foundWinner); +}; + +const claimPrize = (winner, wine) => { + return wineRepository + .addWine(wine) + .then(_ => lottery.deleteWineById(wine.id)) // prelotteryWine.deleteById + .then(_ => highscoreRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine + .then(_ => lottery.addWinnerWithWine(winner, wine)) + .then(_ => message.sendWineConfirmation(winner, wine)); +}; + +const notifyNextWinner = async () => { + let nextWinner = undefined; + + const winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); + const winesLeft = await PreLotteryWine.find(); + + if (winnersLeft.length > 1) { + console.log("multiple winners left, choose next in line"); + nextWinner = winnersLeft[0]; // multiple winners left, choose next in line + } else if (winnersLeft.length == 1 && winesLeft.length > 1) { + console.log("one winner left, but multiple wines"); + nextWinner = winnersLeft[0]; // one winner left, but multiple wines + } else if (winnersLeft.length == 1 && winesLeft.length == 1) { + console.log("one winner and one wine left, choose for user"); + nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user + wine = winesLeft[0]; + return claimPrize(nextWinner, wine); + } + + if (nextWinner) { + return message.sendPrizeSelectionLink(nextWinner).then(_ => startTimeout(nextWinner.id)); + } else { + console.info("All winners notified. Could start cleanup here."); + return Promise.resolve({ + message: "All winners notified." + }); + } +}; + +// these need to be register somewhere to cancel if something +// goes wrong and we want to start prize distribution again +function startTimeout(id) { + const minute = 60000; + const minutesForTimeout = 10; + + console.log(`Starting timeout for user ${id}.`); + console.log(`Timeout duration: ${minutesForTimeout * minute}`); + setTimeout(async () => { + let virtualWinner = await VirtualWinner.findOne({ id: id }); + if (!virtualWinner) { + console.log(`Timeout done for user ${id}, but user has already sent data.`); + return; + } + console.log(`Timeout done for user ${id}, sending update to user.`); + + message.sendWineSelectMessageTooLate(virtualWinner); + + virtualWinner.timestamp_drawn = new Date().getTime(); + virtualWinner.timestamp_limit = null; + virtualWinner.timestamp_sent = null; + await virtualWinner.save(); + + findAndNotifyNextWinner(); + }, minutesForTimeout * minute); + + return Promise.resolve(); +} + +module.exports = { + verifyWinnerNextInLine, + claimPrize, + notifyNextWinner +}; From 1b1a99ccc385e5f8cbe1f0e1515d9cac3c0d1cab Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:43:18 +0100 Subject: [PATCH 031/115] Linting and more clear function names. --- api/message.js | 123 +++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/api/message.js b/api/message.js index e4faf26..15d42da 100644 --- a/api/message.js +++ b/api/message.js @@ -2,34 +2,50 @@ const https = require("https"); const path = require("path"); const config = require(path.join(__dirname + "/../config/defaults/lottery")); -const dateString = (date) => { - if (typeof(date) == "string") { +const dateString = date => { + if (typeof date == "string") { date = new Date(date); } - const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date) - const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date) - const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date) + const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); + const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); + const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); - return `${da}-${mo}-${ye}` + return `${da}-${mo}-${ye}`; +}; + +async function sendInitialMessageToWinners(winners) { + const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` })); + + const body = { + sender: "Vinlottis", + message: "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!", + recipients: numbers + }; + + return gatewayRequest(body); } -async function sendWineSelectMessage(winnerObject) { - winnerObject.timestamp_sent = new Date().getTime(); - winnerObject.timestamp_limit = new Date().getTime() * 600000; - await winnerObject.save(); +async function sendPrizeSelectionLink(winner) { + winner.timestamp_sent = new Date().getTime(); + winner.timestamp_limit = new Date().getTime() + 1000 * 600; + await winner.save(); - let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin"); + const { id, name, phoneNumber } = winner; + const url = new URL(`/#/winner/${id}`, "https://lottis.vin"); + const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \ +å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \ +i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`; - return sendMessageToUser( - winnerObject.phoneNumber, - `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)` - ) + return sendMessageToNumber(phoneNumber, message); } async function sendWineConfirmation(winnerObject, wineObject, date) { date = dateString(date); - return sendMessageToUser(winnerObject.phoneNumber, - `Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`) + return sendMessageToNumber( + winnerObject.phoneNumber, + `Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\ +\nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!` + ); } async function sendLastWinnerMessage(winnerObject, wineObject) { @@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) { winnerObject.timestamp_limit = new Date().getTime(); await winnerObject.save(); - return sendMessageToUser( + return sendMessageToNumber( winnerObject.phoneNumber, - `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!` + `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \ +du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!` ); } async function sendWineSelectMessageTooLate(winnerObject) { - return sendMessageToUser( + return sendMessageToNumber( winnerObject.phoneNumber, - `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` + `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \ +puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` ); } -async function sendMessageToUser(phoneNumber, message) { - console.log(`Attempting to send message to ${ phoneNumber }.`) +async function sendMessageToNumber(phoneNumber, message) { + console.log(`Attempting to send message to ${phoneNumber}.`); const body = { sender: "Vinlottis", message: message, - recipients: [{ msisdn: `47${ phoneNumber }`}] + recipients: [{ msisdn: `47${phoneNumber}` }] }; return gatewayRequest(body); } - -async function sendInitialMessageToWinners(winners) { - let numbers = []; - for (let i = 0; i < winners.length; i++) { - numbers.push({ msisdn: `47${winners[i].phoneNumber}` }); - } - - const body = { - sender: "Vinlottis", - message: - "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!", - recipients: numbers - } - - return gatewayRequest(body); -} - async function gatewayRequest(body) { return new Promise((resolve, reject) => { const options = { hostname: "gatewayapi.com", post: 443, - path: `/rest/mtsms?token=${ config.gatewayToken }`, + path: `/rest/mtsms?token=${config.gatewayToken}`, method: "POST", headers: { "Content-Type": "application/json" } - } + }; - const req = https.request(options, (res) => { - console.log(`statusCode: ${ res.statusCode }`); - console.log(`statusMessage: ${ res.statusMessage }`); + const req = https.request(options, res => { + console.log(`statusCode: ${res.statusCode}`); + console.log(`statusMessage: ${res.statusMessage}`); - res.setEncoding('utf8'); + res.setEncoding("utf8"); if (res.statusCode == 200) { - res.on("data", (data) => { - console.log("Response from message gateway:", data) + res.on("data", data => { + console.log("Response from message gateway:", data); - resolve(JSON.parse(data)) + resolve(JSON.parse(data)); }); } else { - res.on("data", (data) => { + res.on("data", data => { data = JSON.parse(data); - return reject('Gateway error: ' + data['message'] || data) + return reject("Gateway error: " + data["message"] || data); }); } - }) + }); - req.on("error", (error) => { - console.error(`Error from sms service: ${ error }`); - reject(`Error from sms service: ${ error }`); - }) + req.on("error", error => { + console.error(`Error from sms service: ${error}`); + reject(`Error from sms service: ${error}`); + }); req.write(JSON.stringify(body)); req.end(); @@ -123,9 +124,9 @@ async function gatewayRequest(body) { } module.exports = { - sendWineSelectMessage, + sendInitialMessageToWinners, + sendPrizeSelectionLink, sendWineConfirmation, sendLastWinnerMessage, - sendWineSelectMessageTooLate, - sendInitialMessageToWinners -} + sendWineSelectMessageTooLate +}; From ac829052b628034a1af016d80d52bd35ae116a96 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:52:11 +0100 Subject: [PATCH 032/115] Wine models now have extra year field. --- api/prelotteryWine.js | 2 ++ api/schemas/PreLotteryWine.js | 1 + api/schemas/Wine.js | 1 + api/vinmonopolet.js | 1 + 4 files changed, 5 insertions(+) diff --git a/api/prelotteryWine.js b/api/prelotteryWine.js index c5003e3..453c10f 100644 --- a/api/prelotteryWine.js +++ b/api/prelotteryWine.js @@ -13,6 +13,7 @@ const addWines = wines => { name: wine.name, vivinoLink: wine.vivinoLink, rating: wine.rating, + year: wine.year, image: wine.image, price: wine.price, country: wine.country, @@ -44,6 +45,7 @@ const updateWineById = (id, updateModel) => { name: updateModel.name != null ? updateModel.name : wine.name, vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, rating: updateModel.rating != null ? updateModel.rating : wine.rating, + year: updateModel.year != null ? updateModel.year : wine.year, image: updateModel.image != null ? updateModel.image : wine.image, price: updateModel.price != null ? updateModel.price : wine.price, country: updateModel.country != null ? updateModel.country : wine.country, diff --git a/api/schemas/PreLotteryWine.js b/api/schemas/PreLotteryWine.js index 69295b5..7ff3ad4 100644 --- a/api/schemas/PreLotteryWine.js +++ b/api/schemas/PreLotteryWine.js @@ -6,6 +6,7 @@ const PreLotteryWine = new Schema({ vivinoLink: String, rating: Number, id: String, + year: Number, image: String, price: String, country: String diff --git a/api/schemas/Wine.js b/api/schemas/Wine.js index a24fc83..c4c89f6 100644 --- a/api/schemas/Wine.js +++ b/api/schemas/Wine.js @@ -7,6 +7,7 @@ const Wine = new Schema({ rating: Number, occurences: Number, id: String, + year: Number, image: String, price: String, country: String diff --git a/api/vinmonopolet.js b/api/vinmonopolet.js index 15d40c2..164e09f 100644 --- a/api/vinmonopolet.js +++ b/api/vinmonopolet.js @@ -10,6 +10,7 @@ const convertToOurWineObject = wine => { rating: wine.basic.alcoholContent, occurences: 0, id: wine.basic.productId, + year: wine.basic.vintage, image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`, price: wine.prices[0].salesPrice.toString(), country: wine.origins.origin.country From 87257fd5b2d4e3331208dcd0c6e60f3b147e42e6 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:56:08 +0100 Subject: [PATCH 033/115] Moved everything not lottery out. Moved all logic related to lotteryAttendees, lotteryWinners and prelotteryWines into each their repository and controller. Now only holds draw, archive and get archived lotteries. --- api/controllers/lotteryController.js | 171 ++++++++++++++++ api/lottery.js | 296 ++++++--------------------- 2 files changed, 232 insertions(+), 235 deletions(-) create mode 100644 api/controllers/lotteryController.js diff --git a/api/controllers/lotteryController.js b/api/controllers/lotteryController.js new file mode 100644 index 0000000..a107415 --- /dev/null +++ b/api/controllers/lotteryController.js @@ -0,0 +1,171 @@ +const path = require("path"); +const lotteryRepository = require(path.join(__dirname, "../lottery")); + +const drawWinner = (req, res) => { + return lotteryRepository + .drawWinner() + .then(({ winner, color, winners }) => { + var io = req.app.get("socketio"); + io.emit("winner", { + color: color, + name: winner.name, + winner_count: winners.length + 1 + }); + + return { winner, color, winners }; + }) + .then(({ winner, color, winners }) => + res.send({ + color: color, + winner: winner, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while drawing winner.", + success: false + }); + }); +}; + +const archiveLottery = (req, res) => { + const { lottery } = req.body; + if (lottery == undefined || !lottery instanceof Object) { + return res.status(400).send({ + message: "Missing lottery object.", + success: false + }); + } + + let { stolen, date, raffles, wines } = lottery; + stolen = stolen !== undefined ? stolen : 0; // default = 0 + date = date !== undefined ? date : new Date(); // default + + const validDateFormat = new RegExp("d{4}-d{2}-d{2}"); + if (!validDateFormat.test(date) || isNaN(date)) { + return res.status(400).send({ + message: "Date must be defined as 'yyyy-mm-dd'.", + success: false + }); + } else { + date = Date.parse(date, "yyyy-MM-dd"); + } + + return verifyLotteryPayload(raffles, stolen, wines) + .then(_ => lottery.archive(date, raffles, stolen, wines)) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while submitting lottery.", + success: false + }); + }); +}; + +const lotteryByDate = (req, res) => { + const { epoch } = req.params; + + if (!/^\d+$/.test(epoch)) { + return res.status(400).send({ + message: "Last parameter must be epoch (in seconds).", + success: false + }); + } + const date = new Date(Number(epoch) * 1000); + + return lotteryRepository + .lotteryByDate(date) + .then(lottery => + res.send({ + lottery, + message: `Lottery for date: ${dateToDateString(date)}/${epoch}.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching lottery by date.", + success: false + }); + }); +}; + +const allLotteries = (req, res) => { + const isAdmin = req.isAuthenticated() || true; + + return lotteryRepository + .allLotteries(isAdmin) + .then(lotteries => + res.send({ + lotteries, + message: "All lotteries.", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching all lotteries.", + success: false + }); + }); +}; + +function verifyLotteryPayload(raffles, stolen, wines) { + return new Promise((resolve, reject) => { + if (raffles == undefined || !raffles instanceof Array) { + reject({ + message: "Raffles must be array.", + status: 400 + }); + } + + const requiredColors = [raffles["red"], raffles["blue"], raffles["green"], raffles["yellow"]]; + const correctColorsTypes = requiredColors.filter(color => typeof color === "number"); + if (requiredColors.length !== correctColorsTypes.length) { + reject({ + message: + "Incorrect or missing raffle colors, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.", + status: 400 + }); + } + + if (stolen == undefined || (isNaN(stolen) && stolen >= 0)) { + reject({ + message: "Number of stolen raffles must be positive integer or 0.", + status: 400 + }); + } + + if (wines == undefined || !wines instanceof Array) { + reject({ + message: "Wines must be array.", + status: 400 + }); + } + + resolve(); + }); +} + +function dateToDateString(date) { + const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); + const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); + const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); + + return `${ye}-${mo}-${da}`; +} + +module.exports = { + drawWinner, + archiveLottery, + lotteryByDate, + allLotteries +}; diff --git a/api/lottery.js b/api/lottery.js index dee8b96..aec1c4a 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -1,245 +1,83 @@ const path = require("path"); +const crypto = require("crypto"); const Attendee = require(path.join(__dirname, "/schemas/Attendee")); const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); +const Lottery = require(path.join(__dirname, "/schemas/Purchase")); -const crypto = require("crypto"); +const Message = require(path.join(__dirname, "/message")); +const { + WinnerNotFound, + NoMoreAttendeesToWin, + CouldNotFindNewWinnerAfterNTries, + LotteryByDateNotFound +} = require(path.join(__dirname, "/vinlottisErrors")); -class UserNotFound extends Error { - constructor(message = "User not found.") { - super(message); - this.name = "UserNotFound"; - this.statusCode = 404; - } +const archive = (date, raffles, stolen, wines) => { + const { blue, red, yellow, green } = raffles; + const bought = blue + red + yellow + green; + date = date.setHours(0, 0, 0, 0); - // TODO log missing user -} - -class WineNotFound extends Error { - constructor(message = "Wine not found.") { - super(message); - this.name = "WineNotFound"; - this.statusCode = 404; - } - - // TODO log missing user -} - -class WinnerNotFound extends Error { - constructor(message = "Winner not found.") { - super(message); - this.name = "WinnerNotFound"; - this.statusCode = 404; - } - - // TODO log missing user -} - -class NoMoreAttendeesToWinError extends Error { - constructor(message = "No more attendees left to drawn from.") { - super(message); - this.name = "NoMoreAttendeesToWinError"; - this.statusCode = 404; - } -} - -class CouldNotFindNewWinnerAfterNTriesError extends Error { - constructor(tries) { - let message = `Could not a new winner after ${tries} tries.`; - super(message); - this.name = "CouldNotFindNewWinnerAfterNTriesError"; - this.statusCode = 404; - } -} - -const redactAttendeeInfoMapper = attendee => { - return { - name: attendee.name, - raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, - red: attendee.red, - blue: attendee.blue, - green: attendee.green, - yellow: attendee.yellow - }; -}; - -const redactWinnerInfoMapper = winner => { - return { - name: winner.name, - color: winner.color - }; -}; - -const allAttendees = (isAdmin = false) => { - if (!isAdmin) { - return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); - } else { - return Attendee.find(); - } -}; - -const addAttendee = attendee => { - const { name, red, blue, green, yellow, phoneNumber } = attendee; - - let newAttendee = new Attendee({ - name, - red, - blue, - green, - yellow, - phoneNumber, - winner: false - }); - - return newAttendee.save(); -}; - -const updateAttendeeById = (id, updateModel) => { - return Attendee.findOne({ _id: id }).then(attendee => { - if (attendee == null) { - throw new UserNotFound(); - } - - const updatedAttendee = { - name: updateModel.name != null ? updateModel.name : attendee.name, - green: updateModel.green != null ? updateModel.green : attendee.green, - red: updateModel.red != null ? updateModel.red : attendee.red, - blue: updateModel.blue != null ? updateModel.blue : attendee.blue, - yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow, - phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber, - winner: updateModel.winner != null ? updateModel.winner : attendee.winner - }; - - return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); - }); -}; - -const deleteAttendeeById = id => { - return Attendee.findOne({ _id: id }).then(attendee => { - if (attendee == null) { - throw new UserNotFound(); - } - - return Attendee.deleteOne({ _id: id }).then(_ => attendee); - }); -}; - -const deleteAttendees = () => { - return Attendee.deleteMany(); -}; - -const allWines = () => { - return PreLotteryWine.find(); -}; - -const addWines = wines => { - const prelotteryWines = wines.map(wine => { - let newPrelotteryWine = new PreLotteryWine({ - name: wine.name, - vivinoLink: wine.vivinoLink, - rating: wine.rating, - image: wine.image, - price: wine.price, - country: wine.country, - id: wine.id - }); - - return newPrelotteryWine.save(); - }); - - return Promise.all(prelotteryWines); -}; - -const updateWineById = (id, updateModel) => { - return PreLotteryWine.findOne({ _id: id }).then(wine => { - if (wine == null) { - throw new WineNotFound(); - } - - const updatedWine = { - name: updateModel.name != null ? updateModel.name : wine.name, - vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, - rating: updateModel.rating != null ? updateModel.rating : wine.rating, - image: updateModel.image != null ? updateModel.image : wine.image, - price: updateModel.price != null ? updateModel.price : wine.price, - country: updateModel.country != null ? updateModel.country : wine.country, - id: updateModel.id != null ? updateModel.id : wine.id - }; - - return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine); - }); -}; - -const deleteWineById = id => { - return PreLotteryWine.findOne({ _id: id }).then(wine => { - if (wine == null) { - throw new WineNotFound(); - } - - return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine); - }); -}; - -const deleteWines = () => { - return PreLotteryWine.deleteMany(); -}; - -const addWinners = winners => { - return Promise.all( - winners.map(winner => { - let newWinnerElement = new VirtualWinner({ - name: winner.name, - color: winner.color, - timestamp_drawn: new Date().getTime() - }); - - return newWinnerElement.save(); - }) + return Lottery.findOneAndUpdate( + { date }, + { + date, + blue, + red, + yellow, + green, + bought, + stolen, + wines + }, + { upsert: true } ); }; -const allWinners = (isAdmin = false) => { - if (!isAdmin) { - return VirtualWinner.find().then(winners => winners.map(redactWinnerInfoMapper)); - } else { - return VirtualWinner.find(); - } -}; +const lotteryByDate = date => { + const startOfDay = new Date(date.setHours(0, 0, 0, 0)); + const endOfDay = new Date(date.setHours(24, 59, 59, 99)); -const winnerById = (id, isAdmin = false) => { - return VirtualWinner.findOne({ _id: id }).then(winner => { - if (winner == null) { - throw new WinnerNotFound(); + const query = [ + { + $match: { + date: { + $gte: startOfDay, + $lte: endOfDay + } + } + }, + { + $lookup: { + from: "wines", + localField: "wines", + foreignField: "_id", + as: "wines" + } } + ]; - if (!isAdmin) { - return redactWinnerInfoMapper(winner); - } else { - return winner; + const aggregateLottery = Lottery.aggregate(query); + return aggregateLottery.project("-_id -__v").then(lotteries => { + if (lotteries.length == 0) { + throw new LotteryByDateNotFound(date); } + return lotteries[0]; }); }; -const deleteWinnerById = id => { - return VirtualWinner.findOne({ _id: id }).then(winner => { - if (winner == null) { - throw new WinnerNotFound(); - } - - return VirtualWinner.deleteOne({ _id: id }).then(_ => winner); - }); -}; - -const deleteWinners = () => { - return VirtualWinner.deleteMany(); +const allLotteries = () => { + return Lottery.find() + .select("-_id -__v") + .populate("wines"); }; const drawWinner = async () => { let allContestants = await Attendee.find({ winner: false }); if (allContestants.length == 0) { - throw new NoMoreAttendeesToWinError(); + throw new NoMoreAttendeesToWin(); } let raffleColors = []; @@ -278,7 +116,7 @@ const drawWinner = async () => { tries++; } if (contestantsToChooseFrom == undefined) { - throw new CouldNotFindNewWinnerAfterNTriesError(maxTries); + throw new CouldNotFindNewWinnerAfterNTries(maxTries); } let attendeeListDemocratic = []; @@ -360,20 +198,8 @@ function shuffle(array) { } module.exports = { - allAttendees, - addAttendee, - updateAttendeeById, - deleteAttendeeById, - deleteAttendees, - allWines, - addWines, - updateWineById, - deleteWineById, - deleteWines, - addWinners, - allWinners, - winnerById, - deleteWinnerById, - deleteWinners, - drawWinner + drawWinner, + archive, + lotteryByDate, + allLotteries }; From 4c33708ff4afdd2d1815686d859f0d249b658c47 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 22:59:43 +0100 Subject: [PATCH 034/115] Made some required wine attributes optional. --- api/controllers/lotteryWineController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js index 7c53b50..9e0c283 100644 --- a/api/controllers/lotteryWineController.js +++ b/api/controllers/lotteryWineController.js @@ -32,7 +32,7 @@ const addWines = (req, res) => { const validateAllWines = wines => wines.map(wine => { - const requiredAttributes = ["name", "vivinoLink", "rating", "image", "country", "id", "price"]; + const requiredAttributes = ["name", "vivinoLink", "image", "id", "price"]; return Promise.all( requiredAttributes.map(attr => { From f4a16bc4173aff15210d4b8a216efb4792f3620f Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 23:01:08 +0100 Subject: [PATCH 035/115] Removed always setting isAdmin true. --- api/controllers/lotteryAttendeeController.js | 2 +- api/controllers/lotteryController.js | 2 +- api/controllers/lotteryWinnerController.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/lotteryAttendeeController.js b/api/controllers/lotteryAttendeeController.js index cd7c931..b19acaf 100644 --- a/api/controllers/lotteryAttendeeController.js +++ b/api/controllers/lotteryAttendeeController.js @@ -2,7 +2,7 @@ const path = require("path"); const attendeeRepository = require(path.join(__dirname, "../attendee")); const allAttendees = (req, res) => { - const isAdmin = req.isAuthenticated() || true; + const isAdmin = req.isAuthenticated(); return attendeeRepository .allAttendees(isAdmin) diff --git a/api/controllers/lotteryController.js b/api/controllers/lotteryController.js index a107415..bbbe0bc 100644 --- a/api/controllers/lotteryController.js +++ b/api/controllers/lotteryController.js @@ -97,7 +97,7 @@ const lotteryByDate = (req, res) => { }; const allLotteries = (req, res) => { - const isAdmin = req.isAuthenticated() || true; + const isAdmin = req.isAuthenticated(); return lotteryRepository .allLotteries(isAdmin) diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js index c217e74..2b56eca 100644 --- a/api/controllers/lotteryWinnerController.js +++ b/api/controllers/lotteryWinnerController.js @@ -56,7 +56,7 @@ const addWinners = (req, res) => { }; const allWinners = (req, res) => { - const isAdmin = req.isAuthenticated() || true; + const isAdmin = req.isAuthenticated(); return winnerRepository .allWinners(isAdmin) From ba86bf3ada63913983c51711e2298514cfb60b60 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 23:01:18 +0100 Subject: [PATCH 036/115] New custom errors to throw. --- api/vinlottisErrors.js | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 api/vinlottisErrors.js diff --git a/api/vinlottisErrors.js b/api/vinlottisErrors.js new file mode 100644 index 0000000..d812627 --- /dev/null +++ b/api/vinlottisErrors.js @@ -0,0 +1,90 @@ +class UserNotFound extends Error { + constructor(message = "User not found.") { + super(message); + this.name = "UserNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + +class WineNotFound extends Error { + constructor(message = "Wine not found.") { + super(message); + this.name = "WineNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + +class WinnerNotFound extends Error { + constructor(message = "Winner not found.") { + super(message); + this.name = "WinnerNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + +class WinnersTimelimitExpired extends Error { + constructor(message = "Timelimit expired, you will need to wait until it's your turn again.") { + super(message); + this.name = "WinnersTimelimitExpired"; + this.statusCode = 403; + } +} + +class WineSelectionWinnerNotNextInLine extends Error { + constructor(message = "Not the winner next in line!") { + super(message); + this.name = "WineSelectionWinnerNotNextInLine"; + this.statusCode = 403; + } + + // TODO log missing user +} + +class NoMoreAttendeesToWin extends Error { + constructor(message = "No more attendees left to drawn from.") { + super(message); + this.name = "NoMoreAttendeesToWin"; + this.statusCode = 404; + } +} + +class CouldNotFindNewWinnerAfterNTries extends Error { + constructor(tries) { + let message = `Could not a new winner after ${tries} tries.`; + super(message); + this.name = "CouldNotFindNewWinnerAfterNTries"; + this.statusCode = 404; + } +} + +class LotteryByDateNotFound extends Error { + constructor(date) { + const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); + const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); + const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); + + const dateString = `${ye}-${mo}-${da}`; + const dateUnix = date.getTime(); + const message = `Could not find lottery for date: ${dateString}.`; + super(message); + this.name = "LotteryByDateNotFoundError"; + this.statusCode = 404; + } +} + +module.exports = { + UserNotFound, + WineNotFound, + WinnerNotFound, + WinnersTimelimitExpired, + WineSelectionWinnerNotNextInLine, + NoMoreAttendeesToWin, + CouldNotFindNewWinnerAfterNTries, + LotteryByDateNotFound +}; From ce480e790a3c2c04f25f8a74eca953c1277eaba4 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 23:01:49 +0100 Subject: [PATCH 037/115] Replaced for clearer project structure & new ctrls. These files where directly called from the api endpoints. Now all functoins have been replaced with a controller interfacing to the endpoint and a repository file for each of the functions. --- api/retrieve.js | 154 -------------------- api/update.js | 142 ------------------- api/virtualLottery.js | 281 ------------------------------------- api/virtualRegistration.js | 200 -------------------------- 4 files changed, 777 deletions(-) delete mode 100644 api/retrieve.js delete mode 100644 api/update.js delete mode 100644 api/virtualLottery.js delete mode 100644 api/virtualRegistration.js diff --git a/api/retrieve.js b/api/retrieve.js deleted file mode 100644 index c29133a..0000000 --- a/api/retrieve.js +++ /dev/null @@ -1,154 +0,0 @@ -const path = require("path"); - -const Purchase = require(path.join(__dirname, "/schemas/Purchase")); -const Wine = require(path.join(__dirname, "/schemas/Wine")); -const Highscore = require(path.join(__dirname, "/schemas/Highscore")); -const PreLotteryWine = require(path.join( - __dirname, "/schemas/PreLotteryWine" -)); - -const prelotteryWines = async (req, res) => { - let wines = await PreLotteryWine.find(); - return res.json(wines); -}; - -const allPurchase = async (req, res) => { - let purchases = await Purchase.find() - .populate("wines") - .sort({ date: 1 }); - return res.json(purchases); -}; - -const purchaseByColor = async (req, res) => { - const countColor = await Purchase.find(); - let red = 0; - let blue = 0; - let yellow = 0; - let green = 0; - let stolen = 0; - for (let i = 0; i < countColor.length; i++) { - let element = countColor[i]; - red += element.red; - blue += element.blue; - yellow += element.yellow; - green += element.green; - if (element.stolen != undefined) { - stolen += element.stolen; - } - } - - const highscore = await Highscore.find(); - let redWin = 0; - let blueWin = 0; - let yellowWin = 0; - let greenWin = 0; - for (let i = 0; i < highscore.length; i++) { - let element = highscore[i]; - for (let y = 0; y < element.wins.length; y++) { - let currentWin = element.wins[y]; - switch (currentWin.color) { - case "blue": - blueWin += 1; - break; - case "red": - redWin += 1; - break; - case "yellow": - yellowWin += 1; - break; - case "green": - greenWin += 1; - break; - } - } - } - - const total = red + yellow + blue + green; - - return res.json({ - red: { - total: red, - win: redWin - }, - blue: { - total: blue, - win: blueWin - }, - green: { - total: green, - win: greenWin - }, - yellow: { - total: yellow, - win: yellowWin - }, - stolen: stolen, - total: total - }); -}; - -const highscore = async (req, res) => { - const highscore = await Highscore.find().populate("wins.wine"); - - return res.json(highscore); -}; - -const allWines = async (req, res) => { - const wines = await Wine.find(); - - return res.json(wines); -}; - -const allWinesSummary = async (req, res) => { - const highscore = await Highscore.find().populate("wins.wine"); - let wines = {}; - - for (let i = 0; i < highscore.length; i++) { - let person = highscore[i]; - for (let y = 0; y < person.wins.length; y++) { - let wine = person.wins[y].wine; - let date = person.wins[y].date; - let color = person.wins[y].color; - - if (wines[wine._id] == undefined) { - wines[wine._id] = { - name: wine.name, - occurences: wine.occurences, - vivinoLink: wine.vivinoLink, - rating: wine.rating, - image: wine.image, - id: wine.id, - _id: wine._id, - dates: [date], - winners: [person.name], - red: 0, - blue: 0, - green: 0, - yellow: 0 - }; - wines[wine._id][color] += 1; - } else { - wines[wine._id].dates.push(date); - wines[wine._id].winners.push(person.name); - if (wines[wine._id][color] == undefined) { - wines[wine._id][color] = 1; - } else { - wines[wine._id][color] += 1; - } - } - } - } - - wines = Object.values(wines).reverse() - - return res.json(wines); -}; - -module.exports = { - prelotteryWines, - allPurchase, - purchaseByColor, - highscore, - allWines, - allWinesSummary -}; diff --git a/api/update.js b/api/update.js deleted file mode 100644 index 88df56a..0000000 --- a/api/update.js +++ /dev/null @@ -1,142 +0,0 @@ -const express = require("express"); -const path = require("path"); - -const sub = require(path.join(__dirname, "/subscriptions")); - -const _wineFunctions = require(path.join(__dirname, "/wine")); -const _personFunctions = require(path.join(__dirname, "/person")); -const Subscription = require(path.join(__dirname, "/schemas/Subscription")); -const Lottery = require(path.join(__dirname, "/schemas/Purchase")); -const PreLotteryWine = require(path.join( - __dirname, "/schemas/PreLotteryWine" -)); - -const submitWines = async (req, res) => { - const wines = req.body; - for (let i = 0; i < wines.length; i++) { - let wine = wines[i]; - let newWonWine = new PreLotteryWine({ - name: wine.name, - vivinoLink: wine.vivinoLink, - rating: wine.rating, - image: wine.image, - price: wine.price, - country: wine.country, - id: wine.id - }); - await newWonWine.save(); - } - - let subs = await Subscription.find(); - console.log("Sending new wines w/ push notification to all subscribers.") - for (let i = 0; i < subs.length; i++) { - let subscription = subs[i]; //get subscription from your databse here. - - const message = JSON.stringify({ - message: "Dagens vin er lagt til, se den på lottis.vin/dagens!", - title: "Ny vin!", - link: "/#/dagens" - }); - - try { - sub.sendNotification(subscription, message); - } catch (error) { - console.error("Error when trying to send push notification to subscriber."); - console.error(error); - } - } - - return res.send({ - message: "Submitted and notified push subscribers of new wines!", - success: true - }); -}; - -const schema = async (req, res) => { - let schema = { ...PreLotteryWine.schema.obj }; - let nulledSchema = Object.keys(schema).reduce((accumulator, current) => { - accumulator[current] = ""; - return accumulator - }, {}); - - return res.send(nulledSchema); -} - -// TODO IMPLEMENT WITH FRONTEND (unused) -const submitWinesToLottery = async (req, res) => { - const { lottery } = req.body; - const { date, wines } = lottery; - const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine))) - - return Lottery.findOneAndUpdate({ date: date }, { - date: date, - wines: wineObjects - }, { - upsert: true - }).then(_ => res.send(true)) - .catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.', - success: false, - exception: err.message })); -} - - /** - * @apiParam (Request body) {Array} winners List of winners - */ -const submitWinnersToLottery = async (req, res) => { - const { lottery } = req.body; - const { winners, date } = lottery; - - for (let i = 0; i < winners.length; i++) { - let currentWinner = winners[i]; - let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery - await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson - } - - return res.json(true); -} - - /** - * @apiParam (Request body) {Date} date Date of lottery - * @apiParam (Request body) {Number} blue Number of blue tickets - * @apiParam (Request body) {Number} red Number of red tickets - * @apiParam (Request body) {Number} green Number of green tickets - * @apiParam (Request body) {Number} yellow Number of yellow tickets - * @apiParam (Request body) {Number} bought Number of tickets bought - * @apiParam (Request body) {Number} stolen Number of tickets stolen - */ -const submitLottery = async (req, res) => { - const { lottery } = req.body - - const { date, - blue, - red, - yellow, - green, - bought, - stolen } = lottery; - - return Lottery.findOneAndUpdate({ date: date }, { - date: date, - blue: blue, - yellow: yellow, - red: red, - green: green, - bought: bought, - stolen: stolen - }, { - upsert: true - }).then(_ => res.send(true)) - .catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.', - success: false, - exception: err.message })); - - return res.send(true); -}; - -module.exports = { - submitWines, - schema, - submitLottery, - submitWinnersToLottery, - submitWinesToLottery -}; diff --git a/api/virtualLottery.js b/api/virtualLottery.js deleted file mode 100644 index 706016b..0000000 --- a/api/virtualLottery.js +++ /dev/null @@ -1,281 +0,0 @@ -const path = require("path"); -const crypto = require("crypto"); - -const config = require(path.join(__dirname, "/../config/defaults/lottery")); -const Message = require(path.join(__dirname, "/message")); -const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration")); - -const Attendee = require(path.join(__dirname, "/schemas/Attendee")); -const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); -const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); - - -const winners = async (req, res) => { - let winners = await VirtualWinner.find(); - let winnersRedacted = []; - let winner; - for (let i = 0; i < winners.length; i++) { - winner = winners[i]; - winnersRedacted.push({ - name: winner.name, - color: winner.color - }); - } - res.json(winnersRedacted); -}; - -const winnersSecure = async (req, res) => { - let winners = await VirtualWinner.find(); - - return res.json(winners); -}; - -const deleteWinners = async (req, res) => { - await VirtualWinner.deleteMany(); - var io = req.app.get('socketio'); - io.emit("refresh_data", {}); - return res.json(true); -}; - -const attendees = async (req, res) => { - let attendees = await Attendee.find(); - let attendeesRedacted = []; - let attendee; - for (let i = 0; i < attendees.length; i++) { - attendee = attendees[i]; - attendeesRedacted.push({ - name: attendee.name, - raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, - red: attendee.red, - blue: attendee.blue, - green: attendee.green, - yellow: attendee.yellow - }); - } - return res.json(attendeesRedacted); -}; - -const attendeesSecure = async (req, res) => { - let attendees = await Attendee.find(); - - return res.json(attendees); -}; - -const addAttendee = async (req, res) => { - const attendee = req.body; - const { red, blue, yellow, green } = attendee; - - let newAttendee = new Attendee({ - name: attendee.name, - red, - blue, - green, - yellow, - phoneNumber: attendee.phoneNumber, - winner: false - }); - await newAttendee.save(); - - - var io = req.app.get('socketio'); - io.emit("new_attendee", {}); - - return res.send(true); -}; - -const deleteAttendees = async (req, res) => { - await Attendee.deleteMany(); - var io = req.app.get('socketio'); - io.emit("refresh_data", {}); - return res.json(true); -}; - -const drawWinner = async (req, res) => { - let allContestants = await Attendee.find({ winner: false }); - - if (allContestants.length == 0) { - return res.json({ - success: false, - message: "No attendees left that have not won." - }); - } - let raffleColors = []; - for (let i = 0; i < allContestants.length; i++) { - let currentContestant = allContestants[i]; - for (let blue = 0; blue < currentContestant.blue; blue++) { - raffleColors.push("blue"); - } - for (let red = 0; red < currentContestant.red; red++) { - raffleColors.push("red"); - } - for (let green = 0; green < currentContestant.green; green++) { - raffleColors.push("green"); - } - for (let yellow = 0; yellow < currentContestant.yellow; yellow++) { - raffleColors.push("yellow"); - } - } - - raffleColors = shuffle(raffleColors); - - let colorToChooseFrom = - raffleColors[Math.floor(Math.random() * raffleColors.length)]; - let findObject = { winner: false }; - - findObject[colorToChooseFrom] = { $gt: 0 }; - - let tries = 0; - const maxTries = 3; - let contestantsToChooseFrom = undefined; - while (contestantsToChooseFrom == undefined && tries < maxTries) { - const hit = await Attendee.find(findObject); - if (hit && hit.length) { - contestantsToChooseFrom = hit; - break; - } - tries++; - } - if (contestantsToChooseFrom == undefined) { - return res.status(404).send({ - success: false, - message: `Klarte ikke trekke en vinner etter ${maxTries} forsøk.` - }); - } - - let attendeeListDemocratic = []; - - let currentContestant; - for (let i = 0; i < contestantsToChooseFrom.length; i++) { - currentContestant = contestantsToChooseFrom[i]; - for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) { - attendeeListDemocratic.push({ - name: currentContestant.name, - phoneNumber: currentContestant.phoneNumber, - red: currentContestant.red, - blue: currentContestant.blue, - green: currentContestant.green, - yellow: currentContestant.yellow - }); - } - } - - attendeeListDemocratic = shuffle(attendeeListDemocratic); - - let winner = - attendeeListDemocratic[ - Math.floor(Math.random() * attendeeListDemocratic.length) - ]; - - let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ - timestamp_drawn: 1 - }); - - var io = req.app.get('socketio'); - io.emit("winner", { - color: colorToChooseFrom, - name: winner.name, - winner_count: winners.length + 1 - }); - - let newWinnerElement = new VirtualWinner({ - name: winner.name, - phoneNumber: winner.phoneNumber, - color: colorToChooseFrom, - red: winner.red, - blue: winner.blue, - green: winner.green, - yellow: winner.yellow, - id: sha512(winner.phoneNumber, genRandomString(10)), - timestamp_drawn: new Date().getTime() - }); - - await Attendee.update( - { name: winner.name, phoneNumber: winner.phoneNumber }, - { $set: { winner: true } } - ); - - await newWinnerElement.save(); - return res.json({ - success: true, - winner - }); -}; - -const finish = async (req, res) => { - if (!config.gatewayToken) { - return res.json({ - message: "Missing api token for sms gateway.", - success: false - }); - } - - let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ - timestamp_drawn: 1 - }); - - if (winners.length == 0) { - return res.json({ - message: "No winners to draw from.", - success: false - }); - } - - Message.sendInitialMessageToWinners(winners.slice(1)); - - return findAndNotifyNextWinner() - .then(() => res.json({ - success: true, - message: "Sent wine select message to first winner and update message to rest of winners." - })) - .catch(error => res.json({ - message: error["message"] || "Unable to send message to first winner.", - success: false - })) -}; - -const genRandomString = function(length) { - return crypto - .randomBytes(Math.ceil(length / 2)) - .toString("hex") /** convert to hexadecimal format */ - .slice(0, length); /** return required number of characters */ -}; - -const sha512 = function(password, salt) { - var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */ - hash.update(password); - var value = hash.digest("hex"); - return value; -}; - -function shuffle(array) { - let currentIndex = array.length, - temporaryValue, - randomIndex; - - // While there remain elements to shuffle... - while (0 !== currentIndex) { - // Pick a remaining element... - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex -= 1; - - // And swap it with the current element. - temporaryValue = array[currentIndex]; - array[currentIndex] = array[randomIndex]; - array[randomIndex] = temporaryValue; - } - - return array; -} - -module.exports = { - deleteWinners, - deleteAttendees, - winners, - winnersSecure, - drawWinner, - finish, - attendees, - attendeesSecure, - addAttendee -} - diff --git a/api/virtualRegistration.js b/api/virtualRegistration.js deleted file mode 100644 index ec869a3..0000000 --- a/api/virtualRegistration.js +++ /dev/null @@ -1,200 +0,0 @@ -const path = require("path"); - -const _wineFunctions = require(path.join(__dirname, "/wine")); -const _personFunctions = require(path.join(__dirname, "/person")); -const Message = require(path.join(__dirname, "/message")); -const VirtualWinner = require(path.join( - __dirname, "/schemas/VirtualWinner" -)); -const PreLotteryWine = require(path.join( - __dirname, "/schemas/PreLotteryWine" -)); - - -const getWinesToWinnerById = async (req, res) => { - let id = req.params.id; - let foundWinner = await VirtualWinner.findOne({ id: id }); - - if (!foundWinner) { - return res.json({ - success: false, - message: "No winner with this id.", - existing: false, - turn: false - }); - } - - let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); - if ( - allWinners[0].id != foundWinner.id || - foundWinner.timestamp_limit == undefined || - foundWinner.timestamp_sent == undefined - ) { - return res.json({ - success: false, - message: "Not the winner next in line!", - existing: true, - turn: false - }); - } - - return res.json({ - success: true, - existing: true, - turn: true, - name: foundWinner.name, - color: foundWinner.color - }); -}; - -const registerWinnerSelection = async (req, res) => { - let id = req.params.id; - let wineName = req.body.wineName; - let foundWinner = await VirtualWinner.findOne({ id: id }); - - if (!foundWinner) { - return res.json({ - success: false, - message: "No winner with this id." - }) - } else if (foundWinner.timestamp_limit < new Date().getTime()) { - return res.json({ - success: false, - message: "Timelimit expired, you will receive a wine after other users have chosen.", - limit: true - }) - } - - let date = new Date(); - date.setHours(5, 0, 0, 0); - let prelotteryWine = await PreLotteryWine.findOne({ name: wineName }); - - if (!prelotteryWine) { - return res.json({ - success: false, - message: "No wine with this name.", - wine: false - }); - } - - let wonWine = await _wineFunctions.findSaveWine(prelotteryWine); - await prelotteryWine.delete(); - await _personFunctions.findSavePerson(foundWinner, wonWine, date); - await Message.sendWineConfirmation(foundWinner, wonWine, date); - - await foundWinner.delete(); - console.info("Saved winners choice."); - - return findAndNotifyNextWinner() - .then(() => res.json({ - message: "Choice saved and next in line notified.", - success: true - })) - .catch(error => res.json({ - message: error["message"] || "Error when notifing next winner.", - success: false - })) -}; - -const chooseLastWineForUser = (winner, preLotteryWine) => { - let date = new Date(); - date.setHours(5, 0, 0, 0); - - return _wineFunctions.findSaveWine(preLotteryWine) - .then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date)) - .then(() => preLotteryWine.delete()) - .then(() => Message.sendLastWinnerMessage(winner, preLotteryWine)) - .then(() => winner.delete()) - .catch(err => { - console.log("Error thrown from chooseLastWineForUser: " + err); - throw err; - }) -} - -const findAndNotifyNextWinner = async () => { - let nextWinner = undefined; - - let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); - let winesLeft = await PreLotteryWine.find(); - - if (winnersLeft.length > 1) { - console.log("multiple winners left, choose next in line") - nextWinner = winnersLeft[0]; // multiple winners left, choose next in line - } else if (winnersLeft.length == 1 && winesLeft.length > 1) { - console.log("one winner left, but multiple wines") - nextWinner = winnersLeft[0] // one winner left, but multiple wines - } else if (winnersLeft.length == 1 && winesLeft.length == 1) { - console.log("one winner and one wine left, choose for user") - nextWinner = winnersLeft[0] // one winner and one wine left, choose for user - wine = winesLeft[0] - return chooseLastWineForUser(nextWinner, wine); - } - - if (nextWinner) { - return Message.sendWineSelectMessage(nextWinner) - .then(messageResponse => startTimeout(nextWinner.id)) - } else { - console.info("All winners notified. Could start cleanup here."); - return Promise.resolve({ - message: "All winners notified." - }) - } -}; - -const sendNotificationToWinnerById = async (req, res) => { - const { id } = req.params; - let winner = await VirtualWinner.findOne({ id: id }); - - if (!winner) { - return res.json({ - message: "No winner with this id.", - success: false - }) - } - - return Message.sendWineSelectMessage(winner) - .then(success => res.json({ - success: success, - message: `Message sent to winner ${id} successfully!` - })) - .catch(err => res.json({ - success: false, - message: "Error while trying to send sms.", - error: err - })) -} - -function startTimeout(id) { - const minute = 60000; - const minutesForTimeout = 10; - - console.log(`Starting timeout for user ${id}.`); - console.log(`Timeout duration: ${ minutesForTimeout * minute }`) - setTimeout(async () => { - let virtualWinner = await VirtualWinner.findOne({ id: id }); - if (!virtualWinner) { - console.log(`Timeout done for user ${id}, but user has already sent data.`); - return; - } - console.log(`Timeout done for user ${id}, sending update to user.`); - - Message.sendWineSelectMessageTooLate(virtualWinner); - - virtualWinner.timestamp_drawn = new Date().getTime(); - virtualWinner.timestamp_limit = null; - virtualWinner.timestamp_sent = null; - await virtualWinner.save(); - - findAndNotifyNextWinner(); - }, minutesForTimeout * minute); - - return Promise.resolve() -} - -module.exports = { - getWinesToWinnerById, - registerWinnerSelection, - findAndNotifyNextWinner, - - sendNotificationToWinnerById -}; From b321f2cfdd8e4d2eaabeab1c1e80a83ef576d51c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 23:04:15 +0100 Subject: [PATCH 038/115] Fixed import location for redis. --- api/controllers/chatController.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/controllers/chatController.js b/api/controllers/chatController.js index fb3d8b6..b680cb7 100644 --- a/api/controllers/chatController.js +++ b/api/controllers/chatController.js @@ -1,5 +1,6 @@ const path = require("path"); -const { history, clearHistory } = require(path.join(__dirname + "/../api/redis")); +const { history, clearHistory } = require(path.join(__dirname + "/../redis")); +console.log("loading chat"); const getAllHistory = (req, res) => { let { page, limit } = req.query; From 56095cb3e22874b2ada9a560c3cf50dd4196696a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 23:04:49 +0100 Subject: [PATCH 039/115] Linting. --- server.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/server.js b/server.js index cb9d42e..b06a6d7 100644 --- a/server.js +++ b/server.js @@ -18,24 +18,26 @@ const MongoStore = require("connect-mongo")(session); // mongoose / database console.log("Trying to connect with mongodb.."); mongoose.promise = global.Promise; -mongoose.connect("mongodb://localhost/vinlottis", { - useCreateIndex: true, - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 10000 // initial connection timeout -}).then(_ => console.log("Mongodb connection established!")) -.catch(err => { - console.log(err); - console.error("ERROR! Mongodb required to run."); - process.exit(1); -}) +mongoose + .connect("mongodb://localhost/vinlottis", { + useCreateIndex: true, + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 10000 // initial connection timeout + }) + .then(_ => console.log("Mongodb connection established!")) + .catch(err => { + console.log(err); + console.error("ERROR! Mongodb required to run."); + process.exit(1); + }); mongoose.set("debug", false); // middleware const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS")); const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders")); -app.use(setupCORS) -app.use(setupHeaders) +app.use(setupCORS); +app.use(setupHeaders); // parse application/json app.use(express.json()); @@ -52,7 +54,7 @@ app.use( }) ); -app.set('socketio', io); // set io instance to key "socketio" +app.set("socketio", io); // set io instance to key "socketio" const passport = require("passport"); const LocalStrategy = require("passport-local"); From cb4a30b5e989f849790abb06f5dcc68f07f0c457 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Tue, 26 Jan 2021 23:05:52 +0100 Subject: [PATCH 040/115] BIGBOY rewrite. All endpoints have been re-worked to be more clear on what they do. They also all have their own controller now. --- api/router.js | 117 ++++++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/api/router.js b/api/router.js index 43d4c2a..a6bf0c4 100644 --- a/api/router.js +++ b/api/router.js @@ -4,69 +4,84 @@ const path = require("path"); const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated")); const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated")); -const update = require(path.join(__dirname, "/update")); -const retrieve = require(path.join(__dirname, "/retrieve")); -const request = require(path.join(__dirname, "/request")); -const subscriptionApi = require(path.join(__dirname, "/subscriptions")); -const userApi = require(path.join(__dirname, "/user")); -const wineinfo = require(path.join(__dirname, "/wineinfo")); -const virtualApi = require(path.join(__dirname, "/virtualLottery")); -const virtualRegistrationApi = require(path.join( - __dirname, "/virtualRegistration" -)); -const lottery = require(path.join(__dirname, "/lottery")); -const chatHistoryApi = require(path.join(__dirname, "/chatHistory")); - const requestController = require(path.join(__dirname, "/controllers/requestController")); +const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController")); +const chatController = require(path.join(__dirname, "/controllers/chatController")); +const userController = require(path.join(__dirname, "/controllers/userController")); +const historyController = require(path.join(__dirname, "/controllers/historyController")); +const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController")); +const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController")); +const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController")); +const lotteryController = require(path.join(__dirname, "/controllers/lotteryController")); +const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController")); const router = express.Router(); -router.get("/wineinfo/search", wineinfo.wineSearch); +router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines); +router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN); +router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById); +router.get("/vinmonopolet/stores/", vinmonopoletController.allStores); +router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores); -router.get("/request/all", setAdminHeaderIfAuthenticated, requestController.getAllRequests); -router.post("/request/new-wine", requestController.addRequest); -router.delete("/request/:id", requestController.deleteRequest); +router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests); +router.post("/request", requestController.addRequest); +router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest); -router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); -router.get("/wineinfo/:ean", wineinfo.byEAN); +// router.get("/wines", wineController.all); // sort = by-date, by-name, by-occurences +// router.update("/wine/:id", mustBeAuthenticated, wineController.update); -router.post("/log/wines", mustBeAuthenticated, update.submitWines); -router.post("/lottery", update.submitLottery); -router.post("/lottery/wines", update.submitWinesToLottery); -// router.delete("/lottery/wine/:id", update.deleteWineFromLottery); -router.post("/lottery/winners", update.submitWinnersToLottery); +router.get("/history", historyController.all); +router.get("/history/latest", historyController.latest); +router.get("/history/by-wins/", historyController.orderByWins); +router.get("/history/by-color/", historyController.groupByColor); +router.get("/history/by-date/:date", historyController.byDate); +router.get("/history/by-name/:name", historyController.byName); +router.get("/history/by-date/", historyController.groupByDate); +// router.delete("/highscore/:id", highscoreController.deletePersonById); -router.get("/wines/prelottery", retrieve.prelotteryWines); -router.get("/purchase/statistics", retrieve.allPurchase); -router.get("/purchase/statistics/color", retrieve.purchaseByColor); -router.get("/highscore/statistics", retrieve.highscore) -router.get("/wines/statistics", retrieve.allWines); -router.get("/wines/statistics/overall", retrieve.allWinesSummary); +router.get("/lottery/wines", prelotteryWineController.allWines); +router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById); +router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines); +router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines); +router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById); +router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById); -router.get("/lottery/all", lottery.all); -router.get("/lottery/latest", lottery.latest); -router.get("/lottery/by-date/:date", lottery.byEpochDate); -router.get("/lottery/by-name/:name", lottery.byName); +router.get("/lottery/attendees", attendeeController.allAttendees); +router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees); +router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee); +router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById); +router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById); -router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners); -router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees); -router.get('/virtual/winner/draw', virtualApi.drawWinner); -router.get('/virtual/winner/all', virtualApi.winners); -router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure); -router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish); -router.get('/virtual/attendee/all', virtualApi.attendees); -router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure); -router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee); +router.get("/lottery/winners", winnerController.allWinners); +router.get("/lottery/winner/:id", winnerController.winnerById); +router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners); +router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners); +router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById); -router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById); -router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById); -router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection); +router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner); +router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery); +router.get("/lottery/:epoch", lotteryController.lotteryByDate); +router.get("/lottery/", lotteryController.allLotteries); -router.get('/chat/history', chatHistoryApi.getAllHistory) -router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory) +// router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status); +router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start); +// router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop); +router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById); +router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById); -router.post('/login', userApi.login); -router.post('/register', mustBeAuthenticated, userApi.register); -router.get('/logout', userApi.logout); +router.get("/chat/history", chatController.getAllHistory); +router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory); + +router.post("/login", userController.login); +router.post("/register", userController.register); +router.get("/logout", userController.logout); + +// router.get("/", documentation.apiInfo); + +// router.get("/wine/schema", mustBeAuthenticated, update.schema); +// router.get("/purchase/statistics", retrieve.allPurchase); +// router.get("/highscore/statistics", retrieve.highscore); +// router.get("/wines/statistics", retrieve.allWines); +// router.get("/wines/statistics/overall", retrieve.allWinesSummary); module.exports = router; From 57fe7d444b8f7fe2e9674094e4ef2e184a106fb2 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 15 Feb 2021 22:34:51 +0100 Subject: [PATCH 041/115] Used correct func name for wines from vinmonpolet. --- api/controllers/vinmonopoletController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/vinmonopoletController.js b/api/controllers/vinmonopoletController.js index eac34b1..b68ec51 100644 --- a/api/controllers/vinmonopoletController.js +++ b/api/controllers/vinmonopoletController.js @@ -28,7 +28,7 @@ function wineByEAN(req, res) { function wineById(req, res) { const { id } = req.params; - return vinmonopoletRepository.searchById(id).then(wines => + return vinmonopoletRepository.wineById(id).then(wines => res.json({ wine: wines[0], success: true From 7292cf7983ccea309740b60fc8a23620f0f12a64 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 15 Feb 2021 22:36:49 +0100 Subject: [PATCH 042/115] Endpoint for getting wine schema. This is for manual registration of prelottery wines from the admin page. --- api/controllers/lotteryWineController.js | 23 ++++++++++++++++++++++- api/prelotteryWine.js | 13 ++++++++++++- api/router.js | 1 + 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js index 9e0c283..7274dac 100644 --- a/api/controllers/lotteryWineController.js +++ b/api/controllers/lotteryWineController.js @@ -169,11 +169,32 @@ const deleteWines = (req, res) => { }); }; +const wineSchema = (req, res) => { + return prelotteryWineRepository + .wineSchema() + .then(schema => + res.send({ + schema: schema, + message: `Wine schema template.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch wine schema template." + }); + }); +}; + module.exports = { allWines, addWines, wineById, updateWineById, deleteWineById, - deleteWines + deleteWines, + wineSchema }; diff --git a/api/prelotteryWine.js b/api/prelotteryWine.js index 453c10f..53b87cf 100644 --- a/api/prelotteryWine.js +++ b/api/prelotteryWine.js @@ -70,11 +70,22 @@ const deleteWines = () => { return PreLotteryWine.deleteMany(); }; +const wineSchema = () => { + let schema = { ...PreLotteryWine.schema.obj }; + let nulledSchema = Object.keys(schema).reduce((accumulator, current) => { + accumulator[current] = ""; + return accumulator; + }, {}); + + return Promise.resolve(nulledSchema); +}; + module.exports = { allWines, addWines, wineById, updateWineById, deleteWineById, - deleteWines + deleteWines, + wineSchema }; diff --git a/api/router.js b/api/router.js index a6bf0c4..5ab7f16 100644 --- a/api/router.js +++ b/api/router.js @@ -40,6 +40,7 @@ router.get("/history/by-date/", historyController.groupByDate); // router.delete("/highscore/:id", highscoreController.deletePersonById); router.get("/lottery/wines", prelotteryWineController.allWines); +router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema); router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById); router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines); router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines); From 3d99a3e5f26c4d42e101b2fd35ec2555faa21b98 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 17 Feb 2021 19:17:36 +0100 Subject: [PATCH 043/115] Wine repository gets function for all wines. --- api/wine.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/wine.js b/api/wine.js index 90142a0..d4bca3c 100644 --- a/api/wine.js +++ b/api/wine.js @@ -25,6 +25,11 @@ const addWine = async wine => { } }; -module.exports = { - addWine +const allWines = () => { + return Wine.find(); +}; + +module.exports = { + addWine, + allWines }; From c98ccbc3f0fe423a0b6495690349fb3a6a436b83 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 17 Feb 2021 19:19:13 +0100 Subject: [PATCH 044/115] We want a mongo instance of both winner and wine. The inputs to these functions are objects, if we want to use mongoose we need to get a new instance of wine and winner. --- api/history.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/history.js b/api/history.js index e01bc29..72680d1 100644 --- a/api/history.js +++ b/api/history.js @@ -1,7 +1,7 @@ const path = require("path"); const Winner = require(path.join(__dirname, "/schemas/Highscore")); -const Wine = require(path.join(__dirname, "/schemas/Wine")); +const WineRepository = require(path.join(__dirname, "/wine")); class HistoryByDateNotFound extends Error { constructor(message = "History for given date not found.") { @@ -24,10 +24,15 @@ const addWinnerWithWine = async (winner, wine) => { const exisitingWinner = await Winner.findOne({ name: winner.name }); + const savedWine = await WineRepository.addWine(wine); const date = new Date(); date.setHours(5, 0, 0, 0); - const winObject = { date, wine, color: winner.color }; + const winObject = { + date: date, + wine: savedWine, + color: winner.color + }; if (exisitingWinner == undefined) { const newWinner = new Winner({ From 38eb98e68b5447757e183031cdbb656ab21b6e14 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 17 Feb 2021 19:20:52 +0100 Subject: [PATCH 045/115] Sorting happens in mongo query, no longer in js. --- api/history.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/history.js b/api/history.js index 72680d1..30ed510 100644 --- a/api/history.js +++ b/api/history.js @@ -172,7 +172,8 @@ const latest = () => { }; // lottery - byDate -const groupByDate = (includeWines = false, sort = "desc") => { +const groupByDate = (includeWines = false, sort = "asc") => { + const sortDirection = sort == "asc" ? -1 : 1; const query = [ { $unwind: "$wins" @@ -198,7 +199,7 @@ const groupByDate = (includeWines = false, sort = "desc") => { }, { $sort: { - _id: -1 + date: sortDirection } } ]; @@ -214,7 +215,7 @@ const groupByDate = (includeWines = false, sort = "desc") => { }); } - return Winner.aggregate(query).then(lotteries => (sort != "asc" ? lotteries : lotteries.reverse())); + return Winner.aggregate(query); }; // highscore - byColor From bca4558d590c41cde247263b04a604743107c16a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 17 Feb 2021 19:21:33 +0100 Subject: [PATCH 046/115] orderByWins has limit parameter to slice response Limit the number of rows returned, used for frontpage where we display max 20. --- api/history.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/api/history.js b/api/history.js index 30ed510..6febeb4 100644 --- a/api/history.js +++ b/api/history.js @@ -269,7 +269,7 @@ const groupByColor = (includeWines = false) => { // highscore - byWineOccurences // highscore - byWinCount -const orderByWins = (includeWines = false) => { +const orderByWins = (includeWines = false, limit = undefined) => { let query = [ { $project: { @@ -315,16 +315,15 @@ const orderByWins = (includeWines = false) => { query = includeWinesSubQuery.concat(query); } - return Winner.aggregate(query); + return Winner.aggregate(query).then(winners => { + if (limit == null) { + return winners; + } + + return winners.slice(0, limit); + }); }; -// highscore - deleteWinner : remove for GDPR purpose - -// lottery - deleteWinner : remove for GDPR purpose -// lottery - update : manual lottery -// lottery - add : manual lottery -// lottery - archive - module.exports = { addWinnerWithWine, all, From 1c40fae69de3f8905662d4e6b0b0e1f0a426f1c9 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 17 Feb 2021 19:24:38 +0100 Subject: [PATCH 047/115] Repository gets new search winner by query func. Has sort parameter that defaults to newest first. --- api/history.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/history.js b/api/history.js index 6febeb4..dce97f7 100644 --- a/api/history.js +++ b/api/history.js @@ -125,6 +125,18 @@ const byName = (name, sort = "desc") => { }); }; +// highscore +const search = (query, sort = "desc") => { + return Winner.find({ name: { $regex: query, $options: "i" } }, ["name"]).then(winners => { + if (winners) { + winners = sort === "desc" ? winners.reverse() : winners; + return winners; + } else { + throw new HistoryForUserNotFound(); + } + }); +}; + // lottery const latest = () => { const query = [ @@ -328,6 +340,8 @@ module.exports = { addWinnerWithWine, all, byDate, + byName, + search, latest, groupByDate, groupByColor, From 56d2513a9cd940e6d9fbb4f17de9b404d663cde1 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:38:49 +0100 Subject: [PATCH 048/115] Incorrect history.js function called. --- api/controllers/historyController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/historyController.js b/api/controllers/historyController.js index 382a9c5..345a624 100644 --- a/api/controllers/historyController.js +++ b/api/controllers/historyController.js @@ -91,7 +91,7 @@ const groupByDate = (req, res) => { } return historyRepository - .groupedByDate(includeWines == "true", sort) + .groupByDate(includeWines == "true", sort) .then(lotteries => res.send({ lotteries: lotteries, From 68b4e96ad0f338785476ad2260ed73445fa47534 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:39:27 +0100 Subject: [PATCH 049/115] Import with lowercase name, it's not a class. --- api/history.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/history.js b/api/history.js index dce97f7..46195ce 100644 --- a/api/history.js +++ b/api/history.js @@ -1,7 +1,7 @@ const path = require("path"); const Winner = require(path.join(__dirname, "/schemas/Highscore")); -const WineRepository = require(path.join(__dirname, "/wine")); +const wineRepository = require(path.join(__dirname, "/wine")); class HistoryByDateNotFound extends Error { constructor(message = "History for given date not found.") { @@ -24,7 +24,7 @@ const addWinnerWithWine = async (winner, wine) => { const exisitingWinner = await Winner.findOne({ name: winner.name }); - const savedWine = await WineRepository.addWine(wine); + const savedWine = await wineRepository.addWine(wine); const date = new Date(); date.setHours(5, 0, 0, 0); From 787882e753d6bb18460bb221f91ca57e5320deb8 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:40:14 +0100 Subject: [PATCH 050/115] History ordered by wins has limit parameter. --- api/controllers/historyController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/historyController.js b/api/controllers/historyController.js index 345a624..99e4710 100644 --- a/api/controllers/historyController.js +++ b/api/controllers/historyController.js @@ -185,7 +185,7 @@ const groupByColor = (req, res) => { }; const orderByWins = (req, res) => { - const { includeWines } = req.query; + const { includeWines, limit } = req.query; if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { return res.status(400).send({ @@ -195,7 +195,7 @@ const orderByWins = (req, res) => { } return historyRepository - .orderByWins(includeWines == "true") + .orderByWins(includeWines == "true", limit) .then(winners => res.send({ winners: winners, From 930c458d9c74f07340d810bf98818824ed0238bf Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:40:35 +0100 Subject: [PATCH 051/115] Search history for winner name. --- api/controllers/historyController.js | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/api/controllers/historyController.js b/api/controllers/historyController.js index 99e4710..a62634a 100644 --- a/api/controllers/historyController.js +++ b/api/controllers/historyController.js @@ -156,6 +156,34 @@ const byName = (req, res) => { }); }; +const search = (req, res) => { + const { name, sort } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + return historyRepository + .search(name, sort) + .then(winners => + res.send({ + winners: winners || [], + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by name." + }); + }); +}; + const groupByColor = (req, res) => { const { includeWines } = req.query; @@ -218,6 +246,7 @@ module.exports = { groupByDate, latest, byName, + search, groupByColor, orderByWins }; From 4bd3b688e9e7c4fdc26d7ecb22c7586c2bc15155 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:44:54 +0100 Subject: [PATCH 052/115] Get wines by id or search with wine object. Search with wine object tries to find wines matching name, year and id. This is used for finding a wine from a prelottery wine where their _id do not match. --- api/router.js | 3 ++- api/wine.js | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/api/router.js b/api/router.js index 5ab7f16..af2546a 100644 --- a/api/router.js +++ b/api/router.js @@ -27,7 +27,8 @@ router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequ router.post("/request", requestController.addRequest); router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest); -// router.get("/wines", wineController.all); // sort = by-date, by-name, by-occurences +router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences +router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences // router.update("/wine/:id", mustBeAuthenticated, wineController.update); router.get("/history", historyController.all); diff --git a/api/wine.js b/api/wine.js index d4bca3c..a15b05d 100644 --- a/api/wine.js +++ b/api/wine.js @@ -1,6 +1,8 @@ const path = require("path"); const Wine = require(path.join(__dirname, "/schemas/Wine")); +const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + const addWine = async wine => { let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }); @@ -29,7 +31,29 @@ const allWines = () => { return Wine.find(); }; +const wineById = id => { + return Wine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return wine; + }); +}; + +const findWine = wine => { + return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return wine; + }); +}; + module.exports = { addWine, - allWines + allWines, + wineById, + findWine }; From a28a8ccacb3c91c843dbba2192778756b6a44391 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:50:30 +0100 Subject: [PATCH 053/115] When selecting prize, add winner to wine. Changed the way we register a prize for winner. Now we have a new prize_selected boolean field on a winner. This is used to filter on when finding what winners have not selected a prize yet. This also replaces the previous method of removing virtualWinners after they selected a prize. PrelotteryWine also get's a winner reference. This is used to filter on when finding what prizes are left, and also makes it easier to archive/register a lottery when the wine has a winner attached. --- api/prelotteryWine.js | 14 +++++++++++++- api/prizeDistribution.js | 15 ++++++++------- api/schemas/PreLotteryWine.js | 6 +++++- api/schemas/VirtualWinner.js | 4 ++++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/api/prelotteryWine.js b/api/prelotteryWine.js index 53b87cf..8f6f866 100644 --- a/api/prelotteryWine.js +++ b/api/prelotteryWine.js @@ -4,7 +4,11 @@ const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); const allWines = () => { - return PreLotteryWine.find(); + return PreLotteryWine.find().populate("winner"); +}; + +const allWinesWithoutWinner = () => { + return PreLotteryWine.find({ winner: { $exists: false } }); }; const addWines = wines => { @@ -56,6 +60,12 @@ const updateWineById = (id, updateModel) => { }); }; +const addWinnerToWine = (wine, winner) => { + wine.winner = winner; + winner.prize_selected = true; + return Promise.all(wine.save(), winner.save()); +}; + const deleteWineById = id => { return PreLotteryWine.findOne({ _id: id }).then(wine => { if (wine == null) { @@ -82,8 +92,10 @@ const wineSchema = () => { module.exports = { allWines, + allWinesWithoutWinner, addWines, wineById, + addWinnerToWine, updateWineById, deleteWineById, deleteWines, diff --git a/api/prizeDistribution.js b/api/prizeDistribution.js index e530574..4aafb4f 100644 --- a/api/prizeDistribution.js +++ b/api/prizeDistribution.js @@ -24,10 +24,11 @@ const verifyWinnerNextInLine = async id => { } let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); + if ( - allWinners[0].id != foundWinner.id || foundWinner.timestamp_limit == undefined || - foundWinner.timestamp_sent == undefined + foundWinner.timestamp_sent == undefined || + foundWinner.prize_selected == true ) { throw new WineSelectionWinnerNotNextInLine(); } @@ -47,8 +48,8 @@ const claimPrize = (winner, wine) => { const notifyNextWinner = async () => { let nextWinner = undefined; - const winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); - const winesLeft = await PreLotteryWine.find(); + const winnersLeft = await VirtualWinner.find({ prize_selected: false }).sort({ timestamp_drawn: 1 }); + const winesLeft = await PreLotteryWine.find({ winner: { $exists: false } }); if (winnersLeft.length > 1) { console.log("multiple winners left, choose next in line"); @@ -60,7 +61,7 @@ const notifyNextWinner = async () => { console.log("one winner and one wine left, choose for user"); nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user wine = winesLeft[0]; - return claimPrize(nextWinner, wine); + return claimPrize(wine, nextWinner); } if (nextWinner) { @@ -82,7 +83,7 @@ function startTimeout(id) { console.log(`Starting timeout for user ${id}.`); console.log(`Timeout duration: ${minutesForTimeout * minute}`); setTimeout(async () => { - let virtualWinner = await VirtualWinner.findOne({ id: id }); + let virtualWinner = await VirtualWinner.findOne({ id: id, prize_selected: false }); if (!virtualWinner) { console.log(`Timeout done for user ${id}, but user has already sent data.`); return; @@ -96,7 +97,7 @@ function startTimeout(id) { virtualWinner.timestamp_sent = null; await virtualWinner.save(); - findAndNotifyNextWinner(); + notifyNextWinner(); }, minutesForTimeout * minute); return Promise.resolve(); diff --git a/api/schemas/PreLotteryWine.js b/api/schemas/PreLotteryWine.js index 7ff3ad4..64f950c 100644 --- a/api/schemas/PreLotteryWine.js +++ b/api/schemas/PreLotteryWine.js @@ -9,7 +9,11 @@ const PreLotteryWine = new Schema({ year: Number, image: String, price: String, - country: String + country: String, + winner: { + type: Schema.Types.ObjectId, + ref: "VirtualWinner" + } }); module.exports = mongoose.model("PreLotteryWine", PreLotteryWine); diff --git a/api/schemas/VirtualWinner.js b/api/schemas/VirtualWinner.js index 94f69d1..f18ced1 100644 --- a/api/schemas/VirtualWinner.js +++ b/api/schemas/VirtualWinner.js @@ -10,6 +10,10 @@ const VirtualWinner = new Schema({ red: Number, yellow: Number, id: String, + prize_selected: { + type: Boolean, + default: false + }, timestamp_drawn: Number, timestamp_sent: Number, timestamp_limit: Number From 70c80849df7a3145685d701ba1bf364897d4a2be Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:54:15 +0100 Subject: [PATCH 054/115] No longer delete winner as part of selecting prize. Winners are rather marked with prize_selected and have it's own endpoint to remove all winners. --- api/prizeDistribution.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/prizeDistribution.js b/api/prizeDistribution.js index 4aafb4f..e04eb89 100644 --- a/api/prizeDistribution.js +++ b/api/prizeDistribution.js @@ -5,9 +5,10 @@ const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); const message = require(path.join(__dirname, "/message")); -const highscoreRepository = require(path.join(__dirname, "/winner")); +const historyRepository = require(path.join(__dirname, "/history")); +const winnerRepository = require(path.join(__dirname, "/winner")); const wineRepository = require(path.join(__dirname, "/wine")); -const lottery = require(path.join(__dirname, "/lottery")); +const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine")); const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join( __dirname, @@ -36,12 +37,11 @@ const verifyWinnerNextInLine = async id => { return Promise.resolve(foundWinner); }; -const claimPrize = (winner, wine) => { +const claimPrize = (wine, winner) => { return wineRepository .addWine(wine) - .then(_ => lottery.deleteWineById(wine.id)) // prelotteryWine.deleteById - .then(_ => highscoreRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine - .then(_ => lottery.addWinnerWithWine(winner, wine)) + .then(_ => prelotteryWineRepository.addWinnerToWine(wine, winner)) // prelotteryWine.deleteById + .then(_ => historyRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine .then(_ => message.sendWineConfirmation(winner, wine)); }; From 9fd67a6bc3254386fc5a0f9dad104d6d510832c0 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:55:41 +0100 Subject: [PATCH 055/115] Removed unused parameter. --- api/prelotteryWine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/prelotteryWine.js b/api/prelotteryWine.js index 8f6f866..bac62de 100644 --- a/api/prelotteryWine.js +++ b/api/prelotteryWine.js @@ -30,7 +30,7 @@ const addWines = wines => { return Promise.all(prelotteryWines); }; -const wineById = (id, updateModel) => { +const wineById = id => { return PreLotteryWine.findOne({ _id: id }).then(wine => { if (wine == null) { throw new WineNotFound(); From dcaaeae51f1835bff6abed11f79662a632ca8952 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:58:09 +0100 Subject: [PATCH 056/115] Get prizes only returns wines without a winner already. --- api/controllers/prizeDistributionController.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/controllers/prizeDistributionController.js b/api/controllers/prizeDistributionController.js index b0c176a..8e129f4 100644 --- a/api/controllers/prizeDistributionController.js +++ b/api/controllers/prizeDistributionController.js @@ -39,10 +39,13 @@ const getPrizesForWinnerById = (req, res) => { return prizeDistribution .verifyWinnerNextInLine(id) - .then(_ => lottery.allWines()) - .then(wines => + .then(winner => { + return prelotteryWineRepository.allWinesWithoutWinner().then(wines => [wines, winner]); + }) + .then(([wines, winner]) => res.send({ wines: wines, + winner: winner, message: "Wines to select from", success: true }) From ab58a45da5c4bfbfc4eea4fa078485f17c2b7224 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:58:40 +0100 Subject: [PATCH 057/115] Better var name and response message text. --- api/controllers/prizeDistributionController.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/controllers/prizeDistributionController.js b/api/controllers/prizeDistributionController.js index 8e129f4..e470f95 100644 --- a/api/controllers/prizeDistributionController.js +++ b/api/controllers/prizeDistributionController.js @@ -1,14 +1,15 @@ const path = require("path"); const prizeDistribution = require(path.join(__dirname, "../prizeDistribution")); -const winner = require(path.join(__dirname, "../winner")); +const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine")); +const winnerRepository = require(path.join(__dirname, "../winner")); const message = require(path.join(__dirname, "../message")); const start = async (req, res) => { - const allWinners = await winners.allWinners(); + const allWinners = await winnerRepository.allWinners(); if (allWinners.length === 0) { return res.status(503).send({ - message: "No winners left.", + message: "No winners found to distribute prizes to.", success: false }); } From 6003151e3b33189c624ccabe65b08d2bcaaa4a8f Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 20:58:57 +0100 Subject: [PATCH 058/115] Better error handling for claim prize. We wrap await function calls with try catch and return the res if any error occur. --- api/controllers/prizeDistributionController.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/api/controllers/prizeDistributionController.js b/api/controllers/prizeDistributionController.js index e470f95..2a91dd0 100644 --- a/api/controllers/prizeDistributionController.js +++ b/api/controllers/prizeDistributionController.js @@ -65,15 +65,25 @@ const submitPrizeForWinnerById = async (req, res) => { const { id } = req.params; const { wine } = req.body; - const winner = await prizeDistribution.verifyWinnerNextInLine(id); - const prelotteryWine = await lottery.wineById(wine.id); + let prelotteryWine, winner; + try { + prelotteryWine = await prelotteryWineRepository.wineById(wine._id); + winner = await winnerRepository.winnerById(id, true); + } catch (error) { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while claiming prize.", + success: false + }); + } return prizeDistribution - .claimPrize(winner, prelotteryWine) + .claimPrize(prelotteryWine, winner) .then(_ => prizeDistribution.notifyNextWinner()) .then(_ => res.send({ - message: `${winner.name} successfully claimed prize: ${wine.name}`, + message: `${winner.name} successfully claimed prize: ${prelotteryWine.name}`, success: true }) ) From 824bd60c02ade3ec00063cb5fbb7449a39356090 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:00:32 +0100 Subject: [PATCH 059/115] Updated internal name of wine schema. --- api/schemas/Wine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/schemas/Wine.js b/api/schemas/Wine.js index c4c89f6..b3c9029 100644 --- a/api/schemas/Wine.js +++ b/api/schemas/Wine.js @@ -1,7 +1,7 @@ const mongoose = require("mongoose"); const Schema = mongoose.Schema; -const Wine = new Schema({ +const WineSchema = new Schema({ name: String, vivinoLink: String, rating: Number, @@ -13,4 +13,4 @@ const Wine = new Schema({ country: String }); -module.exports = mongoose.model("Wine", Wine); +module.exports = mongoose.model("Wine", WineSchema); From fc029f80df6bc77bbd63c2ef05fef417e7210ec3 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:02:07 +0100 Subject: [PATCH 060/115] New message & wine controllers! These interface towards the respective repositories. Wine: - /api/wine/:id - getWineById - /api/wines - allWines Messages: - /api/lottery/messages/winner/:id - notifyWinnerById --- api/controllers/messageController.js | 30 +++++++++++++++++ api/controllers/wineController.js | 50 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 api/controllers/messageController.js create mode 100644 api/controllers/wineController.js diff --git a/api/controllers/messageController.js b/api/controllers/messageController.js new file mode 100644 index 0000000..94dffe9 --- /dev/null +++ b/api/controllers/messageController.js @@ -0,0 +1,30 @@ +const path = require("path"); +const messageRepository = require(path.join(__dirname, "../message")); +const winnerRepository = require(path.join(__dirname, "../winner")); + +const notifyWinnerById = (req, res) => { + const { id } = req.params; + const isAdmin = req.isAuthenticated(); + + return winnerRepository + .winnerById(id, isAdmin) + .then(winner => messageRepository.sendPrizeSelectionLink(winner)) + .then(messageResponse => + res.send({ + messageResponse, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while sending message to winner by id.", + success: false + }); + }); +}; + +module.exports = { + notifyWinnerById +}; diff --git a/api/controllers/wineController.js b/api/controllers/wineController.js new file mode 100644 index 0000000..d9e8cc6 --- /dev/null +++ b/api/controllers/wineController.js @@ -0,0 +1,50 @@ +const path = require("path"); +const wineRepository = require(path.join(__dirname, "../wine")); + +const allWines = (req, res) => { + // TODO add "includeWinners" + + return wineRepository + .allWines() + .then(wines => + res.send({ + wines: wines, + message: `All wines.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch all wines." + }); + }); +}; + +const wineById = (req, res) => { + const { id } = req.params; + + return wineRepository + .wineById(id) + .then(wine => { + res.send({ + wine, + success: true + }); + }) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching wine by id.", + success: false + }); + }); +}; + +module.exports = { + allWines, + wineById +}; From d0fa89b92bd2479ed35955e2233124118f8252dd Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:05:40 +0100 Subject: [PATCH 061/115] Update lottery winner by id. Endpoint, controller and repository function for updating lottery winner by id and json payload. Keeps previous definition for undefined/null values. --- api/controllers/lotteryWinnerController.js | 31 ++++++++++++++++++++++ api/winner.js | 23 ++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js index 2b56eca..baa7c22 100644 --- a/api/controllers/lotteryWinnerController.js +++ b/api/controllers/lotteryWinnerController.js @@ -98,6 +98,36 @@ const winnerById = (req, res) => { }); }; +const updateWinnerById = (req, res) => { + const { id } = req.params; + const { winner } = req.body; + + if (id == null || id == "undefined") { + return res.status(400).send({ + message: "Unable to update without id.", + success: false + }); + } + + return winnerRepository + .updateWinnerById(id, winner) + .then(winner => + res.send({ + winner, + message: `Updated winner: ${winner.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while updating winner by id.", + success: false + }); + }); +}; + const deleteWinnerById = (req, res) => { const isAdmin = req.isAuthenticated(); const { id } = req.params; @@ -152,6 +182,7 @@ module.exports = { addWinners, allWinners, winnerById, + updateWinnerById, deleteWinnerById, deleteWinners }; diff --git a/api/winner.js b/api/winner.js index 5aa784b..d53a40d 100644 --- a/api/winner.js +++ b/api/winner.js @@ -43,6 +43,28 @@ const winnerById = (id, isAdmin = false) => { }); }; +const updateWinnerById = (id, updateModel) => { + return VirtualWinner.findOne({ id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + const updatedWinner = { + name: updateModel.name != null ? updateModel.name : winner.name, + phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : winner.phoneNumber, + red: updateModel.red != null ? updateModel.red : winner.red, + green: updateModel.green != null ? updateModel.green : winner.green, + blue: updateModel.blue != null ? updateModel.blue : winner.blue, + yellow: updateModel.yellow != null ? updateModel.yellow : winner.yellow, + timestamp_drawn: updateModel.timestamp_drawn != null ? updateModel.timestamp_drawn : winner.timestamp_drawn, + timestamp_limit: updateModel.timestamp_limit != null ? updateModel.timestamp_limit : winner.timestamp_limit, + timestamp_sent: updateModel.timestamp_sent != null ? updateModel.timestamp_sent : winner.timestamp_sent + }; + + return VirtualWinner.updateOne({ id: id }, updatedWinner).then(_ => updatedWinner); + }); +}; + const deleteWinnerById = id => { return VirtualWinner.findOne({ _id: id }).then(winner => { if (winner == null) { @@ -61,6 +83,7 @@ module.exports = { addWinners, allWinners, winnerById, + updateWinnerById, deleteWinnerById, deleteWinners }; From 9823197a480b747b240da810d12b04ec43eeec50 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:06:49 +0100 Subject: [PATCH 062/115] Better validation and error resp validating wines. --- api/controllers/lotteryWineController.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js index 7274dac..38685ed 100644 --- a/api/controllers/lotteryWineController.js +++ b/api/controllers/lotteryWineController.js @@ -21,7 +21,7 @@ const allWines = (req, res) => { }; const addWines = (req, res) => { - const { wines } = req.body; + let { wines } = req.body; if (!(wines instanceof Array)) { return res.status(400).send({ @@ -36,7 +36,7 @@ const addWines = (req, res) => { return Promise.all( requiredAttributes.map(attr => { - if (typeof wine[attr] === "undefined") { + if (typeof wine[attr] === "undefined" || wine[attr] == "") { return Promise.reject({ message: `Incorrect or missing attribute: ${attr}.`, statusCode: 400, @@ -96,6 +96,13 @@ const updateWineById = (req, res) => { const { id } = req.params; const { wine } = req.body; + if (id == null || id == "undefined") { + return res.status(400).send({ + message: "Unable to update without id.", + success: false + }); + } + return prelotteryWineRepository .updateWineById(id, wine) .then(updatedWine => { From b57fb5f1f81bd3aebb5e364f21099b176d4251e6 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:08:37 +0100 Subject: [PATCH 063/115] Admin gets new tabs. Tabs for registering wines, adding attendees, drawing winner, registering/archiving lottery and sending push notifications. Each tab also has a slug and counter attribute. --- frontend/components/AdminPage.vue | 55 +++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/frontend/components/AdminPage.vue b/frontend/components/AdminPage.vue index db9d31b..4a78ce7 100644 --- a/frontend/components/AdminPage.vue +++ b/frontend/components/AdminPage.vue @@ -1,34 +1,75 @@ - From 011aec3dea85b6c0cc90ba632938707c5c8cc43b Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:10:14 +0100 Subject: [PATCH 064/115] Tabs redesigned, update url and can show counter. Tabs have been redesigned and can now display a counter. This counter can be set by emiting a "counter" event from the child. Tabs read and set a ?tab={slug} query parameter when changing tab, when loaded we check for this parameter to automatically select the correct tab. --- frontend/ui/Tabs.vue | 109 +++++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/frontend/ui/Tabs.vue b/frontend/ui/Tabs.vue index b2b7cc2..1495987 100644 --- a/frontend/ui/Tabs.vue +++ b/frontend/ui/Tabs.vue @@ -1,51 +1,80 @@ From 72c18967476389747ea2fad0ef0f924aee900a51 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:14:34 +0100 Subject: [PATCH 065/115] Do api call from within the component itself. --- frontend/components/AllRequestedWines.vue | 46 ++++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/components/AllRequestedWines.vue b/frontend/components/AllRequestedWines.vue index a2996d1..b13e93e 100644 --- a/frontend/components/AllRequestedWines.vue +++ b/frontend/components/AllRequestedWines.vue @@ -5,37 +5,47 @@

Ingen har foreslått noe enda!

- +
\ No newline at end of file + From 2cf4095b970bfa8cc6ce872c1b2622c50a8d010f Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:17:22 +0100 Subject: [PATCH 066/115] Api call from component itself and linting. --- frontend/components/AllWinesPage.vue | 42 ++++++++++------------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/frontend/components/AllWinesPage.vue b/frontend/components/AllWinesPage.vue index 57a65bf..3bb821f 100644 --- a/frontend/components/AllWinesPage.vue +++ b/frontend/components/AllWinesPage.vue @@ -2,10 +2,9 @@

Alle viner

-
+
- Vinnende lodd:
{{ wine.blue == null ? 0 : wine.blue }} @@ -19,43 +18,44 @@
  • {{ winner }} - -  - {{ dateString(wine.dates[index]) }} + -  + {{ + dateString(wine.dates[index]) + }}
-
@@ -84,18 +84,6 @@ h1 { font-weight: 600; } -#wines-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - grid-gap: 2rem; - - - > div { - justify-content: flex-start; - margin-bottom: 2rem; - } -} - .name-wins { display: flex; flex-direction: column; From b5cca00ed4e4ecfc9d7a912c8a887fbbdb9683c8 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:18:01 +0100 Subject: [PATCH 067/115] Liting --- frontend/components/GeneratePage.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/components/GeneratePage.vue b/frontend/components/GeneratePage.vue index 80996d2..4e9f638 100644 --- a/frontend/components/GeneratePage.vue +++ b/frontend/components/GeneratePage.vue @@ -5,7 +5,7 @@ Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'

- + @@ -43,16 +43,16 @@ export default { this.hardStart = true; }, track() { - window.ga('send', 'pageview', '/lottery/generate'); + window.ga("send", "pageview", "/lottery/generate"); } } }; \ No newline at end of file + diff --git a/frontend/components/TodaysPage.vue b/frontend/components/TodaysPage.vue index aece77a..13a989f 100644 --- a/frontend/components/TodaysPage.vue +++ b/frontend/components/TodaysPage.vue @@ -23,7 +23,9 @@ export default { }; }, async mounted() { - prelottery().then(wines => this.wines = wines); + fetch("/api/lottery/wines") + .then(resp => resp.json()) + .then(response => (this.wines = response.wines)); } }; @@ -42,19 +44,18 @@ h1 { } .wines-container { - display: flex; - flex-wrap: wrap; - justify-content: space-evenly; - margin: 0 2rem; + width: 90vw; + padding: 5vw; + + @include desktop { + width: 80vw; + padding: 0 10vw; + } @media (min-width: 1500px) { max-width: 1500px; margin: 0 auto; } - - @include mobile { - flex-direction: column; - } } h3 { @@ -65,23 +66,6 @@ h3 { } } -.inner-wine-container { - display: flex; - flex-direction: row; - margin: auto; - width: 500px; - font-family: Arial; - margin-bottom: 30px; - - @include desktop { - justify-content: center; - } - - @include mobile { - width: auto; - } -} - .right { display: flex; flex-direction: column; From eaf57115e8aef436231fed7e19ce768f46e12e27 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:42:03 +0100 Subject: [PATCH 071/115] Liting. --- frontend/components/VinlottisPage.vue | 80 +++++++++++---------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/frontend/components/VinlottisPage.vue b/frontend/components/VinlottisPage.vue index bbc0719..e41e1f1 100644 --- a/frontend/components/VinlottisPage.vue +++ b/frontend/components/VinlottisPage.vue @@ -1,8 +1,6 @@ @@ -96,11 +91,7 @@ export default { if (!("PushManager" in window)) { return false; } - return ( - Notification.permission !== "granted" || - !this.pushAllowed || - localStorage.getItem("push") == null - ); + return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null; } }, async mounted() { @@ -120,7 +111,7 @@ export default { this.hardStart = way; }, track() { - window.ga('send', 'pageview', '/'); + window.ga("send", "pageview", "/"); }, startCountdown() { this.hardStart = true; @@ -145,7 +136,7 @@ export default { align-items: center; justify-items: start; - @include mobile{ + @include mobile { padding-bottom: 2em; height: 15em; grid-template-rows: repeat(7, 1fr); @@ -156,13 +147,13 @@ export default { grid-column: 2 / -1; display: flex; - h1{ + h1 { font-size: 2em; font-weight: 400; } @include tablet { - h1{ + h1 { font-size: 3em; } grid-row: 2 / 4; @@ -170,7 +161,7 @@ export default { } } - .notification-request-button{ + .notification-request-button { cursor: pointer; } @@ -229,7 +220,7 @@ export default { .icons-container { grid-column: 1 / -1; grid-row: 7 / -1; - @include mobile{ + @include mobile { margin-top: 2em; display: none; } @@ -239,7 +230,7 @@ export default { grid-column: 7 / -1; } - @include desktop{ + @include desktop { grid-row: 4 / -3; grid-column: 7 / 11; } @@ -257,30 +248,27 @@ export default { i { font-size: 5em; - &.icon--heart-sparks{ + &.icon--heart-sparks { grid-column: 2 / 4; grid-row: 2 / 4; align-self: center; justify-self: center; - } - &.icon--face-1{ + &.icon--face-1 { grid-column: 4 / 7; grid-row: 2 / 4; justify-self: center; - } - &.icon--face-3{ + &.icon--face-3 { grid-column: 7 / 10; grid-row: 1 / 4; align-self: center; } - &.icon--ballon{ + &.icon--ballon { grid-column: 9 / 11; grid-row: 3 / 5; - } - &.icon--bottle{ + &.icon--bottle { grid-row: 4 / -1; &:nth-of-type(5) { @@ -297,14 +285,13 @@ export default { &:nth-of-type(8) { grid-column: 7 / 8; } - &:nth-of-type(9){ + &:nth-of-type(9) { grid-column: 8 / 9; align-self: center; } } } } - } h1 { @@ -312,12 +299,12 @@ h1 { font-family: "knowit"; } -.to-lottery{ - color: #333; - text-decoration: none; - display: block; - text-align: center; - margin-bottom: 0; +.to-lottery { + color: #333; + text-decoration: none; + display: block; + text-align: center; + margin-bottom: 0; } .content-container { @@ -326,10 +313,10 @@ h1 { row-gap: 5em; .scroll-info { - display: flex; - align-items: center; - column-gap: 10px; - grid-column: 2 / -2; + display: flex; + align-items: center; + column-gap: 10px; + grid-column: 2 / -2; } .chart-container { @@ -346,8 +333,8 @@ h1 { grid-column: 2 / -2; } - .wines-container { - grid-column: 2 / -2; + .wine-container { + grid-column: 3 / -3; } .icon--arrow-long-right { @@ -356,8 +343,7 @@ h1 { } @include tablet { - - .scroll-info{ + .scroll-info { grid-column: 3 / -3; } From 8bd41cc691ebbad4c806a198e74e7aedc5f90a15 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 21:44:33 +0100 Subject: [PATCH 072/115] Much simpler using prizeDistribution endpoints. --- frontend/components/WinnerPage.vue | 96 +++++++++++++++++------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/frontend/components/WinnerPage.vue b/frontend/components/WinnerPage.vue index 45b0834..7e64433 100644 --- a/frontend/components/WinnerPage.vue +++ b/frontend/components/WinnerPage.vue @@ -1,21 +1,23 @@ diff --git a/frontend/ui/Chat.vue b/frontend/ui/Chat.vue index 55a36e8..dc0db2a 100644 --- a/frontend/ui/Chat.vue +++ b/frontend/ui/Chat.vue @@ -1,6 +1,9 @@ @@ -17,10 +17,9 @@ @@ -35,14 +34,14 @@ export default { components: { Wine }, - data(){ + data() { return { wine: this.requestedElement.wine, locallyRequested: false - } + }; }, props: { - requestedElement: { + requestedElement: { required: true, type: Object }, @@ -53,27 +52,26 @@ export default { } }, methods: { - request(){ - if (this.locallyRequested) - return - console.log("requesting", this.wine) - this.locallyRequested = true - this.requestedElement.count = this.requestedElement.count +1 - requestNewWine(this.wine) + request() { + if (this.locallyRequested) return; + + this.locallyRequested = true; + this.requestedElement.count = this.requestedElement.count + 1; + requestNewWine(this.wine); }, async deleteWine() { - const wine = this.wine + const wine = this.wine; if (window.confirm("Er du sikker på at du vil slette vinen?")) { let response = await deleteRequestedWine(wine); - if (response['success'] == true) { - this.$emit('wineDeleted', wine); + if (response["success"] == true) { + this.$emit("wineDeleted", wine); } else { alert("Klarte ikke slette vinen"); } } - }, - }, -} + } + } +}; \ No newline at end of file + diff --git a/frontend/ui/ScanToVinmonopolet.vue b/frontend/ui/ScanToVinmonopolet.vue index 272a8ac..2567ba2 100644 --- a/frontend/ui/ScanToVinmonopolet.vue +++ b/frontend/ui/ScanToVinmonopolet.vue @@ -1,5 +1,5 @@ From 3886313351406261bb63484d4d603cf3f074d21d Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 22:22:23 +0100 Subject: [PATCH 079/115] Renamed to reflect admin attendee functionality. --- .../registerAttendeePage.vue} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/components/{VirtualLotteryRegistrationPage.vue => admin/registerAttendeePage.vue} (100%) diff --git a/frontend/components/VirtualLotteryRegistrationPage.vue b/frontend/components/admin/registerAttendeePage.vue similarity index 100% rename from frontend/components/VirtualLotteryRegistrationPage.vue rename to frontend/components/admin/registerAttendeePage.vue From 2734e9a840ae8c29a11bbd68551d7fdd934f3703 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 18 Feb 2021 22:42:25 +0100 Subject: [PATCH 080/115] Now dedicated to submitting/archiving lottery. Removed all other features than adding raffles bought, money received and mapping of wines and winners. Also now does api calls from within the component, not external api.js. Better validation and use of toast plugin for user feedback. --- frontend/components/RegisterPage.vue | 669 ++++++++------------------- 1 file changed, 191 insertions(+), 478 deletions(-) diff --git a/frontend/components/RegisterPage.vue b/frontend/components/RegisterPage.vue index 2709b2a..e748687 100644 --- a/frontend/components/RegisterPage.vue +++ b/frontend/components/RegisterPage.vue @@ -1,471 +1,247 @@