From e7a3a0a673d64709987a6e1137c6fe276778b5d4 Mon Sep 17 00:00:00 2001 From: Oskar Date: Tue, 8 Jun 2021 00:47:00 +0200 Subject: [PATCH 1/8] Update config to search non-repo local files (in /etc) first In order to allow for secrets in the local config file that should be nowhere near a public repo --- config.ini | 6 ++++++ ftracker/config.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/config.ini b/config.ini index 1ac2ff4..bb1ace1 100644 --- a/config.ini +++ b/config.ini @@ -24,3 +24,9 @@ guideline_url = https://fasttube.de/wp-content/uploads/2020/12/Cororna-Regeln-St # JSON indentation for debugging json_indent = 4 + +# VAPID credentials for push notifications +# private key: base64url encoded private part of an EC-Prime256v1 keypair. See INSTALL.md +# sender info: usually mailto link to responsible party to contact about issues +push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789 +push_sender_info = mailto:it@fasttube.de diff --git a/ftracker/config.py b/ftracker/config.py index fe369c6..a2dc075 100644 --- a/ftracker/config.py +++ b/ftracker/config.py @@ -9,10 +9,10 @@ class Config: def findConfigFile(): if len(sys.argv) > 1: return sys.argv[1] - elif Path('config.ini').is_file(): - return 'config.ini' elif Path('/etc/ftracker/config.ini').is_file(): return '/etc/ftracker/config.ini' + elif Path('config.ini').is_file(): + return 'config.ini' else: return None -- 2.45.2 From e521edd62b2a3039b8c7df772b00ab372eeb28a7 Mon Sep 17 00:00:00 2001 From: Oskar Date: Tue, 8 Jun 2021 00:48:11 +0200 Subject: [PATCH 2/8] Add basic webpush backend functionality and endpoints for testing --- ftracker/core.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 57 insertions(+) diff --git a/ftracker/core.py b/ftracker/core.py index e5cc1d0..1c8e3a7 100644 --- a/ftracker/core.py +++ b/ftracker/core.py @@ -22,6 +22,10 @@ from flask import Flask, request, redirect app = Flask(__name__, static_folder='../web') +from pywebpush import webpush, WebPushException +pushsubs = db.table('pushsubs') + + if config['delete_after_days']: from .deleter import Deleter deleter = Deleter(db, int(config['delete_after_days'])) @@ -159,3 +163,55 @@ def get_data(): ) return json.dumps(r, indent=SPACES), 200 + + +@app.route('/pushsubscribe', methods=['POST']) +def post_pushsub(): + + try: + payload = request.data.decode('UTF-8') + data = json.loads(payload) + except ValueError as e: + return 'Error: JSON decode error:\n' + str(e), 400 + + name = slugify(data['name']) + data['name'] = name + print(json.dumps(data, indent=SPACES)) + + Entry = Query() + pushsubs.upsert(data, Entry.name == name) + + return 'OK', 201 + +@app.route('/testpush/') +def testpush(name): + + Entry = Query() + ps = pushsubs.search(Entry.name == name)[0] + + arr = db.search((Entry.name == name) & (Entry.departure == None)) + arr = arr if len(arr) else None + + print(ps) + + subscription = ps['sub'] + notification = { + 'title': "Forgot to sign out?", + 'body': "You didn't sign out of ftracker yet", + 'arr': arr + } + + try: + webpush( + subscription, + json.dumps(notification, indent=SPACES), + vapid_private_key = config['push_private_key'], + vapid_claims = { + 'sub': config['push_sender_info'] + }, + verbose=True + ) + return 'OK', 201 + except WebPushException as exc: + print(exc) + return 'Error', 500 diff --git a/setup.py b/setup.py index 86b1180..d2ca234 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ st.setup( "flask", "tinydb", "python-slugify", + "pywebpush", ], license=license_text, classifiers=[ -- 2.45.2 From b8e704c3004839311dba2a85887259bd020f69d2 Mon Sep 17 00:00:00 2001 From: Oskar Date: Tue, 8 Jun 2021 00:49:42 +0200 Subject: [PATCH 3/8] Update frontend to allow requesting and receiving notifications Includes requesting permissions and adding a basic service worker as well as re-working and restructuring a bunch of the existing logic for parsing URL parameters, sending data and adding another form field --- web/index.html | 30 +++++++++------ web/main.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++---- web/sw.js | 35 ++++++++++++++++++ 3 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 web/sw.js diff --git a/web/index.html b/web/index.html index a3148bc..1d4c56f 100644 --- a/web/index.html +++ b/web/index.html @@ -13,25 +13,27 @@ 'departure': 'I have cleaned my workspace' } var testCheckBox = '' + var editTimeBox = '' function getParams() { - var h = document.location.href - var qparam = h.split('?')[1] || null - if (qparam == null) - return null - var vals = qparam.split('=') - if (vals.length < 2 || !cbt.hasOwnProperty(vals[0])) - return null - return { - action: vals[0], - room: vals[1] + var qparams = document.location.search.substr(1) + if (qparams == "") return {} + qparams = qparams.split('&') + var qps = {} + for (var qparam of qparams) { + var vals = qparam.split('=') + qps[vals[0]] = vals[1] || null } + // Backwards compat + if (qps.arrival) {qps.action = 'arrival'; qps.room = qps.arrival} + if (qps.departure) {qps.action = 'departure'; qps.room = qps.departure} + return qps } var qp = getParams()

This is a web app to track which people @@ -56,6 +58,10 @@ Full Name:
+ diff --git a/web/main.js b/web/main.js index 38d0e83..6e6b6f0 100644 --- a/web/main.js +++ b/web/main.js @@ -1,7 +1,9 @@ +var pushServerPublicKey = 'BBwBPYxhogHLU3B1FpxfQNzO3q7qZpmD1n1KaaL8WJbcVmJSHhi1uB-VmvsVjjUHWYCeqKyLT7w-1LBfpIcbbcg' + var spage = document.getElementById('startpage') var mform = document.getElementById('mainform') -if (qp) { +if (qp.action) { spage.style.display = 'none' mform.style.display = 'block' } @@ -12,18 +14,26 @@ if (savedName && qp) document.getElementById('name').value = savedName // 2nd script, server API communication -var name, agreed, tested +var name, datetime, agreed, tested mform.onsubmit = function(e) { e.preventDefault() - name = e.srcElement[0].value - agreed = e.srcElement[1].checked - if (e.srcElement.length > 2) - tested = e.srcElement[2].checked + var i = 0; + name = e.srcElement[i++].value + if (qp.edittime && qp.edittime == 1) { + var value = e.srcElement[i++].value + datetime = new Date(value).toISOString() + } + agreed = e.srcElement[i++].checked + if (qp.action && qp.action == 'arrival') + tested = e.srcElement[i++].checked + sendMainData() + initPush(name) + } function sendMainData() { @@ -33,11 +43,13 @@ function sendMainData() { { 'room': qp.room, 'name': name, + 'arrival': datetime, 'agreetoguidelines': agreed, 'tested': tested } : { 'name': name, + 'departure': datetime, 'cleanedworkspace': agreed } @@ -159,7 +171,7 @@ function handleRequest(res) {
${doubleT} @@ -179,3 +191,76 @@ function handleRequest(res) { }) } + + +if (qp.edittime && qp.edittime == 1) { + var now = localISOTimeMinutes(new Date()) + document.getElementById('datetime').value = now; + document.getElementById('datetime').max = now; +} + + +/* Push Notifications */ + +function sendNotification() { + + navigator.serviceWorker.ready.then(function(serviceWorker) { + serviceWorker.showNotification("Forgot to sign out?", { + body: "You didn't sign out of ftracker yet", + icon: "/favicon.ico", + actions: [{ + action: "depart", + title: "Sign Out" + }] + }) + }) + +} + +function initPush(name) { + + // Check availability + var supported = "serviceWorker" in navigator && "PushManager" in window + if (!supported) { + console.warn("Push Notifications not supported!") + return + } + + // Register service worker + navigator.serviceWorker.register("/sw.js").then(function(swRegistration) { + console.log("ServiceWorker registered:", swRegistration) + }) + + // Request permission + // TODO: Only do this AFTER the first? SUCCESSFUL sign-in + Notification.requestPermission(function(result) { + return (result === 'granted') + }).then(function(consent) { + console.log('Notifications', consent ? 'enabled' : 'denied'); + }) + + // Check if already initialized + if (localStorage.getItem('pushsub')) + return + + // Register push service + navigator.serviceWorker.ready.then(function(serviceWorker) { + serviceWorker.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: pushServerPublicKey + }).then(function(subscription) { + console.log("User is subscribed:", subscription); + localStorage.setItem('pushsub', subscription); + + fetch('/pushsubscribe', { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + name: name, + sub: subscription + }) + }); + }); + }); + +} diff --git a/web/sw.js b/web/sw.js new file mode 100644 index 0000000..613f9f6 --- /dev/null +++ b/web/sw.js @@ -0,0 +1,35 @@ +function receivePushNotification(event) { + + var data = event.data.json(); + + console.log("[Service Worker] Push Received:", data) + + var room = data.arr ? data.arr.room : 'test' + + var options = { + data: `/?departure=${room}&edittime=1`, + body: data.body, + icon: "/favicon.ico", + actions: [{ + action: "depart", + title: "Sign Out" + }] + }; + + event.waitUntil(self.registration.showNotification(data.title, options)) + +} + +self.addEventListener("push", receivePushNotification) + + +function openPushNotification(event) { + + console.log("[Service Worker] Notification click Received.", event.notification.data) + + event.notification.close() + event.waitUntil(clients.openWindow(event.notification.data)) + +} + +self.addEventListener("notificationclick", openPushNotification) -- 2.45.2 From 618f00a09adfd85be216ee3446f6155dc62d27ae Mon Sep 17 00:00:00 2001 From: Oskar Date: Thu, 10 Jun 2021 23:14:15 +0200 Subject: [PATCH 4/8] Send notifications N hours after arrival, polling once per hour --- config.ini | 2 ++ ftracker/config.py | 3 ++ ftracker/core.py | 55 ++++++++++++++-------------- ftracker/notifier.py | 86 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 ftracker/notifier.py diff --git a/config.ini b/config.ini index bb1ace1..76bc5fb 100644 --- a/config.ini +++ b/config.ini @@ -30,3 +30,5 @@ json_indent = 4 # sender info: usually mailto link to responsible party to contact about issues push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789 push_sender_info = mailto:it@fasttube.de +# when to notify users, in hours after arrival +notify_after_hrs = 10 diff --git a/ftracker/config.py b/ftracker/config.py index a2dc075..19cdf74 100644 --- a/ftracker/config.py +++ b/ftracker/config.py @@ -30,3 +30,6 @@ class Config: def __getitem__(self, key): return self.config['global'].get(key) + + def __repr__(self): + return repr(self.config.items('global')) diff --git a/ftracker/core.py b/ftracker/core.py index 1c8e3a7..a2e824c 100644 --- a/ftracker/core.py +++ b/ftracker/core.py @@ -22,14 +22,19 @@ from flask import Flask, request, redirect app = Flask(__name__, static_folder='../web') -from pywebpush import webpush, WebPushException -pushsubs = db.table('pushsubs') - - if config['delete_after_days']: from .deleter import Deleter deleter = Deleter(db, int(config['delete_after_days'])) + +if config['notify_after_hrs']: + from .notifier import Notifier + notifier = Notifier(db, int(config['notify_after_hrs']), { + 'private_key': config['push_private_key'], + 'claims': {'sub': config['push_sender_info']} + }) + + def shutdown(): print('\rReceived stop signal, stopping threads...') deleter.stop() @@ -37,6 +42,7 @@ def shutdown(): atexit.register(shutdown) + @app.route('/guidelines') def get_guidelines(): dest = config['guideline_url'] or None @@ -186,32 +192,25 @@ def post_pushsub(): @app.route('/testpush/') def testpush(name): + if not 'Authorization' in request.headers: + return 'Error: No Authorization', 401, {'WWW-Authenticate': 'Basic'} + + if request.authorization.username != config['admin_user']: + return "Wrong username", 403 + + if request.authorization.password != config['admin_pass']: + return "Wrong password", 403 + Entry = Query() - ps = pushsubs.search(Entry.name == name)[0] + arrivals = db.search((Entry.name == name) & (Entry.departure == None)) - arr = db.search((Entry.name == name) & (Entry.departure == None)) - arr = arr if len(arr) else None + if len(arrivals) == 0: + print("User is not logged in :(") + return "Error: User is not logged in :(", 409 - print(ps) + error = notifier.notify_user(arrivals[0]) - subscription = ps['sub'] - notification = { - 'title': "Forgot to sign out?", - 'body': "You didn't sign out of ftracker yet", - 'arr': arr - } + if error: + return 'Error: ' + error, 201 - try: - webpush( - subscription, - json.dumps(notification, indent=SPACES), - vapid_private_key = config['push_private_key'], - vapid_claims = { - 'sub': config['push_sender_info'] - }, - verbose=True - ) - return 'OK', 201 - except WebPushException as exc: - print(exc) - return 'Error', 500 + return 'OK', 201 diff --git a/ftracker/notifier.py b/ftracker/notifier.py new file mode 100644 index 0000000..5cb7286 --- /dev/null +++ b/ftracker/notifier.py @@ -0,0 +1,86 @@ +import json +from threading import Thread, Event +from datetime import datetime, timedelta + +from tinydb import Query + +from pywebpush import webpush, WebPushException + +ONE_HOUR_IN_S = 60 * 60 + +class Notifier(Thread): + + def notify_user(self, arrival): + + pushsubs = self.db.table('pushsubs') + + Entry = Query() + ps = pushsubs.search(Entry.name == arrival['name']) + if len(ps) == 0: + print("User is not subscribed to notifications :(") + return "User is not subscribed to notifications :(" + + ps = ps[0] + + print("Sending notification", arrival, ps) + + subscription = ps['sub'] + notification = { + 'title': "Forgot to sign out?", + 'body': "You didn't sign out of ftracker yet", + 'arr': arrival + } + + try: + webpush( + subscription, + json.dumps(notification), + vapid_private_key = self.vapid_creds['private_key'], + vapid_claims = self.vapid_creds['claims'] + ) + print("Notification sent") + return None + except WebPushException as exc: + print("Notification failed", exc) + return repr(exc) + + + def notify_logged_in_users(self): + + print("Notifying users that aren't signed out yet...") + + td = timedelta(hours=self.hours) + + threshold = datetime.utcnow() - td + + iso = threshold.isoformat() + 'Z' + + Entry = Query() + arrivals = self.db.search( + (Entry.arrival < iso) & (Entry.departure == None) + ) + + for arrival in arrivals: + self.notify_user(arrival) + + print("Notified everything until UTC", iso) + + def __init__(self, db, hours, vapid_creds): + + self.db = db + self.hours = hours + self.vapid_creds = vapid_creds + + self.notify_logged_in_users() + + Thread.__init__(self, daemon=True) + self.stopped = Event() + self.start() + + def run(self): + + while not self.stopped.wait(ONE_HOUR_IN_S): + self.notify_logged_in_users() + + def stop(self): + self.stopped.set() -- 2.45.2 From 4ee4869f82807284646df69c729c6d2ea6ec8054 Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 11 Jun 2021 01:04:44 +0200 Subject: [PATCH 5/8] Minor fixes & improvements - Move push subscription handling to Notifier class - Allow duplicate options in config - Only save subscription in frontend if sub call was successful --- ftracker/config.py | 2 +- ftracker/core.py | 9 ++------- ftracker/notifier.py | 11 +++++++++++ web/main.js | 4 +++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ftracker/config.py b/ftracker/config.py index 19cdf74..4c4cd8b 100644 --- a/ftracker/config.py +++ b/ftracker/config.py @@ -19,7 +19,7 @@ class Config: configfile = findConfigFile() if configfile: - self.config = ConfigParser() + self.config = ConfigParser(strict=False) self.config.read(configfile) else: raise Exception("No config file found") diff --git a/ftracker/core.py b/ftracker/core.py index a2e824c..8c79207 100644 --- a/ftracker/core.py +++ b/ftracker/core.py @@ -180,12 +180,7 @@ def post_pushsub(): except ValueError as e: return 'Error: JSON decode error:\n' + str(e), 400 - name = slugify(data['name']) - data['name'] = name - print(json.dumps(data, indent=SPACES)) - - Entry = Query() - pushsubs.upsert(data, Entry.name == name) + notifier.subscribe_user(data) return 'OK', 201 @@ -211,6 +206,6 @@ def testpush(name): error = notifier.notify_user(arrivals[0]) if error: - return 'Error: ' + error, 201 + return 'Error: ' + error, 404 return 'OK', 201 diff --git a/ftracker/notifier.py b/ftracker/notifier.py index 5cb7286..68ab7aa 100644 --- a/ftracker/notifier.py +++ b/ftracker/notifier.py @@ -1,4 +1,5 @@ import json +from slugify import slugify from threading import Thread, Event from datetime import datetime, timedelta @@ -10,6 +11,16 @@ ONE_HOUR_IN_S = 60 * 60 class Notifier(Thread): + def subscribe_user(self, data): + + pushsubs = self.db.table('pushsubs') + + name = slugify(data['name']) + data['name'] = name + + Entry = Query() + pushsubs.upsert(data, Entry.name == name) + def notify_user(self, arrival): pushsubs = self.db.table('pushsubs') diff --git a/web/main.js b/web/main.js index 6e6b6f0..0ee42c9 100644 --- a/web/main.js +++ b/web/main.js @@ -250,7 +250,6 @@ function initPush(name) { applicationServerKey: pushServerPublicKey }).then(function(subscription) { console.log("User is subscribed:", subscription); - localStorage.setItem('pushsub', subscription); fetch('/pushsubscribe', { method: "POST", @@ -259,6 +258,9 @@ function initPush(name) { name: name, sub: subscription }) + }).then(function(res) { + if (res.ok) + localStorage.setItem('pushsub', subscription); }); }); }); -- 2.45.2 From 3a872bceb297f16f5a6b34735d8e2d98c55da09c Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 11 Jun 2021 01:06:59 +0200 Subject: [PATCH 6/8] Enable Docker container to generate its own VAPID credentials --- Dockerfile | 4 +++- res/config.deploy.ini | 11 +++++++++++ res/docker-entrypoint.sh | 20 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5083d2c..d91fae5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ FROM alpine:latest RUN apk add --update --no-cache \ - bash python3 py3-pip nginx uwsgi uwsgi-python3 certbot certbot-nginx + bash python3 py3-pip nginx uwsgi uwsgi-python3 certbot certbot-nginx npm jq + +RUN npm install -g web-push diff --git a/res/config.deploy.ini b/res/config.deploy.ini index 546da41..4afb438 100644 --- a/res/config.deploy.ini +++ b/res/config.deploy.ini @@ -4,6 +4,9 @@ # Remove or leave empty for temporary (/tmp/ftracker-db.json) storage db_file = /var/ftracker/db.json +# Delete all information after X days (e.g. for GDPR compliance) +delete_after_days = 28 + # List of people to be allowed, in .csv format (comma, no delimiters) # Col1: First Name(s), Col2: Last Name(s), Col3 (optional): EMail # Remove or leave empty for no check @@ -21,3 +24,11 @@ guideline_url = https://youtu.be/oHg5SJYRHA0 # JSON indentation for debugging json_indent = 4 + +# VAPID credentials for push notifications +# private key: base64url encoded private part of an EC-Prime256v1 keypair. See INSTALL.md +# sender info: usually mailto link to responsible party to contact about issues +push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789 +push_sender_info = mailto:admin@example.com +# when to notify users, in hours after arrival +notify_after_hrs = 10 diff --git a/res/docker-entrypoint.sh b/res/docker-entrypoint.sh index ef08f87..b1e6713 100644 --- a/res/docker-entrypoint.sh +++ b/res/docker-entrypoint.sh @@ -1,5 +1,25 @@ #!/bin/bash +echo " >>> Checking / Creating & patching VAPID creds <<< " + +VAPID_CREDS_FILE=/etc/ftracker/vapid-creds.json +if [[ ! -f $VAPID_CREDS_FILE ]] +then + + echo "Generating keypair ..." + + web-push generate-vapid-keys --json > $VAPID_CREDS_FILE + + echo "Patching public key into frontend ..." + PUB_KEY=`cat $VAPID_CREDS_FILE | jq -r .publicKey` + sed -i "s/pushServerPublicKey = '[a-zA-Z0-9_\-]*'/pushServerPublicKey = '${PUB_KEY}'/" /var/www/html/ftracker/main.js + + echo "Patching private key into backend config ..." + PRIV_KEY=`cat $VAPID_CREDS_FILE | jq -r .privateKey` + echo "push_private_key = ${PRIV_KEY}" >> /etc/ftracker/config.ini + +fi + echo " >>> Starting nginx <<< " mkdir /run/nginx # needed because of bug in package -- 2.45.2 From 711fbfd821291b1ab547126eb8735c2977b5112f Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 11 Jun 2021 01:28:56 +0200 Subject: [PATCH 7/8] Update docs --- INSTALL.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index b1b6f26..f19025d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -161,3 +161,40 @@ jänE doé ``` would still work, but `Jane D` wouldn't). + +### Automatic data deletion (GDPR compliance) + +The `delete_after_days` configuration option can be set to a number of days +after which attendance records are purged from the database. If it is not set +(or empty) automatic deletion is deactivated. Automatic deletion is final and +non-recoverable. This option is intended to help make the system fully GDPR +compliant by guaranteeing deletion after a certain period. Keep in mind that a +legally binding data protection guideline and user consent are still required. + +### User notification on forgotten sign-out + +`ftracker` is capable of notifying users if they forgot to sign-out at the end +of a day using modern web push notifications using the VAPID system. To make +this work, a few things are needed: + +Firstly, you need an EC-Prime256v1 keypair in base64url encoding. If you're +using the Docker container, this is automatically generated for you. If not, +the easiest way to create one is to install the `web-push` `npm` package and +run it: + +```bash +sudo npm install -g web-push +web-push generate-vapid-keys +``` + +The public Key needs to be copied into `web/main.js` (first line), while the +private key is put into the config option `push_private_key`. + +Then, to be VAPID compliant you have to announce an contact address claim to +the push services so they can contact you if anything is going wrong with your +notifications. Do this by entering your email address as a `mailto:` link in +the `push_sender_info` option, like `mailto:it@fasttube.de`. + +Finally, you can use the `notify_after_hrs` option to specify how long the +system should wait after a user's arrival to notify them of their missing +departure. -- 2.45.2 From 39a461df565d0543491d523bdf2aadeb7cb43703 Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 11 Jun 2021 01:57:32 +0200 Subject: [PATCH 8/8] Move VAPID public key config to backend for easier config Also enables the frontend not asking for notiffication permission if it doesn't need them. Should also help if it ever needs to be changed to circumvent cache. --- INSTALL.md | 6 +++--- config.ini | 2 ++ ftracker/core.py | 19 +++++++++++++++++++ res/config.deploy.ini | 2 ++ res/docker-entrypoint.sh | 5 ++--- web/main.js | 14 ++++++++++++-- 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index f19025d..919743f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -187,10 +187,10 @@ sudo npm install -g web-push web-push generate-vapid-keys ``` -The public Key needs to be copied into `web/main.js` (first line), while the -private key is put into the config option `push_private_key`. +The keys then need to be copied into the config options `push_public_key` and +`push_private_key` respectively so the backend can handle the rest. -Then, to be VAPID compliant you have to announce an contact address claim to +Next, to be VAPID compliant you have to announce an contact address claim to the push services so they can contact you if anything is going wrong with your notifications. Do this by entering your email address as a `mailto:` link in the `push_sender_info` option, like `mailto:it@fasttube.de`. diff --git a/config.ini b/config.ini index 76bc5fb..3eee4db 100644 --- a/config.ini +++ b/config.ini @@ -26,8 +26,10 @@ guideline_url = https://fasttube.de/wp-content/uploads/2020/12/Cororna-Regeln-St json_indent = 4 # VAPID credentials for push notifications +# private key: base64url encoded public part of an EC-Prime256v1 keypair. See INSTALL.md # private key: base64url encoded private part of an EC-Prime256v1 keypair. See INSTALL.md # sender info: usually mailto link to responsible party to contact about issues +push_public_key = BBwBPYxhogHLU3B1FpxfQNzO3q7qZpmD1n1KaaL8WJbcVmJSHhi1uB-VmvsVjjUHWYCeqKyLT7w-1LBfpIcbbcg push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789 push_sender_info = mailto:it@fasttube.de # when to notify users, in hours after arrival diff --git a/ftracker/core.py b/ftracker/core.py index 8c79207..9a61240 100644 --- a/ftracker/core.py +++ b/ftracker/core.py @@ -171,6 +171,25 @@ def get_data(): return json.dumps(r, indent=SPACES), 200 +@app.route('/pushinfo') +def get_pushinfo(): + + if config['notify_after_hrs']: + + r = { + 'enabled': True, + 'publickey': config['push_public_key'] + } + + else: + + r = { + 'enabled': False, + 'publickey': None + } + + return json.dumps(r, indent=SPACES), 200 + @app.route('/pushsubscribe', methods=['POST']) def post_pushsub(): diff --git a/res/config.deploy.ini b/res/config.deploy.ini index 4afb438..07ba13e 100644 --- a/res/config.deploy.ini +++ b/res/config.deploy.ini @@ -26,8 +26,10 @@ guideline_url = https://youtu.be/oHg5SJYRHA0 json_indent = 4 # VAPID credentials for push notifications +# private key: base64url encoded public part of an EC-Prime256v1 keypair. See INSTALL.md # private key: base64url encoded private part of an EC-Prime256v1 keypair. See INSTALL.md # sender info: usually mailto link to responsible party to contact about issues +push_public_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789abcdefghijklm_NOPQRSTUVWXYZ-0123456789abcdefghijklm_NOPQRSTUVWXYZ-0123456789 push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789 push_sender_info = mailto:admin@example.com # when to notify users, in hours after arrival diff --git a/res/docker-entrypoint.sh b/res/docker-entrypoint.sh index b1e6713..209b90f 100644 --- a/res/docker-entrypoint.sh +++ b/res/docker-entrypoint.sh @@ -10,11 +10,10 @@ then web-push generate-vapid-keys --json > $VAPID_CREDS_FILE - echo "Patching public key into frontend ..." + echo "Patching keypair into config ..." PUB_KEY=`cat $VAPID_CREDS_FILE | jq -r .publicKey` - sed -i "s/pushServerPublicKey = '[a-zA-Z0-9_\-]*'/pushServerPublicKey = '${PUB_KEY}'/" /var/www/html/ftracker/main.js + echo "pushServerPublicKey = ${PUB_KEY}" >> /var/www/html/ftracker/main.js - echo "Patching private key into backend config ..." PRIV_KEY=`cat $VAPID_CREDS_FILE | jq -r .privateKey` echo "push_private_key = ${PRIV_KEY}" >> /etc/ftracker/config.ini diff --git a/web/main.js b/web/main.js index 0ee42c9..d97172f 100644 --- a/web/main.js +++ b/web/main.js @@ -1,5 +1,3 @@ -var pushServerPublicKey = 'BBwBPYxhogHLU3B1FpxfQNzO3q7qZpmD1n1KaaL8WJbcVmJSHhi1uB-VmvsVjjUHWYCeqKyLT7w-1LBfpIcbbcg' - var spage = document.getElementById('startpage') var mform = document.getElementById('mainform') @@ -226,6 +224,18 @@ function initPush(name) { return } + fetch('/pushinfo').then(function(res) { + if (res.ok) + res.json().then(function(push) { + if (push.enabled) + registerPush(name, push.publickey); + }); + }); + +} + +function registerPush(name, pushServerPublicKey) { + // Register service worker navigator.serviceWorker.register("/sw.js").then(function(swRegistration) { console.log("ServiceWorker registered:", swRegistration) -- 2.45.2