Add push notifications for forgotten departures #26
|
@ -1,7 +1,9 @@
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
37
INSTALL.md
37
INSTALL.md
|
@ -161,3 +161,40 @@ jänE doé
|
||||||
```
|
```
|
||||||
|
|
||||||
would still work, but `Jane D` wouldn't).
|
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 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.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
10
config.ini
10
config.ini
|
@ -24,3 +24,13 @@ guideline_url = https://fasttube.de/wp-content/uploads/2020/12/Cororna-Regeln-St
|
||||||
|
|
||||||
# JSON indentation for debugging
|
# JSON indentation for debugging
|
||||||
json_indent = 4
|
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
|
||||||
|
notify_after_hrs = 10
|
||||||
|
|
|
@ -9,17 +9,17 @@ class Config:
|
||||||
def findConfigFile():
|
def findConfigFile():
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
return sys.argv[1]
|
return sys.argv[1]
|
||||||
elif Path('config.ini').is_file():
|
|
||||||
return 'config.ini'
|
|
||||||
elif Path('/etc/ftracker/config.ini').is_file():
|
elif Path('/etc/ftracker/config.ini').is_file():
|
||||||
return '/etc/ftracker/config.ini'
|
return '/etc/ftracker/config.ini'
|
||||||
|
elif Path('config.ini').is_file():
|
||||||
|
return 'config.ini'
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
configfile = findConfigFile()
|
configfile = findConfigFile()
|
||||||
|
|
||||||
if configfile:
|
if configfile:
|
||||||
self.config = ConfigParser()
|
self.config = ConfigParser(strict=False)
|
||||||
self.config.read(configfile)
|
self.config.read(configfile)
|
||||||
else:
|
else:
|
||||||
raise Exception("No config file found")
|
raise Exception("No config file found")
|
||||||
|
@ -30,3 +30,6 @@ class Config:
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.config['global'].get(key)
|
return self.config['global'].get(key)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.config.items('global'))
|
||||||
|
|
|
@ -26,6 +26,15 @@ if config['delete_after_days']:
|
||||||
from .deleter import Deleter
|
from .deleter import Deleter
|
||||||
deleter = Deleter(db, int(config['delete_after_days']))
|
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():
|
def shutdown():
|
||||||
print('\rReceived stop signal, stopping threads...')
|
print('\rReceived stop signal, stopping threads...')
|
||||||
deleter.stop()
|
deleter.stop()
|
||||||
|
@ -33,6 +42,7 @@ def shutdown():
|
||||||
|
|
||||||
atexit.register(shutdown)
|
atexit.register(shutdown)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/guidelines')
|
@app.route('/guidelines')
|
||||||
def get_guidelines():
|
def get_guidelines():
|
||||||
dest = config['guideline_url'] or None
|
dest = config['guideline_url'] or None
|
||||||
|
@ -159,3 +169,62 @@ def get_data():
|
||||||
)
|
)
|
||||||
|
|
||||||
return json.dumps(r, indent=SPACES), 200
|
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():
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = request.data.decode('UTF-8')
|
||||||
|
data = json.loads(payload)
|
||||||
|
except ValueError as e:
|
||||||
|
return 'Error: JSON decode error:\n' + str(e), 400
|
||||||
|
|
||||||
|
notifier.subscribe_user(data)
|
||||||
|
|
||||||
|
return 'OK', 201
|
||||||
|
|
||||||
|
@app.route('/testpush/<name>')
|
||||||
|
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()
|
||||||
|
arrivals = db.search((Entry.name == name) & (Entry.departure == None))
|
||||||
|
|
||||||
|
if len(arrivals) == 0:
|
||||||
|
print("User is not logged in :(")
|
||||||
|
return "Error: User is not logged in :(", 409
|
||||||
|
|
||||||
|
error = notifier.notify_user(arrivals[0])
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return 'Error: ' + error, 404
|
||||||
|
|
||||||
|
return 'OK', 201
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import json
|
||||||
|
from slugify import slugify
|
||||||
|
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 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')
|
||||||
|
|
||||||
|
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()
|
|
@ -4,6 +4,9 @@
|
||||||
# Remove or leave empty for temporary (/tmp/ftracker-db.json) storage
|
# Remove or leave empty for temporary (/tmp/ftracker-db.json) storage
|
||||||
db_file = /var/ftracker/db.json
|
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)
|
# List of people to be allowed, in .csv format (comma, no delimiters)
|
||||||
# Col1: First Name(s), Col2: Last Name(s), Col3 (optional): EMail
|
# Col1: First Name(s), Col2: Last Name(s), Col3 (optional): EMail
|
||||||
# Remove or leave empty for no check
|
# Remove or leave empty for no check
|
||||||
|
@ -21,3 +24,13 @@ guideline_url = https://youtu.be/oHg5SJYRHA0
|
||||||
|
|
||||||
# JSON indentation for debugging
|
# JSON indentation for debugging
|
||||||
json_indent = 4
|
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
|
||||||
|
notify_after_hrs = 10
|
||||||
|
|
|
@ -1,5 +1,24 @@
|
||||||
#!/bin/bash
|
#!/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 keypair into config ..."
|
||||||
|
PUB_KEY=`cat $VAPID_CREDS_FILE | jq -r .publicKey`
|
||||||
|
echo "pushServerPublicKey = ${PUB_KEY}" >> /var/www/html/ftracker/main.js
|
||||||
|
|
||||||
|
PRIV_KEY=`cat $VAPID_CREDS_FILE | jq -r .privateKey`
|
||||||
|
echo "push_private_key = ${PRIV_KEY}" >> /etc/ftracker/config.ini
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
echo " >>> Starting nginx <<< "
|
echo " >>> Starting nginx <<< "
|
||||||
|
|
||||||
mkdir /run/nginx # needed because of bug in package
|
mkdir /run/nginx # needed because of bug in package
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -20,6 +20,7 @@ st.setup(
|
||||||
"flask",
|
"flask",
|
||||||
"tinydb",
|
"tinydb",
|
||||||
"python-slugify",
|
"python-slugify",
|
||||||
|
"pywebpush",
|
||||||
],
|
],
|
||||||
license=license_text,
|
license=license_text,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
|
@ -13,25 +13,27 @@
|
||||||
'departure': 'I have cleaned my workspace'
|
'departure': 'I have cleaned my workspace'
|
||||||
}
|
}
|
||||||
var testCheckBox = '<label class="checkbox"><input type="checkbox" name="tested" id="tested"><span>I have been tested negative for COVID in the last 24 hours</span></label>'
|
var testCheckBox = '<label class="checkbox"><input type="checkbox" name="tested" id="tested"><span>I have been tested negative for COVID in the last 24 hours</span></label>'
|
||||||
|
var editTimeBox = '<label>Departure Date/Time:<input type="datetime-local" name="datetime" id="datetime" required></label>'
|
||||||
function getParams() {
|
function getParams() {
|
||||||
var h = document.location.href
|
var qparams = document.location.search.substr(1)
|
||||||
var qparam = h.split('?')[1] || null
|
if (qparams == "") return {}
|
||||||
if (qparam == null)
|
qparams = qparams.split('&')
|
||||||
return null
|
var qps = {}
|
||||||
var vals = qparam.split('=')
|
for (var qparam of qparams) {
|
||||||
if (vals.length < 2 || !cbt.hasOwnProperty(vals[0]))
|
var vals = qparam.split('=')
|
||||||
return null
|
qps[vals[0]] = vals[1] || null
|
||||||
return {
|
|
||||||
action: vals[0],
|
|
||||||
room: vals[1]
|
|
||||||
}
|
}
|
||||||
|
// 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()
|
var qp = getParams()
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1><script>
|
<h1><script>
|
||||||
document.write(qp ? (qp.action + "<br>Room " + qp.room) : 'FTracker<br>V1')
|
document.write(qp.action ? (qp.action + "<br>Room " + qp.room) : 'FTracker<br>V1')
|
||||||
</script></h1>
|
</script></h1>
|
||||||
<div id="startpage">
|
<div id="startpage">
|
||||||
This is a web app to track which people
|
This is a web app to track which people
|
||||||
|
@ -56,6 +58,10 @@
|
||||||
Full Name:<br>
|
Full Name:<br>
|
||||||
<input type="text" name="name" id="name" placeholder="John Doe" required>
|
<input type="text" name="name" id="name" placeholder="John Doe" required>
|
||||||
</label>
|
</label>
|
||||||
|
<script>
|
||||||
|
if (qp.edittime && qp.edittime == 1)
|
||||||
|
document.write(editTimeBox)
|
||||||
|
</script>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" name="agree" id="agree" required>
|
<input type="checkbox" name="agree" id="agree" required>
|
||||||
<span><script>
|
<span><script>
|
||||||
|
@ -63,7 +69,7 @@
|
||||||
</script></span>
|
</script></span>
|
||||||
</label>
|
</label>
|
||||||
<script>
|
<script>
|
||||||
if (qp && qp.action == 'arrival')
|
if (qp.action && qp.action == 'arrival')
|
||||||
document.write(testCheckBox)
|
document.write(testCheckBox)
|
||||||
</script>
|
</script>
|
||||||
<input type="submit">
|
<input type="submit">
|
||||||
|
|
111
web/main.js
111
web/main.js
|
@ -1,7 +1,7 @@
|
||||||
var spage = document.getElementById('startpage')
|
var spage = document.getElementById('startpage')
|
||||||
var mform = document.getElementById('mainform')
|
var mform = document.getElementById('mainform')
|
||||||
|
|
||||||
if (qp) {
|
if (qp.action) {
|
||||||
spage.style.display = 'none'
|
spage.style.display = 'none'
|
||||||
mform.style.display = 'block'
|
mform.style.display = 'block'
|
||||||
}
|
}
|
||||||
|
@ -12,18 +12,26 @@ if (savedName && qp)
|
||||||
document.getElementById('name').value = savedName
|
document.getElementById('name').value = savedName
|
||||||
|
|
||||||
// 2nd script, server API communication
|
// 2nd script, server API communication
|
||||||
var name, agreed, tested
|
var name, datetime, agreed, tested
|
||||||
mform.onsubmit = function(e) {
|
mform.onsubmit = function(e) {
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
name = e.srcElement[0].value
|
var i = 0;
|
||||||
agreed = e.srcElement[1].checked
|
name = e.srcElement[i++].value
|
||||||
if (e.srcElement.length > 2)
|
if (qp.edittime && qp.edittime == 1) {
|
||||||
tested = e.srcElement[2].checked
|
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()
|
sendMainData()
|
||||||
|
|
||||||
|
initPush(name)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMainData() {
|
function sendMainData() {
|
||||||
|
@ -33,11 +41,13 @@ function sendMainData() {
|
||||||
{
|
{
|
||||||
'room': qp.room,
|
'room': qp.room,
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'arrival': datetime,
|
||||||
'agreetoguidelines': agreed,
|
'agreetoguidelines': agreed,
|
||||||
'tested': tested
|
'tested': tested
|
||||||
} :
|
} :
|
||||||
{
|
{
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'departure': datetime,
|
||||||
'cleanedworkspace': agreed
|
'cleanedworkspace': agreed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +169,7 @@ function handleRequest(res) {
|
||||||
<form id="reqform">
|
<form id="reqform">
|
||||||
<label>
|
<label>
|
||||||
${reqt[json.request]}
|
${reqt[json.request]}
|
||||||
<input type="datetime-local" id="datetime" ${minD} max="${now}" required>
|
<input type="datetime-local" ${minD} max="${now}" required>
|
||||||
${aInfo}
|
${aInfo}
|
||||||
</label>
|
</label>
|
||||||
${doubleT}
|
${doubleT}
|
||||||
|
@ -179,3 +189,90 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
fetch('/pushsubscribe', {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
sub: subscription
|
||||||
|
})
|
||||||
|
}).then(function(res) {
|
||||||
|
if (res.ok)
|
||||||
|
localStorage.setItem('pushsub', subscription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue