| @@ -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 | |||
| })); | |||
| } | |||