diff --git a/.gitignore b/.gitignore index 6623953..86fe51d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ public/index.html public/sw/ config/env/lottery.config.js +config/env/push.config.js # Logs logs diff --git a/api/subscriptions.js b/api/subscriptions.js new file mode 100644 index 0000000..62ac66b --- /dev/null +++ b/api/subscriptions.js @@ -0,0 +1,64 @@ +const express = require("express"); +const path = require("path"); +const router = express.Router(); +const webpush = require("web-push"); //requiring the web-push module +const mongoose = require("mongoose"); +const schedule = require("node-schedule"); + +mongoose.connect("mongodb://localhost:27017/vinlottis", { + useNewUrlParser: true +}); + +const config = require(path.join(__dirname + "/../config/env/push.config")); +const Subscription = require(path.join(__dirname + "/../schemas/Subscription")); + +const vapidKeys = { + publicKey: config.publicKey, + privateKey: config.privateKey +}; +//setting our previously generated VAPID keys +webpush.setVapidDetails( + config.mailto, + vapidKeys.publicKey, + vapidKeys.privateKey +); + +const sendNotification = (subscription, dataToSend = "") => { + webpush.sendNotification(subscription, dataToSend); +}; + +router.use((req, res, next) => { + next(); +}); + +router.route("/save-subscription").post(async (req, res) => { + const subscription = req.body; + await saveToDatabase(subscription); //Method to save the subscription to Database + res.json({ message: "success" }); +}); + +const saveToDatabase = async subscription => { + let found = await Subscription.find({ + endpoint: subscription.endpoint, + "keys.p256dh": subscription.keys.p256dh, + "keys.auth": subscription.keys.auth + }); + + if (found.length > 0) { + return; + } else { + let newSubscription = new Subscription(subscription); + await newSubscription.save(); + } +}; + +schedule.scheduleJob("0 50 14 * * 5", async () => { + let subs = await Subscription.find(); + for (let i = 0; i < subs.length; i++) { + let subscription = subs[i]; //get subscription from your databse here. + const message = "Husk vinlotteriet, det begynner om 10 minutter!"; + sendNotification(subscription, message); + } +}); + +module.exports = router; diff --git a/config/env/push.config.example.js b/config/env/push.config.example.js new file mode 100644 index 0000000..c686afd --- /dev/null +++ b/config/env/push.config.example.js @@ -0,0 +1,5 @@ +module.exports = { + publicKey: "", + privateKey: "", + mailto: "" +}; diff --git a/config/service-worker.config.js b/config/service-worker.config.js index 0bd635b..d042f4b 100644 --- a/config/service-worker.config.js +++ b/config/service-worker.config.js @@ -41,7 +41,8 @@ const ServiceWorkerConfig = { }, plugins: [ new webpack.DefinePlugin({ - __DATE__: new Date().getTime() + __DATE__: new Date().getTime(), + __PUBLICKEY__: JSON.stringify(require("./env/push.config").publicKey) }) ] }; diff --git a/package-lock.json b/package-lock.json index e7ffa8e..5ba9523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1177,6 +1177,14 @@ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -1607,8 +1615,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { "version": "1.19.0", @@ -1816,6 +1823,11 @@ "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -2644,6 +2656,15 @@ "sha.js": "^2.4.8" } }, + "cron-parser": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.13.0.tgz", + "integrity": "sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ==", + "requires": { + "is-nan": "^1.2.1", + "moment-timezone": "^0.5.25" + } + }, "cross-env": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", @@ -3024,7 +3045,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -3284,6 +3304,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3428,8 +3456,15 @@ "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } }, "escape-html": { "version": "1.0.3", @@ -5379,12 +5414,39 @@ "sshpk": "^1.7.0" } }, + "http_ece": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz", + "integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==", + "requires": { + "urlsafe-base64": "~1.0.0" + } + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5694,6 +5756,14 @@ "is-extglob": "^2.1.1" } }, + "is-nan": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.0.tgz", + "integrity": "sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==", + "requires": { + "define-properties": "^1.1.3" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -5919,6 +5989,25 @@ "verror": "1.10.0" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kareem": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", @@ -6036,6 +6125,11 @@ "integrity": "sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ==", "dev": true }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6276,8 +6370,7 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", @@ -6419,6 +6512,14 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, + "moment-timezone": { + "version": "0.5.27", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", + "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==", + "requires": { + "moment": ">= 2.9.0" + } + }, "mongodb": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.2.tgz", @@ -6747,6 +6848,16 @@ } } }, + "node-schedule": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.2.tgz", + "integrity": "sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw==", + "requires": { + "cron-parser": "^2.7.3", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.0.0" + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -6876,8 +6987,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -9162,6 +9272,11 @@ } } }, + "sorted-array-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz", + "integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg==" + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -10257,6 +10372,11 @@ "requires-port": "^1.0.0" } }, + "urlsafe-base64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz", + "integrity": "sha1-I/iQaabGL0bPOh07ABac77kL4MY=" + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -10438,6 +10558,32 @@ "minimalistic-assert": "^1.0.0" } }, + "web-push": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.4.3.tgz", + "integrity": "sha512-nt/hRSlfRDTwvem//7jle1+cy62lBoxFshad8ai2Q4SlHZS00oHnrw5Dul3jSWXR+bOcnZkwnRs3tW+daNTuyA==", + "requires": { + "asn1.js": "^5.0.0", + "http_ece": "1.1.0", + "https-proxy-agent": "^3.0.0", + "jws": "^3.1.3", + "minimist": "^1.2.0", + "urlsafe-base64": "^1.0.0" + }, + "dependencies": { + "asn1.js": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.3.0.tgz", + "integrity": "sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + } + } + }, "webpack": { "version": "4.41.5", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.5.tgz", diff --git a/package.json b/package.json index a89db21..8c0edab 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mongoose": "^5.8.7", "node-fetch": "^2.6.0", "node-sass": "^4.13.0", + "node-schedule": "^1.3.2", "passport": "^0.4.1", "passport-local": "^1.0.0", "passport-local-mongoose": "^6.0.1", @@ -36,7 +37,8 @@ "vue": "~2.6", "vue-analytics": "^5.22.1", "vue-router": "~3.0", - "vuex": "^3.1.1" + "vuex": "^3.1.1", + "web-push": "^3.4.3" }, "devDependencies": { "@babel/core": "~7.2", diff --git a/public/service-worker.js b/public/service-worker.js index 9a63270..3ffc69e 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -6,11 +6,29 @@ var STATIC_CACHE_URLS = ["/"]; console.log("Nåværende versjon:", version); self.addEventListener("activate", event => { + event.waitUntil( + new Promise(resolve => { + const applicationServerKey = urlB64ToUint8Array(__PUBLICKEY__); + const options = { applicationServerKey, userVisibleOnly: true }; + self.registration.pushManager.subscribe(options).then(subscription => + saveSubscription(subscription).then(() => { + resolve(); + }) + ); + }) + ); event.waitUntil(removeCache(CACHE_NAME)); event.waitUntil(removeCache(CACHE_NAME_API)); event.waitUntil(addCache(CACHE_NAME, STATIC_CACHE_URLS)); }); +self.addEventListener("push", function(event) { + if (event.data) { + showLocalNotification("Vinlotteri!", event.data.text(), self.registration); + } else { + } +}); + self.addEventListener("install", event => { console.log("Arbeids arbeideren installerer seg."); self.skipWaiting(); @@ -49,6 +67,41 @@ self.addEventListener("fetch", event => { } }); +function showLocalNotification(title, body, swRegistration) { + const options = { + body, + icon: "https://lottis.vin/public/assets/images/favicon.png", + image: "https://lottis.vin/public/assets/images/favicon.png", + vibrate: [300] + }; + swRegistration.showNotification(title, options); +} + +async function saveSubscription(subscription) { + const SERVER_URL = "https://lottis.vin/subscription/save-subscription"; + const response = await fetch(SERVER_URL, { + method: "post", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(subscription) + }); + return response.json(); +} + +const urlB64ToUint8Array = base64String => { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, "+") + .replace(/_/g, "/"); + const rawData = atob(base64); + const outputArray = new Uint8Array(rawData.length); + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +}; + function addCache(cacheKey, cacheUrls) { return caches.open(cacheKey).then(cache => { console.log("Legger til cache", cache); diff --git a/schemas/Subscription.js b/schemas/Subscription.js new file mode 100644 index 0000000..9150726 --- /dev/null +++ b/schemas/Subscription.js @@ -0,0 +1,13 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; + +const Subscription = new Schema({ + endpoint: String, + expirationTime: Number, + keys: { + p256dh: String, + auth: String + } +}); + +module.exports = mongoose.model("Subscription", Subscription); diff --git a/server.js b/server.js index 66450d4..b2c853d 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ const User = require(path.join(__dirname + "/schemas/User")); const updateApi = require(path.join(__dirname + "/api/update")); const retrieveApi = require(path.join(__dirname + "/api/retrieve")); +const subscriptionApi = require(path.join(__dirname + "/api/subscriptions")); const loginApi = require(path.join(__dirname + "/api/login")); const bodyParser = require("body-parser"); @@ -79,6 +80,7 @@ app.use("/dist", express.static(path.join(__dirname, "public/dist"))); app.use("/", loginApi); app.use("/api/", updateApi); app.use("/api/", retrieveApi); +app.use("/subscription", subscriptionApi); app.use("/service-worker.js", function(req, res) { res.sendFile(path.join(__dirname, "public/sw/serviceWorker.js")); diff --git a/src/Vinlottis.vue b/src/Vinlottis.vue index c3115df..970ee49 100644 --- a/src/Vinlottis.vue +++ b/src/Vinlottis.vue @@ -23,6 +23,15 @@ export default { console.log( "Arbeids arbeideren din er installert. Du kan nå gå offline frem til neste trekning." ); + + if (!("PushManager" in window)) { + throw new Error("No Push API Support!"); + } + window.Notification.requestPermission().then(permission => { + if (permission !== "granted") { + throw new Error("Permission not granted for Notification"); + } + }); }) .catch(error => { console.error("Arbeids arbeideren klarer ikke arbeide.", error); @@ -40,7 +49,7 @@ export default { @font-face { font-family: "knowit"; font-weight: 600; - src: url("/../public/assets/fonts/bold.eot"), + src: url("/../public/assets/fonts/bold.woff"), url("/../public/assets/fonts/bold.woff") format("woff"), local("Arial"); font-display: swap; } diff --git a/src/styles/global.scss b/src/styles/global.scss index c451318..5b5a04f 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -3,7 +3,7 @@ @font-face { font-family: "knowit"; font-weight: 600; - src: url("/../../public/assets/fonts/bold.eot"); + src: url("/../../public/assets/fonts/bold.woff"); } @font-face {