Inheritance and Dependency Injection in CleverStack’s node-seed

posted in: API, Cleverstack, NodeJS | 0

So let me state the scenario for which there was a problem.

CleverStack comes with a backend and frontend component. The backend Node.js based and the frontend Angular.js based. This article is specifically aimed at the backend Node.js component.

So in the backend there are two default modules, CleverOrm and CleverAuth, that supply functionality for database operations and authentication related tasks respectively. We consider these modules to be ‘code that we shouldn’t change’ and ‘code that is maintained by someone else’ (e.g. the developer of CleverStack, a.k.a Richard Gustin).

The problem we faced was the following: We developed a module for user management that we can change, edit, manipulate and extend as we like, but the problem was that CleverAuth took control of Users seeing as it managed sessions and did authentication and added new users to the system. So now we have 2 modules, one doing the authentication and managing its own users and our custom user management module. Two different modules, two different data sets and they had nothing to do with one another. This is where the real problem came to pass. We built the custom user management module for a reason, because we wanted proper CRUD capabilities for users and we wanted to change what a user looked like quickly and easily, but we also wanted our users to be the users that would be authenticated by the system. This was currently not the case and we needed to find a solution.

The initial solution was easy, but not correct as per our philosophy regarding to changing code we are not responsible for. How this was done was by simply replacing Clever-Auth’s model that it refers to in it’s service from UserModel to IteUserModel, ensuring that you injected IteUser as a dependency into the backend and also injecting IteUserModel into the service. This worked like a charm, except that we changed code we were not suppose to…..

So back to the drawing board for the second attempt and Inheritance and Dependency came to mind. CleverStack makes dependency injection easy, so to do that was simple, but the Inheritance was a bit different. Thinking back to the basics of OOP the idea of inheritance was relatively simple, but what would it look like in this then very foreign and now still foreign ecosystem called Node.js, Express and and and….

So let’s keep the code out of it and talk in concepts and we end up with this result: a middle layer between CleverAuth and IteUser which we called IteAuth. It does the following:

  • It extends the UserController of CleverAuth
  • It mimics the UserService of CleverAuth
  • Its service points to IteUserModel
  • The passport that is used in UserController of CleverAuth is overwritten in IteAuthController in order to achieve full control over the authentication process.

 

node-seed Inheritance

Now lets look at the practical implementation of this diagram and what it looks like in the code.

First of all, lets look at the two controllers, UserController in CleverAuth and IteAuthController in IteAuth.

UserController:

var crypto = require( 'crypto' )
  , moment = require( 'moment' )
  , LocalStrategy = require( 'passport-local' ).Strategy;

module.exports = function ( Controller, passport, UserService ) {

    ....

    passport.use( new LocalStrategy( function ( username, password, done ) {
        var credentials = {
            email: username,
            password: crypto.createHash( 'sha1' ).update( password ).digest( 'hex' ),
            confirmed: true,
            active: true
        };

        UserService.authenticate( credentials )
            .then( done.bind( null, null ) )
            .catch( done );
    }));

    return Controller.extend(
        {
            ....

            service: UserService,

            ....
        },
        {
            getAction: function () {
                if ( !!this.req.params.id ) {
                    UserService.find( this.req.params.id )
                        .then( this.proxy( 'handleServiceMessage' ) )
                        .catch( this.proxy( 'handleException' ) );
                } else {
                    this.listAction();
                }
            }, //tested

            postAction: function () {
                var data = this.req.body;

                if ( data.id ) {
                    return this.putAction();
                }

                if ( !data.email ) {
                    this.send( 'Email is mandatory', 400 );
                    return;
                }

                var tplData = {
                    firstName: data.firstname,
                    userEmail: data.email,
                    tplTitle: 'User Confirmation',
                    subject: data.firstname || data.email + ' wants to add you to their recruiting team!'
                };

                UserService
                    .create( data, tplData )
                    .then( this.proxy( 'loginUserJson' ) )
                    .catch( this.proxy( 'handleException' ) );
            }, //tested without email confirmation

            putAction: function () {
                UserService
                    .update( this.req.params.id, this.req.body )
                    .then( this.proxy( 'handleSessionUpdate' ) )
                    .catch( this.proxy( 'handleException' ) );
            }, //tested

            deleteAction: function( req, res ) {
                UserService
                    .destroy( this.req.params.id, this.req.body )
                    .then( this.proxy( !!(this.req.params.id === this.req.user.id) ? 'logoutAction' : 'handleServiceMessage' ) )
                    .catch( this.proxy( 'handleException' ) );
            },

            ....

            currentAction: function () {
                var user = this.req.user
                  , reload = this.req.query.reload || false;

                if ( !user ) {
                    this.send( {}, 404 );
                    return;
                }

                if ( !reload ) {
                    this.send( user, 200 );
                    return;
                }

                UserService
                    .find( user.id )
                    .then( this.proxy( 'loginUserJson' ) )
                    .catch( this.proxy( 'handleException' ) );

            }, //tested

            ....

        } );
};

IteAuthController:

var crypto = require( 'crypto' )
    , moment = require( 'moment' )
    , LocalStrategy = require( 'passport-local' ).Strategy;

module.exports = function( UserController, passport, IteAuthService ) {

    //
    // This overrides the passport.use implementation used by default in clever-auth
    // (see 'modules/clever-auth/UserController) and
    // authenticates according to this new authentication configuration
    //
    passport.use( new LocalStrategy( function ( username, password, done ) {
        var credentials = {
            email: username,
            password: crypto.createHash( 'sha1' ).update( password ).digest( 'hex' ),
            confirmed: true,
            active: true
        };

        IteAuthService.authenticate( credentials )
            .then( done.bind( null, null ) )
            .catch( done );
    }));

    return UserController.extend(
    /** @Class **/
    {
        service: IteAuthService
    },
    /** @Prototype **/
    {
        getAction: function () {
            if ( !!this.req.params.id ) {
                IteAuthService.find( this.req.params.id )
                    .then( this.proxy( 'handleServiceMessage' ) )
                    .catch( this.proxy( 'handleException' ) );
            } else {
                this.listAction();
            }
        },

        postAction: function () {
            var data = this.req.body;

            if ( data.id ) {
                return this.putAction();
            }

            if ( !data.email ) {
                this.send( 'Email is mandatory', 400 );
                return;
            }

            var tplData = {
                firstName: data.firstname,
                userEmail: data.email,
                tplTitle: 'User Confirmation',
                subject: data.firstname || data.email + ' wants to add you to their recruiting team!'
            };

            IteAuthService
                .create( data, tplData )
                .then( this.proxy( 'loginUserJson' ) )
                .catch( this.proxy( 'handleException' ) );
        },

        putAction: function () {
            IteAuthService
                .update( this.req.params.id, this.req.body )
                .then( this.proxy( 'handleSessionUpdate' ) )
                .catch( this.proxy( 'handleException' ) );
        },

        deleteAction: function( req, res ) {
            IteAuthService
                .destroy( this.req.params.id, this.req.body )
                .then( this.proxy( !!(this.req.params.id === this.req.user.id) ? 'logoutAction' : 'handleServiceMessage' ) )
                .catch( this.proxy( 'handleException' ) );
        },

        currentAction: function () {
            var user = this.req.user
                , reload = this.req.query.reload || false;

            if ( !user ) {
                this.send( {}, 404 );
                return;
            }

            if ( !reload ) {
                this.send( user, 200 );
                return;
            }

            IteAuthService
                .find( user.id )
                .then( this.proxy( 'loginUserJson' ) )
                .catch( this.proxy( 'handleException' ) );

        }
    });
}

As you can see from the above to code snippets, IteAuthController extends UserController (see line 25) which now means that IteAuthController has all the functions from UserController in an instant. From here, we override methods in IteAuthController which are inherited from UserController that deal specifically with GET, POST, PUT, DELETE etc. These overridden functions know point to IteAuthService and we can edit these functions in any way we desire without tampering with UserController.

Another important piece of overridding that happens is in line 12 where the content of passport.use() gets changed and altered to suite our needs. In the code snippets, these two are currently still identical, but lets say you want IteAuth to authenticate a user based on their employee number, then you can simply assign the incoming username to employeeNumber instead of email (line 14).

Next, lets look at UserService and IteAuthService.

UserService:

var crypto      = require( 'crypto' )
  , Promise     = require( 'bluebird' )
  , moment      = require( 'moment' )
  , injector    = require( 'injector' )
  , config      = require( 'config' )
  , debug       = require( 'debug' )( 'cleverAuth' );

module.exports = function ( Service, UserModel ) {
    var EmailService = null;

    return Service.extend({
        model: UserModel,

        //tested
        authenticate: function ( credentials ) {
            return new Promise( function( resolve, reject ) {
                UserModel
                    .find( credentials )
                    .then( function( user ) {
                        if ( !!user && !!user.id ) {
                            if ( !!user.active ) {
                                user.accessedAt = Date.now();
                                user.save()
                                    .then( resolve )
                                    .catch( reject );
                            } else {
                                resolve( { statuscode: 403, message: "Login is not active for " + user.email + '.' }  );
                            }
                        } else {
                            resolve( { statuscode: 403, message: "User doesn't exist." }  );
                        }
                    })
                    .catch( reject );
            });
        },
        
        ....

        //tested
        create: function( data, tplData ) {
            var _super = this._super
              , that   = this;

            return new Promise( function( resolve, reject ) {
                UserModel
                    .find( { email: data.email } )
                    .then( function( user ) {
                        if ( user !== null ) {
                            return resolve( { statuscode: 400, message: 'Email already exist' } );
                        }

                        try {
                            EmailService = require( 'services' )[ 'EmailService' ];
                        } catch ( err ) {
                            console.error( err );
                        }

                        // Prepare the data
                        data.username = data.username || data.email;
                        data.active = true;
                        data.password = data.password
                            ? crypto.createHash( 'sha1' ).update( data.password ).digest( 'hex' )
                            : Math.random().toString( 36 ).slice( -14 );

                        if ( EmailService === null || !config[ 'clever-auth' ].email_confirmation ) {

                            data.confirmed = true;

                            _super.apply( that, [ data ] )
                                .then( resolve )
                                .catch( reject );

                        } else {

                            data.confirmed = false;

                            _super.apply( that, [ data ] )
                                .then( function( user ) {
                                    return service.generatePasswordResetHash( user, tplData );
                                })
                                .then( service.mailPasswordRecoveryToken )
                                .then( resolve )
                                .catch( reject );
                        }
                    })
                    .catch( reject );

            });
        },

        ....

        update: function ( userId, data ) {
            if ( data.new_password ) {
                data.password = crypto.createHash( 'sha1' ).update( data.new_password ).digest( 'hex' );
                delete data.new_password;
            }

            return this._super( userId, data );
        } //tested

    });
};

IteAuthService:

var crypto      = require( 'crypto' )
    , Promise     = require( 'bluebird' )
    , moment      = require( 'moment' )
    , injector    = require( 'injector' )
    , config      = require( 'config' )
    , debug       = require( 'debug' )( 'cleverAuth' );

module.exports = function ( Service, IteUserModel ) {

    return Service.extend({
        model: IteUserModel,

        authenticate: function ( credentials ) {
            return new Promise( function( resolve, reject ) {
                IteUserModel
                    .find( credentials )
                    .then( function( user ) {
                        if ( !!user && !!user.id ) {
                            if ( !!user.active ) {
                                user.accessedAt = Date.now();
                                user.save()
                                    .then( resolve )
                                    .catch( reject );
                            } else {
                                resolve( { statuscode: 403, message: "Login is not active for " + user.email + '.' }  );
                            }
                        } else {
                            resolve( { statuscode: 403, message: "User doesn't exist." }  );
                        }
                    })
                    .catch( reject );
            });
        },

        //tested
        create: function( data, tplData ) {
            var _super = this._super
                , that   = this;

            return new Promise( function( resolve, reject ) {
                IteUserModel
                    .find( { email: data.email } )
                    .then( function( user ) {
                        if ( user !== null ) {
                            return resolve( { statuscode: 400, message: 'Email already exist' } );
                        }

                        try {
                            EmailService = require( 'services' )[ 'EmailService' ];
                        } catch ( err ) {
                            console.error( err );
                        }

                        // Prepare the data
                        data.username = data.username || data.email;
                        data.active = true;
                        data.password = data.password
                            ? crypto.createHash( 'sha1' ).update( data.password ).digest( 'hex' )
                            : Math.random().toString( 36 ).slice( -14 );

                        if ( EmailService === null || !config[ 'clever-auth' ].email_confirmation ) {

                            data.confirmed = true;

                            _super.apply( that, [ data ] )
                                .then( resolve )
                                .catch( reject );

                        } else {

                            data.confirmed = false;

                            _super.apply( that, [ data ] )
                                .then( function( user ) {
                                    return service.generatePasswordResetHash( user, tplData );
                                })
                                .then( service.mailPasswordRecoveryToken )
                                .then( resolve )
                                .catch( reject );
                        }
                    })
                    .catch( reject );

            });
        }

    });
};

The main thing to notice here is that both the services extend from the base service and that the only the functions that point to UserModel are duplicated and made to point to IteUserModel in IteAuthService.

Leave a Reply