summaryrefslogtreecommitdiff
path: root/modules/timetrack2/timetrack2.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/timetrack2/timetrack2.js')
-rw-r--r--modules/timetrack2/timetrack2.js270
1 files changed, 270 insertions, 0 deletions
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();
+ });
+};