From 383b50f12be6931f329bd8d35a9da836b9cd06be Mon Sep 17 00:00:00 2001 From: kg Date: Tue, 20 Oct 2015 16:25:14 -0700 Subject: [PATCH] adds onFailure callback support and others includes: - adds support to pass an options object to done callback in an activity handler function. - adds onFailure callback support in the above options object. - adds a 401 unauthorized status code to all returned error objects. - updates README.md - refactors and adds unit tests (100% coverage!) --- README.md | 26 +++++++++++++++++++++++++- lib/candoo.js | 32 +++++++++++++++++++++++++++----- spec/candoo_spec.js | 42 +++++++++++++++++++++++++++++++++++------- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cf0e490..a4ceb9f 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,37 @@ can.configureActivities({ done(req.user !== undefined); }, - // you can pass a custom error message to the callback for a failure + /** + * You can pass a custom error message to the callback for a failure. + */ 'view.admin.page': function(req, done) { if (req.user && req.user.role === 'admin') { done(true); } else { done(false, 'admins only!'); } + }, + + /** + * You can pass an options object for further functionality. + * + * The following options are supported: + * { + * onFailure: function(req, res, next) {...} + * } + * + * Currently the only option that is recognized is an `onFailure` callback. + * This gives you more granular control when there is an unauthorized request. + * For example, one may have the need to redirect unauthorized requests to + * different endpoints, instead of relying on error handlers further down the + * line. + */ + 'view.stats': function(req, done) { + if (req.user && req.user.isOwner(someModelObject)) { + done(true); + } else { + done(false, '', { onFailure: helpers.redirectToLogin }); + } } }); diff --git a/lib/candoo.js b/lib/candoo.js index 7903ef8..6141b97 100644 --- a/lib/candoo.js +++ b/lib/candoo.js @@ -5,22 +5,34 @@ var util = require('util'); +/** + * Candoo constructor. + * + * Prepares all internal data structures and constant values. + */ var Candoo = function() { // create activity registry this._activityRegistry = {}; this._notRegisteredMessage = 'The activity (%s) is not registered'; + this._unauthorizedStatusCode = 401; }; -// configure user defined activity handlers /** + * Configure user defined activity handlers. * + * @param {Object} config */ Candoo.prototype.configureActivities = function(config) { this._activityRegistry = config; }; /** + * Produces an express/connect middleware function for authorization. + * + * This function runs the function associated with the given `activity` to + * authorize an action. * + * @param {String} activity The name of an activity registered with Candoo. */ Candoo.prototype.do = function(activity) { var that = this; @@ -31,18 +43,28 @@ Candoo.prototype.do = function(activity) { // check the registry for the given activity if (that._activityRegistry[activity]) { - // call handler and return result + // call handler function for the given `activity`. var handler = that._activityRegistry[activity]; - handler(req, function(isAuthorized, errorMessage) { + handler(req, function(isAuthorized, errorMessage, options) { if (isAuthorized) { next(); } else { - next(new Error(errorMessage)); + // if `onfailure` callback is passed, call that. Otherwise pass an + // error to next. + if (options && options.onFailure) { + options.onFailure(req, res, next); + } else { + // create error with given `errorMessage` that has the unauthorized + // http status code. + var error = new Error(errorMessage); + error.status = that._unauthorizedStatusCode; + next(error); + } } }); } else { - // activity is not registered, throw error with relevant message + // activity is not registered, throw error with relevant message. var message = util.format(that._notRegisteredMessage, activity); next(new Error(message)); } diff --git a/spec/candoo_spec.js b/spec/candoo_spec.js index d790b6e..77c3f0b 100644 --- a/spec/candoo_spec.js +++ b/spec/candoo_spec.js @@ -13,12 +13,15 @@ describe('candoo', function() { it('should contain all defined functions', function() { expect('_activityRegistry' in candoo).toEqual(true); + expect('_notRegisteredMessage' in candoo).toEqual(true); + expect('_unauthorizedStatusCode' in candoo).toEqual(true); expect('configureActivities' in candoo).toEqual(true); expect('do' in candoo).toEqual(true); }); it('should define an empty object for the registry', function() { var length = Object.keys(candoo._activityRegistry).length; + expect(candoo._activityRegistry instanceof Object).toBe(true); expect(length).toEqual(0); }); @@ -37,7 +40,8 @@ describe('candoo', function() { it('should add a single activity to the registry', function() { var activity = { - 'view.profile': function(req, done) {} + 'view.profile': null + // 'view.profile': function(req, done) {} }; candoo.configureActivities(activity); @@ -50,10 +54,10 @@ describe('candoo', function() { it('should add a multiple activities to the registry', function() { var activities = { - 'view.profile': function(req, done) {}, - 'view.admin.page': function(req, done) {}, - 'view.settings': function(req, done) {}, - 'view.stats': function(req, done) {} + 'view.profile': null, + 'view.admin.page': null, + 'view.settings': null, + 'view.stats': null }; candoo.configureActivities(activities); @@ -83,10 +87,10 @@ describe('candoo', function() { } }; - can.configureActivities(activity); + can._activityRegistry = activity; var middleware = can.do('view.profile'); - var req = { user: {}, params: {} }; + var req = { user: {} }; var res = {}; var spy = sinon.spy(); @@ -122,6 +126,7 @@ describe('candoo', function() { expect(spy.callCount).toEqual(1); expect(args[0]).not.toEqual(undefined); expect(args[0].message).toEqual(rejectMessage); + expect(args[0].status).toEqual(401); }); it('should call next with an error for unregistered activity', function() { @@ -131,6 +136,7 @@ describe('candoo', function() { var res = {}; var next = sinon.spy(); var expectedMessage = util.format(can._notRegisteredMessage, activityName); + var expectedStatus = 401; middleware(req, res, next); @@ -139,6 +145,28 @@ describe('candoo', function() { expect(args[0].message).toEqual(expectedMessage); }); + it('should fail and call the onFailure callback', function() { + var onFailureSpy = sinon.spy(); + var activity = { + 'view.profile': function(req, done) { + done(req.user !== undefined, null, { onFailure: onFailureSpy }); + } + }; + + can._activityRegistry = activity; + + var middleware = can.do('view.profile'); + var req = {}; + var res = {}; + var spy = sinon.spy(); + + middleware(req, res, spy); + + expect(onFailureSpy.callCount).toEqual(1); + expect(onFailureSpy.firstCall.args).toEqual([req, res, spy]); + expect(spy.callCount).toEqual(0); + }); + }); });