@@ -5,7 +5,7 @@ | |||
"host": "localhost", | |||
"user": "dbuser", | |||
"password": "dbpass", | |||
"database": "mimg" | |||
"database": "spus" | |||
}, | |||
"use_https": false, | |||
"https": { | |||
@@ -20,5 +20,6 @@ | |||
"session_timeout": 1800000, | |||
"dir": { | |||
"imgs": "imgs" | |||
} | |||
}, | |||
"debug": false | |||
} |
@@ -1,9 +1,17 @@ | |||
var formidable = require("formidable"); | |||
var crypto = require("crypto"); | |||
var preprocess = require("./preprocess.js"); | |||
var sessions = {}; | |||
function templatify(str, args) { | |||
function templatify(str, args, ctx) { | |||
str = preprocess(str, { | |||
session: ctx.session, | |||
template: function(key) { | |||
return ctx.template(key); | |||
} | |||
}); | |||
if (args == undefined) | |||
return str; | |||
@@ -46,11 +54,16 @@ module.exports = function(options) { | |||
setTimeout(function() { | |||
delete sessions[key]; | |||
}, this.conf.session_timeout); | |||
this.session = sessions[key]; | |||
} | |||
} | |||
module.exports.prototype = { | |||
end: function(str) { | |||
if (this.statusCode) | |||
this.res.writeHead(this.statusCode); | |||
this.res.end(str); | |||
}, | |||
@@ -58,10 +71,17 @@ module.exports.prototype = { | |||
obj = obj || {}; | |||
obj.success = true; | |||
this.res.setHeader("Content-Type", "application/json"); | |||
this.end(JSON.stringify(obj)); | |||
}, | |||
fail: function(err) { | |||
console.log("Sending error to client:"); | |||
console.trace(err); | |||
this.res.setHeader("Content-Type", "application/json"); | |||
obj = {}; | |||
obj.success = false; | |||
obj.error = err.toString(); | |||
@@ -73,7 +93,7 @@ module.exports.prototype = { | |||
if (!str) | |||
throw new Error("No such template: "+name); | |||
return templatify(str, args); | |||
return templatify(str, args, this); | |||
}, | |||
view: function(name, args) { | |||
@@ -81,7 +101,7 @@ module.exports.prototype = { | |||
if (!str) | |||
throw new Error("No such view: "+name); | |||
return templatify(str, args); | |||
return templatify(str, args, this); | |||
}, | |||
getPostData: function(cb) { | |||
@@ -102,5 +122,9 @@ module.exports.prototype = { | |||
cb(null, data, files); | |||
}.bind(this)); | |||
}, | |||
setStatus: function(code) { | |||
this.statusCode = code; | |||
} | |||
} |
@@ -1,32 +1,24 @@ | |||
var fs = require("fs"); | |||
var minify = require("./minify"); | |||
var minify = require("./minify.js"); | |||
var preprocess = require("./preprocess.js"); | |||
var globalRegex = /{{([^}]+)#([^}]+)}}/g; | |||
var localRegex = /{{([^}]+)#([^}]+)}}/; | |||
var cache = {}; | |||
module.exports = function load(path, conf) { | |||
var html = fs.readFileSync(path, "utf8"); | |||
var placeholders = html.match(globalRegex); | |||
if (!placeholders) | |||
return minify.html(html); | |||
placeholders.forEach(function(p) { | |||
var parts = html.match(localRegex); | |||
var s = parts[0]; | |||
var ns = parts[1]; | |||
var key = parts[2]; | |||
switch (ns) { | |||
case "conf": | |||
html = html.replace(s, conf[key]); | |||
break; | |||
case "template": | |||
html = html.replace(s, load("templates/"+key+".html", conf)); | |||
break; | |||
var env = { | |||
conf: conf.web, | |||
template: function(key) { | |||
var str = load("templates/"+key+".html", conf); | |||
return str; | |||
} | |||
}); | |||
} | |||
html = preprocess(html, env); | |||
return minify.html(html); | |||
if (conf.minify) | |||
return minify.html(html); | |||
else | |||
return html; | |||
} |
@@ -54,14 +54,14 @@ exports.load = function(endpoints, conf) { | |||
var templates = {}; | |||
fs.readdirSync("templates").forEach(function(f) { | |||
var name = f.replace(/\.html$/, ""); | |||
res.templates[name] = includeHtml("templates/"+f, conf.web); | |||
res.templates[name] = includeHtml("templates/"+f, conf); | |||
}); | |||
//Prepare all views | |||
var views = {}; | |||
fs.readdirSync("views").forEach(function(f) { | |||
var name = f.replace(/\.html$/, ""); | |||
res.views[name] = includeHtml("views/"+f, conf.web); | |||
res.views[name] = includeHtml("views/"+f, conf); | |||
}); | |||
return res; |
@@ -0,0 +1,68 @@ | |||
var valueRegex = "([a-zA-Z0-9_\\-]+)#([a-zA-Z0-9_\\-]+)"; | |||
var regexStr = | |||
"{{"+ //{{ | |||
valueRegex+ //foo#bar - $1#$2 | |||
"(?:"+ //<optional> | |||
" \\? "+ //? | |||
valueRegex+ //foo#bar - $3#$4 | |||
" : "+ //: | |||
valueRegex+ //foo#bar - $5#$6 | |||
")?"+ //</optional> | |||
"}}"; //}} | |||
var localRegex = new RegExp(regexStr); | |||
var globalRegex = new RegExp(regexStr, "g"); | |||
function getVal(ns, key, env) { | |||
var n = env[ns]; | |||
if (typeof n === "function") | |||
return n(key); | |||
else if (n) | |||
return n[key]; | |||
else | |||
throw new Error("Namespace "+ns+" doesn't exist."); | |||
} | |||
module.exports = function(str, env) { | |||
var placeholders = str.match(globalRegex); | |||
if (!placeholders) | |||
return str; | |||
placeholders.forEach(function(p) { | |||
var parts = p.match(localRegex); | |||
var s = parts[0]; | |||
//Ternary | |||
if (parts[6]) { | |||
try { | |||
var cond = getVal(parts[1], parts[2], env); | |||
var val1 = getVal(parts[3], parts[4], env); | |||
var val2 = getVal(parts[5], parts[6], env); | |||
} catch (err) { | |||
return; | |||
} | |||
if (cond === true) | |||
str = str.replace(s, val1); | |||
else | |||
str = str.replace(s, val2); | |||
} | |||
//Direct value | |||
else { | |||
try { | |||
var val = getVal(parts[1], parts[2], env); | |||
} catch (err) { | |||
return; | |||
} | |||
if (val !== undefined && val !== null) | |||
str = str.replace(s, val); | |||
} | |||
}); | |||
return str; | |||
} |
@@ -19,6 +19,7 @@ | |||
"formidable": "^1.0.17", | |||
"html-minifier": "^0.7.2", | |||
"pg": "^4.4.0", | |||
"scrypt": "^4.0.7", | |||
"uglify-js": "^2.4.24", | |||
"uglifycss": "0.0.15" | |||
} |
@@ -3,15 +3,15 @@ var pg = require("pg"); | |||
var conf = JSON.parse(fs.readFileSync("./conf.json")); | |||
var sql = fs.readFileSync("sql/reset.sql", "utf8"); | |||
var sql = fs.readFileSync("scripts/sql/reset.sql", "utf8"); | |||
var client = new pg.Client( | |||
"postgres://"+ | |||
conf.db.user+":"+ | |||
conf.db.pass+"@"+ | |||
conf.db.host+"/"+ | |||
conf.db.database | |||
); | |||
var client = new pg.Client(conf.db); | |||
function deleteFiles(dir) { | |||
fs.readdirSync(dir).forEach(function(f) { | |||
fs.unlinkSync(dir+"/"+f); | |||
}); | |||
} | |||
client.connect(function(err) { | |||
if (err) { | |||
@@ -25,6 +25,8 @@ client.connect(function(err) { | |||
} else { | |||
console.log("Database reset."); | |||
} | |||
deleteFiles(conf.dir.imgs); | |||
process.exit(); | |||
}); | |||
}); |
@@ -3,15 +3,9 @@ var pg = require("pg"); | |||
var conf = JSON.parse(fs.readFileSync("./conf.json")); | |||
var sql = fs.readFileSync("sql/setup.sql", "utf8"); | |||
var sql = fs.readFileSync("scripts/sql/setup.sql", "utf8"); | |||
var client = new pg.Client( | |||
"postgres://"+ | |||
conf.db.user+":"+ | |||
conf.db.pass+"@"+ | |||
conf.db.host+"/"+ | |||
conf.db.database | |||
); | |||
var client = new pg.Client(conf.db); | |||
client.connect(function(err) { | |||
if (err) { |
@@ -1,13 +1,14 @@ | |||
CREATE TABLE users ( | |||
id SERIAL PRIMARY KEY, | |||
username VARCHAR(64) UNIQUE NOT NULL, | |||
pass_hash CHAR(128) NOT NULL, | |||
pass_hash VARCHAR(256) NOT NULL, | |||
date_created TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW() | |||
); | |||
CREATE TABLE collections ( | |||
id SERIAL PRIMARY KEY, | |||
name VARCHAR(64), | |||
date_created TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), | |||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE | |||
); |
@@ -29,8 +29,11 @@ var endpoints = { | |||
"/i": "i/index.node.js", | |||
//API files | |||
"/api/template": "api/template.node.js", | |||
"/api/image_create": "api/image_create.node.js", | |||
"/api/collection_create": "api/collection_create.node.js" | |||
"/api/collection_create": "api/collection_create.node.js", | |||
"/api/account_create": "api/account_create.node.js", | |||
"/api/account_login": "api/account_login.node.js" | |||
} | |||
var loaded = loader.load(endpoints, conf); | |||
@@ -41,26 +44,28 @@ var db = new pg.Client(conf.db); | |||
function onRequest(req, res) { | |||
console.log("Request for "+req.url); | |||
var ctx = new Context({ | |||
req: req, | |||
res: res, | |||
templates: loaded.templates, | |||
views: loaded.views, | |||
db: db, | |||
conf: conf | |||
}); | |||
var ep = loaded.endpoints[req.url.split("?")[0]]; | |||
//If the file doesn't exist, we 404. | |||
if (!ep) { | |||
ep = loaded.endpoints["/404"]; | |||
res.writeHead(404); | |||
ctx.setStatus(404); | |||
} | |||
//Execute if it's a .node.js, or just respond with the contents of the file | |||
if (typeof ep == "function") { | |||
ep(new Context({ | |||
req: req, | |||
res: res, | |||
templates: loaded.templates, | |||
views: loaded.views, | |||
db: db, | |||
conf: conf | |||
})); | |||
ep(ctx); | |||
} else { | |||
res.end(ep); | |||
ctx.end(ep); | |||
} | |||
} | |||
@@ -80,10 +85,12 @@ db.connect(function() { | |||
}); | |||
//We don't want to crash even if something throws an uncaught exception. | |||
var d = domain.create(); | |||
d.on("error", function(err) { | |||
console.trace(err); | |||
}); | |||
process.on("uncaughtException", function(err) { | |||
console.trace(err); | |||
}); | |||
if (!conf.debug) { | |||
var d = domain.create(); | |||
d.on("error", function(err) { | |||
console.trace(err); | |||
}); | |||
process.on("uncaughtException", function(err) { | |||
console.trace(err); | |||
}); | |||
} |
@@ -2,8 +2,8 @@ | |||
<div class="navbar-inner"> | |||
<div class="container-fluid"> | |||
<a class="navbar-brand" href="/">{{conf#title}}</a> | |||
<ul class="nav navbar-nav navbar-right"> | |||
{{profile}} | |||
<ul id="navbar-profile-container" class="nav navbar-nav navbar-right"> | |||
{{session#loggedIn ? template#navbar-loggedin : template#navbar-login}} | |||
</ul> | |||
</div> | |||
</div> |
@@ -0,0 +1,3 @@ | |||
<li> | |||
<a href="/profile?{{session#userId}}">{{session#username}}</a> | |||
</li> |
@@ -7,16 +7,17 @@ | |||
<form id="login-form"> | |||
<div class="form-group"> | |||
<label>Username<br> | |||
<input type="text"> | |||
<input type="text" id="login-username"> | |||
</label> | |||
</div> | |||
<div class="form-group"> | |||
<label>Password<br> | |||
<input type="password"> | |||
<input type="password" id="login-password"> | |||
</label> | |||
</div> | |||
<div class="submit-container"> | |||
<button type="submit" class="btn btn-default">Log In</button> | |||
<a class="btn btn-default register" href="/register">Register</a> | |||
</div> | |||
</form> | |||
</li></ul> |
@@ -0,0 +1,38 @@ | |||
var scrypt = require("scrypt"); | |||
module.exports = function(ctx) { | |||
ctx.getPostData(function(err, data) { | |||
if (err) | |||
return ctx.fail(err); | |||
if (!data.username || !data.password) | |||
return ctx.fail("You must provide a username and a password."); | |||
var params = scrypt.params(1); | |||
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 )"+ | |||
"RETURNING id", | |||
[data.username, hash.toString("hex")], | |||
queryCallback | |||
); | |||
}); | |||
}); | |||
function queryCallback(err, res) { | |||
if (err) | |||
return ctx.fail(err); | |||
ctx.session.loggedIn = true; | |||
ctx.session.userId = res.rows[0].id; | |||
ctx.session.username = ctx.postData.username; | |||
ctx.succeed({ | |||
id: res.rows[0].id | |||
}); | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
var scrypt = require("scrypt"); | |||
module.exports = function(ctx) { | |||
ctx.getPostData(function(err, data) { | |||
if (err) | |||
return ctx.fail(err); | |||
if (!data.username || !data.password) | |||
return ctx.fail("You must provide a username and a password."); | |||
ctx.db.query( | |||
"SELECT id, username, pass_hash "+ | |||
"FROM users "+ | |||
"WHERE username=$1", | |||
[data.username], | |||
queryCallback | |||
); | |||
}); | |||
function queryCallback(err, res) { | |||
if (err) | |||
return ctx.fail(err); | |||
var user = res.rows[0]; | |||
ctx.session.loggedIn = true; | |||
ctx.session.userId = user.id; | |||
ctx.session.username = user.username; | |||
if (!user) | |||
return ctx.fail("Wrong username or password."); | |||
scrypt.verify( | |||
new Buffer(user.pass_hash, "hex"), | |||
new Buffer(ctx.postData.data.password), | |||
function(err, success) { | |||
if (success) { | |||
ctx.succeed({ | |||
id: user.id | |||
}) | |||
} else { | |||
ctx.fail("Wrong username or password."); | |||
} | |||
} | |||
); | |||
} | |||
} |
@@ -1,6 +1,7 @@ | |||
module.exports = function(ctx) { | |||
ctx.getPostData(function(err, data) { | |||
if (err) return ctx.fail(err); | |||
if (err) | |||
return ctx.fail(err); | |||
ctx.db.query( | |||
"INSERT INTO collections (name) "+ | |||
@@ -12,7 +13,8 @@ module.exports = function(ctx) { | |||
}); | |||
function queryCallback(err, res) { | |||
if (err) return ctx.fail(err); | |||
if (err) | |||
return ctx.fail(err); | |||
ctx.session.collectionId = res.rows[0].id; | |||
@@ -0,0 +1,18 @@ | |||
module.exports = function(ctx) { | |||
var name = ctx.req.url.split("?")[1]; | |||
if (!name) | |||
return ctx.fail("You must supply a template name."); | |||
ctx.getPostData(function(err, data) { | |||
if (err) | |||
return ctx.fail(err); | |||
try { | |||
ctx.succeed({ | |||
html: ctx.template(name, data) | |||
}); | |||
} catch (err) { | |||
ctx.fail(err); | |||
} | |||
}); | |||
} |
@@ -18,6 +18,9 @@ | |||
#login-dropdown .submit-container { | |||
text-align: right; | |||
} | |||
#login-dropdown .submit-container .btn { | |||
width: 75px; | |||
} | |||
#notify-box { | |||
transition: max-height 0.2s; | |||
@@ -27,10 +30,10 @@ | |||
border: 1px solid #E7E7E7; | |||
overflow: hidden; | |||
line-height: 48px; | |||
position: fixed; | |||
bottom: 0px; | |||
width: 100%; | |||
box-sizing: content-box; | |||
} | |||
#notify-box .close { | |||
@@ -47,15 +50,16 @@ | |||
} | |||
#notify-box .title, | |||
#notify-box .body { | |||
line-height: 48px; | |||
margin-left: 12px; | |||
width: calc(100% - 42px); | |||
margin-top: 16px; | |||
} | |||
#notify-box .title { | |||
font-weight: bold; | |||
} | |||
#notify-box.active { | |||
min-height: 48px; | |||
max-height: 48px; | |||
} | |||
#notify-box.active:hover { |
@@ -16,16 +16,10 @@ | |||
$("#notify-box").on("mouseenter", function() { | |||
clearTimeout(util.notify.timeout); | |||
}); | |||
$("#login-form").on("submit", function(evt) { | |||
evt.stopPropagation(); | |||
evt.preventDefault(); | |||
util.notify("Feature Not Implemented", "This feature is not implemented."); | |||
}); | |||
}); | |||
util.error = function(body) { | |||
util.notify("An error occurred.", body); | |||
util.notify("Error: "+body); | |||
} | |||
util.htmlEntities = function(str) { | |||
@@ -59,11 +53,10 @@ | |||
}).done(function(res) { | |||
console.log("response from "+name+":"); | |||
console.log(res); | |||
var obj = JSON.parse(res); | |||
if (obj.success) | |||
cb(null, obj); | |||
if (res.success) | |||
cb(null, res); | |||
else | |||
cb(obj.error); | |||
cb(res.error); | |||
}); | |||
} | |||
@@ -86,4 +79,35 @@ | |||
n -= 1; | |||
} | |||
} | |||
window.display = {}; | |||
window.display.loggedIn = function() { | |||
util.api("template?navbar-loggedin", {}, function(err, res) { | |||
if (err) | |||
return util.error(err); | |||
$("#navbar-profile-container").html(res.html); | |||
}); | |||
} | |||
$(document).ready(function() { | |||
$("#login-form").on("submit", function(evt) { | |||
evt.stopPropagation(); | |||
evt.preventDefault(); | |||
var username = $("#login-username").val(); | |||
var password = $("#login-password").val(); | |||
util.api("account_login", { | |||
username: username, | |||
password: password | |||
}, function(err, res) { | |||
if (err) | |||
util.error(err); | |||
else | |||
display.loggedIn(); | |||
}); | |||
}); | |||
}); | |||
})(); |
@@ -1,5 +1,3 @@ | |||
module.exports = function(ctx) { | |||
ctx.end(ctx.view("index", { | |||
profile: ctx.template("navbar-profile-login") | |||
})); | |||
ctx.end(ctx.view("index")); | |||
} |
@@ -68,7 +68,8 @@ | |||
util.api("collection_create", { | |||
name: "New Collection" | |||
}, function(err, res) { | |||
if (err) return util.error(err); | |||
if (err) | |||
return util.error(err); | |||
var collectionId = res.id; | |||
@@ -106,7 +107,8 @@ | |||
collectionId: collectionId, | |||
file: f | |||
}, function(err, res) { | |||
if (err) return util.error(err); | |||
if (err) | |||
return util.error(err); | |||
a(); | |||
}, getXhr); |
@@ -27,7 +27,6 @@ module.exports = function(ctx) { | |||
}); | |||
ctx.end(ctx.view("view", { | |||
profile: ctx.template("navbar-profile-login"), | |||
images: images | |||
})); | |||
} |