diff --git a/assets/js/client.js b/assets/js/client.js index b96c415..f114744 100644 --- a/assets/js/client.js +++ b/assets/js/client.js @@ -4,6 +4,7 @@ //= require 'libs/ICanHaz.min.js' //= require 'libs/bootstrap.min.js' //= require 'libs/ircparser.min.js' +//= require 'libs/jquery.cookie.min.js' //= require 'utils.js' //= require 'models.js' //= require 'collections.js' @@ -51,6 +52,14 @@ $(function() { } }); + irc.delete_session = function() { + // Deletes the session cookie at both server and client side + if ($.cookie('auth_token')) { + irc.socket.emit('session_delete', { auth_token: $.cookie('auth_token') }); + $.removeCookie('auth_token'); + } + } + // Registration (server joined) irc.socket.on('registered', function(data) { var window = irc.chatWindows.getByName('status'); @@ -69,6 +78,9 @@ $(function() { irc.socket.on('login_success', function(data) { window.irc.loggedIn = true; + + $.cookie('auth_token', data.auth_token, { expires: 7 }); + if(data.exists){ irc.socket.emit('connect', {}); } else { @@ -77,7 +89,9 @@ $(function() { }); irc.socket.on('disconnect', function() { + // The server probably went down. irc.connected = false; + irc.delete_session(); alert('You were disconnected from the server.'); $('.container-fluid').css('opacity', '0.5'); }); @@ -85,9 +99,18 @@ $(function() { irc.socket.on('register_success', function(data) { window.irc.loggedIn = true; + $.cookie('auth_token', data.auth_token, { expires: 7 }); irc.appView.overview.render({currentTarget: {id: "connection"}}); }); + irc.socket.on('session_not_found', function(data) { + // The client has a session cookie, but it is not found server-side. + // Delete it at the client, as something is not in sync, and render the overview page. + console.log("A session was found at client, but not in the server."); + irc.delete_session(); + irc.appView.overview.render(); + }); + irc.socket.on('restore_connection', function(data) { irc.me = new User({nick: data.nick, server: data.server}); irc.connected = true; @@ -283,6 +306,7 @@ $(function() { channel.stream.add(quitMessage); } } + irc.delete_session(); }); irc.socket.on('names', function(data) { @@ -347,6 +371,7 @@ $(function() { }); irc.socket.on('reset', function(data) { + irc.delete_session(); irc.chatWindows = new WindowList(); irc.connected = false; irc.loggedIn = false; diff --git a/assets/js/libs/jquery.cookie.min.js b/assets/js/libs/jquery.cookie.min.js new file mode 100644 index 0000000..9c99853 --- /dev/null +++ b/assets/js/libs/jquery.cookie.min.js @@ -0,0 +1,2 @@ +/*! jquery.cookie v1.4.0 | MIT */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{a=decodeURIComponent(a.replace(g," "))}catch(b){return}try{return h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setDate(k.getDate()+j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0!==a.cookie(b)?(a.cookie(b,"",a.extend({},c,{expires:-1})),!0):!1}}); \ No newline at end of file diff --git a/assets/js/views/overview.js b/assets/js/views/overview.js index e5cec74..410a58d 100644 --- a/assets/js/views/overview.js +++ b/assets/js/views/overview.js @@ -1,6 +1,14 @@ var OverviewView = Backbone.View.extend({ initialize: function() { - this.render(); + // Check if we are logged in with a cookie. + if ($.cookie('auth_token')){ + irc.socket.emit('session_check', { + auth_token: $.cookie('auth_token'), + }); + } else { + // If not, we render the overview page. + this.render(); + } }, events: { diff --git a/config.js b/config.js index 4935d25..86e21dd 100644 --- a/config.js +++ b/config.js @@ -33,5 +33,11 @@ module.exports = { use_polling: process.env.USE_POLLING || false, // Use polling if websockets aren't supported // limit each user's connection log to this amount of messages (***not implemented yet***) - max_log_size: 4096 + max_log_size: 4096, + + // How long you want to store a cookie, both server and client side, in hours. + cookie_time: 7 * 24, + + // Secret key used to generate a unique and secure session cookie hash. + secret_key: "MY-SUPER-SECRET-KEY" }; diff --git a/lib/models.js b/lib/models.js index 2c27163..d85b94d 100644 --- a/lib/models.js +++ b/lib/models.js @@ -52,6 +52,13 @@ module.exports = function (schema) { chanserv_password: { type: String } }); + schema.define('Session', { + auth_token: { type: String }, + username: { type: String }, + user_id: { type: String }, + expires: { type: Date } + }); + schema.autoupdate(); return schema; diff --git a/lib/socket.js b/lib/socket.js index cdcab1c..c0253c9 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -1,5 +1,6 @@ var bcrypt = require('bcrypt-nodejs'), - uuid = require('node-uuid'); + uuid = require('node-uuid'), + crypto = require('crypto'); function isChannel(name) { return ['#','&'].indexOf(name[0]) >= 0; @@ -23,6 +24,7 @@ module.exports = function(socket, app) { var Message = app.db.models.Message; var PM = app.db.models.PM; var Channel = app.db.models.Channel; + var Session = app.db.models.Session; // signal an IRC connection belonging to the user socket.signalIRC = function(connection, event, dict) { @@ -49,11 +51,13 @@ module.exports = function(socket, app) { , password: hash , joined: Date.now() }, function (err, user) { - socket.emit('register_success', {username: data.username}); - socket.userID = user.user_id; - // subscribe to all online IRC connections using socket.io room - socket.join(socket.userID); - socket.socketIORoom = socket.userID; + socket.session_create(data, user.user_id, function(data, auth_token) { + socket.emit('register_success', {username: data.username, auth_token: auth_token}); + socket.userID = user.user_id; + // subscribe to all online IRC connections using socket.io room + socket.join(socket.userID); + socket.socketIORoom = socket.userID; + }); }); }); }); @@ -87,7 +91,11 @@ module.exports = function(socket, app) { socket.connID = connections[0].id; socket.conn = connections[0]; } - socket.emit('login_success', {username: data.username, exists: exists}); + socket.session_create(data, user.user_id, function(data, auth_token){ + socket.emit('login_success', { username: data.username, + exists: exists, + auth_token: auth_token }); + }); }); } else { socket.emit('login_error', {message: 'Wrong password'}); @@ -98,6 +106,75 @@ module.exports = function(socket, app) { } }); }); + + socket.session_create = function(data, user_id, callback) { + /* + * Add the successful login to the session database. + */ + // Create a sha1-hash based on username and secret key + var hash = crypto.createHash('sha1'); + var auth_token = hash.update(data.username + app.config.secret_key).digest('hex'); + + // Delete any previous sessions for this user_id. + Session.all({ where: { user_id: user_id } }, function(err, sessions){ + sessions.forEach(function(session){ + session.destroy(); + }); + }) + + var now = new Date(); + Session.create({ + auth_token: auth_token, + username: data.username, + user_id: user_id, + expires: new Date().setHours(now.getHours() + app.config.cookie_time), + }); + callback(data, auth_token); + } + + socket.on('session_check', function(data){ + /* + * Check if the token provides is valid, that is: in the database and not + * expired. If found we restore the users connections, if any, and log in. + */ + Session.findOne({ where: { auth_token: data.auth_token}}, function(err, session){ + var now = new Date(); + if(session && session.expires > now) { + // TODO: Join the logic inside here, with the logic inside on('login') + socket.userID = session.user_id; + socket.join(socket.userID); + socket.socketIORoom = socket.userID; + + Connection.all({where: { user_id: session.user_id } }, function (err, connections) { + var exists = false; + if (connections.length > 0) { + exists = true; + // TEMPORARY - read note at top of this file on socket.connID + socket.connID = connections[0].id; + socket.conn = connections[0]; + } + socket.emit('login_success', { username: session.username, exists: exists }) + }); + } else { + if (session && session.expires < now) { + // Delete session if it is expired. + session.destroy(); + } + // In this situation, which should be rare, the client has a cookie + // which is not in the database. Thus we just want to delete the cookie + // at the client. + socket.emit('session_not_found'); + } + }) + }); + + socket.on('session_delete', function(data){ + Session.findOne({ where: {auth_token: data.auth_token}}, function(err, session){ + if (session) { + session.destroy(); + } + }); + }); // connection creation/restore socket.on('connect', function(data) { diff --git a/views/templates.jade b/views/templates.jade index 113d8c4..13c8f19 100644 --- a/views/templates.jade +++ b/views/templates.jade @@ -30,12 +30,14 @@ script(id="overview_home", type="text/html") li.overview_button#settings img(src="/assets/images/settings.svg") span Settings + {{^loggedIn}} li.overview_button#login img(src="/assets/images/login.svg") span Login li.overview_button#register img(src="/assets/images/register.svg") span Register + {{/loggedIn}} script(id="overview", type="text/html") #overview