"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(); }); };