"host": "localhost", | "host": "localhost", | ||||
"user": "dbuser", | "user": "dbuser", | ||||
"password": "dbpass", | "password": "dbpass", | ||||
"database": "mimg" | |||||
"database": "spus" | |||||
}, | }, | ||||
"use_https": false, | "use_https": false, | ||||
"https": { | "https": { | ||||
"session_timeout": 1800000, | "session_timeout": 1800000, | ||||
"dir": { | "dir": { | ||||
"imgs": "imgs" | "imgs": "imgs" | ||||
} | |||||
}, | |||||
"debug": false | |||||
} | } |
var formidable = require("formidable"); | var formidable = require("formidable"); | ||||
var crypto = require("crypto"); | var crypto = require("crypto"); | ||||
var preprocess = require("./preprocess.js"); | |||||
var sessions = {}; | 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) | if (args == undefined) | ||||
return str; | return str; | ||||
setTimeout(function() { | setTimeout(function() { | ||||
delete sessions[key]; | delete sessions[key]; | ||||
}, this.conf.session_timeout); | }, this.conf.session_timeout); | ||||
this.session = sessions[key]; | |||||
} | } | ||||
} | } | ||||
module.exports.prototype = { | module.exports.prototype = { | ||||
end: function(str) { | end: function(str) { | ||||
if (this.statusCode) | |||||
this.res.writeHead(this.statusCode); | |||||
this.res.end(str); | this.res.end(str); | ||||
}, | }, | ||||
obj = obj || {}; | obj = obj || {}; | ||||
obj.success = true; | obj.success = true; | ||||
this.res.setHeader("Content-Type", "application/json"); | |||||
this.end(JSON.stringify(obj)); | this.end(JSON.stringify(obj)); | ||||
}, | }, | ||||
fail: function(err) { | fail: function(err) { | ||||
console.log("Sending error to client:"); | |||||
console.trace(err); | |||||
this.res.setHeader("Content-Type", "application/json"); | |||||
obj = {}; | obj = {}; | ||||
obj.success = false; | obj.success = false; | ||||
obj.error = err.toString(); | obj.error = err.toString(); | ||||
if (!str) | if (!str) | ||||
throw new Error("No such template: "+name); | throw new Error("No such template: "+name); | ||||
return templatify(str, args); | |||||
return templatify(str, args, this); | |||||
}, | }, | ||||
view: function(name, args) { | view: function(name, args) { | ||||
if (!str) | if (!str) | ||||
throw new Error("No such view: "+name); | throw new Error("No such view: "+name); | ||||
return templatify(str, args); | |||||
return templatify(str, args, this); | |||||
}, | }, | ||||
getPostData: function(cb) { | getPostData: function(cb) { | ||||
cb(null, data, files); | cb(null, data, files); | ||||
}.bind(this)); | }.bind(this)); | ||||
}, | |||||
setStatus: function(code) { | |||||
this.statusCode = code; | |||||
} | } | ||||
} | } |
var fs = require("fs"); | 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) { | module.exports = function load(path, conf) { | ||||
var html = fs.readFileSync(path, "utf8"); | 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; | |||||
} | } |
var templates = {}; | var templates = {}; | ||||
fs.readdirSync("templates").forEach(function(f) { | fs.readdirSync("templates").forEach(function(f) { | ||||
var name = f.replace(/\.html$/, ""); | var name = f.replace(/\.html$/, ""); | ||||
res.templates[name] = includeHtml("templates/"+f, conf.web); | |||||
res.templates[name] = includeHtml("templates/"+f, conf); | |||||
}); | }); | ||||
//Prepare all views | //Prepare all views | ||||
var views = {}; | var views = {}; | ||||
fs.readdirSync("views").forEach(function(f) { | fs.readdirSync("views").forEach(function(f) { | ||||
var name = f.replace(/\.html$/, ""); | var name = f.replace(/\.html$/, ""); | ||||
res.views[name] = includeHtml("views/"+f, conf.web); | |||||
res.views[name] = includeHtml("views/"+f, conf); | |||||
}); | }); | ||||
return res; | return res; |
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; | |||||
} |
"formidable": "^1.0.17", | "formidable": "^1.0.17", | ||||
"html-minifier": "^0.7.2", | "html-minifier": "^0.7.2", | ||||
"pg": "^4.4.0", | "pg": "^4.4.0", | ||||
"scrypt": "^4.0.7", | |||||
"uglify-js": "^2.4.24", | "uglify-js": "^2.4.24", | ||||
"uglifycss": "0.0.15" | "uglifycss": "0.0.15" | ||||
} | } |
var conf = JSON.parse(fs.readFileSync("./conf.json")); | 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) { | client.connect(function(err) { | ||||
if (err) { | if (err) { | ||||
} else { | } else { | ||||
console.log("Database reset."); | console.log("Database reset."); | ||||
} | } | ||||
deleteFiles(conf.dir.imgs); | |||||
process.exit(); | process.exit(); | ||||
}); | }); | ||||
}); | }); |
var conf = JSON.parse(fs.readFileSync("./conf.json")); | 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) { | client.connect(function(err) { | ||||
if (err) { | if (err) { |
CREATE TABLE users ( | CREATE TABLE users ( | ||||
id SERIAL PRIMARY KEY, | id SERIAL PRIMARY KEY, | ||||
username VARCHAR(64) UNIQUE NOT NULL, | 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() | date_created TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW() | ||||
); | ); | ||||
CREATE TABLE collections ( | CREATE TABLE collections ( | ||||
id SERIAL PRIMARY KEY, | id SERIAL PRIMARY KEY, | ||||
name VARCHAR(64), | name VARCHAR(64), | ||||
date_created TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), | |||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE | user_id INTEGER REFERENCES users(id) ON DELETE CASCADE | ||||
); | ); |
"/i": "i/index.node.js", | "/i": "i/index.node.js", | ||||
//API files | //API files | ||||
"/api/template": "api/template.node.js", | |||||
"/api/image_create": "api/image_create.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); | var loaded = loader.load(endpoints, conf); | ||||
function onRequest(req, res) { | function onRequest(req, res) { | ||||
console.log("Request for "+req.url); | 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]]; | var ep = loaded.endpoints[req.url.split("?")[0]]; | ||||
//If the file doesn't exist, we 404. | //If the file doesn't exist, we 404. | ||||
if (!ep) { | if (!ep) { | ||||
ep = loaded.endpoints["/404"]; | 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 | //Execute if it's a .node.js, or just respond with the contents of the file | ||||
if (typeof ep == "function") { | if (typeof ep == "function") { | ||||
ep(new Context({ | |||||
req: req, | |||||
res: res, | |||||
templates: loaded.templates, | |||||
views: loaded.views, | |||||
db: db, | |||||
conf: conf | |||||
})); | |||||
ep(ctx); | |||||
} else { | } else { | ||||
res.end(ep); | |||||
ctx.end(ep); | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
//We don't want to crash even if something throws an uncaught exception. | //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); | |||||
}); | |||||
} |
<div class="navbar-inner"> | <div class="navbar-inner"> | ||||
<div class="container-fluid"> | <div class="container-fluid"> | ||||
<a class="navbar-brand" href="/">{{conf#title}}</a> | <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> | </ul> | ||||
</div> | </div> | ||||
</div> | </div> |
<li> | |||||
<a href="/profile?{{session#userId}}">{{session#username}}</a> | |||||
</li> |
<form id="login-form"> | <form id="login-form"> | ||||
<div class="form-group"> | <div class="form-group"> | ||||
<label>Username<br> | <label>Username<br> | ||||
<input type="text"> | |||||
<input type="text" id="login-username"> | |||||
</label> | </label> | ||||
</div> | </div> | ||||
<div class="form-group"> | <div class="form-group"> | ||||
<label>Password<br> | <label>Password<br> | ||||
<input type="password"> | |||||
<input type="password" id="login-password"> | |||||
</label> | </label> | ||||
</div> | </div> | ||||
<div class="submit-container"> | <div class="submit-container"> | ||||
<button type="submit" class="btn btn-default">Log In</button> | <button type="submit" class="btn btn-default">Log In</button> | ||||
<a class="btn btn-default register" href="/register">Register</a> | |||||
</div> | </div> | ||||
</form> | </form> | ||||
</li></ul> | </li></ul> |
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 | |||||
}); | |||||
} | |||||
} |
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."); | |||||
} | |||||
} | |||||
); | |||||
} | |||||
} |
module.exports = function(ctx) { | module.exports = function(ctx) { | ||||
ctx.getPostData(function(err, data) { | ctx.getPostData(function(err, data) { | ||||
if (err) return ctx.fail(err); | |||||
if (err) | |||||
return ctx.fail(err); | |||||
ctx.db.query( | ctx.db.query( | ||||
"INSERT INTO collections (name) "+ | "INSERT INTO collections (name) "+ | ||||
}); | }); | ||||
function queryCallback(err, res) { | function queryCallback(err, res) { | ||||
if (err) return ctx.fail(err); | |||||
if (err) | |||||
return ctx.fail(err); | |||||
ctx.session.collectionId = res.rows[0].id; | ctx.session.collectionId = res.rows[0].id; | ||||
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); | |||||
} | |||||
}); | |||||
} |
#login-dropdown .submit-container { | #login-dropdown .submit-container { | ||||
text-align: right; | text-align: right; | ||||
} | } | ||||
#login-dropdown .submit-container .btn { | |||||
width: 75px; | |||||
} | |||||
#notify-box { | #notify-box { | ||||
transition: max-height 0.2s; | transition: max-height 0.2s; | ||||
border: 1px solid #E7E7E7; | border: 1px solid #E7E7E7; | ||||
overflow: hidden; | overflow: hidden; | ||||
line-height: 48px; | |||||
position: fixed; | position: fixed; | ||||
bottom: 0px; | bottom: 0px; | ||||
width: 100%; | width: 100%; | ||||
box-sizing: content-box; | |||||
} | } | ||||
#notify-box .close { | #notify-box .close { | ||||
} | } | ||||
#notify-box .title, | #notify-box .title, | ||||
#notify-box .body { | #notify-box .body { | ||||
line-height: 48px; | |||||
margin-left: 12px; | margin-left: 12px; | ||||
width: calc(100% - 42px); | width: calc(100% - 42px); | ||||
margin-top: 16px; | |||||
} | } | ||||
#notify-box .title { | #notify-box .title { | ||||
font-weight: bold; | font-weight: bold; | ||||
} | } | ||||
#notify-box.active { | #notify-box.active { | ||||
min-height: 48px; | |||||
max-height: 48px; | max-height: 48px; | ||||
} | } | ||||
#notify-box.active:hover { | #notify-box.active:hover { |
$("#notify-box").on("mouseenter", function() { | $("#notify-box").on("mouseenter", function() { | ||||
clearTimeout(util.notify.timeout); | 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.error = function(body) { | ||||
util.notify("An error occurred.", body); | |||||
util.notify("Error: "+body); | |||||
} | } | ||||
util.htmlEntities = function(str) { | util.htmlEntities = function(str) { | ||||
}).done(function(res) { | }).done(function(res) { | ||||
console.log("response from "+name+":"); | console.log("response from "+name+":"); | ||||
console.log(res); | console.log(res); | ||||
var obj = JSON.parse(res); | |||||
if (obj.success) | |||||
cb(null, obj); | |||||
if (res.success) | |||||
cb(null, res); | |||||
else | else | ||||
cb(obj.error); | |||||
cb(res.error); | |||||
}); | }); | ||||
} | } | ||||
n -= 1; | 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(); | |||||
}); | |||||
}); | |||||
}); | |||||
})(); | })(); |
module.exports = function(ctx) { | module.exports = function(ctx) { | ||||
ctx.end(ctx.view("index", { | |||||
profile: ctx.template("navbar-profile-login") | |||||
})); | |||||
ctx.end(ctx.view("index")); | |||||
} | } |
util.api("collection_create", { | util.api("collection_create", { | ||||
name: "New Collection" | name: "New Collection" | ||||
}, function(err, res) { | }, function(err, res) { | ||||
if (err) return util.error(err); | |||||
if (err) | |||||
return util.error(err); | |||||
var collectionId = res.id; | var collectionId = res.id; | ||||
collectionId: collectionId, | collectionId: collectionId, | ||||
file: f | file: f | ||||
}, function(err, res) { | }, function(err, res) { | ||||
if (err) return util.error(err); | |||||
if (err) | |||||
return util.error(err); | |||||
a(); | a(); | ||||
}, getXhr); | }, getXhr); |
}); | }); | ||||
ctx.end(ctx.view("view", { | ctx.end(ctx.view("view", { | ||||
profile: ctx.template("navbar-profile-login"), | |||||
images: images | images: images | ||||
})); | })); | ||||
} | } |