From 32d9b5349eb969790c8f5eb6c9eb647d94fa3e92 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 2 May 2013 12:45:22 -0400 Subject: [PATCH 1/3] beginning google integration --- config.default.js | 4 ++++ package.json | 1 + public/css/style.less | 16 ++++++++++++++++ public/images/google_login.png | Bin 0 -> 7546 bytes public/templates/login.tpl | 1 + src/webserver.js | 25 ++++++++++++++++++++++++- 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 public/images/google_login.png diff --git a/config.default.js b/config.default.js index 8c5a546c50..0e20f9672b 100644 --- a/config.default.js +++ b/config.default.js @@ -17,6 +17,10 @@ var config = { "twitter": { "key": '', "secret": '' + }, + "google": { + "id": '', + "secret": '' } } diff --git a/package.json b/package.json index cb853c9be6..072503960a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "passport": "0.1.16", "passport-local": "0.1.6", "passport-twitter": "0.1.4", + "passport-google-oauth": "0.1.5", "less-middleware": "0.1.11" }, "devDependencies": {}, diff --git a/public/css/style.less b/public/css/style.less index 06d0cda1e8..44079ba5a3 100644 --- a/public/css/style.less +++ b/public/css/style.less @@ -165,7 +165,23 @@ footer.footer { padding: 0; li { + vertical-align: top; .inline-block; .pointer; + + &.google { + width: 202px; + height: 32px; + background: transparent; + background-image: url('../images/google_login.png'); + + &:hover { + background-position-x: -265px; + } + + &:active { + background-position-x: -535px; + } + } } } \ No newline at end of file diff --git a/public/images/google_login.png b/public/images/google_login.png new file mode 100644 index 0000000000000000000000000000000000000000..55abc2bc78330903688245e2818d1f8da7311fb7 GIT binary patch literal 7546 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 Logins
  • +
\ No newline at end of file diff --git a/src/webserver.js b/src/webserver.js index 95d55d9d29..9400f36316 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -9,6 +9,7 @@ var express = require('express'), passport = require('passport'), passportLocal = require('passport-local').Strategy, passportTwitter = require('passport-twitter').Strategy, + passportGoogle = require('passport-google-oauth').OAuth2Strategy, login_strategies = []; passport.use(new passportLocal(function(user, password, next) { @@ -22,7 +23,7 @@ if (config.twitter.key.length > 0 && config.twitter.secret.length > 0) { passport.use(new passportTwitter({ consumerKey: config.twitter.key, consumerSecret: config.twitter.secret, - callbackURL: config.url + "auth/twitter/callback" + callbackURL: config.url + 'auth/twitter/callback' }, function(token, tokenSecret, profile, done) { global.modules.user.loginViaTwitter(profile.id, profile.username, function(err, user) { if (err) { return done(err); } @@ -33,6 +34,19 @@ if (config.twitter.key.length > 0 && config.twitter.secret.length > 0) { login_strategies.push('twitter'); } +if (config.google.id.length > 0 && config.google.secret.length > 0) { + passport.use(new passportGoogle({ + clientID: config.google.id, + clientSecret: config.google.secret, + callbackURL: config.url + 'auth/google/callback' + }, function(accessToken, refreshToken, profile, done) { + console.log(accessToken, refreshToken, profile); + done('hardcode fail'); + })) + + login_strategies.push('google'); +} + passport.serializeUser(function(user, done) { done(null, user.uid); }); @@ -161,6 +175,15 @@ passport.deserializeUser(function(uid, done) { })); } + if (login_strategies.indexOf('google') !== -1) { + app.get('/auth/google', passport.authenticate('google', { scope: {} })); + + app.get('/auth/google/callback', passport.authenticate('google', { + successRedirect: '/', + failureRedirect: '/login' + })); + } + app.get('/reset/:code', function(req, res) { res.send(templates['header'] + templates['reset_code'].parse({ reset_code: req.params.code }) + templates['footer']); }); From 39b24436d0b4952281a62a006dad9de4a33ecad7 Mon Sep 17 00:00:00 2001 From: Baris Usakli Date: Thu, 2 May 2013 12:54:27 -0400 Subject: [PATCH 2/3] removed global.socket, passing in socket to the functions now --- app.js | 7 ------- src/posts.js | 2 +- src/topics.js | 5 ++--- src/user.js | 24 +++++++++++++----------- src/websockets.js | 32 +++++++++++++++++++------------- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/app.js b/app.js index b9759de5bc..fb70d170c8 100644 --- a/app.js +++ b/app.js @@ -17,13 +17,6 @@ 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; diff --git a/src/posts.js b/src/posts.js index 9e46407864..35fdaa30a1 100644 --- a/src/posts.js +++ b/src/posts.js @@ -60,7 +60,7 @@ var RDB = require('./redis.js'), } - Posts.reply = function(tid, uid, content) { + Posts.reply = function(socket, tid, uid, content) { Posts.create(uid, content, function(pid) { RDB.rpush('tid:' + tid + ':posts', pid); diff --git a/src/topics.js b/src/topics.js index 15cadbb806..ba8da84519 100644 --- a/src/topics.js +++ b/src/topics.js @@ -93,8 +93,8 @@ var RDB = require('./redis.js'), }); } - - Topics.post = function(uid, title, content, category) { + Topics.post = function(socket, uid, title, content, category) { + if (uid === 0) { socket.emit('event:alert', { title: 'Thank you for posting', @@ -144,7 +144,6 @@ var RDB = require('./redis.js'), // User Details - move this out later RDB.lpush('uid:' + uid + ':topics', tid); - socket.emit('event:alert', { title: 'Thank you for posting', message: 'You have successfully posted. Click here to view your post.', diff --git a/src/user.js b/src/user.js index 1d3ae16402..2b077e9163 100644 --- a/src/user.js +++ b/src/user.js @@ -7,7 +7,7 @@ var config = require('../config.js'), (function(User) { - User.get = function(uid, fields) { + User.get = function(socket, uid, fields) { if (uid > 0) { var keys = [], returnData = { @@ -48,7 +48,7 @@ var config = require('../config.js'), } } - User.login = function(user) { + User.login = function(socket, user) { if (user.username == null || user.password == null) { return socket.emit('user.login', {'status': 0, 'message': 'Missing fields'}); } @@ -160,7 +160,7 @@ var config = require('../config.js'), } User.create = function(username, password, email, callback) { - User.exists(username, function(exists) { + User.exists(null, username, function(exists) { if (exists) { return callback('user-exists', 0); } @@ -187,7 +187,7 @@ var config = require('../config.js'), }; - User.exists = function(username, callback) { + User.exists = function(socket, username, callback) { User.get_uid_by_username(username, function(exists) { exists = !!exists; @@ -195,12 +195,14 @@ var config = require('../config.js'), else socket.emit('user.exists', {exists: exists}); }); }; - User.count = function() { + + User.count = function(socket) { RDB.get('user:count', function(count) { socket.emit('user.count', {count: (count === null) ? 0 : count}); }); }; - User.latest = function() { + + User.latest = function(socket) { RDB.lrange('user:users', 0, 0, function(username) { socket.emit('user.latest', {username: username}); }); @@ -233,7 +235,7 @@ var config = require('../config.js'), } User.reset = { - validate: function(code, callback) { + validate: function(socket, code, callback) { if (typeof callback !== 'function') callback = undefined; RDB.get('reset:' + code + ':uid', function(uid) { @@ -256,7 +258,7 @@ var config = require('../config.js'), } }); }, - send: function(email) { + send: function(socket, email) { User.get_uid_by_email(email, function(uid) { if (uid !== null) { // Generate a new reset code @@ -305,7 +307,7 @@ var config = require('../config.js'), } }); }, - commit: function(code, password) { + commit: function(socket, code, password) { this.validate(code, function(validated) { if (validated) { RDB.get('reset:' + code + ':uid', function(uid) { @@ -321,7 +323,7 @@ var config = require('../config.js'), } User.email = { - exists: function(email, callback) { + exists: function(socket, email, callback) { User.get_uid_by_email(email, function(exists) { exists = !!exists; if (typeof callback !== 'function') socket.emit('user.email.exists', { exists: exists }); @@ -331,7 +333,7 @@ var config = require('../config.js'), } User.active = { - get_record : function() { + get_record : function(socket) { 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]}); }); diff --git a/src/websockets.js b/src/websockets.js index b47277af36..b5370b84aa 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -38,60 +38,66 @@ var SocketIO = require('socket.io').listen(global.server), }); io.sockets.on('connection', function(socket) { - global.socket = socket; if (DEVELOPMENT === true) { // refreshing templates modules.templates.init(); } + + process.on('uncaughtException', function(err) { + // handle the error safely + console.log("error message "+err); + socket.emit('event:consolelog',{type:'uncaughtException', stack:err.stack, error:err.toString()}); + }); + socket.emit('event:connect', {status: 1}); // BEGIN: API calls (todo: organize) // julian: :^) socket.on('api:user.get', function(data) { - modules.user.get(uid, data.fields); + modules.user.get(socket, uid, data.fields); }); socket.on('user.exists', function(data) { - modules.user.exists(data.username); + modules.user.exists(socket, data.username); }); socket.on('user.count', function(data) { - modules.user.count(data); + modules.user.count(socket, data); }); socket.on('user.latest', function(data) { - modules.user.latest(data); + modules.user.latest(socket, data); }); socket.on('user.login', function(data) { data.sessionID = sessionID; - modules.user.login(data); + modules.user.login(socket, data); }); socket.on('user.email.exists', function(data) { - modules.user.email.exists(data.email); + modules.user.email.exists(socket, data.email); }); socket.on('user:reset.send', function(data) { - modules.user.reset.send(data.email); + modules.user.reset.send(socket, data.email); }); socket.on('user:reset.valid', function(data) { - modules.user.reset.validate(data.code); + modules.user.reset.validate(socket, data.code); }); socket.on('user:reset.commit', function(data) { - modules.user.reset.commit(data.code, data.password); + modules.user.reset.commit(socket, data.code, data.password); }); socket.on('api:topics.post', function(data) { - modules.topics.post(uid, data.title, data.content); + modules.topics.post(socket, uid, data.title, data.content); }); socket.on('api:posts.reply', function(data) { - modules.posts.reply(data.topic_id, uid, data.content); + modules.posts.reply(socket, data.topic_id, uid, data.content); }); socket.on('api:user.active.get', function() { @@ -99,7 +105,7 @@ var SocketIO = require('socket.io').listen(global.server), }); socket.on('api:user.active.get_record', function() { - modules.user.active.get_record(); + modules.user.active.get_record(socket); }); }); From 4c41011d830f2f7a7fa8497f7ce7e275624e1f61 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 2 May 2013 14:51:57 -0400 Subject: [PATCH 3/3] minor stylistic change to header --- public/css/style.less | 1 + 1 file changed, 1 insertion(+) diff --git a/public/css/style.less b/public/css/style.less index 999af998d2..55196a5724 100644 --- a/public/css/style.less +++ b/public/css/style.less @@ -154,6 +154,7 @@ footer.footer { border: 1px solid #999; margin-right: 8px; margin-top: -2px; + float: left; } span {