From e2b57902c51d475ba9ec029b4007b19b0af7ed43 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 9 May 2014 21:51:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?code=20refactoring=E2=80=A6..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.js | 29 ++- config/config.js | 37 ++++ config/express.js | 46 +++++ controller/push.js | 450 ++++++++++++++++++++++++++++++++++++++++ routes/index.js | 501 ++++++--------------------------------------- routes/index3.js | 440 +++++++++++++++++++++++++++++++++++++++ server.js | 40 ++++ 7 files changed, 1107 insertions(+), 436 deletions(-) create mode 100644 config/config.js create mode 100644 config/express.js create mode 100644 controller/push.js create mode 100644 routes/index3.js create mode 100644 server.js diff --git a/config.js b/config.js index 6b29fbf..e488935 100644 --- a/config.js +++ b/config.js @@ -1,4 +1,4 @@ -module.exports.admin_userid = process.env.ADMIN_USERID; +/*module.exports.admin_userid = process.env.ADMIN_USERID; module.exports.sitename = process.env.SITENAME; module.exports.siteurl = process.env.SITEURL; @@ -9,4 +9,29 @@ module.exports.gh_consumer_key = process.env.GH_CONSUMER_KEY; module.exports.gh_consumer_secret = process.env.GH_CONSUMER_SECRET; module.exports.gh_callback = process.env.GH_CALLBACK; -module.exports.express_session_secret = process.env.EXPRESS_SESSION_SECRET; +module.exports.express_session_secret = process.env.EXPRESS_SESSION_SECRET;*/ + +/** + * You can even config for different enviroment + * module.exports = {development: {}, production: {}} + * + * + */ +module.exports = { + + admin_userid: process.env.ADMIN_USERID, + + sitename: process.env.SITENAME, + + siteurl: process.env.SITEURL, + + mongo_uri: process.env.MONGOLAB_URI, + + gh_consumer_key: process.env.GH_CONSUMER_KEY, + + gh_consumer_secret: process.env.GH_CONSUMER_SECRET, + + gh_callback: process.env.GH_CALLBACK, + + express_session_secret: process.env.EXPRESS_SESSION_SECRET +}; diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..e488935 --- /dev/null +++ b/config/config.js @@ -0,0 +1,37 @@ +/*module.exports.admin_userid = process.env.ADMIN_USERID; + +module.exports.sitename = process.env.SITENAME; +module.exports.siteurl = process.env.SITEURL; + +module.exports.mongo_uri = process.env.MONGOLAB_URI; + +module.exports.gh_consumer_key = process.env.GH_CONSUMER_KEY; +module.exports.gh_consumer_secret = process.env.GH_CONSUMER_SECRET; +module.exports.gh_callback = process.env.GH_CALLBACK; + +module.exports.express_session_secret = process.env.EXPRESS_SESSION_SECRET;*/ + +/** + * You can even config for different enviroment + * module.exports = {development: {}, production: {}} + * + * + */ +module.exports = { + + admin_userid: process.env.ADMIN_USERID, + + sitename: process.env.SITENAME, + + siteurl: process.env.SITEURL, + + mongo_uri: process.env.MONGOLAB_URI, + + gh_consumer_key: process.env.GH_CONSUMER_KEY, + + gh_consumer_secret: process.env.GH_CONSUMER_SECRET, + + gh_callback: process.env.GH_CALLBACK, + + express_session_secret: process.env.EXPRESS_SESSION_SECRET +}; diff --git a/config/express.js b/config/express.js new file mode 100644 index 0000000..95338ec --- /dev/null +++ b/config/express.js @@ -0,0 +1,46 @@ +var express = require('express'), + MongoStore = require('connect-mongo')(express); + +module.exports = function (app, config) { + + app.use(express.static(__dirname + '/public')); + + app.set('views', __dirname + '/views'); + app.set('view engine', 'jade'); + + app.configure(function () { + + // cookieParser should above session + app.use(express.cookieParser()); + + // use urlencoded and json to fix connection 3.0 warning + app.use(express.urlencoded()); + app.use(express.json()); + app.use(express.methodOverride()); + + // express/mongo session storage + app.use(express.session({ + secret: config.express_session_secret, + store: new MongoStore({ + url: config.mongo_uri, + db: 'pushserver_sessions', + clear_interval: 600 + }, function () { + console.log('connected to mongo!'); + }) + })); + + // routes should be at last + app.use(app.router); + }); + + // development env config + app.configure('development', function () { + app.use(express.errorHandler({ dumpException: true, showStack: true })); + }); + + // production env config + app.configure('production', function () { + app.use(express.errorHandler()); + }); +} diff --git a/controller/push.js b/controller/push.js new file mode 100644 index 0000000..57ca3ad --- /dev/null +++ b/controller/push.js @@ -0,0 +1,450 @@ +var config = require("../config"); +var GHAPI = require("../gh-api").GHAPI; +var Device = require("../models/device").Device; +var User = require("../models/user").User; + +var apns = require("apn"); + +var _log = function(msg) { + console.log(msg); +}; + +var arrRemove = function (arr) { + var what, a = arguments, L = a.length, ax; + while (L > 1 && arr.length) { + what = a[--L]; + while ((ax= arr.indexOf(what)) !== -1) { + arr.splice(ax, 1); + } + } + return arr; +}; + + +function getClientIp(req) { + + var ipAddress; + // Amazon EC2 / Heroku workaround to get real client IP + var forwardedIpsStr = req.header('x-forwarded-for'); + if (forwardedIpsStr) { + + // 'x-forwarded-for' header may return multiple IP addresses in + // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the + // the first one + var forwardedIps = forwardedIpsStr.split(','); + ipAddress = forwardedIps[0]; + } + if (!ipAddress) { + // Ensure getting client IP address still works in + // development environment + ipAddress = req.connection.remoteAddress; + } + return ipAddress; +}; + +exports.index = function(req, res) { + render(req, res, "index"); +}; + + +exports.updateTokenDev = function(req, res) { + updateToken(req, res, false); +}; + +exports.updateTokenProd = function(req, res) { + updateToken(req, res, true); +}; + +var updateToken = function(req, res, isProduction) { + var deviceToken = req.body.deviceToken; + var channels = (req.body.channels && req.body.channels.length) ? req.body.channels.split(",") : []; + var unsubscribe = (req.body.unsubscribe && req.body.unsubscribe === "true") ? true : false; + + _log(deviceToken); + _log(unsubscribe); + + Device.findOne({deviceToken: deviceToken}, function(err, device) { + if (err) { + console.log("ERROR: " + err); + res.send(500, "error"); + res.end(); + return; + } + if (!device) { + _log("no device!"); + device = new Device(); + device.deviceToken = deviceToken; + device.channels = []; + } else { + _log("found device!"); + // device exists + } + + if (channels.length) { + if (unsubscribe) { + // unsubscribe from channels + for (var i in channels) { + var channel = channels[i]; + arrRemove(device.channels, channel); + } + + } else { + // subscribe to the channels + for (var i in channels) { + var channel = channels[i]; + if (device.channels.indexOf(channel) === -1) { + device.channels.push(channel); + } + } + + } + } + + device.valid = true; + device.production = isProduction; + device.updatedAt = new Date(); + device.save(function(e,d){}); + res.end('updated. for production: ' + isProduction); + }); +}; + +exports.channelListDev = function(req, res) { + channelList(req, res, false); +}; + +exports.channelListProd = function(req, res) { + channelList(req, res, true); +}; + +var channelList = function(req, res, isProduction) { + var channel = req.params.channel; + _log("looking for channel: " + channel); + getDevicesForChannel(channel, isProduction, function(err, devices) { + if (err) { + res.end('error! ' + error); + return; + } + res.end('ok'); + }); +}; + +exports.pushChannelDev = function(req, res) { + pushChannel(req, res, false); +}; + +exports.pushChannelProd = function(req, res) { + pushChannel(req, res, true); +}; + +var validateDeviceToken = function(token) { + if (token.length < 64) { + return false; + } + return true; +}; + +var pushChannel = function(req, res, isProduction) { + var channel = req.params.channel; + var message = req.body.message; + var badge = parseInt(req.body.badge, 10) || 0; + + doPushChannel(channel, message, badge, isProduction, function(err) { + if (err) { + res.end('error! ' + err); + } else { + res.end('pushed!'); + } + }); +}; + +var doPushChannel = function(channel, message, badge, isProduction, callback) { + _log("badge " + badge); + _log("pushing to channel: " + channel); + getDevicesForChannel(channel, isProduction, function(err, devices) { + if (err) { + callback(err); + return; + } + if (devices) { + + var connectionOptions = { + cert: "certs/apns_" + (isProduction ? "prod" : "dev") + "_cert.pem", + key: "certs/apns_" + (isProduction ? "prod" : "dev") + "_key.unencrypted.pem", + production: isProduction + /* legacy: true */ + }; + + var apnsConnection = new apns.Connection(connectionOptions); + + apnsConnection.on("connected", function(openSockets) { + _log("connected to apns!"); + }); + + apnsConnection.on("disconnected", function(openSockets) { + _log("disconnected from apns!"); + }); + + apnsConnection.on("transmitted", function(note, device) { + _log("sent note to device: " + device); + }); + + apnsConnection.on("error", function(err) { + _log("apns connection error: " + err); + }); + + apnsConnection.on("socketError", function(err) { + _log("socket error: " + error); + }); + + for (var i in devices) { + var device = devices[i]; + var deviceToken = device.deviceToken; + + if (!validateDeviceToken(deviceToken)) { + continue; + } + + var apnsDevice = new apns.Device(deviceToken); + + var note = new apns.Notification(); + note.badge = badge; + note.alert = message; + note.sound = "default"; + + apnsConnection.pushNotification(note, apnsDevice); + + _log("sending note to deviceToken: " + deviceToken); + } + + apnsConnection.shutdown(); + callback(null); + + } else { + callback(null); + } + }); +}; + +exports.channelListForDevice = function(req, res) { + var deviceToken = req.params.token; + if (!deviceToken) { + res.end('no token!'); + } else { + getDevicesForToken(deviceToken, function(err, devices) { + if (err) { + res.send(500, 'error! ' + err); + res.end(); + } else if (devices.length) { + // we're looking up by token, so we only want one result + var device = devices[0]; + res.end(device.channels.join(',')); + } else { + res.send(404, 'no devices found'); + res.end(); + } + }); + } +}; + +var getDevicesForToken = function(deviceToken, callback) { + var query = { + deviceToken: deviceToken + }; + + queryDevices(query, callback); +}; + +var getDevicesForChannel = function(channel, isProduction, callback) { + var query = { + valid: true, + production: isProduction, + channels: { + "$in" : [channel] + } + }; + queryDevices(query, callback); +}; + +var queryDevices = function(query, callback) { + + Device.find(query, function(err, devices) { + if (err) { + callback(err, null); + } else if (devices) { + _log("found some devices! " + devices.length); + _log(devices); + callback(null, devices); + } else { + callback(null, null); + } + + }); +}; + +//////// feedback service + +var startFeedback = function(isProduction) { + var connectionOptions = { + cert: "certs/apns_" + (isProduction ? "prod" : "dev") + "_cert.pem", + key: "certs/apns_" + (isProduction ? "prod" : "dev") + "_key.unencrypted.pem", + production: isProduction + }; + + _log("starting feedback for " + (isProduction ? "prod" : "dev")); + var feedbackConnection = apns.Feedback(connectionOptions); + + feedbackConnection.on('feedback', function(feedbackData) { + if (feedbackData.length) { + _log("got feedback for " + feedbackData.length + " devices!"); + for (var i in feedbackData) { + var data = feedbackData[i]; + + var timestamp = data.time; + var deviceToken = data.device.toString(); // this is a node-apn Device object + + getDevicesForToken(deviceToken, function(err, deviceObjects) { + if (!err && deviceObjects && deviceObjects.length) { + _log("got deviceToken: " + deviceToken); + var device = deviceObjects[0]; // this is our mongodb Device object + + // check to see if feedback timestamp is newer than our device.updatedAt + // if so, mark device as invalid to prevent sending push until marked valid again + var updatedAtTimestamp = device.updatedAt.getTime(); + + if (timestamp > updatedAtTimestamp) { + _log("marking " + deviceToken + " as invalid for " + (isProduction ? "prod" : "dev")); + device.valid = false; + device.invalidatedAt = new Date(timestamp * 1000); + device.save(function(e,d){}); + } + } + }); + } + } + }); + + feedbackConnection.on('error', function(error) { + _log("feedback module error! " + error); + }); + + feedbackConnection.on('feedbackError', function(error) { + _log("feedback processing error! " + error); + }); + +}; + +// start the polling for dev and prod +startFeedback(false); +startFeedback(true); + + +//////// web interface + +var render = function(req, res, path, opts) { + opts = opts || {}; + opts.title = config.sitename; + if (req.session.user) { + opts.user = req.session.user; + } + res.render(path, opts); +}; + + +exports.pushDashboard = function(req, res) { + + render(req, res, "dashboard"); + +}; + +exports.pushDashboardPost = function(req, res) { + var channel = req.body.push_channel; + var message = req.body.push_message; + var badge = parseInt(req.body.push_badge, 10) || 0; + var environment = req.body.push_environment; + + var isProduction = (environment === "prod") ? true : false; + + doPushChannel(channel, message, badge, isProduction, function(err) { + if (err) { + res.send(500, "error sending push! " + err); + res.end(); + } else { + + render(req, res, "push_success"); + } + }); +}; + + +////////// login auth + +exports.login = function(req, res) { + res.redirect("https://github.com/login/oauth/authorize?client_id=" + config.gh_consumer_key + "&response_type=token&redirect_uri=" + config.gh_callback); +}; + +exports.logout = function(req, res) { + req.session.destroy(function(err) { + if (err) { + console.log("error logging out: " + err); + } + res.redirect("/"); + }); +}; + +exports.oauth_return = function(req, res) { + console.log("code is: " + req.query.code); + var ghapi = new GHAPI(); + ghapi.getAccessToken(req.query.code, function(err, result, data) { + console.log(data); + console.log("got access token: " + data.access_token); + if (data.error) { + res.end('error: ' + data.error); + return; + } + + ghapi.access_token = data.access_token; + ghapi.request(ghapi.client.user.get, {}, function(err, user) { + console.log("got user result: " + JSON.stringify(user)); + req.session.user = { + "userid": user.id.toString(), + "username": user.login, + "avatar": user.avatar_url, + "access_token": data.access_token, + "is_admin": user.id.toString() === config.admin_userid ? true : false, + "name": user.name + }; + console.log('user.id: ' + user.id.toString()); + console.log('config.admin: ' + config.admin_userid); + + User.findOne({gh_userid: user.id}, function(err, doc) { + if (err) { + console.log("ERROR: " + err); + res.end('error'); + return; + } + if (!doc) { + doc = new User(); + } + doc.username = user.login; + doc.gh_userid = user.id.toString(); + doc.name = user.name; + doc.gh_access_token = data.access_token; + doc.avatar = user.avatar_url; + doc.is_admin = user.id.toString() === config.admin_userid ? true : false; + doc.save(function(e,d){}); + + res.redirect('/'); + }); + }); + }); +}; + +/** + * Ping handler + * + */ +exports.ping = function (req, res) { + console.log('ping'); + res.end('pong'); +} + diff --git a/routes/index.js b/routes/index.js index ec9978e..89288a6 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,440 +1,73 @@ -var config = require("../config"); -var GHAPI = require("../gh-api").GHAPI; -var Device = require("../models/device").Device; -var User = require("../models/user").User; +var push = require('../controller/push'); -var apns = require("apn"); - -var _log = function(msg) { - console.log(msg); -}; - -var arrRemove = function (arr) { - var what, a = arguments, L = a.length, ax; - while (L > 1 && arr.length) { - what = a[--L]; - while ((ax= arr.indexOf(what)) !== -1) { - arr.splice(ax, 1); - } - } - return arr; -}; - - -function getClientIp(req) { - - var ipAddress; - // Amazon EC2 / Heroku workaround to get real client IP - var forwardedIpsStr = req.header('x-forwarded-for'); - if (forwardedIpsStr) { - - // 'x-forwarded-for' header may return multiple IP addresses in - // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the - // the first one - var forwardedIps = forwardedIpsStr.split(','); - ipAddress = forwardedIps[0]; - } - if (!ipAddress) { - // Ensure getting client IP address still works in - // development environment - ipAddress = req.connection.remoteAddress; - } - return ipAddress; -}; - -exports.index = function(req, res) { - render(req, res, "index"); -}; - - -exports.updateTokenDev = function(req, res) { - updateToken(req, res, false); -}; - -exports.updateTokenProd = function(req, res) { - updateToken(req, res, true); -}; - -var updateToken = function(req, res, isProduction) { - var deviceToken = req.body.deviceToken; - var channels = (req.body.channels && req.body.channels.length) ? req.body.channels.split(",") : []; - var unsubscribe = (req.body.unsubscribe && req.body.unsubscribe === "true") ? true : false; - - _log(deviceToken); - _log(unsubscribe); - - Device.findOne({deviceToken: deviceToken}, function(err, device) { - if (err) { - console.log("ERROR: " + err); - res.send(500, "error"); - res.end(); - return; - } - if (!device) { - _log("no device!"); - device = new Device(); - device.deviceToken = deviceToken; - device.channels = []; - } else { - _log("found device!"); - // device exists - } - - if (channels.length) { - if (unsubscribe) { - // unsubscribe from channels - for (var i in channels) { - var channel = channels[i]; - arrRemove(device.channels, channel); - } - - } else { - // subscribe to the channels - for (var i in channels) { - var channel = channels[i]; - if (device.channels.indexOf(channel) === -1) { - device.channels.push(channel); - } - } - - } - } - - device.valid = true; - device.production = isProduction; - device.updatedAt = new Date(); - device.save(function(e,d){}); - res.end('updated. for production: ' + isProduction); - }); -}; - -exports.channelListDev = function(req, res) { - channelList(req, res, false); -}; - -exports.channelListProd = function(req, res) { - channelList(req, res, true); -}; - -var channelList = function(req, res, isProduction) { - var channel = req.params.channel; - _log("looking for channel: " + channel); - getDevicesForChannel(channel, isProduction, function(err, devices) { - if (err) { - res.end('error! ' + error); - return; - } - res.end('ok'); - }); -}; - -exports.pushChannelDev = function(req, res) { - pushChannel(req, res, false); -}; - -exports.pushChannelProd = function(req, res) { - pushChannel(req, res, true); -}; - -var validateDeviceToken = function(token) { - if (token.length < 64) { - return false; - } - return true; -}; - -var pushChannel = function(req, res, isProduction) { - var channel = req.params.channel; - var message = req.body.message; - var badge = parseInt(req.body.badge, 10) || 0; - - doPushChannel(channel, message, badge, isProduction, function(err) { - if (err) { - res.end('error! ' + err); - } else { - res.end('pushed!'); - } - }); -}; - -var doPushChannel = function(channel, message, badge, isProduction, callback) { - _log("badge " + badge); - _log("pushing to channel: " + channel); - getDevicesForChannel(channel, isProduction, function(err, devices) { - if (err) { - callback(err); - return; - } - if (devices) { - - var connectionOptions = { - cert: "certs/apns_" + (isProduction ? "prod" : "dev") + "_cert.pem", - key: "certs/apns_" + (isProduction ? "prod" : "dev") + "_key.unencrypted.pem", - production: isProduction - /* legacy: true */ - }; - - var apnsConnection = new apns.Connection(connectionOptions); - - apnsConnection.on("connected", function(openSockets) { - _log("connected to apns!"); - }); - - apnsConnection.on("disconnected", function(openSockets) { - _log("disconnected from apns!"); - }); - - apnsConnection.on("transmitted", function(note, device) { - _log("sent note to device: " + device); - }); - - apnsConnection.on("error", function(err) { - _log("apns connection error: " + err); - }); - - apnsConnection.on("socketError", function(err) { - _log("socket error: " + error); - }); - - for (var i in devices) { - var device = devices[i]; - var deviceToken = device.deviceToken; - - if (!validateDeviceToken(deviceToken)) { - continue; - } - - var apnsDevice = new apns.Device(deviceToken); - - var note = new apns.Notification(); - note.badge = badge; - note.alert = message; - note.sound = "default"; - - apnsConnection.pushNotification(note, apnsDevice); - - _log("sending note to deviceToken: " + deviceToken); - } - - apnsConnection.shutdown(); - callback(null); +module.exports = function (app) { + // Middleware + var admin = function (req, res, next) { + if (req.session.user && req.session.user.is_admin === true) { + next(); } else { - callback(null); - } - }); -}; - -exports.channelListForDevice = function(req, res) { - var deviceToken = req.params.token; - if (!deviceToken) { - res.end('no token!'); - } else { - getDevicesForToken(deviceToken, function(err, devices) { - if (err) { - res.send(500, 'error! ' + err); - res.end(); - } else if (devices.length) { - // we're looking up by token, so we only want one result - var device = devices[0]; - res.end(device.channels.join(',')); - } else { - res.send(404, 'no devices found'); - res.end(); - } - }); - } -}; - -var getDevicesForToken = function(deviceToken, callback) { - var query = { - deviceToken: deviceToken - }; - - queryDevices(query, callback); -}; - -var getDevicesForChannel = function(channel, isProduction, callback) { - var query = { - valid: true, - production: isProduction, - channels: { - "$in" : [channel] - } - }; - queryDevices(query, callback); -}; - -var queryDevices = function(query, callback) { - - Device.find(query, function(err, devices) { - if (err) { - callback(err, null); - } else if (devices) { - _log("found some devices! " + devices.length); - _log(devices); - callback(null, devices); - } else { - callback(null, null); - } - - }); -}; - -//////// feedback service - -var startFeedback = function(isProduction) { - var connectionOptions = { - cert: "certs/apns_" + (isProduction ? "prod" : "dev") + "_cert.pem", - key: "certs/apns_" + (isProduction ? "prod" : "dev") + "_key.unencrypted.pem", - production: isProduction - }; - - _log("starting feedback for " + (isProduction ? "prod" : "dev")); - var feedbackConnection = apns.Feedback(connectionOptions); - - feedbackConnection.on('feedback', function(feedbackData) { - if (feedbackData.length) { - _log("got feedback for " + feedbackData.length + " devices!"); - for (var i in feedbackData) { - var data = feedbackData[i]; - - var timestamp = data.time; - var deviceToken = data.device.toString(); // this is a node-apn Device object - - getDevicesForToken(deviceToken, function(err, deviceObjects) { - if (!err && deviceObjects && deviceObjects.length) { - _log("got deviceToken: " + deviceToken); - var device = deviceObjects[0]; // this is our mongodb Device object - - // check to see if feedback timestamp is newer than our device.updatedAt - // if so, mark device as invalid to prevent sending push until marked valid again - var updatedAtTimestamp = device.updatedAt.getTime(); - - if (timestamp > updatedAtTimestamp) { - _log("marking " + deviceToken + " as invalid for " + (isProduction ? "prod" : "dev")); - device.valid = false; - device.invalidatedAt = new Date(timestamp * 1000); - device.save(function(e,d){}); - } - } - }); - } + res.redirect('/'); } - }); - - feedbackConnection.on('error', function(error) { - _log("feedback module error! " + error); - }); - - feedbackConnection.on('feedbackError', function(error) { - _log("feedback processing error! " + error); - }); - -}; - -// start the polling for dev and prod -startFeedback(false); -startFeedback(true); - - -//////// web interface - -var render = function(req, res, path, opts) { - opts = opts || {}; - opts.title = config.sitename; - if (req.session.user) { - opts.user = req.session.user; } - res.render(path, opts); -}; - - -exports.pushDashboard = function(req, res) { - - render(req, res, "dashboard"); - -}; - -exports.pushDashboardPost = function(req, res) { - var channel = req.body.push_channel; - var message = req.body.push_message; - var badge = parseInt(req.body.push_badge, 10) || 0; - var environment = req.body.push_environment; - - var isProduction = (environment === "prod") ? true : false; - - doPushChannel(channel, message, badge, isProduction, function(err) { - if (err) { - res.send(500, "error sending push! " + err); - res.end(); - } else { - - render(req, res, "push_success"); - } - }); -}; - - -////////// login auth - -exports.login = function(req, res) { - res.redirect("https://github.com/login/oauth/authorize?client_id=" + config.gh_consumer_key + "&response_type=token&redirect_uri=" + config.gh_callback); -}; - -exports.logout = function(req, res) { - req.session.destroy(function(err) { - if (err) { - console.log("error logging out: " + err); - } - res.redirect("/"); - }); -}; - -exports.oauth_return = function(req, res) { - console.log("code is: " + req.query.code); - var ghapi = new GHAPI(); - ghapi.getAccessToken(req.query.code, function(err, result, data) { - console.log(data); - console.log("got access token: " + data.access_token); - if (data.error) { - res.end('error: ' + data.error); - return; - } - - ghapi.access_token = data.access_token; - ghapi.request(ghapi.client.user.get, {}, function(err, user) { - console.log("got user result: " + JSON.stringify(user)); - req.session.user = { - "userid": user.id.toString(), - "username": user.login, - "avatar": user.avatar_url, - "access_token": data.access_token, - "is_admin": user.id.toString() === config.admin_userid ? true : false, - "name": user.name - }; - console.log('user.id: ' + user.id.toString()); - console.log('config.admin: ' + config.admin_userid); - - User.findOne({gh_userid: user.id}, function(err, doc) { - if (err) { - console.log("ERROR: " + err); - res.end('error'); - return; - } - if (!doc) { - doc = new User(); - } - doc.username = user.login; - doc.gh_userid = user.id.toString(); - doc.name = user.name; - doc.gh_access_token = data.access_token; - doc.avatar = user.avatar_url; - doc.is_admin = user.id.toString() === config.admin_userid ? true : false; - doc.save(function(e,d){}); - res.redirect('/'); - }); - }); - }); -}; + /** + * Update device token to Dev + * + */ + app.post('/dev/updateDeviceToken', push.updateTokenDev); + + /** + * Update device token to Prod + * + */ + app.post('/dev/updateDeviceToken', push.updateTokenProd); + + /** + * Get channel list for a device token + * + */ + app.get('/channel_list/:token', push.channelListForDevice); + + /** + * Index page + * + */ + app.get('/', push.index); + + /** + * Dashboard page + * + */ + app.get('/dashboard', admin, push.pushDashboard); + + /** + * Dashboard post + * + */ + app.post('/dashboard_push', admin, push.pushDashboardPost); + + /** + * Login auth + * + */ + app.get('/login', push.login); + + /** + * Logout + * + */ + ap.get('/logout', push.logout); + + /** + * OAuth + * + */ + app.get('/oauth/callback', push.oauth_return); + + /** + * Prevents the app from sleeping when running on Heroku + * + */ + app.get('/ping', push.ping); +} diff --git a/routes/index3.js b/routes/index3.js new file mode 100644 index 0000000..ec9978e --- /dev/null +++ b/routes/index3.js @@ -0,0 +1,440 @@ +var config = require("../config"); +var GHAPI = require("../gh-api").GHAPI; +var Device = require("../models/device").Device; +var User = require("../models/user").User; + +var apns = require("apn"); + +var _log = function(msg) { + console.log(msg); +}; + +var arrRemove = function (arr) { + var what, a = arguments, L = a.length, ax; + while (L > 1 && arr.length) { + what = a[--L]; + while ((ax= arr.indexOf(what)) !== -1) { + arr.splice(ax, 1); + } + } + return arr; +}; + + +function getClientIp(req) { + + var ipAddress; + // Amazon EC2 / Heroku workaround to get real client IP + var forwardedIpsStr = req.header('x-forwarded-for'); + if (forwardedIpsStr) { + + // 'x-forwarded-for' header may return multiple IP addresses in + // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the + // the first one + var forwardedIps = forwardedIpsStr.split(','); + ipAddress = forwardedIps[0]; + } + if (!ipAddress) { + // Ensure getting client IP address still works in + // development environment + ipAddress = req.connection.remoteAddress; + } + return ipAddress; +}; + +exports.index = function(req, res) { + render(req, res, "index"); +}; + + +exports.updateTokenDev = function(req, res) { + updateToken(req, res, false); +}; + +exports.updateTokenProd = function(req, res) { + updateToken(req, res, true); +}; + +var updateToken = function(req, res, isProduction) { + var deviceToken = req.body.deviceToken; + var channels = (req.body.channels && req.body.channels.length) ? req.body.channels.split(",") : []; + var unsubscribe = (req.body.unsubscribe && req.body.unsubscribe === "true") ? true : false; + + _log(deviceToken); + _log(unsubscribe); + + Device.findOne({deviceToken: deviceToken}, function(err, device) { + if (err) { + console.log("ERROR: " + err); + res.send(500, "error"); + res.end(); + return; + } + if (!device) { + _log("no device!"); + device = new Device(); + device.deviceToken = deviceToken; + device.channels = []; + } else { + _log("found device!"); + // device exists + } + + if (channels.length) { + if (unsubscribe) { + // unsubscribe from channels + for (var i in channels) { + var channel = channels[i]; + arrRemove(device.channels, channel); + } + + } else { + // subscribe to the channels + for (var i in channels) { + var channel = channels[i]; + if (device.channels.indexOf(channel) === -1) { + device.channels.push(channel); + } + } + + } + } + + device.valid = true; + device.production = isProduction; + device.updatedAt = new Date(); + device.save(function(e,d){}); + res.end('updated. for production: ' + isProduction); + }); +}; + +exports.channelListDev = function(req, res) { + channelList(req, res, false); +}; + +exports.channelListProd = function(req, res) { + channelList(req, res, true); +}; + +var channelList = function(req, res, isProduction) { + var channel = req.params.channel; + _log("looking for channel: " + channel); + getDevicesForChannel(channel, isProduction, function(err, devices) { + if (err) { + res.end('error! ' + error); + return; + } + res.end('ok'); + }); +}; + +exports.pushChannelDev = function(req, res) { + pushChannel(req, res, false); +}; + +exports.pushChannelProd = function(req, res) { + pushChannel(req, res, true); +}; + +var validateDeviceToken = function(token) { + if (token.length < 64) { + return false; + } + return true; +}; + +var pushChannel = function(req, res, isProduction) { + var channel = req.params.channel; + var message = req.body.message; + var badge = parseInt(req.body.badge, 10) || 0; + + doPushChannel(channel, message, badge, isProduction, function(err) { + if (err) { + res.end('error! ' + err); + } else { + res.end('pushed!'); + } + }); +}; + +var doPushChannel = function(channel, message, badge, isProduction, callback) { + _log("badge " + badge); + _log("pushing to channel: " + channel); + getDevicesForChannel(channel, isProduction, function(err, devices) { + if (err) { + callback(err); + return; + } + if (devices) { + + var connectionOptions = { + cert: "certs/apns_" + (isProduction ? "prod" : "dev") + "_cert.pem", + key: "certs/apns_" + (isProduction ? "prod" : "dev") + "_key.unencrypted.pem", + production: isProduction + /* legacy: true */ + }; + + var apnsConnection = new apns.Connection(connectionOptions); + + apnsConnection.on("connected", function(openSockets) { + _log("connected to apns!"); + }); + + apnsConnection.on("disconnected", function(openSockets) { + _log("disconnected from apns!"); + }); + + apnsConnection.on("transmitted", function(note, device) { + _log("sent note to device: " + device); + }); + + apnsConnection.on("error", function(err) { + _log("apns connection error: " + err); + }); + + apnsConnection.on("socketError", function(err) { + _log("socket error: " + error); + }); + + for (var i in devices) { + var device = devices[i]; + var deviceToken = device.deviceToken; + + if (!validateDeviceToken(deviceToken)) { + continue; + } + + var apnsDevice = new apns.Device(deviceToken); + + var note = new apns.Notification(); + note.badge = badge; + note.alert = message; + note.sound = "default"; + + apnsConnection.pushNotification(note, apnsDevice); + + _log("sending note to deviceToken: " + deviceToken); + } + + apnsConnection.shutdown(); + callback(null); + + } else { + callback(null); + } + }); +}; + +exports.channelListForDevice = function(req, res) { + var deviceToken = req.params.token; + if (!deviceToken) { + res.end('no token!'); + } else { + getDevicesForToken(deviceToken, function(err, devices) { + if (err) { + res.send(500, 'error! ' + err); + res.end(); + } else if (devices.length) { + // we're looking up by token, so we only want one result + var device = devices[0]; + res.end(device.channels.join(',')); + } else { + res.send(404, 'no devices found'); + res.end(); + } + }); + } +}; + +var getDevicesForToken = function(deviceToken, callback) { + var query = { + deviceToken: deviceToken + }; + + queryDevices(query, callback); +}; + +var getDevicesForChannel = function(channel, isProduction, callback) { + var query = { + valid: true, + production: isProduction, + channels: { + "$in" : [channel] + } + }; + queryDevices(query, callback); +}; + +var queryDevices = function(query, callback) { + + Device.find(query, function(err, devices) { + if (err) { + callback(err, null); + } else if (devices) { + _log("found some devices! " + devices.length); + _log(devices); + callback(null, devices); + } else { + callback(null, null); + } + + }); +}; + +//////// feedback service + +var startFeedback = function(isProduction) { + var connectionOptions = { + cert: "certs/apns_" + (isProduction ? "prod" : "dev") + "_cert.pem", + key: "certs/apns_" + (isProduction ? "prod" : "dev") + "_key.unencrypted.pem", + production: isProduction + }; + + _log("starting feedback for " + (isProduction ? "prod" : "dev")); + var feedbackConnection = apns.Feedback(connectionOptions); + + feedbackConnection.on('feedback', function(feedbackData) { + if (feedbackData.length) { + _log("got feedback for " + feedbackData.length + " devices!"); + for (var i in feedbackData) { + var data = feedbackData[i]; + + var timestamp = data.time; + var deviceToken = data.device.toString(); // this is a node-apn Device object + + getDevicesForToken(deviceToken, function(err, deviceObjects) { + if (!err && deviceObjects && deviceObjects.length) { + _log("got deviceToken: " + deviceToken); + var device = deviceObjects[0]; // this is our mongodb Device object + + // check to see if feedback timestamp is newer than our device.updatedAt + // if so, mark device as invalid to prevent sending push until marked valid again + var updatedAtTimestamp = device.updatedAt.getTime(); + + if (timestamp > updatedAtTimestamp) { + _log("marking " + deviceToken + " as invalid for " + (isProduction ? "prod" : "dev")); + device.valid = false; + device.invalidatedAt = new Date(timestamp * 1000); + device.save(function(e,d){}); + } + } + }); + } + } + }); + + feedbackConnection.on('error', function(error) { + _log("feedback module error! " + error); + }); + + feedbackConnection.on('feedbackError', function(error) { + _log("feedback processing error! " + error); + }); + +}; + +// start the polling for dev and prod +startFeedback(false); +startFeedback(true); + + +//////// web interface + +var render = function(req, res, path, opts) { + opts = opts || {}; + opts.title = config.sitename; + if (req.session.user) { + opts.user = req.session.user; + } + res.render(path, opts); +}; + + +exports.pushDashboard = function(req, res) { + + render(req, res, "dashboard"); + +}; + +exports.pushDashboardPost = function(req, res) { + var channel = req.body.push_channel; + var message = req.body.push_message; + var badge = parseInt(req.body.push_badge, 10) || 0; + var environment = req.body.push_environment; + + var isProduction = (environment === "prod") ? true : false; + + doPushChannel(channel, message, badge, isProduction, function(err) { + if (err) { + res.send(500, "error sending push! " + err); + res.end(); + } else { + + render(req, res, "push_success"); + } + }); +}; + + +////////// login auth + +exports.login = function(req, res) { + res.redirect("https://github.com/login/oauth/authorize?client_id=" + config.gh_consumer_key + "&response_type=token&redirect_uri=" + config.gh_callback); +}; + +exports.logout = function(req, res) { + req.session.destroy(function(err) { + if (err) { + console.log("error logging out: " + err); + } + res.redirect("/"); + }); +}; + +exports.oauth_return = function(req, res) { + console.log("code is: " + req.query.code); + var ghapi = new GHAPI(); + ghapi.getAccessToken(req.query.code, function(err, result, data) { + console.log(data); + console.log("got access token: " + data.access_token); + if (data.error) { + res.end('error: ' + data.error); + return; + } + + ghapi.access_token = data.access_token; + ghapi.request(ghapi.client.user.get, {}, function(err, user) { + console.log("got user result: " + JSON.stringify(user)); + req.session.user = { + "userid": user.id.toString(), + "username": user.login, + "avatar": user.avatar_url, + "access_token": data.access_token, + "is_admin": user.id.toString() === config.admin_userid ? true : false, + "name": user.name + }; + console.log('user.id: ' + user.id.toString()); + console.log('config.admin: ' + config.admin_userid); + + User.findOne({gh_userid: user.id}, function(err, doc) { + if (err) { + console.log("ERROR: " + err); + res.end('error'); + return; + } + if (!doc) { + doc = new User(); + } + doc.username = user.login; + doc.gh_userid = user.id.toString(); + doc.name = user.name; + doc.gh_access_token = data.access_token; + doc.avatar = user.avatar_url; + doc.is_admin = user.id.toString() === config.admin_userid ? true : false; + doc.save(function(e,d){}); + + res.redirect('/'); + }); + }); + }); +}; diff --git a/server.js b/server.js new file mode 100644 index 0000000..93c63a6 --- /dev/null +++ b/server.js @@ -0,0 +1,40 @@ +var express = require('express'), + mongoose = require('mongoose'), + config = require('./config/config'), + request = require('request'), + app = express(); + +// Bootstrap db connection +// connect to the mongodb +var connect = function () { + var options = { server: { socketOptions: { keepAlive: 1 } } }; + mongoose.connect(config.mongo_uri || 'mongodb://localhost/pushserver_test'); +}; +connect(); + +// Error handler +mongoose.connection.on('error', function (err) { + console.log(err); +}); + +// Reconnect when closed +mongoose.connection.on('disconnected', function () { + connect(); +}); + +// Bootstrap exress settings +require('./config/express')(app, config); + +// Bootstrap routes +require('./routes')(app); + +// Actually this may has problems +setInterval(function () { + request(config.siteurl + '/ping'); +}, 60000); + +// Start the app by listening at +var port = process.env.PORT || 3000; +app.listen(port, function(){ + console.log("Express server listening on port %d in %s mode", process.env.PORT, app.settings.env); +}); From e50dde4bc39f4ae4251d097b26d5fe07f94e50c5 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 16 May 2014 21:40:39 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix=20the=20controller=20bug=20and=20update?= =?UTF-8?q?=20the=20package.json=20file=E2=80=A6.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/push.js | 2 +- package.json | 6 ++++-- routes/{index3.js => index2.js} | 0 3 files changed, 5 insertions(+), 3 deletions(-) rename routes/{index3.js => index2.js} (100%) diff --git a/controller/push.js b/controller/push.js index 57ca3ad..2f4da8c 100644 --- a/controller/push.js +++ b/controller/push.js @@ -1,4 +1,4 @@ -var config = require("../config"); +var config = require("../config/config"); var GHAPI = require("../gh-api").GHAPI; var Device = require("../models/device").Device; var User = require("../models/user").User; diff --git a/package.json b/package.json index 648624c..17472ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { - "name": "my-node-app", - "version": "0.0.1", + "name": "PushServer", + "description": "A nodejs server that runs on heroku for sending Apple APNS push notifications", + "version": "0.0.2", "private": true, "dependencies": { "express": "3.3.4", @@ -15,6 +16,7 @@ "github": "0.1.8", "apn": "1.5.2" }, + "main": "server", "engines": { "node": "0.10.x", "npm": "1.3.x" diff --git a/routes/index3.js b/routes/index2.js similarity index 100% rename from routes/index3.js rename to routes/index2.js