@@ -16,6 +16,9 @@ | |||
"title": "Spus", | |||
"base_url": "http://example.com" | |||
}, | |||
"scrypt": { | |||
"maxtime": 1 | |||
}, | |||
"minify": true, | |||
"session_timeout": 1800000, | |||
"dir": { |
@@ -9,7 +9,10 @@ function templatify(str, args, ctx, env) { | |||
str = preprocess(str, { | |||
session: ctx.session, | |||
arg: args, | |||
arg: function(key) { | |||
return ctx.htmlEntities(args[key]); | |||
}, | |||
noescape: args, | |||
env: env, | |||
template: function(key) { | |||
return ctx.template(key); | |||
@@ -39,10 +42,10 @@ module.exports = function(options) { | |||
}.bind(this)); | |||
//Handle sessions | |||
var key; | |||
if (sessions[this.cookies.session]) { | |||
this.session = sessions[this.cookies.session]; | |||
key = this.cookies.session; | |||
} else { | |||
var key; | |||
do { | |||
key = crypto.randomBytes(64).toString("hex"); | |||
} while (sessions[key]); | |||
@@ -54,9 +57,16 @@ module.exports = function(options) { | |||
setTimeout(function() { | |||
delete sessions[key]; | |||
}, this.conf.session_timeout); | |||
this.session = sessions[key]; | |||
} | |||
this.session = sessions[key]; | |||
//Reset session delete timer | |||
if (this.session._timeout) | |||
clearTimeout(this.session._timeout); | |||
this.session._timeout = setTimeout(function() { | |||
delete sessions[key]; | |||
}, this.conf.session_timeout); | |||
} | |||
module.exports.prototype = { | |||
@@ -173,5 +183,16 @@ module.exports.prototype = { | |||
else | |||
n -= 1; | |||
} | |||
}, | |||
htmlEntities: function(arg) { | |||
if (typeof arg === "string") { | |||
return arg.replace(/&/g, "&") | |||
.replace(/</g, "<") | |||
.replace(/>/g, ">") | |||
.replace(/"/g, """); | |||
} else { | |||
return arg; | |||
} | |||
} | |||
} |
@@ -31,6 +31,11 @@ var endpoints = { | |||
"/profile/style.css": "profile/style.css", | |||
"/profile/script.js": "profile/script.js", | |||
//Settings | |||
"/settings": "settings/index.node.js", | |||
"/settings/style.css": "settings/style.css", | |||
"/settings/script.js": "settings/script.js", | |||
//Viewer | |||
"/view": "view/index.node.js", | |||
"/view/style.css": "view/style.css", | |||
@@ -44,7 +49,8 @@ var endpoints = { | |||
"/api/collection_create": "api/collection_create.node.js", | |||
"/api/account_create": "api/account_create.node.js", | |||
"/api/account_login": "api/account_login.node.js", | |||
"/api/account_logout": "api/account_logout.node.js" | |||
"/api/account_logout": "api/account_logout.node.js", | |||
"/api/account_change_password": "api/account_change_password.node.js" | |||
} | |||
var loaded = loader.load(endpoints, conf); |
@@ -1,4 +1,4 @@ | |||
<div class="collection"> | |||
<a class="name" href="/view?{{arg#id}}">{{arg#name}}</a> | |||
<span class="date-created">{{arg#date_created}}</span> | |||
<a class="name" href="/view?{{arg#id}}">{{arg#name}}</a> | |||
</div> |
@@ -1,3 +1,3 @@ | |||
<li> | |||
<a id="navbar-button" href="/profile?{{session#userId}}">{{session#username}}</a> | |||
<a id="navbar-button" href="/settings">{{session#username}}</a> | |||
</li> |
@@ -1,10 +1,13 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="utf-8"> | |||
{{template#head}} | |||
</head> | |||
<body> | |||
404, file not found.<br> | |||
{{env#url}} | |||
{{template#body}} | |||
<div class="container"> | |||
<div class="title">404 not found.</div> | |||
{{env#url}} | |||
</div> | |||
</body> | |||
</html> |
@@ -9,9 +9,10 @@ | |||
<div id="uploader" class="container"> | |||
<input type="file" accept="image/*" id="uploader-input" class="hidden" multiple> | |||
<button class="btn btn-default" onclick="$('#uploader-input').click()"> | |||
<button class="btn btn-default" onclick="$('#uploader-input').click()" id="uploader-select-files"> | |||
Select Files | |||
</button> | |||
<input type="text" id="uploader-collection-name" placeholder="Collection"> | |||
<button class="btn btn-default" id="uploader-upload" disabled> | |||
Upload | |||
</button> |
@@ -6,10 +6,10 @@ | |||
<body> | |||
{{template#body}} | |||
<div id="profile" class="container"> | |||
<div id="collections" class="container"> | |||
<div class="name">{{arg#username}}</div> | |||
{{arg#collections}} | |||
<div id="profile" class="container small-width-container"> | |||
<div id="collections" class="container small-width"> | |||
<div class="title">{{arg#username}}</div> | |||
{{noescape#collections}} | |||
</div> | |||
</div> | |||
</body> |
@@ -6,8 +6,8 @@ | |||
<body> | |||
{{template#body}} | |||
<div class="container" id="register"> | |||
<form id="register-form"> | |||
<div id="register" class="container small-width-container"> | |||
<form id="register-form" class="container small-width"> | |||
<div class="form-group"> | |||
<label>Username<br> | |||
<input type="text" id="register-username"> |
@@ -0,0 +1,42 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
{{template#head}} | |||
</head> | |||
<body> | |||
{{template#body}} | |||
<div id="settings" class="container small-width-container"> | |||
<a class="title" href="/profile?{{session#userId}}">{{session#username}}</a> | |||
<form id="password-form" class="container small-width"> | |||
<div class="title">Change Password</div> | |||
<div class="form-group"> | |||
<label>Old Password<br> | |||
<input type="password" id="password-old"> | |||
</label> | |||
</div> | |||
<div class="form-group"> | |||
<label>New Password<br> | |||
<input type="password" id="password-new"> | |||
</label> | |||
</div> | |||
<div class="form-group"> | |||
<label>Repeat Password<br> | |||
<input type="password" id="password-repeat"> | |||
</label> | |||
</div> | |||
<div class="submit-container"> | |||
<button type="submit" class="btn btn-default">Submit</button> | |||
</div> | |||
</form> | |||
<form id="logout-form" class="container small-width"> | |||
<div class="title">Log Out</div> | |||
<div class="submit-container"> | |||
<button type="submit" class="btn btn-default">Log Out</button> | |||
</div> | |||
</form> | |||
</div> | |||
</body> | |||
</html> |
@@ -7,7 +7,7 @@ | |||
{{template#body}} | |||
<div id="viewer" class="container"> | |||
{{arg#images}} | |||
{{noescape#images}} | |||
</div> | |||
</body> | |||
</html> |
@@ -0,0 +1,68 @@ | |||
var scrypt = require("scrypt"); | |||
module.exports = function(ctx) { | |||
ctx.getPostData(function(err, data) { | |||
if (err) | |||
return ctx.fail(err); | |||
if (!data.oldPassword || !data.newPassword) | |||
return ctx.fail("You must provide passwords."); | |||
if (!ctx.session.loggedIn) | |||
return ctx.fail("You're not logged in."); | |||
ctx.db.query( | |||
"SELECT id, pass_hash "+ | |||
"FROM users "+ | |||
"WHERE id = $1", | |||
[ctx.session.userId], | |||
queryCallback | |||
); | |||
}); | |||
function queryCallback(err, res) { | |||
if (err) | |||
return ctx.fail(err); | |||
var user = res.rows[0]; | |||
if (!user) | |||
return ctx.fail("User doesn't exist."); | |||
scrypt.verify( | |||
new Buffer(user.pass_hash, "hex"), | |||
new Buffer(ctx.postData.data.oldPassword), | |||
function(err, success) { | |||
if (!success) | |||
return ctx.fail("Wrong password."); | |||
updatePassword(); | |||
} | |||
); | |||
} | |||
function updatePassword() { | |||
var params = scrypt.params(ctx.conf.scrypt.maxtime); | |||
scrypt.hash( | |||
new Buffer(ctx.postData.data.newPassword), | |||
params, | |||
function(err, hash) { | |||
if (err) | |||
return ctx.fail(err); | |||
ctx.db.query( | |||
"UPDATE users "+ | |||
"SET pass_hash = $1 "+ | |||
"WHERE id = $2", | |||
[hash.toString("hex"), ctx.session.userId], | |||
function(err, res) { | |||
if (err) | |||
return ctx.fail(err); | |||
ctx.succeed(); | |||
} | |||
); | |||
} | |||
); | |||
} | |||
} |
@@ -8,14 +8,17 @@ module.exports = function(ctx) { | |||
if (!data.username || !data.password) | |||
return ctx.fail("You must provide a username and a password."); | |||
var params = scrypt.params(1); | |||
if (!/^[a-zA-Z0-9_\-]+$/.test(data.username)) | |||
return ctx.fail("Username contains illegal characters."); | |||
var params = scrypt.params(ctx.conf.scrypt.maxtime); | |||
scrypt.hash(new Buffer(data.password), params, function(err, hash) { | |||
if (err) | |||
return ctx.fail(err); | |||
ctx.db.query( | |||
"INSERT INTO users (username, pass_hash) "+ | |||
"VALUES ($1, $2 )"+ | |||
"VALUES ($1, $2) "+ | |||
"RETURNING id", | |||
[data.username, hash.toString("hex")], | |||
queryCallback |
@@ -6,6 +6,10 @@ | |||
margin-bottom: 0px; | |||
} | |||
.navbar form { | |||
border: none; | |||
} | |||
#login-dropdown .dropdown-menu { | |||
padding: 10px; | |||
} | |||
@@ -23,7 +27,36 @@ form .submit-container { | |||
text-align: right; | |||
} | |||
form .submit-container .btn { | |||
width: 75px; | |||
width: 100%; | |||
} | |||
form input[type="text"], | |||
form input[type="password"] { | |||
width: 100%; | |||
} | |||
form label { | |||
width: 100%; | |||
} | |||
form.container { | |||
border: 1px solid #CCC; | |||
border-radius: 4px; | |||
padding: 10px !important; | |||
margin-bottom: 10px; | |||
} | |||
.title { | |||
font-weight: bold; | |||
font-size: 1.2em; | |||
margin-bottom: 6px; | |||
} | |||
.small-width-container { | |||
text-align: center !important; | |||
} | |||
.small-width { | |||
width: auto !important; | |||
text-align: left !important; | |||
max-width: 400px !important; | |||
width: 90% !important; | |||
} | |||
#notify-box { |
@@ -40,8 +40,8 @@ | |||
util.htmlEntities = function(str) { | |||
return str.replace(/&/g, "&") | |||
.replace(/</g, "<") | |||
.replace(/>/g, "<") | |||
.replace(/"/g, """); | |||
.replace(/>/g, ">") | |||
.replace(/"/g, """); | |||
} | |||
util.api = function(name, data, cb, getXhr) { | |||
@@ -64,6 +64,9 @@ | |||
getXhr(xhr); | |||
return xhr; | |||
}, | |||
error: function(xhr, status, err) { | |||
cb(err); | |||
} | |||
}).done(function(res) { | |||
console.log("response from "+name+":"); | |||
@@ -114,9 +117,20 @@ | |||
util.pad(date.getMinutes().toString(), 2, "0"); | |||
} | |||
util.prevent = function(evt) { | |||
evt.preventDefault(); | |||
evt.stopPropagation(); | |||
} | |||
util.redirect = function(url, timeout) { | |||
setTimeout(function() { | |||
location.href = url; | |||
}, timeout || 1000); | |||
} | |||
window.display = {}; | |||
window.display.loggedIn = function() { | |||
display.loggedIn = function() { | |||
util.api("template?navbar-loggedin", {}, function(err, res) { | |||
if (err) | |||
return util.error(err); | |||
@@ -127,10 +141,20 @@ | |||
}); | |||
} | |||
display.logIn = function() { | |||
util.api("template?navbar-login", {}, function(err, res) { | |||
if (err) | |||
return util.error(err); | |||
$("#navbar-profile-container").html(res.html); | |||
util.notify("Logged Out", "You are now logged out."); | |||
}); | |||
} | |||
$(document).ready(function() { | |||
$("#login-form").on("submit", function(evt) { | |||
evt.stopPropagation(); | |||
evt.preventDefault(); | |||
util.prevent(evt); | |||
var username = $("#login-username").val(); | |||
var password = $("#login-password").val(); |
@@ -67,7 +67,7 @@ $(document).on("ready", function() { | |||
//First, create a collection | |||
util.api("collection_create", { | |||
name: "New Collection" | |||
name: ($("#uploader-collection-name").val() || "Collection") | |||
}, function(err, res) { | |||
if (err) | |||
return util.error(err); | |||
@@ -75,10 +75,11 @@ $(document).on("ready", function() { | |||
var collectionId = res.id; | |||
//Go to collection once files are uploaded | |||
var a = util.async(files.length, function() { | |||
setTimeout(function() { | |||
location.href = "/view?"+collectionId; | |||
}, 1000); | |||
var a = util.async(files.length, function(res) { | |||
if (res.error) | |||
util.redirect("/", 5000); | |||
else | |||
util.redirect("/view?"+collectionId); | |||
}); | |||
//Loop through files, uploading them | |||
@@ -108,8 +109,10 @@ $(document).on("ready", function() { | |||
collectionId: collectionId, | |||
file: f | |||
}, function(err, res) { | |||
if (err) | |||
if (err) { | |||
a("error", true); | |||
return util.error(err); | |||
} | |||
a(); | |||
}, getXhr); |
@@ -23,3 +23,16 @@ | |||
#uploader-upload { | |||
float: right; | |||
} | |||
#uploader-select-files, #uploader-upload { | |||
width: 100px; | |||
} | |||
#uploader-select-files { | |||
float: left; | |||
} | |||
#uploader-collection-name { | |||
width: calc(100% - 200px); | |||
border: 1px solid #CCC; | |||
border-radius: 4px; | |||
padding: 6px 12px; | |||
} |
@@ -6,7 +6,7 @@ module.exports = function(ctx) { | |||
"FROM collections "+ | |||
"WHERE user_id = $1", | |||
[id], | |||
function(err, res) { a("collections", res.rows, err) } | |||
function(err, res) { a("collections", res, err) } | |||
); | |||
ctx.db.query( | |||
@@ -14,19 +14,22 @@ module.exports = function(ctx) { | |||
"FROM users "+ | |||
"WHERE id = $1", | |||
[id], | |||
function(err, res) { a("users", res.rows, err) } | |||
function(err, res) { a("users", res, err) } | |||
); | |||
var a = ctx.async(2, function(err, res) { | |||
if (err) | |||
return ctx.fail(err); | |||
var user = res.users[0]; | |||
if (!res.collections || !res.users) | |||
return ctx.end(ctx.view("404")); | |||
var user = res.users.rows[0]; | |||
if (!user) | |||
return ctx.end(ctx.view("404")); | |||
var collections = ""; | |||
res.collections.forEach(function(row) { | |||
res.collections.rows.forEach(function(row) { | |||
var d = new Date(row.date_created); | |||
collections += ctx.template("collection", { |
@@ -1,9 +0,0 @@ | |||
#profile { | |||
text-align: center; | |||
} | |||
#collections { | |||
text-align: left; | |||
width: auto; | |||
display: inline-block; | |||
} |
@@ -1,8 +1,6 @@ | |||
$(document).on("ready", function() { | |||
$("#register-form").on("submit", function(evt) { | |||
console.log(evt); | |||
evt.preventDefault(); | |||
evt.stopPropagation(); | |||
util.prevent(evt); | |||
var username = $("#register-username").val(); | |||
var password = $("#register-password").val(); | |||
@@ -26,9 +24,7 @@ $(document).on("ready", function() { | |||
display.loggedIn(); | |||
setTimeout(function() { | |||
location.href = "/profile?"+res.id; | |||
}, 1000); | |||
util.redirect("/settings"); | |||
}); | |||
}); | |||
}); |
@@ -1,8 +0,0 @@ | |||
#register-form { | |||
text-align: left; | |||
display: inline-block; | |||
} | |||
#register { | |||
text-align: center; | |||
} |
@@ -0,0 +1,6 @@ | |||
module.exports = function(ctx) { | |||
if (!ctx.session.loggedIn) | |||
return ctx.end(ctx.view("404")); | |||
ctx.end(ctx.view("settings")); | |||
} |
@@ -0,0 +1,36 @@ | |||
$(document).on("ready", function() { | |||
$("#password-form").on("submit", function(evt) { | |||
util.prevent(evt); | |||
var oldPass = $("#password-old").val(); | |||
var newPass = $("#password-new").val(); | |||
var repeatPass = $("#password-repeat").val(); | |||
if (newPass !== repeatPass) | |||
return util.error("Passwords don't match."); | |||
util.api("account_change_password", { | |||
oldPassword: oldPass, | |||
newPassword: newPass | |||
}, function(err, res) { | |||
if (err) | |||
return util.error(err); | |||
util.notify("Password changed!"); | |||
}); | |||
}); | |||
$("#logout-form").on("submit", function(evt) { | |||
evt.preventDefault(); | |||
evt.stopPropagation(); | |||
util.api("account_logout", {}, function(err, res) { | |||
if (err) | |||
return util.error(err); | |||
display.logIn(); | |||
util.redirect("/"); | |||
}); | |||
}); | |||
}); |