Commit 2b86109a authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Refactor the backend considerably.

- Validator class now takes care of all the validations.
- Request class implements the actual requests performed.
- TableFactory sets up all database connections and provides access to
  Table classes.
parent ff9f77ed
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ module.exports = {
        return {
            _id: comment.id,
            author: comment.username,
            content: comment.content,
            contentHtml: comment.content_html,
            createdAt: String(comment.created_at),
            score: comment.vote,
+54 −143
Original line number Diff line number Diff line
var config = require('./config');
var express = require('express');
var MySQLStore = require('connect-mysql-session')(express);
var services = require('./services');
var ApiAdapter = require('./api_adapter');
var Request = require('./request');
var validator = require('./validator');

var app = express();

@@ -60,27 +60,21 @@ app.configure(function() {
// Authentication

app.get('/auth/session', function(req, res) {
    if (req.session && req.session.user) {
    new Request(req).getUser(function(user) {
        if (user) {
            res.json({
            userName: req.session.user.username,
            mod: req.session.user.moderator
                userName: user.username,
                mod: user.moderator
            });
        }
        else {
            res.json(false);
        }
    });
});

app.post('/auth/login', services.users, function(req, res) {
    req.users.login(req.body.username, req.body.password, function(err, user) {
        if (err) {
            res.json({ success: false, reason: err });
            return;
        }

        req.session = req.session || {};
        req.session.user = user;

app.post('/auth/login', validator.attemptLogin, function(req, res) {
    new Request(req).getUser(function(user) {
        res.json({
            userName: user.username,
            mod: user.moderator,
@@ -90,8 +84,7 @@ app.post('/auth/login', services.users, function(req, res) {
    });
});

app.post('/auth/logout', function(req, res) {
    req.session.user = null;
app.post('/auth/logout', validator.doLogout, function(req, res) {
    res.json({ success: true });
});

@@ -100,162 +93,80 @@ app.post('/auth/logout', function(req, res) {

// Returns number of comments for each class/member,
// and when user is logged in, all his subscriptions.
app.get('/auth/:sdk/:version/comments_meta', services.comments, services.subscriptions, function(req, res) {
    req.comments.countsPerTarget(function(err, counts) {
        if (req.session.user) {
            req.subscriptions.findTargetsByUser(req.session.user.id, function(err, targets) {
                res.send({
                    comments: counts,
                    subscriptions: targets.map(ApiAdapter.targetToJson)
app.get('/auth/:sdk/:version/comments_meta', function(req, res) {
    var r = new Request(req);
    r.getCommentCountsPerTarget(function(counts) {
        r.getSubscriptions(function(subs) {
            res.json({ comments: counts, subscriptions: subs });
        });
    });
        }
        else {
            res.send({
                comments: counts,
                subscriptions: []
            });
        }
    });
});

// Returns a list of comments for a particular target (eg class, guide, video)
app.get('/auth/:sdk/:version/comments', services.comments, function(req, res) {
    if (!req.query.startkey) {
        res.json({error: 'Invalid request'});
        return;
    }

    var target = ApiAdapter.targetFromJson(JSON.parse(req.query.startkey));
    if (req.session.user) {
        req.comments.showVoteDirBy(req.session.user.id);
    }
    req.comments.find(target, function(err, comments) {
        res.json(comments.map(ApiAdapter.commentToJson));
app.get('/auth/:sdk/:version/comments', validator.hasStartKey, function(req, res) {
    new Request(req).getComments(req.query.startkey, function(comments) {
        res.json(comments);
    });
});

// Adds new comment
app.post('/auth/:sdk/:version/comments', services.requireLogin, services.comments, function(req, res) {
    var comment = {
        user_id: req.session.user.id,
        target: ApiAdapter.targetFromJson(JSON.parse(req.body.target)),
        content: req.body.comment
    };

    req.comments.add(comment, function(err, comment_id) {
        res.json({
            id: comment_id,
            success: true
        });
app.post('/auth/:sdk/:version/comments', validator.isLoggedIn, function(req, res) {
    new Request(req).addComment(req.body.target, req.body.comment, function(comment_id) {
        res.json({ id: comment_id, success: true });
    });
});

// Returns plain markdown content of individual comment (used when editing a comment)
app.get('/auth/:sdk/:version/comments/:commentId', services.comments, function(req, res) {
    req.comments.getById(req.params.commentId, function(err, comment) {
app.get('/auth/:sdk/:version/comments/:commentId', function(req, res) {
    new Request(req).getComment(req.params.commentId, function(comment) {
        res.json({ success: true, content: comment.content });
    });
});

// Updates an existing comment (for voting or updating contents)
app.post('/auth/:sdk/:version/comments/:commentId', services.requireLogin, services.comments, services.users, function(req, res) {
    req.comments.getById(req.params.commentId, function(err, comment) {
app.post('/auth/:sdk/:version/comments/:commentId', validator.isLoggedIn, function(req, res) {
    if (req.body.vote) {
            if (req.session.user.id === comment.user_id) {
                res.json({success: false, reason: 'You cannot vote on your own content'});
                return;
            }

            var vote = {
                user_id: req.session.user.id,
                comment_id: comment.id,
                value: req.body.vote === "up" ? 1 : -1
            };

            req.comments.vote(vote, function(err, voteDir, total) {
        validator.canVote(req, res, function() {
            new Request(req).vote(req.params.commentId, req.body.vote, function(direction, total) {
                res.json({
                    success: true,
                    direction: voteDir === 1 ? "up" : (voteDir === -1 ? "down" : null),
                    direction: direction,
                    total: total
                });
            });
        });
    }
    else {
            if (!req.users.canModify(req.session.user, comment)) {
                res.json({ success: false, reason: 'Forbidden' }, 403);
                return;
            }

            var update = {
                id: comment.id,
                user_id: req.session.user.id,
                content: req.body.content
            };

            req.comments.update(update, function(err) {
                req.comments.getById(comment.id, function(err, comment) {
                    res.json({ success: true, content: comment.content_html });
        validator.canModify(req, res, function() {
            new Request(req).updateComment(req.params.commentId, req.body.content, function(comment) {
                res.json({ success: true, content: comment.contentHtml });
            });
        });
    }
});
});

// Deletes a comment
app.post('/auth/:sdk/:version/comments/:commentId/delete', services.requireLogin, services.comments, services.users, function(req, res) {
    req.comments.getById(req.params.commentId, function(err, comment) {
        if (!req.users.canModify(req.session.user, comment)) {
            res.json({ success: false, reason: 'Forbidden' }, 403);
            return;
        }

        var action = {
            id: req.params.commentId,
            user_id: req.session.user.id,
            deleted: true
        };
        req.comments.setDeleted(action, function(err) {
app.post('/auth/:sdk/:version/comments/:commentId/delete', validator.isLoggedIn, validator.canModify, function(req, res) {
    new Request(req).setDeleted(req.params.commentId, true, function() {
        res.send({ success: true });
    });
});
});

// Restores a deleted comment
app.post('/auth/:sdk/:version/comments/:commentId/undo_delete', services.requireLogin, services.comments, services.users, function(req, res) {
    req.comments.showDeleted(true);
    req.comments.getById(req.params.commentId, function(err, comment) {
        if (!req.users.canModify(req.session.user, comment)) {
            res.json({ success: false, reason: 'Forbidden' }, 403);
            return;
        }

        var action = {
            id: req.params.commentId,
            user_id: req.session.user.id,
            deleted: false
        };
        req.comments.setDeleted(action, function(err) {
            res.send({ success: true, comment: ApiAdapter.commentToJson(comment) });
app.post('/auth/:sdk/:version/comments/:commentId/undo_delete', validator.isLoggedIn, validator.canModify, function(req, res) {
    new Request(req).setDeleted(req.params.commentId, false, function() {
        r.getComment(req.params.commentId, function(comment) {
            res.send({ success: true, comment: comment });
        });
    });
});

// Returns all subscriptions for logged in user
app.get('/auth/:sdk/:version/subscriptions', services.subscriptions, function(req, res) {
    if (req.session.user.id) {
        req.subscriptions.findTargetsByUser(req.session.user.id, function(err, targets) {
            res.json({
                subscriptions: targets.map(ApiAdapter.targetToJson)
app.get('/auth/:sdk/:version/subscriptions', function(req, res) {
    new Request(req).getSubscriptions(function(subs) {
        res.json({ subscriptions: subs });
    });
});
    }
    else {
        res.json({
            subscriptions: []
        });
    }
});

app.listen(config.port);
console.log("Server started at port "+config.port+"...");

comments/request.js

0 → 100644
+142 −0
Original line number Diff line number Diff line
var TableFactory = require("./table_factory");
var ApiAdapter = require("./api_adapter");

/**
 * Handles JSON API request.
 *
 * @constructor
 * Initializes with current HTTP request object.
 * @param {Object} req The request object from Express.
 */
function Request(req) {
    this.req = req;
    if (req.params.sdk && +req.params.version) {
        this.domain = req.params.sdk+"-"+req.params.version;
    }
    this.db = new TableFactory(this.domain);
}

Request.prototype = {
    getCommentCountsPerTarget: function(callback) {
        this.db.comments().countsPerTarget(function(err, counts) {
            callback(counts);
        });
    },

    getComments: function(target, callback) {
        var targetObj = ApiAdapter.targetFromJson(JSON.parse(target));

        if (this.isLoggedIn()) {
            this.db.comments().showVoteDirBy(this.getUserId());
        }

        this.db.comments().find(targetObj, function(err, comments) {
            callback(comments.map(ApiAdapter.commentToJson));
        });
    },

    getComment: function(comment_id, callback) {
        this.db.comments().getById(comment_id, function(err, comment) {
            callback(ApiAdapter.commentToJson(comment));
        });
    },

    addComment: function(target, content, callback) {
        var comment = {
            user_id: this.getUserId(),
            target: ApiAdapter.targetFromJson(JSON.parse(target)),
            content: content
        };

        this.db.comments().add(comment, function(err, comment_id) {
            callback(comment_id);
        });
    },

    setDeleted: function(comment_id, deleted, callback) {
        if (deleted === false) {
            this.db.comments().showDeleted(true);
        }

        var action = {
            id: comment_id,
            user_id: this.getUserId(),
            deleted: deleted
        };
        this.db.comments().setDeleted(action, function(err) {
            callback();
        });
    },

    getSubscriptions: function(callback) {
        if (!this.isLoggedIn()) {
            callback([]);
            return;
        }

        this.db.subscriptions().findTargetsByUser(this.getUserId(), function(err, targets) {
            callback(targets.map(ApiAdapter.targetToJson));
        });
    },

    updateComment: function(comment_id, content, callback) {
        var update = {
            id: comment_id,
            user_id: this.getUserId(),
            content: content
        };

        this.db.comments().update(update, function(err) {
            this.db.comments().getById(comment_id, function(err, comment) {
                callback(ApiAdapter.commentToJson(comment));
            });
        }.bind(this));
    },

    vote: function(comment_id, vote, callback) {
        var voteObj = {
            user_id: this.getUserId(),
            comment_id: comment_id,
            value: vote === "up" ? 1 : -1
        };

        this.db.comments().vote(voteObj, function(err, voteDir, total) {
            var direction = voteDir === 1 ? "up" : (voteDir === -1 ? "down" : null);
            callback(direction, total);
        });
    },

    getUser: function(callback) {
        callback(this.req.session && this.req.session.user);
    },

    getUserId: function() {
        return this.req.session.user.id;
    },

    /**
     * True when user is logged in
     * @return {Boolean}
     */
    isLoggedIn: function() {
        return this.req.session && this.req.session.user;
    },

    /**
     * True when logged in user can modify comment with given ID.
     * Works also for deleted comments.
     * @param {Number} comment_id
     * @param {Function} callback
     * @param {Function} callback.success True when user can modify the comment.
     */
    canModify: function(comment_id, callback) {
        this.db.comments().showDeleted(true);
        this.db.comments().getById(comment_id, function(err, comment) {
            this.db.comments().showDeleted(false);
            var user = this.req.session.user;
            callback(user.moderator || user.id == comment.user_id);
        }.bind(this));
    }
};

module.exports = Request;

comments/services.js

deleted100644 → 0
+0 −51
Original line number Diff line number Diff line
var DbFacade = require('./db_facade');
var Comments = require('./comments');
var Users = require('./users');
var Subscriptions = require('./subscriptions');
var ForumAuth = require('./forum_auth');
var config = require('./config');

/**
 * @class services
 * Methods to use as filters in express.
 */
module.exports = {
    /**
     * Adds comments service to request.
     */
    comments: function(req, res, next) {
        var db = new DbFacade(config.mysql);
        req.comments = new Comments(db, req.params.sdk+"-"+req.params.version);
        next();
    },

    /**
     * Adds users service to request.
     */
    users: function(req, res, next) {
        var forumAuth = new ForumAuth(new DbFacade(config.forumDb));
        req.users = new Users(new DbFacade(config.mysql), forumAuth);
        next();
    },

    /**
     * Adds subscriptions service to request.
     */
    subscriptions: function(req, res, next) {
        var db = new DbFacade(config.mysql);
        req.subscriptions = new Subscriptions(db, req.params.sdk+"-"+req.params.version);
        next();
    },

    /**
     * Requires that user is logged in.
     */
    requireLogin: function(req, res, next) {
        if (!req.session || !req.session.user) {
            res.json({success: false, reason: 'Forbidden'}, 403);
        }
        else {
            next();
        }
    }
};
+68 −0
Original line number Diff line number Diff line
var DbFacade = require('./db_facade');
var Comments = require('./comments');
var Users = require('./users');
var Subscriptions = require('./subscriptions');
var ForumAuth = require('./forum_auth');
var config = require('./config');

/**
 * Produces instances of classes representing different tables.  Each
 * instance is factored only once and a cached instance is returned on
 * subsequent calls.
 *
 * @constructor
 * Initializes factory to work within a specific comments domain.
 * @param {String} domain
 */
function TableFactory(domain) {
    this.domain = domain;
}

TableFactory.prototype = {
    /**
     * Returns Comments instance.
     * @return {Comments}
     */
    comments: function() {
        return this.cache("comments", function() {
            return new Comments(this.database(), this.domain);
        });
    },

    /**
     * Returns Users instance.
     * @return {Users}
     */
    users: function() {
        return this.cache("users", function() {
            var forumAuth = new ForumAuth(new DbFacade(config.forumDb));
            return new Users(this.database(), forumAuth);
        });
    },

    /**
     * Returns Subscriptions instance.
     * @return {Subscriptions}
     */
    subscriptions: function() {
        return this.cache("subscriptions", function() {
            return new Subscriptions(this.database(), this.domain);
        });
    },

    database: function() {
        return this.cache("database", function() {
            return new DbFacade(config.mysql);
        });
    },

    cache: function(name, fn) {
        var key = "__" + name;
        if (!this[key]) {
            this[key] = fn.call(this);
        }
        return this[key];
    }
};

module.exports = TableFactory;
Loading