Compare commits
11 Commits
3a872bceb2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
1982a45392
|
|||
|
3cfe42023b
|
|||
|
e043f475da
|
|||
|
aef2a1ae60
|
|||
|
215e848efd
|
|||
|
01089aaf12
|
|||
|
9c0aa29bff
|
|||
|
404a995e4f
|
|||
|
062330bcaa
|
|||
|
39a461df56
|
|||
|
711fbfd821
|
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.
|
||||||
|
|||||||
@ -26,8 +26,10 @@ guideline_url = https://fasttube.de/wp-content/uploads/2020/12/Cororna-Regeln-St
|
|||||||
json_indent = 4
|
json_indent = 4
|
||||||
|
|
||||||
# VAPID credentials for push notifications
|
# 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
|
# 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
|
# 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_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789
|
||||||
push_sender_info = mailto:it@fasttube.de
|
push_sender_info = mailto:it@fasttube.de
|
||||||
# when to notify users, in hours after arrival
|
# when to notify users, in hours after arrival
|
||||||
|
|||||||
@ -6,6 +6,6 @@
|
|||||||
|
|
||||||
# Corona time tracker
|
# Corona time tracker
|
||||||
|
|
||||||
VERSION = (1, 0, 0)
|
VERSION = (1, 1, 0)
|
||||||
|
|
||||||
__version__ = '.'.join(map(str, VERSION))
|
__version__ = '.'.join(map(str, VERSION))
|
||||||
|
|||||||
@ -171,6 +171,25 @@ 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'])
|
@app.route('/pushsubscribe', methods=['POST'])
|
||||||
def post_pushsub():
|
def post_pushsub():
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import copy
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -33,7 +34,7 @@ class Notifier(Thread):
|
|||||||
|
|
||||||
ps = ps[0]
|
ps = ps[0]
|
||||||
|
|
||||||
print("Sending notification", arrival, ps)
|
print("Sending notification", arrival, ps, self.vapid_creds)
|
||||||
|
|
||||||
subscription = ps['sub']
|
subscription = ps['sub']
|
||||||
notification = {
|
notification = {
|
||||||
@ -41,13 +42,15 @@ class Notifier(Thread):
|
|||||||
'body': "You didn't sign out of ftracker yet",
|
'body': "You didn't sign out of ftracker yet",
|
||||||
'arr': arrival
|
'arr': arrival
|
||||||
}
|
}
|
||||||
|
privkey = self.vapid_creds['private_key']
|
||||||
|
claims = copy.copy(self.vapid_creds['claims'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
webpush(
|
webpush(
|
||||||
subscription,
|
subscription,
|
||||||
json.dumps(notification),
|
json.dumps(notification),
|
||||||
vapid_private_key = self.vapid_creds['private_key'],
|
vapid_private_key = privkey,
|
||||||
vapid_claims = self.vapid_creds['claims']
|
vapid_claims = claims
|
||||||
)
|
)
|
||||||
print("Notification sent")
|
print("Notification sent")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -26,8 +26,10 @@ guideline_url = https://youtu.be/oHg5SJYRHA0
|
|||||||
json_indent = 4
|
json_indent = 4
|
||||||
|
|
||||||
# VAPID credentials for push notifications
|
# 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
|
# 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
|
# 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_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789
|
||||||
push_sender_info = mailto:admin@example.com
|
push_sender_info = mailto:admin@example.com
|
||||||
# when to notify users, in hours after arrival
|
# when to notify users, in hours after arrival
|
||||||
|
|||||||
@ -10,11 +10,10 @@ then
|
|||||||
|
|
||||||
web-push generate-vapid-keys --json > $VAPID_CREDS_FILE
|
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`
|
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`
|
PRIV_KEY=`cat $VAPID_CREDS_FILE | jq -r .privateKey`
|
||||||
echo "push_private_key = ${PRIV_KEY}" >> /etc/ftracker/config.ini
|
echo "push_private_key = ${PRIV_KEY}" >> /etc/ftracker/config.ini
|
||||||
|
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -8,7 +8,7 @@ with open("LICENSE.md", "r") as f:
|
|||||||
|
|
||||||
st.setup(
|
st.setup(
|
||||||
name="ftracker",
|
name="ftracker",
|
||||||
version="1.0.0",
|
version="1.1.0",
|
||||||
author="Oskar @ FaSTTUBe",
|
author="Oskar @ FaSTTUBe",
|
||||||
author_email="o.winkels@fasttube.de",
|
author_email="o.winkels@fasttube.de",
|
||||||
description="Small webapp to track who was in which room at which time to backtrace potential viral infections",
|
description="Small webapp to track who was in which room at which time to backtrace potential viral infections",
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
'arrival': 'I have read and will adhere to the <a href="/guidelines" target="_blank">protection guidelines</a>',
|
'arrival': 'I have read and will adhere to the <a href="/guidelines" target="_blank">protection guidelines</a>',
|
||||||
'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 fullfill one of the <a href="https://www.bundesregierung.de/breg-de/aktuelles/bund-laender-beratung-corona-1949606">3G requirements</a></span></label>'
|
||||||
var editTimeBox = '<label>Departure Date/Time:<input type="datetime-local" name="datetime" id="datetime" required></label>'
|
var editTimeBox = '<label>Departure Date/Time:<input type="datetime-local" name="datetime" id="datetime" required></label>'
|
||||||
function getParams() {
|
function getParams() {
|
||||||
var qparams = document.location.search.substr(1)
|
var qparams = document.location.search.substr(1)
|
||||||
@ -33,27 +33,26 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1><script>
|
<h1><script>
|
||||||
document.write(qp.action ? (qp.action + "<br>Room " + qp.room) : 'FTracker<br>V1')
|
document.write(qp.action ? (qp.action + "<br>Room " + qp.room) : 'FTracker<br>V1.1')
|
||||||
</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 were in the same rooms at
|
||||||
were in the same rooms at which times in order to backtrace
|
which times in order to backtrace potential viral infections.<br><br>
|
||||||
potential viral infections.<br><br>
|
If you've reached this page that either means your're testing
|
||||||
If you've reached this page that either means your're
|
things or something has gone quite wrong with the URL.<br>
|
||||||
testing things or something has gone quite wrong with the\
|
|
||||||
URL.<br>
|
|
||||||
In the former case: Yay it works! In the latter you should
|
In the former case: Yay it works! In the latter you should
|
||||||
probably contact an admin or a dev nearby :(<br><br>
|
probably contact an admin or a dev nearby :(<br><br>
|
||||||
Here are a few links for testing:<br>
|
Here are a few links for testing:<br>
|
||||||
<a href="/view">View Data</a>,
|
<a href="/view">View Data</a>,
|
||||||
<a href="/QRgen">Door Sign Generator</a>,
|
<a href="/QRgen">Door Sign Generator</a>,
|
||||||
<a href="/?arrival=42">Test Arrival</a>,
|
<a href="/?arrival=42">Test Arrival</a>,
|
||||||
<a href="/?departure=42">Test Departure</a><br><br>
|
<a href="/?departure=42">Test Departure</a>,
|
||||||
|
<a href="javascript:localStorage.removeItem('pushsub')">Reset Push Subscription</a><br><br>
|
||||||
© 2020 made by <a target="_blank" href="mailto:o.winkels@fasttube.de">Oskar</a>
|
© 2020 made by <a target="_blank" href="mailto:o.winkels@fasttube.de">Oskar</a>
|
||||||
for <a target="_blank" href="//fasttube.de">FaSTTUBe</a>.<br>
|
for <a target="_blank" href="//fasttube.de">FaSTTUBe</a>.<br>
|
||||||
For source code & licensing see <a href="//git.fasttube.de/FaSTTUBe/ftracker">git repo</a>
|
For source code & licensing see <a href="//git.fasttube.de/FaSTTUBe/ftracker">git repo</a>
|
||||||
</div>
|
</div>
|
||||||
<form id="mainform" style="display: none">
|
<form id="mainform" action="javascript:void(0);" style="display: none">
|
||||||
<label>
|
<label>
|
||||||
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>
|
||||||
|
|||||||
43
web/main.js
43
web/main.js
@ -1,5 +1,3 @@
|
|||||||
var pushServerPublicKey = 'BBwBPYxhogHLU3B1FpxfQNzO3q7qZpmD1n1KaaL8WJbcVmJSHhi1uB-VmvsVjjUHWYCeqKyLT7w-1LBfpIcbbcg'
|
|
||||||
|
|
||||||
var spage = document.getElementById('startpage')
|
var spage = document.getElementById('startpage')
|
||||||
var mform = document.getElementById('mainform')
|
var mform = document.getElementById('mainform')
|
||||||
|
|
||||||
@ -8,10 +6,17 @@ if (qp.action) {
|
|||||||
mform.style.display = 'block'
|
mform.style.display = 'block'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefill the name field if it was successfully entered before
|
// Get the name field if it was successfully entered before
|
||||||
var savedName = localStorage.getItem('name')
|
var savedName = localStorage.getItem('name')
|
||||||
if (savedName && qp)
|
if (qp && qp.name) {
|
||||||
|
// Forced Admin checkout - prefill qp name and auto-agree
|
||||||
|
document.getElementById('name').value = qp.name.replace(/-/g, ' ').toUpperCase();
|
||||||
|
document.getElementById('agree').checked = true
|
||||||
|
document.getElementById('agree').parentElement.style.display = 'none'
|
||||||
|
} else if (savedName && qp) {
|
||||||
|
// Prefill the client's locally saved name
|
||||||
document.getElementById('name').value = savedName
|
document.getElementById('name').value = savedName
|
||||||
|
}
|
||||||
|
|
||||||
// 2nd script, server API communication
|
// 2nd script, server API communication
|
||||||
var name, datetime, agreed, tested
|
var name, datetime, agreed, tested
|
||||||
@ -45,7 +50,7 @@ function sendMainData() {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'arrival': datetime,
|
'arrival': datetime,
|
||||||
'agreetoguidelines': agreed,
|
'agreetoguidelines': agreed,
|
||||||
'tested': tested
|
'tested': tested // = 3G
|
||||||
} :
|
} :
|
||||||
{
|
{
|
||||||
'name': name,
|
'name': name,
|
||||||
@ -106,7 +111,7 @@ function handleRequestSubmit(e, json) {
|
|||||||
var iso = new Date(input).toISOString()
|
var iso = new Date(input).toISOString()
|
||||||
|
|
||||||
if (e.srcElement.length > 1)
|
if (e.srcElement.length > 1)
|
||||||
tested = e.srcElement[1].checked
|
tested = e.srcElement[1].checked // = 3G
|
||||||
|
|
||||||
// POST JSON. See docs/API.md
|
// POST JSON. See docs/API.md
|
||||||
var payload = (json.request == 'arrival') ?
|
var payload = (json.request == 'arrival') ?
|
||||||
@ -115,7 +120,7 @@ function handleRequestSubmit(e, json) {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'arrival': iso,
|
'arrival': iso,
|
||||||
'agreetoguidelines': agreed,
|
'agreetoguidelines': agreed,
|
||||||
'tested': tested
|
'tested': tested // = 3G
|
||||||
} :
|
} :
|
||||||
{
|
{
|
||||||
'name': name,
|
'name': name,
|
||||||
@ -226,6 +231,18 @@ function initPush(name) {
|
|||||||
return
|
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
|
// Register service worker
|
||||||
navigator.serviceWorker.register("/sw.js").then(function(swRegistration) {
|
navigator.serviceWorker.register("/sw.js").then(function(swRegistration) {
|
||||||
console.log("ServiceWorker registered:", swRegistration)
|
console.log("ServiceWorker registered:", swRegistration)
|
||||||
@ -251,16 +268,18 @@ function initPush(name) {
|
|||||||
}).then(function(subscription) {
|
}).then(function(subscription) {
|
||||||
console.log("User is subscribed:", subscription);
|
console.log("User is subscribed:", subscription);
|
||||||
|
|
||||||
|
var jsonSub = JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
sub: subscription
|
||||||
|
});
|
||||||
|
|
||||||
fetch('/pushsubscribe', {
|
fetch('/pushsubscribe', {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: JSON.stringify({
|
body: jsonSub
|
||||||
name: name,
|
|
||||||
sub: subscription
|
|
||||||
})
|
|
||||||
}).then(function(res) {
|
}).then(function(res) {
|
||||||
if (res.ok)
|
if (res.ok)
|
||||||
localStorage.setItem('pushsub', subscription);
|
localStorage.setItem('pushsub', jsonSub);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -67,9 +67,15 @@ main > section.times, #timeheader {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
-webkit-text-stroke: .4px #c50e1f;
|
-webkit-text-stroke: .4px #c50e1f;
|
||||||
}
|
}
|
||||||
|
.times span.tested { /* = 3G */
|
||||||
|
background: rgb(0,136,0);
|
||||||
|
}
|
||||||
.times span.implausible {
|
.times span.implausible {
|
||||||
background: linear-gradient(to right, #c50e1f, rgba(197,14,31,0.2) 1000px);
|
background: linear-gradient(to right, #c50e1f, rgba(197,14,31,0.2) 1000px);
|
||||||
}
|
}
|
||||||
|
.times span.implausible.tested { /* = 3G */
|
||||||
|
background: linear-gradient(to right, rgb(0,136,0), rgba(0,136,0,0.2) 1000px);
|
||||||
|
}
|
||||||
.viewheader.row {
|
.viewheader.row {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background: #ddd !important;
|
background: #ddd !important;
|
||||||
|
|||||||
15
web/view.js
15
web/view.js
@ -88,6 +88,14 @@ function exportCSV() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function offerUserCheckout(event) {
|
||||||
|
|
||||||
|
var name = event.target.getAttribute('data-name')
|
||||||
|
var room = event.target.textContent
|
||||||
|
window.open(`/?departure=${room}&edittime=1&name=${name}`, '_blank').focus();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function renderData() {
|
function renderData() {
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
@ -163,10 +171,13 @@ function renderData() {
|
|||||||
block.style.left = arr + 'px' // 1px/min
|
block.style.left = arr + 'px' // 1px/min
|
||||||
block.style.width = Math.max(0,(dur-14)) + 'px' // 1px/min
|
block.style.width = Math.max(0,(dur-14)) + 'px' // 1px/min
|
||||||
if (entry.tested)
|
if (entry.tested)
|
||||||
block.style.background = '#080'
|
block.classList.add('tested') // = 3G
|
||||||
|
|
||||||
if (dur > 60 * 24)
|
if (dur > 60 * 24) {
|
||||||
block.classList.add('implausible')
|
block.classList.add('implausible')
|
||||||
|
block.setAttribute('data-name', name)
|
||||||
|
block.addEventListener('click', offerUserCheckout);
|
||||||
|
}
|
||||||
|
|
||||||
row.appendChild(block)
|
row.appendChild(block)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user