diff options
Diffstat (limited to 'modules/timetrack2')
| -rw-r--r-- | modules/timetrack2/timetrack.html | 510 | ||||
| -rw-r--r-- | modules/timetrack2/timetrack2.js | 270 | ||||
| -rw-r--r-- | modules/timetrack2/unknownuser.html | 87 | 
3 files changed, 867 insertions, 0 deletions
diff --git a/modules/timetrack2/timetrack.html b/modules/timetrack2/timetrack.html new file mode 100644 index 0000000..209458d --- /dev/null +++ b/modules/timetrack2/timetrack.html @@ -0,0 +1,510 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>TimeTrack</title> +<script> +"use strict"; + +var ROOT_ENDPOINT="/timetrack2"; + +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 weekstart(date){ +	var d=daystart(date); +	var wkday=(d.getDay()+6)%7; +	return new Date(d - wkday*24*3600*1000); +} + +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 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.text != "") { +		e=document.createElement("span"); +		e.classList.add("eventtext"); +		e.appendChild(document.createTextNode("(" + ev.text + ")")); +		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))); +		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.text+"\") at "+formatdate(ev.indate)+"?"))return; +				fetch("DELETE",ROOT_ENDPOINT+"/event",ev.id,function(status,body){ +					if(status==200)getlist(false); +					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; +	} + +	var maxshown = 40; +	if (list.length > maxshown) { +		var div = document.createElement("div"); +		div.classList.add("moreevents"); +		div.appendChild(document.createTextNode("... more events ...")); +		listelem.appendChild(div); +	} +	for (var i = Math.max(0, list.length - maxshown); i < list.length; i++) { +		listelem.appendChild(tablerowfor(list[i])); +	} +} + +function refreshcalendar(list,npreweeks){ +	if (npreweeks == undefined) npreweeks = 2; + +	var dayTargetH = 38/5 - 1; + +	var weekTotals = new Map();  // only in weeks that have entries +	var totalTime = 0; + +	function datekey(d) { return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDate(); } +	var hist = new Map(); +	for (var i = 0; i < list.length; i++) { +		var key = datekey(list[i].indate); +		var wkey = datekey(weekstart(list[i].indate)); + +		var thistime = list[i].outdate - list[i].indate; +		var yet = hist.has(key) ? hist.get(key) : 0; +		hist.set(key, yet + thistime); + +		var weekyet = weekTotals.has(wkey) ? weekTotals.get(wkey) : 0; +		weekTotals.set(wkey, weekyet + thistime); + +		totalTime += thistime; +	} + +	var totalTargetH = 5 * weekTotals.size * dayTargetH; +	var totalSurplus = totalTime - totalTargetH * 3600 * 1000; + +	var tb = document.getElementById("calendartb"); +	tb.innerHTML = ""; + +	var tr = document.createElement("tr"); +	tr.appendChild(document.createElement("td")); +	var td; +	for (var i = 0; i < 7; i++) { +		td = document.createElement("td"); +		td.appendChild(document.createTextNode(weekdays[i])); +		tr.appendChild(td); +	} +	td = document.createElement("td"); +	var a = document.createElement("a"); +	a.href = "javascript:refreshcalendar(lastlist," + (npreweeks + 4) + ")"; +	a.appendChild(document.createTextNode("\u2191")); +	td.appendChild(a); +	tr.appendChild(td); +	tb.appendChild(tr); + +	var now = new Date(); +	var today = daystart(now), thismonday = weekstart(now); +	for (var wkoff = -npreweeks; wkoff <= 1; wkoff++) { +		var monday = new Date(thismonday.getTime() + wkoff*7*24*3600*1000); + +		tr = document.createElement("tr"); +		td = document.createElement("td"); +		var label = monday.getDate().toString(); +		if (monday.getDate() <= 7 || wkoff == -npreweeks) { +			label = monthnames[monday.getMonth()].slice(0, 3) + " " + label; +		} +		td.appendChild(document.createTextNode(label)); +		tr.appendChild(td); + +		for (var i = 0; i < 7; i++) { +			var day = new Date(thismonday.getTime() + (wkoff*7+i)*24*3600*1000); +			var tm = hist.get(datekey(day)) || 0; +			var descr = Math.round(tm/1000/3600*10)/10 + "h"; +			var target = (day.getDay()+6)%7 < 5 ? 38/5 - 1 : 0; +			var diff = tm - target*3600*1000; +			var surplus = 1 - Math.exp(-(diff>0?diff:0)/(3*3600*1000)); +			var deficit = 1 - Math.exp(-(diff<0?-diff:0)/(3*3600*1000)); +			td = document.createElement("td"); +			td.appendChild(document.createTextNode(descr)); +			td.setAttribute("style", +				"background-color:rgb(" + +				(255 - 255*surplus) + "," + +				(255 - 255*(deficit+surplus)) + "," + +				(255 - 255*deficit) + ")" +			); +			if (day.getTime() == today.getTime()) td.classList.add("today"); +			tr.appendChild(td); +		} + +		if (wkoff == -1) { +			// "prev" refers to "all weeks before this week" +			var thisWeekTotal = weekTotals.get(datekey(thismonday)) || 0; +			var prevTotal = totalTime - thisWeekTotal; +				var numPrevWeeks = weekTotals.size - (thisWeekTotal > 0); +			var prevTarget = numPrevWeeks * 5 * dayTargetH; +			var prevSurplus = prevTotal - prevTarget*3600*1000; +			var descr = Math.round(prevSurplus/1000/3600*10)/10 + "h"; + +			td = document.createElement("td"); +			var span = document.createElement("span"); +			span.setAttribute("style", "display:inline-block;width: max-content;"); +			span.appendChild(document.createTextNode("Account: " + descr)); +			td.appendChild(span); +			tr.appendChild(td); +		} + +		tb.appendChild(tr); +	} +} + +function handleReceivedList(list){ +	lastlist = list; +	refreshlist(list); +	refreshcalendar(list); +} + +function updateCurrentBox(current) { +	var sheet = current ? current.sheet : ""; +	var text = current ? current.text : ""; +	var indate = current ? toinputdate(current.indate) : ""; + +	var e = document.getElementById("currentsheet"); +	e.innerHTML = ""; +	e.appendChild(document.createTextNode(sheet)); +	e = document.getElementById("currenttext"); +	e.innerHTML = ""; +	e.appendChild(document.createTextNode(text)); +	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 + "/list", function(status, body) { +		if (status != 200) { +			alert("Error: "+body); +			return; +		} +		var obj; +		try { +			obj = JSON.parse(body); +		} catch(e){ +			alert("An error occurred!"); +			return; +		} +		var list = obj.list, current = obj.current; +		for (var i = 0; i < list.length; i++) { +			list[i].indate = new Date(list[i].indate); +			list[i].outdate = new Date(list[i].outdate); +		} +		if (current != null) current.indate = new Date(current.indate); +		handleReceivedList(list); +		updateCurrentBox(current); +		if (isinitial) document.getElementById("checkinbox").scrollIntoView(); +		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>"; + +		for (var i = 0; i < sheets.length; i++) { +			var opt = document.createElement("option"); +			opt.value = sheets[i]; +			opt.innerHTML = sheets[i]; +			el.appendChild(opt); +		} +	}); +} + +function doCheckin(atdate) { +	if (atdate) atdate = new Date(document.getElementById("checkindate").value); +	else atdate = new Date(); +	var sheet=document.getElementById("checkinsheet").value; +	var text=document.getElementById("checkintext").value; +	fetch("POST", ROOT_ENDPOINT + "/checkin", JSON.stringify({ +		sheet: sheet, +		text: text, +		date: atdate +	}), function(status, body) { +		if (status != 200) { +			alert("Error performing check-in: " + body); +			return; +		} +		getlist(false); +	}); +} + +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 +	}), function(status, body) { +		if (status != 200) { +			alert("Error performing check-out: " + body); +			return; +		} +		getlist(false); +	}); +} + +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:500px; +} +.event:last-child{ +	border-bottom-width:1px; +} +.eventsheet{ +	font-size:20px; +	margin-left:10px; +} +.eventtext{ +	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{ +	border:1px #ddd solid; +	display:inline-block; +	padding:5px; +} +#checkinbox > input[type="text"], #currentbox > input[type="text"] { +	margin-bottom: 5px; +} +#logoutwrapper{ +	float:right; +} +#calendartb td{ +	width:35px; +} +#calendartb td:first-child{ +	text-align:right; +	width:50px; +} +#calendartb td.today{ +	border:1px black solid; +	font-weight:bold; +} +</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="currenttext"></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="checkintext" 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="calendar"> +	<table><tbody id="calendartb"></tbody></table> +</div> +</body> +</html> diff --git a/modules/timetrack2/timetrack2.js b/modules/timetrack2/timetrack2.js new file mode 100644 index 0000000..cb22132 --- /dev/null +++ b/modules/timetrack2/timetrack2.js @@ -0,0 +1,270 @@ +"use strict"; + +var cmn = require("../$common.js"), +    persist = require("node-persist"), +    crypto = require("crypto"), +    basicAuth = require("basic-auth"), +    fs = require("fs"); + +var moddir = null; + +var ROOT_ENDPOINT = "/timetrack2"; + +persist = persist.create({ +	dir: cmn.persistdir + "/timetrack2", +	continuous: false, +	interval: false +}); +persist.initSync(); + +//accounts: { +//  "user": { +//    "list": [{id: Int, sheet: String, text: String, indate: Date, outdate: Date}], +//    "current": {sheet: String, text: String, indate: Date}/null, +//    "pwhash": hash (String) +//  } +//} +var nextid = persist.getItemSync("nextid"); +if (nextid == null) { +	nextid = 1; +	persist.setItemSync("nextid", nextid); +} +var accounts = persist.getItemSync("accounts"); +var naccounts = 0; +(function() { +	if (accounts == null) { +		accounts = {}; +		persist.setItemSync("accounts", accounts); +	} else { +		for (var user in accounts) { +			naccounts++; +			for (var ev of accounts[user].list) { +				ev.indate = new Date(ev.indate); +				ev.outdate = new Date(ev.outdate); +				if(nextid <= ev.id) nextid = ev.id+1; +			} +			if (accounts[user].current != null) { +				accounts[user].current.indate = new Date(accounts[user].current.indate); +			} +		} +		persist.setItemSync("nextid", nextid); +	} +})(); + + +function scryptHash(password, cb) { +	crypto.randomBytes(16, function(err, salt){ +		if (err) { +			cb(err, null); +			return; +		} +		crypto.scrypt(password, salt, 32, function(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; +	} +	var salt = Buffer.from(hash[0], "hex"), shash = hash[1]; +	crypto.scrypt(password, salt, 32, function(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){ +	var user = basicAuth(req); +	req.authuser = null; +	if (!user || !user.name) { +		sendUnauth(res); +		return; +	} +	req.authuser = user.name; +	if (accounts[req.authuser]) { +		scryptCompare(user.pass, accounts[req.authuser].pwhash, function(err, ok){ +			if (ok) next(); +			else sendUnauth(res); +		}); +	} else { +		unknownUserHandler(req, res, next); +	} +} + +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; +} + + +module.exports = function(app, io, _moddir){ +	moddir = _moddir; + +	//first the endpoints that need to bypass authMiddleware +	app.get(ROOT_ENDPOINT + "/authfail", function(req, res){ +		sendUnauth(res); +	}); +	app.post(ROOT_ENDPOINT + "/createuser", function(req, res){ +		var 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; +		} +		if (accounts[user.name]) { +			res.status(400).send("User already exists"); +			return; +		} +		if (naccounts >= 20) { +			res.status(500).send("Too many accounts created, please contact Tom..."); +			return; +		} +		scryptHash(user.pass, function(err, hash){ +			if (!hash) { +				res.status(500).send("Something went wrong..."); +				console.log(err); +				return; +			} +			accounts[user.name] = { +				list: [], +				current: null, +				pwhash: hash +			}; +			naccounts++; +			persist.setItemSync("accounts", accounts); +			res.status(200).end(); +		}); +	}); + +	app.all([ROOT_ENDPOINT, ROOT_ENDPOINT+"/*"], authMiddleware); //for all the other endpoints + +	app.get(ROOT_ENDPOINT, function(req, res){ +		res.sendFile(moddir + "/timetrack.html"); +	}); +	app.get(ROOT_ENDPOINT + "/list", function(req, res){ +		res.json({ +			list: accounts[req.authuser].list, +			current: accounts[req.authuser].current, +		}); +	}); +	app.get(ROOT_ENDPOINT + "/sheets", function(req, res){ +		var seen = new Map(); +		var list = []; +		for (var i = 0; i < accounts[req.authuser].list.length; i++){ +			if (!seen.has(accounts[req.authuser].list[i].sheet)) { +				seen.set(accounts[req.authuser].list[i].sheet, 0); +				list.push(accounts[req.authuser].list[i].sheet); +			} +		} +		res.json(list); +	}); +	app.delete(ROOT_ENDPOINT + "/event", function(req, res){ +		var id = +req.body; +		var i; +		var fail = false; +		if (id < 0 || ~~id != id || isNaN(id) || !(req.authuser in accounts)) { +			fail = true; +		} else { +			var userevents = accounts[req.authuser].list; +			for (i = 0; i < userevents.length; i++) if (userevents[i].id == id) break; +			if (i == userevents.length) fail = true; +			else { +				userevents.splice(i, 1); +				persist.setItemSync("accounts", accounts); +			} +		} +		if (fail) res.status(404).send("Unknown id"); +		else res.status(200).end(); +	}); +	app.post(ROOT_ENDPOINT + "/checkin", function(req, res){ +		var obj; +		try { +			obj = JSON.parse(req.body); +		} catch (e) { +			res.status(400).send("Invalid request"); +			return; +		} +		var sheet = obj.sheet + "", text = obj.text + "", date = new Date(obj.date); +		if (sheet.length == 0 || isNaN(date.getTime())) { +			res.status(400).send("Invalid data"); +			return; +		} +		if (accounts[req.authuser].list.length >= 4000) { +			res.status(400).send("Isn't 4000 events enough for you?"); +			return; +		} +		if (accounts[req.authuser].current != null) { +			res.status(409).send("Already checked in"); +			return; +		} +		accounts[req.authuser].current = { +			sheet: sheet, +			text: text, +			indate: date, +		}; +		persist.setItemSync("accounts", accounts); +		res.status(200).end(); +	}); +	app.post(ROOT_ENDPOINT + "/checkout", function(req, res){ +		var obj; +		try { +			obj = JSON.parse(req.body); +		} catch (e) { +			res.status(400).send("Invalid request"); +			return; +		} +		var date = new Date(obj.date); +		if (isNaN(date.getTime())) { +			res.status(400).send("Invalid data"); +			return; +		} +		if (accounts[req.authuser].current == null) { +			res.status(409).send("Not checked in"); +			return; +		} +		if (accounts[req.authuser].list.length >= 4000) { +			res.status(400).send("Isn't 4000 events enough for you?"); +			return; +		} +		var current = accounts[req.authuser].current; +		accounts[req.authuser].list.push({ +			id: nextid++, +			sheet: current.sheet, +			text: current.text, +			indate: current.indate, +			outdate: date, +		}); +		accounts[req.authuser].current = null; +		accounts[req.authuser].list.sort(function(a, b) { return a.indate - b.indate; }); +		persist.setItemSync("accounts", accounts); +		persist.setItemSync("nextid", nextid); +		res.status(200).end(); +	}); +}; diff --git a/modules/timetrack2/unknownuser.html b/modules/timetrack2/unknownuser.html new file mode 100644 index 0000000..e2142a1 --- /dev/null +++ b/modules/timetrack2/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="/timetrack2"; + +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>  | 
