From 78eedbb2aba66695cacac8d832a44512379e4a22 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Wed, 9 Mar 2022 00:08:00 +0100 Subject: Timetrack 2 --- modules/timetrack2/timetrack.html | 510 ++++++++++++++++++++++++++++++++++++ modules/timetrack2/timetrack2.js | 270 +++++++++++++++++++ modules/timetrack2/unknownuser.html | 87 ++++++ timetrack-migrate.js | 52 ++++ 4 files changed, 919 insertions(+) create mode 100644 modules/timetrack2/timetrack.html create mode 100644 modules/timetrack2/timetrack2.js create mode 100644 modules/timetrack2/unknownuser.html create mode 100644 timetrack-migrate.js 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 @@ + + + + +TimeTrack + + + + +
+ +
+

TimeTrack

+
+

+
+ Checked into: ()
+ Check-in at:

+ +
+ Or: +
+ +
+

+
+ Sheet: +
+ Text: (optional)
+ +
+ Or: +
+ +
+

+
+
+
+ + 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 @@ + + + + +TimeTrack: Unknown user + + + + +

TimeTrack: Unknown user

+

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.

+Username:
+Password:
+ +

+

You can also if you just can't type.

+ + diff --git a/timetrack-migrate.js b/timetrack-migrate.js new file mode 100644 index 0000000..c94b1b5 --- /dev/null +++ b/timetrack-migrate.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +const fs = require("fs"); + +// accounts: {"user": hash (String)} +const origaccounts_obj = JSON.parse(fs.readFileSync("persist/timetrack/7a90e38a211ece1c346928e7d1f3e968")); +const origaccounts = origaccounts_obj.value; + +// events: {"user": [{id: Int,sheet: String,text: String,date: Date,type: String}]} +// type: "in" / "out" +const origevents_obj = JSON.parse(fs.readFileSync("persist/timetrack/16908b0605f2645dfcb4c3a8d248cef3")); +const origevents = origevents_obj.value; + +const destfname = "persist/timetrack2/7a90e38a211ece1c346928e7d1f3e968"; + +const accounts = {}; +for (let user in origaccounts) { + accounts[user] = { + list: [], + current: null, + pwhash: origaccounts[user], + }; + + let current = null; + for (let ev of origevents[user]) { + if (ev.type == "in" && current == null) { + current = ev; + } else if (ev.type == "out" && current != null) { + if (current.sheet != ev.sheet) { + console.error("Inconsistent in/out pairs (sheet for id " + ev.id + ")"); + process.exit(1); + } + + const text = current.text.length > ev.text.length ? current.text : ev.text; + + accounts[user].list.push({ + id: current.id, // arbitrarily choose check-in id + sheet: current.sheet, + text: text, + indate: current.date, + outdate: ev.date, + }); + + current = null; + } else { + console.error("Inconsistent in/out pairs at id=" + ev.id); + process.exit(1); + } + } +} + +fs.writeFileSync(destfname, JSON.stringify({key:"accounts",value:accounts})); -- cgit v1.2.3-54-g00ecf