Merge branch 'master' of github.com:psychobunny/node-forum

Conflicts:
	public/templates/header.tpl
	src/webserver.js
This commit is contained in:
Julian Lam
2013-05-02 09:23:11 -04:00
24 changed files with 518 additions and 218 deletions

View File

@@ -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) {
RDB.get('tid:' + tid + ':title', function(topic_name) { //do these asynch later
RDB.lrange('tid:' + tid + ':posts', start, end, function(pids) {
var content = [],
uid = [],
timestamp = [];
var content = [],
uid = [],
timestamp = [];
for (var i=0, ii=pids.length; i<ii; i++) {
content.push('pid:' + pids[i] + ':content');
uid.push('pid:' + pids[i] + ':uid');
timestamp.push('pid:' + pids[i] + ':timestamp');
}
for (var i=0, ii=pids.length; i<ii; i++) {
content.push('pid:' + pids[i] + ':content');
uid.push('pid:' + pids[i] + ':uid');
timestamp.push('pid:' + pids[i] + ':timestamp');
}
if (pids.length > 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<ii; i++) {
posts.push({
'content' : content[i],
'uid' : uid[i],
'timestamp' : timestamp[i]
});
}
callback({'posts': posts});
});
} else {
callback({});
}
if (pids.length > 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<ii; i++) {
posts.push({
'content' : content[i],
'uid' : uid[i],
'timestamp' : timestamp[i],
'relativeTime': utils.relativeTime(timestamp[i])
});
}
callback({'TOPIC_NAME':topic_name, 'TOPIC_ID': tid, 'posts': posts});
});
} else {
callback({});
}
});
});
}
Posts.reply = function() {
Posts.reply = function(tid, uid, content) {
Posts.create(uid, content, function(pid) {
RDB.rpush('tid:' + tid + ':posts', pid);
socket.emit('event:alert', {
title: 'Reply Successful',
message: 'You have successfully replied. Click here to view your reply.',
type: 'notify',
timeout: 2000
});
});
};
Posts.create = function(content, callback) {
if (global.uid === null) return;
Posts.create = function(uid, content, callback) {
if (uid === null) return;
RDB.incr('global:next_post_id', function(pid) {
// Posts Info
RDB.set('pid:' + pid + ':content', content);
RDB.set('pid:' + pid + ':uid', global.uid);
RDB.set('pid:' + pid + ':uid', uid);
RDB.set('pid:' + pid + ':timestamp', new Date().getTime());
// User Details - move this out later

View File

@@ -3,7 +3,8 @@
ERROR_LOGS = true,
redis = require('redis'),
db = redis.createClient();
config = require('../config.js'),
db = redis.createClient(config.redis.port, config.redis.host, config.redis.options);
// todo (holy cow): append,auth,bgrewriteaof,bgsave,bitcount,bitop,blpop,brpop,brpoplpush,client kill,client list,client getname,client setname,config get,config set,config resetstat,dbsize,debug object,debug segfault,decrby,discard,dump,echo,eval,evalsha,exec,exists,expireat,flushall,flushdb,getbit,getrange,getset,hdel,hexists,hget,hgetall,hincrby,hincrbyfloat,hkeys,hlen,hmget,hmset,hset,hsetnx,hvals,incrby,incrbyfloat,info,lastsave,lindex,linsert,llen,lpop,lpushx,lrem,lset,ltrim,migrate,monitor,move,mset,msetnx,object,persist,pexpire,pexpireat,ping,psetex,psubscribe,pttl,publish,punsubscribe,quit,randomkey,rename,renamenx,restore,rpop,rpoplpush,rpush,rpushx,sadd,save,scard,script exists,script flush,script kill,script load,sdiff,sdiffstore,select,setbit,setex,setnx,setrange,shutdown,sinter,sinterstore,sismember,slaveof,slowlog,smembers,smove,sort,spop,srandmember,srem,strlen,subscribe,sunion,sunionstore,sync,time,ttl,type,unsubscribe,unwatch,watch,zadd,zcard,zcount,zincrby,zinterstore,zrange,zrangebyscore,zrank,zrem,zremrangebyrank,zremrangebyscore,zrevrange,zrevrangebyscore,zrevrank,zscore,zunionstore
// done: get, set, incr, decr, del, mget, multi, expire, lpush, lrange, keys
@@ -75,6 +76,10 @@
db.lpush(key, item);
}
RedisDB.rpush = function(key, item) {
db.rpush(key, item);
}
RedisDB.lrange = function(key, start, end, callback, error_handler) {
db.lrange(key, start, end, function(error, data) {
return_handler(error, data, callback, error_handler);

View File

@@ -26,8 +26,8 @@ var fs = require('fs');
Templates.init = function() {
loadTemplates([
'header', 'footer', 'register', 'home', 'topic',
'login', 'reset', 'reset_code', 'account_settings',
'logout', '403',
'login', 'reset', 'reset_code', 'logout',
'403',
'emails/reset', 'emails/reset_plaintext'
]);
}
@@ -60,7 +60,11 @@ var fs = require('fs');
var template = this.html, regex, block;
return (function parse(data, namespace, template) {
if (Object.keys(data).length == 0) {
regex = makeRegex('[^]*');
template = template.replace(regex, '');
}
for (var d in data) {
if (data.hasOwnProperty(d)) {
if (data[d] instanceof String || data[d] === null) {
@@ -89,7 +93,7 @@ var fs = require('fs');
block = parse(data[d], namespace, block);
template = setBlock(regex, block, template);
} else {
} else {
template = replace(namespace + d, data[d], template);
}
}

View File

@@ -1,8 +1,6 @@
var RDB = require('./redis.js'),
posts = require('./posts.js');
posts = require('./posts.js'),
utils = require('./utils.js');
(function(Topics) {
//data structure
@@ -82,6 +80,7 @@ var RDB = require('./redis.js'),
'title' : title[i],
'uid' : uid[i],
'timestamp' : timestamp[i],
'relativeTime': utils.relativeTime(timestamp[i]),
'slug' : slug[i],
'post_count' : postcount[i]
});
@@ -97,7 +96,7 @@ var RDB = require('./redis.js'),
Topics.post = function(uid, title, content, category) {
if (uid === 0) {
global.socket.emit('event:alert', {
socket.emit('event:alert', {
title: 'Thank you for posting',
message: 'Since you are unregistered, your post is awaiting approval. Click here to register now.',
type: 'warning',
@@ -137,7 +136,7 @@ var RDB = require('./redis.js'),
RDB.set('topic:slug:' + slug + ':tid', tid);
// Posts
posts.create(content, function(pid) {
posts.create(uid, content, function(pid) {
RDB.lpush('tid:' + tid + ':posts', pid);
});
@@ -146,7 +145,7 @@ var RDB = require('./redis.js'),
RDB.lpush('uid:' + uid + ':topics', tid);
global.socket.emit('event:alert', {
socket.emit('event:alert', {
title: 'Thank you for posting',
message: 'You have successfully posted. Click here to view your post.',
type: 'notify',

View File

@@ -1,5 +1,5 @@
var config = require('../config.js'),
utils = require('../utils.js'),
utils = require('./utils.js'),
RDB = require('./redis.js'),
crypto = require('crypto'),
emailjs = require('emailjs'),
@@ -10,7 +10,9 @@ var config = require('../config.js'),
User.get = function(uid, fields) {
if (uid > 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<numData;x++) {
returnData[fields[x]] = data[x];
}
console.log(returnData);
if (returnData.picture !== undefined) {
var md5sum = crypto.createHash('md5');
md5sum.update(returnData.email.toLowerCase());
@@ -47,17 +49,17 @@ var config = require('../config.js'),
User.login = function(user) {
if (user.username == null || user.password == null) {
return global.socket.emit('user.login', {'status': 0, 'message': 'Missing fields'});
return socket.emit('user.login', {'status': 0, 'message': 'Missing fields'});
}
RDB.get('username:' + user.username + ':uid', function(uid) {
if (uid == null) {
return global.socket.emit('user.login', {'status': 0, 'message': 'Username does not exist.'});
return socket.emit('user.login', {'status': 0, 'message': 'Username does not exist.'});
}
RDB.get('uid:' + uid + ':password', function(password) {
if (user.password != password) {
return global.socket.emit('user.login', {'status': 0, 'message': 'Incorrect username / password combination.'});
return socket.emit('user.login', {'status': 0, 'message': 'Incorrect username / password combination.'});
} else {
// Start, replace, or extend a session
RDB.get('sess:' + user.sessionID, function(session) {
@@ -76,6 +78,51 @@ var config = require('../config.js'),
});
};
User.loginViaLocal = function(username, password, next) {
if (!username || !password) {
return next({
status: 'error',
message: 'invalid-user'
});
} else {
RDB.get('username:' + username + ':uid', function(uid) {
if (uid == null) {
return next({
status: 'error',
message: 'invalid-user'
});
}
RDB.get('uid:' + uid + ':password', function(user_password) {
if (password == user_password) {
// Start, replace, or extend a session
// RDB.get('sess:' + user.sessionID, function(session) {
// if (session !== user.sessionID) {
// RDB.set('sess:' + user.sessionID + ':uid', uid, 60*60*24*14); // Login valid for two weeks
// RDB.set('uid:' + uid + ':session', user.sessionID, 60*60*24*14);
// } else {
// RDB.expire('sess:' + user.sessionID + ':uid', 60*60*24*14); // Defer expiration to two weeks from now
// RDB.expire('uid:' + uid + ':session', 60*60*24*14);
// }
// });
next({
status: "ok",
user: {
uid: uid
}
});
} else {
next({
status: 'error',
message: 'invalid-password'
});
}
});
});
}
}
User.logout = function(sessionID, callback) {
User.get_uid_by_session(sessionID, function(uid) {
if (uid) {
@@ -88,7 +135,7 @@ var config = require('../config.js'),
User.create = function(username, password, email) {
if (username == null || password == null) {
return; global.socket.emit('user.create', {'status': 0, 'message': 'Missing fields'});
return; socket.emit('user.create', {'status': 0, 'message': 'Missing fields'});
}
@@ -111,9 +158,9 @@ var config = require('../config.js'),
RDB.lpush('user:users', username);
io.sockets.emit('user.latest', {username: username});
global.socket.emit('user.create', {'status': 1});
socket.emit('user.create', {'status': 1});
global.socket.emit('event:alert', {
socket.emit('event:alert', {
title: 'Thank you for registering',
message: 'You have successfully registered - welcome to nodebb!',
type: 'notify',
@@ -127,7 +174,7 @@ var config = require('../config.js'),
User.exists = function(username, callback) {
User.get_uid_by_username(username, function(exists) {
exists = !!exists;
global.socket.emit('user.exists', {exists: exists})
socket.emit('user.exists', {exists: exists})
if (callback) {
callback(exists);
@@ -136,12 +183,12 @@ var config = require('../config.js'),
};
User.count = function() {
RDB.get('user:count', function(count) {
global.socket.emit('user.count', {count: (count === null) ? 0 : count});
socket.emit('user.count', {count: (count === null) ? 0 : count});
});
};
User.latest = function() {
RDB.lrange('user:users', 0, 0, function(username) {
global.socket.emit('user.latest', {username: username});
socket.emit('user.latest', {username: username});
});
}
@@ -157,6 +204,14 @@ var config = require('../config.js'),
RDB.get('sess:' + session + ':uid', callback);
};
User.session_ping = function(sessionID, uid) {
// Start, replace, or extend a session
RDB.get('sess:' + sessionID, function(session) {
RDB.set('sess:' + sessionID + ':uid', uid, 60*60*24*14); // Login valid for two weeks
RDB.set('uid:' + uid + ':session', sessionID, 60*60*24*14);
});
}
User.reset = {
validate: function(code, callback) {
if (typeof callback !== 'function') callback = undefined;
@@ -165,18 +220,18 @@ var config = require('../config.js'),
if (uid !== null) {
RDB.get('reset:' + code + ':expiry', function(expiry) {
if (expiry >= +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 {

35
src/utils.js Normal file
View File

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

View File

@@ -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) {
for (var i=0, ii=routes.length; i<ii; i++) {
(function(route) {
app.get('/' + route, function(req, res) {
res.send(templates['header'] + '<script>templates.ready(function(){ajaxify.go("' + route + '");});</script>' + 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)
});
app.get('/topics/:topic_id/:slug', function(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) {

View File

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