/** * JSDuck authentication / commenting server side element. Requires Node.js + MongoDB. * * Authentication assumes a vBulletin forum database, but could easily be adapted (see ForumUser.js) * * Expects a config file, config.js, that looks like this: * * exports.db = { * user: 'forumUsername', * password: 'forumPassword', * host: 'forumHost', * dbName: 'forumDb' * }; * * exports.sessionSecret = 'random string for session cookie encryption'; * * exports.mongoDb = 'mongodb://mongoHost:port/comments'; * */ config = require('./config'); mongoose = require('mongoose'); require('./database'); require('express-namespace'); var mysql = require('mysql'), client = mysql.createClient({ host: config.db.host, user: config.db.user, password: config.db.password, database: config.db.dbName }), express = require('express'), connect = require('connect'), MongoStore = require('connect-session-mongo'), _ = require('underscore'), ForumUser = require('./ForumUser').ForumUser, forumUser = new ForumUser(client), util = require('./util'), crypto = require('crypto'); var app = express.createServer( // Headers for Cross Origin Resource Sharing (CORS) function (req, res, next) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'); next(); }, express.cookieParser(), // Hack to set session cookie if session ID is set as a URL param. // This is because not all browsers support sending cookies via CORS function(req, res, next) { if (req.query.sid) { var sid = req.query.sid.replace(/ /g, '+'); req.cookies = req.cookies || {}; req.cookies['sencha_docs'] = sid; } next(); }, // Use MongoDB for session storage connect.session({ store: new MongoStore, secret: config.sessionSecret, key: 'sencha_docs' }), function(req, res, next) { // IE doesn't get content-type, so default to form encoded. if (!req.headers['content-type']) { req.headers['content-type'] = 'application/x-www-form-urlencoded'; } next(); }, express.bodyParser(), express.methodOverride() ); app.enable('jsonp callback'); // All URLs start with /auth app.namespace('/auth', function(){ /** * Authentication */ app.get('/session', function(req, res) { var result = req.session && req.session.user && forumUser.clientUser(req.session.user); res.json(result || false); }); app.post('/login', function(req, res){ forumUser.login(req.body.username, req.body.password, function(err, result) { if (err) { res.json({ success: false, reason: err }); return; } req.session = req.session || {}; req.session.user = result; var response = _.extend(forumUser.clientUser(result), { sessionID: req.sessionID, success: true }); res.json(response); }); }); // Remove session app.post('/logout', function(req, res){ req.session.user = null; res.json({ success: true }); }); /** * Handles comment unsubscription requests. */ app.get('/unsubscribe/:subscriptionId', function(req, res) { Subscription.findOne({ _id: req.params.subscriptionId }, function(err, subscription) { if (err) throw(err); if (subscription) { if (req.query.all == 'true') { Subscription.remove({ userId: subscription.userId }, function(err) { res.send("You have been unsubscribed from all threads."); }); } else { Subscription.remove({ _id: req.params.subscriptionId }, function(err) { res.send("You have been unsubscribed from that thread."); }); } } else { res.send("You are already unsubscribed.") } }); }); }); /** * Commenting */ app.namespace('/auth/:sdk/:version', function(){ /** * Returns a list of comments for a particular target (eg class, guide, video) */ app.get('/comments', function(req, res) { Comment.find({ target: JSON.parse(req.query.startkey), deleted: { '$ne': true }, sdk: req.params.sdk, version: req.params.version }).sort('createdAt', 1).run(function(err, comments){ res.json(util.formatComments(comments, req)); }); }); /** * Returns 100 most recent comments. */ app.get('/comments_recent', function(req, res) { Comment.find({ deleted: { '$ne': true }, sdk: req.params.sdk, version: req.params.version }).sort('createdAt', -1).limit(100).run(function(err, comments){ res.json(util.formatComments(comments, req)); }); }); /** * Returns number of comments for each class / method */ app.get('/comments_meta', util.getCommentsMeta, util.getCommentSubscriptions, function(req, res) { res.send({ comments: req.commentsMeta, subscriptions: req.commentSubscriptions || [] }); }); /** * Returns an individual comment (used when editing a comment) */ app.get('/comments/:commentId', util.findComment, function(req, res) { res.json({ success: true, content: req.comment.content }); }); /** * Creates a new comment */ app.post('/comments', util.requireLoggedInUser, function(req, res) { var target = JSON.parse(req.body.target); if (target.length === 2) { target.push(''); } var comment = new Comment({ author: req.session.user.username, userId: req.session.user.userid, content: req.body.comment, action: req.body.action, rating: Number(req.body.rating), contentHtml: util.sanitize(req.body.comment), downVotes: [], upVotes: [], createdAt: new Date, target: target, emaiHash: crypto.createHash('md5').update(req.session.user.email).digest("hex"), sdk: req.params.sdk, version: req.params.version, moderator: req.session.user.moderator, title: req.body.title, url: req.body.url }); comment.save(function(err, response) { res.json({ success: true, id: response._id, action: req.body.action }); util.sendEmailUpdates(comment); }); }); /** * Updates an existing comment (for voting or updating contents) */ app.post('/comments/:commentId', util.requireLoggedInUser, util.findComment, function(req, res) { var voteDirection, comment = req.comment; if (req.body.vote) { util.vote(req, res, comment); } else { var canUpdate = _.include(req.session.user.membergroupids, 7) || req.session.user.username == comment.author; if (!canUpdate) { res.json({success: false, reason: 'Forbidden'}, 403); return; } comment.content = req.body.content; comment.contentHtml = util.sanitize(req.body.content); comment.updates = comment.updates || []; comment.updates.push({ updatedAt: String(new Date()), author: req.session.user.username }); comment.save(function(err, response) { res.json({ success: true, content: comment.contentHtml }); }); } }); /** * Deletes a comment */ app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, function(req, res) { var canDelete = false, comment = req.comment; canDelete = _.include(req.session.user.membergroupids, 7) || req.session.user.username == req.comment.author; if (!canDelete) { res.json({ success: false, reason: 'Forbidden' }, 403); return; } comment.deleted = true; comment.save(function(err, response) { res.send({ success: true }); }); }); /** * Get email subscriptions */ app.get('/subscriptions', util.getCommentSubscriptions, function(req, res) { res.json({ subscriptions: req.commentSubscriptions }); }); /** * Subscibe / unsubscribe to a comment thread */ app.post('/subscribe', util.requireLoggedInUser, function(req, res) { var subscriptionBody = { sdk: req.params.sdk, version: req.params.version, target: JSON.parse(req.body.target), userId: req.session.user.userid }; Subscription.findOne(subscriptionBody, function(err, subscription) { if (subscription && req.body.subscribed == 'false') { subscription.remove(function(err, ok) { res.send({ success: true }); }); } else if (!subscription && req.body.subscribed == 'true') { subscription = new Subscription(subscriptionBody); subscription.email = req.session.user.email; subscription.save(function(err, ok) { res.send({ success: true }); }); } }); }); }); app.listen(3000); console.log("Server started...");