summaryrefslogtreecommitdiff
path: root/modules/timetrack/timetrack.js
diff options
context:
space:
mode:
authorTom Smeding <tom@tomsmeding.com>2022-02-03 10:13:46 +0100
committerTom Smeding <tom@tomsmeding.com>2022-02-03 10:13:46 +0100
commitd601d0431d6a35ce55680af9987c2ecfee719870 (patch)
tree8698dc4cbf764f89bf3cdab6eaa71f03df8e5b5a /modules/timetrack/timetrack.js
parent38b2f19f563d560be208b0c8f53bc7b6630c0436 (diff)
timetrack: Initial
Diffstat (limited to 'modules/timetrack/timetrack.js')
-rw-r--r--modules/timetrack/timetrack.js228
1 files changed, 228 insertions, 0 deletions
diff --git a/modules/timetrack/timetrack.js b/modules/timetrack/timetrack.js
new file mode 100644
index 0000000..886ea1e
--- /dev/null
+++ b/modules/timetrack/timetrack.js
@@ -0,0 +1,228 @@
+"use strict";
+
+var cmn=require("../$common.js"),
+ persist=require("node-persist"),
+ crypto=require("crypto"),
+ basicAuth=require("basic-auth"),
+ fs=require("fs");
+
+var moddir=null;
+
+persist=persist.create({
+ dir:cmn.persistdir+"/timetrack",
+ continuous:false,
+ interval:false
+});
+persist.initSync();
+
+//events: {"user": [{id: Int,sheet: String,text: String,date: Date,type: String}]}
+// type: "in" / "out"
+//accounts: {"user": hash (String)}
+var nextid=persist.getItemSync("nextid");
+if(nextid==null){
+ nextid=1;
+ persist.setItemSync("nextid",nextid);
+}
+var events=persist.getItemSync("events");
+(function(){
+ if(events==null){
+ events={};
+ persist.setItemSync("events",events);
+ } else {
+ for(var user in events){
+ for(var ev of events[user]){
+ ev.date=new Date(ev.date);
+ if(nextid<=ev.id)nextid=ev.id+1;
+ }
+ }
+ persist.setItemSync("nextid",nextid);
+ }
+})();
+var accounts=persist.getItemSync("accounts");
+if(accounts==null){
+ accounts={};
+ persist.setItemSync("accounts",accounts);
+}
+var naccounts=0;
+(function(){
+ var user;
+ for(user in accounts)naccounts++;
+})();
+
+
+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],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("/timetrack/authfail",function(req,res){
+ sendUnauth(res);
+ });
+ app.post("/timetrack/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]=hash;
+ events[user.name]=[];
+ naccounts++;
+ persist.setItemSync("accounts",accounts);
+ persist.setItemSync("events",events);
+ res.status(200).end();
+ });
+ });
+
+ app.all(["/timetrack","/timetrack/*"],authMiddleware); //for all the other endpoints
+
+ app.get("/timetrack",function(req,res){
+ res.sendFile(moddir+"/timetrack.html");
+ });
+ app.get("/timetrack/list",function(req,res){
+ res.json(events[req.authuser]);
+ });
+ app.get("/timetrack/sheets",function(req,res){
+ var seen=new Map();
+ var list=[];
+ for (var i=0;i<events[req.authuser].length;i++){
+ if (!seen.has(events[req.authuser][i].sheet)) {
+ seen.set(events[req.authuser][i].sheet, 0);
+ list.push(events[req.authuser][i].sheet);
+ }
+ }
+ res.json(list);
+ });
+ app.delete("/timetrack/event",function(req,res){
+ var id=+req.body;
+ var i;
+ var fail=false;
+ var userevents=events[req.authuser];
+ if(id<0||~~id!=id||isNaN(id)||!userevents){
+ fail=true;
+ } else {
+ 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("events",events);
+ }
+ }
+ if(fail)res.status(404).send("Unknown id");
+ else res.status(200).end();
+ });
+ app.post("/timetrack/event",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),type=obj.type+"";
+ if(sheet.length==0||isNaN(date.getTime())||(type!="in"&&type!="out")){
+ res.status(400).send("Invalid data");
+ return;
+ }
+ if(events[req.authuser].length>=4000){
+ res.status(400).send("Isn't 4000 events enough for you?");
+ return;
+ }
+ events[req.authuser].push({
+ id:nextid++,
+ sheet:sheet,
+ text:text,
+ date:date,
+ type:type
+ });
+ events[req.authuser].sort(function(a, b) { return a.date - b.date; });
+ persist.setItemSync("events",events);
+ persist.setItemSync("nextid",nextid);
+ res.status(200).end();
+ });
+};