diff --git a/.project b/.project new file mode 100644 index 0000000000..d382cb0b53 --- /dev/null +++ b/.project @@ -0,0 +1,12 @@ + + + node-forum + + + + + + + com.aptana.projects.webnature + + diff --git a/app.js b/app.js index 8732c4aee4..b9759de5bc 100644 --- a/app.js +++ b/app.js @@ -17,6 +17,12 @@ global.modules = modules; // global.uid = 1; +process.on('uncaughtException', function(err) { + // handle the error safely + console.log("error message "+err); + global.socket.emit('event:consolelog',{type:'uncaughtException',stack:err.stack,error:err.toString()}); +}); + (function(config) { config['ROOT_DIRECTORY'] = __dirname; @@ -25,6 +31,6 @@ global.modules = modules; // modules.webserver.init(); modules.websockets.init(); - + }(global.configuration)); \ No newline at end of file diff --git a/config.default.js b/config.default.js index 5393551a9e..ed2c5eacc2 100644 --- a/config.default.js +++ b/config.default.js @@ -1,11 +1,18 @@ var config = { + "secret": 'nodebb-secret', "base_url": "http://localhost", "port": 4567, - "url": undefined, // Leave this alone "mailer": { host: 'localhost', port: '25', from: 'mailer@localhost.lan' + }, + "redis": { + port: "6379", + host: "127.0.0.1", + options: { + + } } } diff --git a/nodebb b/nodebb old mode 100644 new mode 100755 diff --git a/package.json b/package.json index a496ed42f6..645c519fe6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "cookie": "0.0.6", "connect-redis": "1.4.5", "path": "0.4.9", - "crypto": "0.0.3" + "crypto": "0.0.3", + "passport": "0.1.16", + "passport-local": "0.1.6" }, "devDependencies": {}, "optionalDependencies": {}, diff --git a/public/config.default.json b/public/config.default.json index 67acc5b7d3..b3794291ce 100644 --- a/public/config.default.json +++ b/public/config.default.json @@ -2,5 +2,6 @@ "socket" : { "address" : "localhost", "port" : "4567" - } + }, + "api_url" : "http://localhost:4567/api/" } \ No newline at end of file diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index ac3d7d4db0..ac96a7d6be 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -22,8 +22,8 @@ var ajaxify = {}; ajaxify.go = function(url, callback) { var url = url.replace(/\/$/, ""); - var tpl_url = (url === '') ? 'home' : url; - + var tpl_url = (url === '' || url === '/') ? 'home' : url.split('/')[0]; + if (templates[tpl_url]) { window.history.pushState({}, url, "/" + url); diff --git a/public/src/app.js b/public/src/app.js index 0fdd83a550..a45effa90d 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -1,6 +1,8 @@ var socket, config, - app = {}; + app = {}, + + API_URL = null; // todo: cleanup,etc (function() { @@ -8,6 +10,8 @@ var socket, $.ajax({ url: '/config.json?v=' + new Date().getTime(), success: function(data) { + API_URL = data.api_url; + config = data; socket = io.connect('http://' + config.socket.address + config.socket.port? ':' + config.socket.port : ''); @@ -18,6 +22,10 @@ var socket, socket.on('event:alert', function(data) { app.alert(data); }); + + socket.on('event:consolelog', function(data) { + console.log(data); + }); }, async: false @@ -74,14 +82,63 @@ var socket, } } - var post_window = null; - app.open_post_window = function() { + var post_window = null, + submit_post_btn = null, + post_title = null, + reply_title = null, + post_content = null; + + + app.open_post_window = function(post_mode, id, title) { + submit_post_btn = submit_post_btn || document.getElementById('submit_post_btn'); + post_title = post_title || document.getElementById('post_title'); + reply_title = reply_title || document.getElementById('reply_title'); + post_content = post_content || document.getElementById('post_content'); + + post_window = post_window || document.getElementById('post_window'); jQuery(post_window).slideToggle(250); - document.getElementById('post_title').focus(); + + if (post_mode == null || post_mode == 'topic') { + post_title.style.display = "block"; + reply_title.style.display = "none"; + post_title.focus(); + submit_post_btn.onclick = function() { + app.post_topic(); + } + } else { + post_title.style.display = "none"; + reply_title.style.display = "block"; + reply_title.innerHTML = 'You are replying to "' + title + '"'; + post_content.focus(); + submit_post_btn.onclick = function() { + app.post_reply(id) + } + } }; + app.post_reply = function(topic_id) { + var content = document.getElementById('post_content').value; + + if (content.length < 5) { + app.alert({ + title: 'Reply Failure', + message: 'You need to write more dude.', + type: 'error', + timeout: 2000 + }); + + return; + } + + socket.emit('api:posts.reply', { + 'topic_id' : topic_id, + 'content' : content + }); + jQuery(post_window).slideToggle(250); + + }; app.post_topic = function() { var title = document.getElementById('post_title').value, content = document.getElementById('post_content').value; @@ -91,10 +148,7 @@ var socket, title: 'Topic Post Failure', message: 'You need to write more dude.', type: 'error', - timeout: 2000, - clickfn: function() { - ajaxify.go('register'); - } + timeout: 2000 }); return; diff --git a/public/src/templates.js b/public/src/templates.js index 29e3850d33..f18b1e50b6 100644 --- a/public/src/templates.js +++ b/public/src/templates.js @@ -1,9 +1,17 @@ var templates = {}; (function() { + var ready_callback; + + templates.ready = function(callback) { + //quick implementation because introducing a lib to handle several async callbacks + if (callback == null) ready_callback(); + else ready_callback = callback; + } function loadTemplates(templatesToLoad) { var timestamp = new Date().getTime(); + var loaded = templatesToLoad.length; for (var t in templatesToLoad) { (function(file) { @@ -18,6 +26,12 @@ var templates = {}; template.prototype.html = String(html); templates[file] = new template; + + loaded--; + if (loaded == 0) templates.ready(); + }).fail(function() { + loaded--; + if (loaded == 0) templates.ready(); }); }(templatesToLoad[t])); } @@ -26,7 +40,7 @@ var templates = {}; function init() { loadTemplates([ - 'header', 'footer', 'register', 'home', + 'header', 'footer', 'register', 'home', 'topic', 'login', 'reset', 'reset_code', 'account_settings', 'emails/reset', 'emails/reset_plaintext' ]); @@ -62,6 +76,10 @@ var templates = {}; var template = this.html, regex, block; return (function parse(data, namespace, template) { + if (data.length == 0) { + regex = makeRegex('[^]*'); + template = template.replace(regex, ''); + } for (var d in data) { if (data.hasOwnProperty(d)) { @@ -112,10 +130,10 @@ function load_template(callback) { rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''); var url = location.href.replace(rootUrl +'/', ''); - if (url == '') url = 'home'; - jQuery.get('api/' + url, function(data) { - - document.getElementById('content').innerHTML = templates[url].parse(JSON.parse(data)); + url = (url === '' || url === '/') ? 'home' : url; + + jQuery.get(API_URL + url, function(data) { + document.getElementById('content').innerHTML = templates[url.split('/')[0]].parse(JSON.parse(data)); if (callback) callback(); }); } \ No newline at end of file diff --git a/public/templates/account_settings.tpl b/public/templates/account_settings.tpl deleted file mode 100644 index d29898b8e4..0000000000 --- a/public/templates/account_settings.tpl +++ /dev/null @@ -1,18 +0,0 @@ -

Account Settings

-
- - -

- If you see this, you are logged in. -

-
- \ No newline at end of file diff --git a/public/templates/footer.tpl b/public/templates/footer.tpl index 0d5ee09518..dd3de05b00 100644 --- a/public/templates/footer.tpl +++ b/public/templates/footer.tpl @@ -4,7 +4,7 @@ @@ -148,6 +180,7 @@
+
@@ -161,7 +194,7 @@
- Submit + Submit Discard
@@ -172,4 +205,5 @@
+
\ No newline at end of file diff --git a/public/templates/home.tpl b/public/templates/home.tpl index 3149404251..b49f4c2e85 100644 --- a/public/templates/home.tpl +++ b/public/templates/home.tpl @@ -1,9 +1,9 @@ diff --git a/public/templates/login.tpl b/public/templates/login.tpl index e9a96e7862..c430b90f17 100644 --- a/public/templates/login.tpl +++ b/public/templates/login.tpl @@ -4,33 +4,10 @@ Failed Login Attempt

-
-
-   +
+
+
+   +
Forgot Password? - - \ No newline at end of file + \ No newline at end of file diff --git a/public/templates/topic.tpl b/public/templates/topic.tpl index 56c230a31f..4c8ebfa9cb 100644 --- a/public/templates/topic.tpl +++ b/public/templates/topic.tpl @@ -1,8 +1,23 @@ - +
+ + diff --git a/src/posts.js b/src/posts.js index 78e55d4562..9e46407864 100644 --- a/src/posts.js +++ b/src/posts.js @@ -1,4 +1,5 @@ -var RDB = require('./redis.js'); +var RDB = require('./redis.js'), + utils = require('./utils.js'); (function(Posts) { //data structure @@ -15,60 +16,70 @@ var RDB = require('./redis.js'); if (start == null) start = 0; if (end == null) end = start + 10; - RDB.lrange('tid:' + tid + ':posts', start, end, function(pids) { - - var content = [], - uid = [], - timestamp = []; - - for (var i=0, ii=pids.length; i 0) { - RDB.multi() - .mget(content) - .mget(uid) - .mget(timestamp) - .exec(function(err, replies) { - content = replies[0]; - uid = replies[1]; - timestamp = replies[2]; - - var posts = []; - for (var i=0, ii=content.length; i 0) { + RDB.multi() + .mget(content) + .mget(uid) + .mget(timestamp) + .exec(function(err, replies) { + content = replies[0]; + uid = replies[1]; + timestamp = replies[2]; + + var posts = []; + for (var i=0, ii=content.length; i 0) { var keys = [], - returnData = {}, + returnData = { + uid: uid + }, removeEmail = false; if (!(fields instanceof Array)) fields = ['username', 'email']; @@ -27,7 +29,7 @@ var config = require('../config.js'), for(var x=0,numData=data.length;x= +new Date()/1000|0) { - if (!callback) global.socket.emit('user:reset.valid', { valid: true }); + if (!callback) socket.emit('user:reset.valid', { valid: true }); else callback(true); } else { // Expired, delete from db RDB.del('reset:' + code + ':uid'); RDB.del('reset:' + code + ':expiry'); - if (!callback) global.socket.emit('user:reset.valid', { valid: false }); + if (!callback) socket.emit('user:reset.valid', { valid: false }); else callback(false); } }); } else { - if (!callback) global.socket.emit('user:reset.valid', { valid: false }); + if (!callback) socket.emit('user:reset.valid', { valid: false }); else callback(false); } }); @@ -208,13 +263,13 @@ var config = require('../config.js'), emailjsServer.send(message, function(err, success) { if (err === null) { - global.socket.emit('user.send_reset', { + socket.emit('user.send_reset', { status: "ok", message: "code-sent", email: email }); } else { - global.socket.emit('user.send_reset', { + socket.emit('user.send_reset', { status: "error", message: "send-failed" }); @@ -222,7 +277,7 @@ var config = require('../config.js'), } }); } else { - global.socket.emit('user.send_reset', { + socket.emit('user.send_reset', { status: "error", message: "invalid-email", email: email @@ -238,7 +293,7 @@ var config = require('../config.js'), RDB.del('reset:' + code + ':uid'); RDB.del('reset:' + code + ':expiry'); - global.socket.emit('user:reset.commit', { status: 'ok' }); + socket.emit('user:reset.commit', { status: 'ok' }); }); } }); @@ -249,14 +304,29 @@ var config = require('../config.js'), exists: function(email, callback) { User.get_uid_by_email(email, function(exists) { exists = !!exists; - if (typeof callback !== 'function') global.socket.emit('user.email.exists', { exists: exists }); + if (typeof callback !== 'function') socket.emit('user.email.exists', { exists: exists }); else callback(exists); }); } } User.active = { + get_record : function() { + RDB.mget(['global:active_user_record', 'global:active_user_record_date'], function(data) { + socket.emit('api:user.active.get_record', {record: data[0], timestamp: data[1]}); + }); + }, + get: function(callback) { + function user_record(total) { + RDB.get('global:active_user_record', function(record) { + if (total > record) { + RDB.set('global:active_user_record', total); + RDB.set('global:active_user_record_date', new Date().getTime()); + } + }); + } + RDB.keys('active:*', function(active) { var returnObj = { users: 0, @@ -282,6 +352,8 @@ var config = require('../config.js'), } } + user_record(returnObj.anon + returnObj.users); + if (callback === undefined) { io.sockets.emit('api:user.active.get', returnObj) } else { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000000..6728175336 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,35 @@ +var utils = { + generateUUID: function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, + relativeTime: function(timestamp) { + var now = +new Date(), + difference = now - Math.floor(parseFloat(timestamp)); + + difference = Math.floor(difference / 1000); + if (difference < 60) return difference + ' second' + (difference !== 1 ? 's' : '') + ' ago'; + + difference = Math.floor(difference / 60); + if (difference < 60) return difference + ' minute' + (difference !== 1 ? 's' : '') + ' ago'; + + difference = Math.floor(difference / 60); + if (difference < 24) return difference + ' hour' + (difference !== 1 ? 's' : '') + ' ago'; + + difference = Math.floor(difference / 24); + if (difference < 3) return difference + ' day' + (difference !== 1 ? 's' : '') + ' ago'; + + // Lastly, just return a formatted date + var date = new Date(timestamp); + // hour = date.getHours(), + // minute = date.getMinutes(), + // day = date.getDate(), + // month = date.getMonth(), + // months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return date.toDateString(); + } +} + +module.exports = utils; \ No newline at end of file diff --git a/src/webserver.js b/src/webserver.js index c4da2ac510..da96c649c3 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -3,7 +3,28 @@ var express = require('express'), server = require('http').createServer(WebServer), RedisStore = require('connect-redis')(express), path = require('path'), - config = require('../config.js'); + config = require('../config.js'), + redis = require('redis'), + redisServer = redis.createClient(config.redis.port, config.redis.host, config.redis.options), + passport = require('passport'), + passportLocal = require('passport-local').Strategy; + +passport.use(new passportLocal(function(user, password, next) { + global.modules.user.loginViaLocal(user, password, function(login) { + if (login.status === 'ok') next(null, login.user); + else next(null, false, login); + }); +})); + +passport.serializeUser(function(user, done) { + done(null, user.uid); +}); + +passport.deserializeUser(function(uid, done) { + done(null, { + uid: uid + }); +}); (function(app) { var templates = global.templates; @@ -24,31 +45,39 @@ var express = require('express'), app.use(express.compress()); app.use(express.session({ store: new RedisStore({ + client: redisServer, ttl: 60*60*24*14 }), - secret: 'nodebb', + secret: config.secret, key: 'express.sid' })); + app.use(passport.initialize()); + app.use(passport.session()); app.use(function(req, res, next) { // Don't bother with session handling for API requests if (/^\/api\//.test(req.url)) return next(); - if (req.session.uid === undefined) { - console.log('info: [Auth] First load, retrieving uid...'); - global.modules.user.get_uid_by_session(req.sessionID, function(uid) { - if (uid !== null) { - req.session.uid = uid; - console.log('info: [Auth] uid ' + req.session.uid + ' found. Welcome back.'); - } else { - req.session.uid = 0; - console.log('info: [Auth] No login session found.'); - } - }); - } else { - // console.log('SESSION: ' + req.sessionID); - // console.log('info: [Auth] Ping from uid ' + req.session.uid); + if (req.user && req.user.uid) { + global.modules.user.session_ping(req.sessionID, req.user.uid); } + // if (req.session.uid === undefined) { + // console.log('info: [Auth] First load, retrieving uid...'); + + // global.modules.user.get_uid_by_session(req.sessionID, function(uid) { + // if (uid !== null) { + // req.session.uid = uid; + // console.log('info: [Auth] uid ' + req.session.uid + ' found. Welcome back.'); + // } else { + // req.session.uid = 0; + // console.log('info: [Auth] No login session found.'); + // } + // }); + // } else { + // // console.log('SESSION: ' + req.sessionID); + // // console.log('info: [Auth] Ping from uid ' + req.session.uid); + // } + // (Re-)register the session as active global.modules.user.active.register(req.sessionID); @@ -59,58 +88,67 @@ var express = require('express'), // Useful if you want to use app.put and app.delete (instead of app.post all the time) // app.use(express.methodOverride()); - app.get('/', function(req, res) { - global.modules.topics.generate_forum_body(function(forum_body) { - res.send(templates['header'] + forum_body + templates['footer']); - }); - }); app.get('/403', function(req, res) { res.send(templates['header'] + templates['403'] + templates['footer']); }); + // Basic Routes (entirely client-side parsed, goal is to move the rest of the crap in this file into this one section) + (function() { + var routes = ['', 'login', 'register']; - // need a proper way to combine these two routes together - app.get('/topics/:topic_id', function(req, res) { - global.modules.topics.generate_topic_body(function(topic_body) { - res.send(templates['header'] + topic_body + templates['footer']); - }, req.params.topic_id) - }); - app.get('/topics/:topic_id/:slug', function(req, res) { + for (var i=0, ii=routes.length; itemplates.ready(function(){ajaxify.go("' + route + '");});' + templates['footer']); + }); + }(routes[i])); + } + }()); + + + function generate_topic_body(req, res) { global.modules.topics.generate_topic_body(function(topic_body) { res.send(templates['header'] + topic_body + templates['footer']); - }, req.params.topic_id) - }); + }, req.params.topic_id); + } + app.get('/topic/:topic_id', generate_topic_body); + app.get('/topic/:topic_id*', generate_topic_body); - app.get('/api/:method', function(req, res) { + function api_method(req, res) { switch(req.params.method) { case 'home' : global.modules.topics.get(function(data) { res.send(JSON.stringify(data)); }); break; + case 'topic' : + global.modules.posts.get(function(data) { + res.send(JSON.stringify(data)); + }, req.params.id); + break; default : res.send('{}'); break; } - }); + } + app.get('/api/:method', api_method); + app.get('/api/:method/:id', api_method); + app.get('/api/:method/:id*', api_method); - app.get('/login', function(req, res) { - res.send(templates['header'] + templates['login'] + templates['footer']); - }); + app.post('/login', passport.authenticate('local', { + successRedirect: '/', + failureRedirect: '/login' + })); app.get('/logout', function(req, res) { console.log('info: [Auth] Session ' + res.sessionID + ' logout (uid: ' + global.uid + ')'); global.modules.user.logout(req.sessionID, function(logout) { - if (logout === true) { - delete(req.session.uid); - req.session.destroy(); - } + req.logout(); + res.send(templates['header'] + templates['logout'] + templates['footer']); }); - - res.send(templates['header'] + templates['logout'] + templates['footer']); }); app.get('/reset/:code', function(req, res) { diff --git a/src/websockets.js b/src/websockets.js index bbe3bf4b8a..832d07964c 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -1,6 +1,7 @@ var SocketIO = require('socket.io').listen(global.server), cookie = require('cookie'), - connect = require('connect'); + connect = require('connect'), + config = require('../config.js'); (function(io) { var modules = null, @@ -16,7 +17,7 @@ var SocketIO = require('socket.io').listen(global.server), io.set('authorization', function(handshakeData, accept) { if (handshakeData.headers.cookie) { handshakeData.cookie = cookie.parse(handshakeData.headers.cookie); - handshakeData.sessionID = connect.utils.parseSignedCookie(handshakeData.cookie['express.sid'], 'nodebb'); + handshakeData.sessionID = connect.utils.parseSignedCookie(handshakeData.cookie['express.sid'], config.secret); if (handshakeData.cookie['express.sid'] == handshakeData.sessionID) { return accept('Cookie is invalid.', false); @@ -93,9 +94,17 @@ var SocketIO = require('socket.io').listen(global.server), modules.topics.post(uid, data.title, data.content); }); + socket.on('api:posts.reply', function(data) { + modules.posts.reply(data.topic_id, uid, data.content); + }); + socket.on('api:user.active.get', function() { modules.user.active.get(); }); + + socket.on('api:user.active.get_record', function() { + modules.user.active.get_record(); + }); }); }(SocketIO)); diff --git a/utils.js b/utils.js deleted file mode 100644 index 7b003e466a..0000000000 --- a/utils.js +++ /dev/null @@ -1,10 +0,0 @@ -var utils = { - generateUUID: function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - } -} - -module.exports = utils; \ No newline at end of file