summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortomsmeding <tom.smeding@gmail.com>2016-10-29 23:21:32 +0200
committertomsmeding <tom.smeding@gmail.com>2016-10-29 23:21:32 +0200
commit848db6a4e3f7091dc4a2e20dcae47f6669801e99 (patch)
tree898702f2313ae5363f02205d3812e11f814cf4b8
parent730a78ba9deb5a70b56497dd61e3aa52e62d58ed (diff)
todo: Introducing multi-user TODO!
-rw-r--r--modules/$common.js1
-rw-r--r--modules/todo/todo.html28
-rw-r--r--modules/todo/todo.js121
-rw-r--r--modules/todo/unknownuser.html87
-rw-r--r--package.json6
5 files changed, 223 insertions, 20 deletions
diff --git a/modules/$common.js b/modules/$common.js
index fa29c50..c5c3635 100644
--- a/modules/$common.js
+++ b/modules/$common.js
@@ -24,6 +24,7 @@ module.exports={
var i;
for(i=0;i<accounts.length;i++){
if(accounts[i][0]==user.name&&accounts[i][1]==user.pass){
+ req.authuser=user.name;
return next();
}
}
diff --git a/modules/todo/todo.html b/modules/todo/todo.html
index b196682..1f2a060 100644
--- a/modules/todo/todo.html
+++ b/modules/todo/todo.html
@@ -6,18 +6,26 @@
<script>
"use strict";
-function fetch(method,url,data/*?*/,cb){
- if(!cb){
+function fetch(method,url,data/*?*/,creds/*?*/,cb){
+ if(!creds){
cb=data;
data=undefined;
- if(!cb)throw new Error("No callback passed to fetch");
+ 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);
};
- xhr.open(method,url);
+ if(creds){
+ xhr.open(method,url,true,creds[0],creds[1]);
+ } else {
+ xhr.open(method,url);
+ }
xhr.send(data);
}
@@ -187,6 +195,12 @@ function doAddTask(ev){
});
}
+function logoutReload(){
+ fetch("GET","/todo/authfail",undefined,["baduser","badpass"],function(status,body){
+ location.href=location.href;
+ });
+}
+
window.addEventListener("load",getlist);
</script>
<style>
@@ -238,9 +252,15 @@ body{
#addtaskform.invisible{
display:none;
}
+#logoutwrapper{
+ float:right;
+}
</style>
</head>
<body>
+<div id="logoutwrapper">
+ <input type="button" onclick="logoutReload();" value="Logout">
+</div>
<h1>TODO</h1>
<div id="todolist"></div>
<br><br>
diff --git a/modules/todo/todo.js b/modules/todo/todo.js
index 9e53c76..58322f2 100644
--- a/modules/todo/todo.js
+++ b/modules/todo/todo.js
@@ -3,7 +3,11 @@
// {"key":"tasks","value":[{"id":1,"subject":"kaas rep","repweeks":0,"date":"2016-10-25T07:46:54.493Z"},{"id":2,"subject":"kaas","repweeks":0,"date":"2016-10-27T07:46:54.493Z"}]}
var cmn=require("../$common.js"),
- persist=require("node-persist");
+ persist=require("node-persist"),
+ bcrypt=require("bcrypt"),
+ basicAuth=require("basic-auth");
+
+var bcryptHashRounds=10;
var moddir=null;
@@ -14,45 +18,132 @@ persist=persist.create({
});
persist.initSync();
-//tasks: [{id: Int,subject: String,repweeks: Int,date: Date}]
+//tasks: {"user": [{id: Int,subject: String,repweeks: Int,date: Date}]}
+//accounts: {"user": hash (String)}
//repweeks==0 implies no repetition
var nextid=persist.getItemSync("nextid");
-if(nextid==null)nextid=1;
+if(nextid==null){
+ nextid=1;
+ persist.setItemSync("nextid",nextid);
+}
var tasks=persist.getItemSync("tasks");
(function(){
- if(!tasks){
- tasks=[];
+ if(tasks==null){
+ tasks={};
persist.setItemSync("tasks",tasks);
} else {
- for(var task of tasks){
- task.date=new Date(task.date);
- if(nextid<=task.id)nextid=task.id+1;
+ for(var user in tasks){
+ for(var task of tasks[user]){
+ task.date=new Date(task.date);
+ if(nextid<=task.id)nextid=task.id+1;
+ }
}
persist.setItemSync("nextid",nextid);
}
})();
+var accounts=persist.getItemSync("accounts");
+if(accounts==null){
+ accounts={};
+ persist.setItemSync("accounts",accounts);
+}
+
+
+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]){
+ bcrypt.compare(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;
- app.all("/todo/task",cmn.authgen());
+
+ //first the endpoints that need to bypass authMiddleware
+ app.get("/todo/authfail",function(req,res){
+ sendUnauth(res);
+ });
+ app.post("/todo/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;
+ }
+ bcrypt.hash(user.pass,bcryptHashRounds,function(err,hash){
+ if(!hash){
+ res.status(500).send("Something went wrong...");
+ console.log(err);
+ return;
+ }
+ accounts[user.name]=hash;
+ tasks[user.name]=[];
+ persist.setItemSync("accounts",accounts);
+ persist.setItemSync("tasks",tasks);
+ res.status(200).end();
+ });
+ });
+
+ app.all(["/todo","/todo/*"],authMiddleware); //for all the other endpoints
+
app.get("/todo",function(req,res){
res.sendFile(moddir+"/todo.html");
});
app.get("/todo/list",function(req,res){
- res.json(tasks);
+ res.json(tasks[req.authuser]);
});
app.delete("/todo/task",function(req,res){
var id=+req.body;
var i;
var fail=false;
- if(id<0||~~id!=id||isNaN(id)){
+ var usertasks=tasks[req.authuser];
+ if(id<0||~~id!=id||isNaN(id)||!usertasks){
fail=true;
} else {
- for(i=0;i<tasks.length;i++)if(tasks[i].id==id)break;
- if(i==tasks.length)fail=true;
+ for(i=0;i<usertasks.length;i++)if(usertasks[i].id==id)break;
+ if(i==usertasks.length)fail=true;
else {
- tasks.splice(i,1);
+ usertasks.splice(i,1);
persist.setItemSync("tasks",tasks);
}
}
@@ -74,7 +165,7 @@ module.exports=function(app,io,_moddir){
res.status(400).send("Invalid data");
return;
}
- tasks.push({
+ tasks[req.authuser].push({
id:nextid++,
subject:subject,
repweeks:repweeks,
diff --git a/modules/todo/unknownuser.html b/modules/todo/unknownuser.html
new file mode 100644
index 0000000..c8590a3
--- /dev/null
+++ b/modules/todo/unknownuser.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>TODO: Unknown user</title>
+<script>
+"use strict";
+
+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","/todo/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;
+ [["Username",username],["Password",password]].forEach(function(name,value){
+ fail=true;
+ if(value.length<3)alert(name+" too short!");
+ else if(value.length>32)alert(name+" too long!");
+ else if(!asciiValid(value))alert("Invalid "+name.toLowerCase()+"! Please use only ASCII characters.");
+ else fail=false;
+ });
+ if(fail)return;
+
+ fetch("POST","/todo/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>TODO: Unknown user</h1>
+<p>The user you entered is not known in the system. You can use the below form 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>
diff --git a/package.json b/package.json
index e45bec2..2b8d5f5 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"main": "webserver.js",
"dependencies": {
"basic-auth": "^1.0.4",
+ "bcrypt": "^0.8.7",
"body-parser": "^1.15.2",
"express": "^4.14.0",
"express-subdomain": "^1.0.5",
@@ -13,7 +14,10 @@
"socket.io": "^1.4.8"
},
"devDependencies": {},
- "repository": {"type": "git", "url": "https://github.com/tomsmeding/webserver"},
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tomsmeding/webserver"
+ },
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},