diff options
author | Tom Smeding <tom@tomsmeding.com> | 2022-09-06 23:11:57 +0200 |
---|---|---|
committer | Tom Smeding <tom@tomsmeding.com> | 2022-09-06 23:11:57 +0200 |
commit | efb81ae3be0ec88193847f2865df124c1a2c6543 (patch) | |
tree | bb603c68c07cae231f846c73bd28ff3ae2e15bfa /modules/timetrack3 | |
parent | ee25ef17677c9360bb03b2d665d5e33ba0d7b9bc (diff) |
timetrack3
Diffstat (limited to 'modules/timetrack3')
-rw-r--r-- | modules/timetrack3/timetrack.html | 431 | ||||
-rw-r--r-- | modules/timetrack3/timetrack3.js | 423 | ||||
-rw-r--r-- | modules/timetrack3/unknownuser.html | 87 |
3 files changed, 941 insertions, 0 deletions
diff --git a/modules/timetrack3/timetrack.html b/modules/timetrack3/timetrack.html new file mode 100644 index 0000000..3790f43 --- /dev/null +++ b/modules/timetrack3/timetrack.html @@ -0,0 +1,431 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>TimeTrack</title> +<script> +"use strict"; + +var ROOT_ENDPOINT="/timetrack3"; + +var lastlist=null; + +function fetch(method,url,data/*?*/,creds/*?*/,cb){ + if(!creds){ + cb=data; + data=undefined; + creds=undefined; + } else if(!cb){ + cb=creds; + creds=undefined; + } + if(!cb)throw new Error("No callback passed to fetch"); + var xhr=new XMLHttpRequest(); + xhr.onreadystatechange=function(ev){ + if(xhr.readyState<4)return; + cb(xhr.status,xhr.responseText); + }; + if(creds){ + xhr.open(method,url,true,creds[0],creds[1]); + } else { + xhr.open(method,url); + } + xhr.send(data); +} + +function pad(s,n,c){ + if(c==null)c=" "; + else c=c[0]; + s=s+""; + while(s.length<n)s=c+s; + return s; +} + +function toinputdate(date) { + return date.getFullYear() + "-" + + pad(date.getMonth()+1,2,"0") + "-" + + pad(date.getDate(),2,"0") + " " + + pad(date.getHours(),2,"0") + ":" + + pad(date.getMinutes(),2,"0") + ":" + + pad(date.getSeconds(),2,"0"); +} + +function daystart(date){ + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +function shiftdays(date, off) { + var d = new Date(date); + d.setDate(d.getDate() + off); + return d; +} + +function weekstart(date){ + return shiftdays(daystart(date), -(date.getDay() + 6) % 7); +} + +var monthnames=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; +var weekdays=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]; + +function formatdate(date) { + return weekdays[(date.getDay() + 6) % 7] + ", " + toinputdate(date); +} + +function formatdaterange(d1, d2) { + var s1 = formatdate(d1); + if (daystart(d1).getTime() == daystart(d2).getTime()) { + return formatdate(d1) + " - " + + pad(d2.getHours(), 2, "0") + ":" + + pad(d2.getMinutes(), 2, "0") + ":" + + pad(d2.getSeconds(), 2, "0") + } else { + return formatdate(d1) + " - " + formatdate(d2); + } +} + +function formatduration(secs) { + var r = ""; + if (secs % 60 != 0) r = secs % 60 + "s" + r; + secs = ~~(secs / 60); + if (secs % 60 != 0) r = secs % 60 + "m" + r; + secs = ~~(secs / 60); + if (secs % 24 != 0) r = secs % 24 + "h" + r; + secs = ~~(secs / 24); + if (secs != 0) r = secs + "d" + r; + if (r == "") r = "0s"; + return r; +} + +function tablerowfor(ev){ + var div=document.createElement("div"); + div.classList.add("event"); + + var e,e2; + + e=document.createElement("span"); + e.classList.add("eventsheet"); + e.appendChild(document.createTextNode(ev.sheet)); + div.appendChild(e); + + if (ev.descr != "") { + e=document.createElement("span"); + e.classList.add("eventdescr"); + e.appendChild(document.createTextNode("(" + ev.descr + ")")); + div.appendChild(e); + } + + var float=document.createElement("div"); + float.classList.add("eventfloat"); + + e=document.createElement("span"); + e.classList.add("eventdate"); + e.appendChild(document.createTextNode(formatdaterange(ev.indate,ev.outdate) + " (" + formatduration(~~((ev.outdate - ev.indate) / 1000)) + ")")); + e.setAttribute("title",ev.indate.toString() + " - " + ev.outdate.toString()); + float.appendChild(e); + + e=document.createElement("div"); + e.classList.add("eventbuttons"); + e2=document.createElement("span"); + e2.classList.add("eventdelete"); + e2.appendChild(document.createTextNode("X")); + e2.addEventListener("click",function(){ + if(!confirm("Really delete event \""+ev.sheet+"\" (\""+ev.descr+"\") at "+formatdate(ev.indate)+"?"))return; + fetch("DELETE",ROOT_ENDPOINT+"/event",ev.id,function(status,body){ + if(status==200){getlist(false); getsheets();} + else alert("Delete failed: "+body); + }); + }); + e.appendChild(e2); + float.appendChild(e); + + div.appendChild(float); + + return div; +} + +function refreshlist(list){ + var listelem = document.getElementById("eventlist"); + listelem.innerHTML = ""; + + if (list.length == 0) { + var div = document.createElement("div"); + div.classList.add("noevents"); + div.appendChild(document.createTextNode("No events yet")); + listelem.appendChild(div); + return; + } + + for (var i = 0; i < list.length; i++) { + listelem.appendChild(tablerowfor(list[i])); + } +} + +function handleReceivedList(list){ + lastlist = list; + refreshlist(list); +} + +function updateCurrentBox(current) { + var sheet = current ? current.sheet : ""; + var descr = current ? current.descr : ""; + var indate = current ? toinputdate(current.indate) : ""; + + var e = document.getElementById("currentsheet"); + e.innerHTML = ""; + e.appendChild(document.createTextNode(sheet)); + e = document.getElementById("currentdescr"); + e.innerHTML = ""; + e.appendChild(document.createTextNode(descr)); + e = document.getElementById("currentindate"); + e.innerHTML = ""; + e.appendChild(document.createTextNode(indate)); + + var box = document.getElementById("currentbox"); + var inputs = box.getElementsByTagName("input"); + if (current == null) { + for (var i = 0; i < inputs.length; i++) inputs[i].setAttribute("disabled", ""); + box.classList.add("disabled"); + box.classList.remove("enabled"); + } else { + for (var i = 0; i < inputs.length; i++) inputs[i].removeAttribute("disabled"); + box.classList.add("enabled"); + box.classList.remove("disabled"); + } +} + +function getlist(isinitial){ + fetch("GET", ROOT_ENDPOINT + "/recent", function(status, body) { + if (status != 200) { + alert("Error: "+body); + return; + } + var obj; + try { + obj = JSON.parse(body); + } catch(e){ + alert("An error occurred!"); + return; + } + for (var i = 0; i < obj.length; i++) { + obj[i].indate = new Date(obj[i].indate * 1000); + obj[i].outdate = new Date(obj[i].outdate * 1000); + } + handleReceivedList(obj); + if (isinitial) document.getElementById("checkinbox").scrollIntoView(); + }); + + fetch("GET", ROOT_ENDPOINT + "/current", function(status, body) { + if (status != 200) { + alert("Error: "+body); + return; + } + var obj; + try { + obj = JSON.parse(body); + } catch(e){ + alert("An error occurred!"); + return; + } + if (obj.sheet) { + obj.indate = new Date(obj.indate * 1000); + updateCurrentBox(obj); + } else { + updateCurrentBox(null); + } + dateToNow("checkindate"); + dateToNow("checkoutdate"); + }); +} + +function getsheets() { + fetch("GET",ROOT_ENDPOINT+"/sheets",function(status,body){ + if(status!=200){ + // alert("Error: "+body); + return; + } + var sheets; + try { + sheets=JSON.parse(body); + } catch(e){ + // alert("An error occurred!"); + return; + } + + var el=document.getElementById("sheetselect"); + el.innerHTML = "<option value=\"\" selected></option>"; + + var totbox_html = ""; + var totbox = document.getElementById("totalsbox"); + totbox.innerHTML = ""; + + for (var i = 0; i < sheets.length; i++) { + var opt = document.createElement("option"); + opt.value = sheets[i].sheet; + opt.innerHTML = sheets[i].sheet; + el.appendChild(opt); + + totbox.appendChild(document.createTextNode(sheets[i].sheet + ": " + formatduration(sheets[i].total))); + totbox.appendChild(document.createElement("br")); + } + }); +} + +function doCheckin(atdate) { + if (atdate) atdate = new Date(document.getElementById("checkindate").value); + else atdate = new Date(); + var sheet = document.getElementById("checkinsheet").value; + var descr = document.getElementById("checkindescr").value; + fetch("POST", ROOT_ENDPOINT + "/checkin", JSON.stringify({ + sheet: sheet, + descr: descr, + date: ~~(atdate.getTime() / 1000) + }), function(status, body) { + if (status != 200) { + alert("Error performing check-in: " + body); + return; + } + getlist(false); + getsheets(); + }); +} + +function doCheckout(atdate) { + if (atdate) atdate = new Date(document.getElementById("checkoutdate").value); + else atdate = new Date(); + fetch("POST", ROOT_ENDPOINT + "/checkout", JSON.stringify({ + date: ~~(atdate.getTime() / 1000) + }), function(status, body) { + if (status != 200) { + alert("Error performing check-out: " + body); + return; + } + getlist(false); + getsheets(); + }); +} + +function setSheetFromSelect() { + document.getElementById("checkinsheet").value = + document.getElementById("sheetselect").value; +} + +function logoutReload(){ + fetch("GET", ROOT_ENDPOINT + "/authfail", undefined, ["x", "x"], function(status, body) { + location.href = location.href; + }); +} + +function dateToNow(textboxid) { + document.getElementById(textboxid).value = toinputdate(new Date()); +} + +window.addEventListener("load", function() { + getlist(true); + getsheets(); + dateToNow("checkindate"); + dateToNow("checkoutdate"); +}); +</script> +<style> +body{ + font-family:Georgia,Times,serif; + font-size:14px; +} +.event{ + border:1px #ddd solid; + border-bottom-width:0px; + padding:0px 9px; + background-color:#f8f8f8; + width:550px; +} +.event:last-child{ + border-bottom-width:1px; +} +.eventsheet{ + font-size:20px; + margin-left:10px; +} +.eventdescr{ + font-size:15px; + margin-left:10px; + width:150px; + display:inline-block; +} +.eventfloat{ + float:right; + text-align:right; +} +.eventdate{ + font-size:12px; + font-style:italic; + display:inline-block; + margin-top:6px; +} +.eventbuttons{ + display:inline-block; + margin-left:60px; + font-size:10px; + vertical-align:middle; + width:10px; + text-align:center; +} +.eventdelete{ + margin-bottom:5px; + font-size:10px; + font-family:sans-serif; + color:red; + cursor:pointer; +} +#currentbox.enabled{ + background-color: #dfd; +} +#currentbox.disabled{ + background-color: #eee; +} +#currentsheet{ + font-weight: bold; +} +#checkinbox, #currentbox, #totalsbox{ + border:1px #ddd solid; + display:inline-block; + padding:5px; +} +#checkinbox > input[type="text"], #currentbox > input[type="text"] { + margin-bottom: 5px; +} +#logoutwrapper{ + float:right; +} +</style> +</head> +<body> +<div id="logoutwrapper"> + <input type="button" onclick="logoutReload();" value="Logout"> +</div> +<h1>TimeTrack</h1> +<div id="eventlist"></div> +<br><br> +<div id="currentbox"> + Checked into: <span id="currentsheet"></span> (<span id="currentdescr"></span>)<br> + Check-in at: <span id="currentindate"></span><br><br> + + <input type="button" onclick="doCheckout(false)" value="Check out now"><br> + Or: <input type="datetime" id="checkoutdate" placeholder="YYYY-MM-DD HH:MM:SS" size="25"> + <input type="button" onclick="dateToNow('checkoutdate')" value="now"><br> + <input type="button" onclick="doCheckout(true)" value="Check out at date"> +</div> +<br><br> +<div id="checkinbox"> + Sheet: <input type="text" id="checkinsheet" placeholder="Sheet"> + <select id="sheetselect" onchange="setSheetFromSelect()"></select> <br> + Text: <input type="text" id="checkindescr" placeholder="Text"> (optional) <br> + + <input type="button" onclick="doCheckin(false)" value="Check in now"><br> + Or: <input type="datetime" id="checkindate" placeholder="YYYY-MM-DD HH:MM:SS" size="25"> + <input type="button" onclick="dateToNow('checkindate')" value="now"><br> + <input type="button" onclick="doCheckin(true)" value="Check in at date"> +</div> +<br><br> +<div id="totalsbox"></div> +</body> +</html> diff --git a/modules/timetrack3/timetrack3.js b/modules/timetrack3/timetrack3.js new file mode 100644 index 0000000..2aa1988 --- /dev/null +++ b/modules/timetrack3/timetrack3.js @@ -0,0 +1,423 @@ +"use strict"; + +const cmn = require("../$common.js"); +const crypto = require("crypto"); +const basicAuth = require("basic-auth"); +const fs = require("fs"); +const sqlite3 = require("sqlite3"); +const mkdirp = require("mkdirp"); + +let moddir = null; + +const ROOT_ENDPOINT = "/timetrack3"; +const DB_DIR = cmn.persistdir + "/timetrack3"; +const DB_PATH = DB_DIR + "/db"; + +let DB = null; + +function openDatabase() { + const predb = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READWRITE, err => { + if (err && err.code == "SQLITE_CANTOPEN") { + mkdirp.sync(DB_DIR); + + const predb2 = new sqlite3.Database(DB_PATH, err => { + if (err) { + console.error("Cannot create database:", err); + process.exit(1); + } + predb2.serialize(); + predb2.exec(` + pragma foreign_keys = ON; + create table users ( + id integer primary key autoincrement not null, + name text not null, + pwhash text not null, -- 16-byte salt + "$" + scrypt(keylen=64) + cursheet text null, + curdescr text null, + curindate integer null, -- unix timestamp + unique (name) + ) strict; + create table events ( + id integer primary key autoincrement not null, + username integer not null, + sheet text not null, + descr text not null, + indate integer not null, -- unix timestamp + outdate integer not null, -- unix timestamp + foreign key (username) references users (name) on delete cascade + ) + `); + DB = predb2; + }); + } else if (err) { + console.error("Cannot open database:", err); + process.exit(1); + } else { + predb.serialize(); + predb.exec("pragma foreign_keys = ON;"); + DB = predb; + } + }); +} + +function scryptHash(password, cb) { + crypto.randomBytes(16, function(err, salt) { + if (err) { + cb(err, null); + return; + } + crypto.scrypt(password, salt, 64, (err, key) => { + if (err) cb(err, null); + else cb(null, salt.toString("hex") + "$" + key.toString("hex")); + }); + }); +} + +function scryptCompare(password, hash, cb) { + hash = hash.split("$"); + if (hash.length != 2) { + cb(new Error("Invalid hash in database"), null); + return; + } + const salt = Buffer.from(hash[0], "hex"), shash = hash[1]; + crypto.scrypt(password, salt, 64, (err, key) => { + if (err) cb(err, null); + else if(key.toString("hex") == shash) cb(null, true); + else cb(null, false); + }); +} + + +function sendUnauth(res) { + res.set("WWW-Authenticate", "Basic realm=Authorization required"); + return res.sendStatus(401); +} + +function unknownUserHandler(req, res, next){ + res.sendFile(moddir + "/unknownuser.html"); +} + +function authMiddleware(req, res, next){ + const user = basicAuth(req); + req.authuser = null; + if (!user || !user.name) { + sendUnauth(res); + return; + } + req.authuser = user.name; + + DB.get("select pwhash from users where name = ?", [user.name], (err, row) => { + if (err || row == undefined) { + unknownUserHandler(req, res, next); + return; + } + scryptCompare(user.pass, row.pwhash, (err, ok) => { + if (ok) next(); + else sendUnauth(res); + }); + }); +} + +function asciiValid(str) { + for (let i = 0; i < str.length; i++) { + const c = str.charCodeAt(i); + if (c < 32 || c >= 127) return false; + } + return true; +} + + +function dbCallback(res, fn) { + return function(err) { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + } else { + fn(...Array.apply(Array, arguments).slice(1)); + } + }; +} + +function fatalRollback() { + DB.exec("rollback", err => { + if (err) { + console.log("ROLLBACK", err); + process.exit(1); + } + }); +} + + +module.exports = function(app, io, _moddir){ + openDatabase(); + + moddir = _moddir; + + // first the endpoints that need to bypass authMiddleware + + // - -> html + app.get(ROOT_ENDPOINT + "/authfail", (req, res) => { + sendUnauth(res); + }); + + // - -> html (account info is in basic auth) + app.post(ROOT_ENDPOINT + "/createuser", (req, res) => { + const user = basicAuth(req); + if (!user || !user.name) { + res.status(400).send("No credentials sent"); + return; + } + if (user.name.length < 3 || user.name.length > 32 || !asciiValid(user.name)) { + res.status(400).send("Invalid username"); + return; + } + if (user.pass.length < 3 || user.pass.length > 32 || !asciiValid(user.pass)) { + res.status(400).send("Invalid password"); + return; + } + + DB.get("select id from users where name = ?", [user.name], dbCallback(res, row => { + if (row != undefined) { + res.status(400).send("User already exists"); + return; + } + + DB.get("select count(*) as cnt from users", dbCallback(res, row => { + if (row.cnt >= 20) { + res.status(500).send("Too many accounts created, please contact Tom..."); + return; + } + + scryptHash(user.pass, (err, hash) => { + if (!hash) { + res.status(500).send("Something went wrong..."); + console.log(err); + return; + } + DB.run("insert into users (name, pwhash) values (?, ?)", [user.name, hash]); + res.status(200).end(); + }); + })); + })); + }); + + // for all the other endpoints, authorisation is needed + app.all([ROOT_ENDPOINT, ROOT_ENDPOINT + "/*"], authMiddleware); + + // - -> html + app.get(ROOT_ENDPOINT, (req, res) => { + res.sendFile(moddir + "/timetrack.html"); + }); + + // - -> {sheet, descr, indate} (date in unix timestamp) + app.get(ROOT_ENDPOINT + "/current", (req, res) => { + DB.get("select cursheet, curdescr, curindate from users where name = ?", [req.authuser], dbCallback(res, row => { + res.json({ + sheet: row.cursheet, + descr: row.curdescr, + indate: row.curindate, + }); + })); + }); + + // - -> [{id, sheet, descr, indate, outdate}] (dates in unix timestamp) + app.get(ROOT_ENDPOINT + "/recent", (req, res) => { + DB.all("select id, sheet, descr, indate, outdate from events where username = ? order by indate desc limit 20", [req.authuser], dbCallback(res, rows => { + // We got the rows in descending order so that we could apply the limit clause; reorder them in ascending order again + rows.reverse(); + + res.json(rows.map(row => ({ + id: row.id, + sheet: row.sheet, + descr: row.descr, + indate: row.indate, + outdate: row.outdate, + }))); + })); + }); + + // - -> [{sheet, total}] (totals in seconds) + app.get(ROOT_ENDPOINT + "/sheets", (req, res) => { + DB.all("select sheet, sum(outdate - indate) as total from events where username = ? group by sheet", [req.authuser], dbCallback(res, rows => { + res.json(rows.map(row => ({ + sheet: row.sheet, + total: row.total, + }))); + })); + }); + + // id -> - + app.delete(ROOT_ENDPOINT + "/event", (req, res) => { + const id = +req.body; + if (id < 0 || ~~id != id || isNaN(id)) { + res.status(404).send("Unknown id"); + return; + } + + DB.run("delete from events where username = ? and id = ?", [req.authuser, id], function(err) { // uses 'this' + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + return; + } + + if (this.changes == 0) { + res.status(404).send("Event not found"); + } else { + res.status(200).send(); + } + }); + }); + + // {sheet, descr, date} -> - (date in unix timestamp) + app.post(ROOT_ENDPOINT + "/checkin", (req, res) => { + let obj; + try { + obj = JSON.parse(req.body); + } catch (e) { + res.status(400).send("Invalid request"); + return; + } + const sheet = obj.sheet + "", descr = obj.descr + "", date = new Date(obj.date * 1000); + if (sheet.length == 0 || isNaN(date.getTime())) { + res.status(400).send("Invalid data"); + return; + } + + // 'immediate' to make this a write transaction + DB.exec("begin immediate", err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + return; + } + + DB.get("select (select count(*) from events where username = ?) as cnt, (select cursheet from users where name = ?) as cursheet", [req.authuser, req.authuser], (err, row) => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + if (row.cnt >= 10000) { + res.status(400).send("Isn't 10000 events enough for you?"); + fatalRollback(); + return; + } + + if (row.cursheet) { + res.status(409).send("Already checked in"); + fatalRollback(); + return; + } + + DB.run("update users set cursheet = ?, curdescr = ?, curindate = ? where name = ?", [sheet, descr, ~~(date.getTime() / 1000), req.authuser], err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + DB.exec("commit", err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + res.status(200).end(); + }); + }); + }); + }); + }); + + // {date} -> - (date in unix timestamp) + app.post(ROOT_ENDPOINT + "/checkout", (req, res) => { + let obj; + try { + obj = JSON.parse(req.body); + } catch (e) { + res.status(400).send("Invalid request"); + return; + } + const date = new Date(obj.date * 1000); + if (isNaN(date.getTime())) { + res.status(400).send("Invalid data"); + return; + } + + // 'immediate' to make this a write transaction + DB.exec("begin immediate", err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + return; + } + + DB.get("select count(*) as cnt from events where username = ?", [req.authuser], (err, row) => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + if (row.cnt >= 10000) { + res.status(400).send("Isn't 10000 events enough for you?"); + fatalRollback(); + return; + } + + DB.get("select cursheet, curdescr, curindate from users where name = ?", [req.authuser], (err, row) => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + if (!row.cursheet) { + res.status(409).send("Not checked in"); + fatalRollback(); + return; + } + + const sheet = row.cursheet, descr = row.curdescr, indate = row.curindate; + + DB.run("update users set cursheet = null, curdescr = null, curindate = null where name = ?", [req.authuser], err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + DB.run("insert into events(username, sheet, descr, indate, outdate) values (?, ?, ?, ?, ?)", [req.authuser, sheet, descr, indate, ~~(date.getTime() / 1000)], err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + DB.exec("commit", err => { + if (err) { + console.error(err); + res.status(500).send("Something went wrong..."); + fatalRollback(); + return; + } + + res.status(200).end(); + }); + }); + }); + }); + }); + }); + }); +}; diff --git a/modules/timetrack3/unknownuser.html b/modules/timetrack3/unknownuser.html new file mode 100644 index 0000000..c7d237f --- /dev/null +++ b/modules/timetrack3/unknownuser.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>TimeTrack: Unknown user</title> +<script> +"use strict"; + +var ROOT_ENDPOINT="/timetrack3"; + +function fetch(method,url,data/*?*/,creds/*?*/,cb){ + if(!creds){ + cb=data; + data=undefined; + creds=undefined; + } else if(!cb){ + cb=creds; + creds=undefined; + } + if(!cb)throw new Error("No callback passed to fetch"); + var xhr=new XMLHttpRequest(); + xhr.onreadystatechange=function(ev){ + if(xhr.readyState<4)return; + cb(xhr.status,xhr.responseText); + }; + if(creds){ + xhr.open(method,url,true,creds[0],creds[1]); + } else { + xhr.open(method,url); + } + xhr.send(data); +} + +function asciiValid(str){ + var i,c; + for(i=0;i<str.length;i++){ + c=str.charCodeAt(i); + if(c<32||c>=127)return false; + } + return true; +} + +function logoutReload(){ + fetch("GET",ROOT_ENDPOINT+"/authfail",undefined,["baduser","badpass"],function(status,body){ + location.href=location.href; + }); +} + +function doCreateUser(){ + var username=document.getElementById("username").value; + var password=document.getElementById("password").value; + var fail=false; + [["Username",username],["Password",password]].forEach(function(name,value){ + if(value.length<3){fail=true;alert(name+" too short!");} + else if(value.length>32){fail=true;alert(name+" too long!");} + else if(!asciiValid(value)){fail=true;alert("Invalid "+name.toLowerCase()+"! Please use only ASCII characters.");} + }); + if(fail)return; + + fetch("POST",ROOT_ENDPOINT+"/createuser",undefined,[username,password],function(status,body){ + if(status==200){ + alert("User \""+username+"\" created successfully. Please login."); + logoutReload(); + } else { + alert("Error: "+body); + } + }); +} +</script> +<style> +body{ + font-family:Georgia,Times,serif; + font-size:14px; +} +</style> +</head> +<body> +<h1>TimeTrack: Unknown user</h1> +<p>The user you entered is not known in the system. You can use the form below to create a new user. +Be aware: this system is not secure.</p> +Username: <input type="text" id="username" placeholder="username"><br> +Password: <input type="password" id="password" placeholder="password"><br> +<input type="button" value="Create user" onclick="doCreateUser();"> +<br><br> +<p>You can also <input type="button" onclick="logoutReload();" value="log out and try again"> if you just can't type.</p> +</body> +</html> |