sails-police
Version:
Simple and flexible authentication workflows for sails
447 lines (395 loc) • 14.4 kB
JavaScript
;
var async = require('async');
var path = require('path');
var moment = require('moment');
//police utilities
var Utils = require(path.join(__dirname, '..', 'utils'));
//police morphs helpers
var Helpers = require(path.join(__dirname, 'helpers'));
/**
* @constructor
* @author lykmapipo
*
* @description Confirmable is responsible to verify if an account is
* already confirmed to sign in, and to send emails with
* confirmation instructions. Confirmation instructions are
* sent to the user email after creating an account and
* when manually requested by a new confirmation instruction request.
*
* See {@link http://www.rubydoc.info/github/plataformatec/devise/master/Devise/Models/Confirmable|Confirmable}
*
* @private
*/
function Confirmable() {
};
/**
* @function
* @author lykmapipo
*
* @description mix confirmable to a given sails model
*
* @param {Object} model a valid sails model definition
* See {@link http://sailsjs.org/#/documentation/concepts/ORM|Sails model}
*
* @return {Object} a sails model with confirmable applied to it.
*
* @public
*/
Confirmable.prototype.mixin = function(model) {
//mix in confirmable attributes
this._mixinAttributes(model);
//mix instance methods
this._mixinInstanceMethods(model);
//mix static methods
this._mixinStaticMethods(model);
//return model
return model;
};
/**
* @function
* @author lykmapipo
*
* @description extend valid sails model with confirmable attributes
*
* @param {Object} model a valid sails model definition
* See {@link http://sailsjs.org/#/documentation/concepts/ORM|Sails model}
*
* @return {Object} a sails model extended with confirmable
* required attributes
*/
Confirmable.prototype._mixinAttributes = function(model) {
//confirmable attributes
var attributes = {
confirmationToken: {
type: 'string',
defaultsTo: null,
index: true
},
confirmationTokenExpiryAt: {
type: 'datetime',
defaultsTo: null
},
confirmedAt: {
type: 'datetime',
defaultsTo: null
},
confirmationSentAt: {
type: 'datetime',
defaultsTo: null
}
};
//mixin confirmable attributes
Helpers.mixAttributes(model, attributes);
};
/**
* @function
* @author lykmapipo
*
* @description extend valid sails model with confirmable instance methods
*
* @param {Object} model a valid sails model definition
* See {@link http://sailsjs.org/#/documentation/concepts/ORM|Sails model}
*
* @return {Object} a sails model extended with confirmable
* required instance methods.
*/
Confirmable.prototype._mixinInstanceMethods = function(model) {
//bind generate confirmation token
//as model instance method
Helpers.mixAttributes(model, {
generateConfirmationToken: this._generateConfirmationToken
});
//bind send confirmation
//as model instance method
Helpers.mixAttributes(model, {
sendConfirmationEmail: this._sendConfirmationEmail
});
};
/**
* @function
* @author lykmapipo
*
* @description extend valid sails model with confirmable static methods
*
* @param {Object} model a valid sails model definition
* See {@link http://sailsjs.org/#/documentation/concepts/ORM|Sails model}
*
* @return {Object} a sails model extended with confirmable
* required static methods.
*/
Confirmable.prototype._mixinStaticMethods = function(model) {
//bind account confirmation
//as model static method
Helpers.mixStaticMethod(model, 'confirm', this._confirm);
};
/**
* @function
* @author lykmapipo
*
* @description generate confirmation token to be used to confirm account creation.
* This function must be called within sails model instamce context
*
* @param {generateConfirmationToken~callback} done callback that handles the response.
* @private
*/
Confirmable.prototype._generateConfirmationToken = function(done) {
//this context is of sails model instance
var confirmable = this;
//set confirmation expiration date
var expiryAt = moment().add(3, 'days');
var confirmationTokenExpiryAt = expiryAt.toDate();
//generate confirmation token based
//on confirmation token expiry at
var tokenizer =
Utils.tokenizer(confirmationTokenExpiryAt.getTime().toString());
var confirmationToken = tokenizer.encrypt(confirmable.email);
//set confirmationToken
confirmable.confirmationToken =
confirmationToken;
//set confirmation token expiry date
confirmable.confirmationTokenExpiryAt =
confirmationTokenExpiryAt;
//clear previous confirm details if any
confirmable.confirmedAt = null;
//return
done(null, confirmable);
};
//documentation for `done` callback of `generateConfirmationToken`
/**
* @description a callback to be called once generate confirmation token is done
* @callback generateConfirmationToken~callback
* @param {Object} error any error encountered during generating confirmation token
* @param {Object} confirmable a confirmable instance with `confirmationToken`,
* and `confirmationTokenExpiryAt` set-ed
*/
/**
* @function
* @author lykmapipo
*
* @description send email confirmation to allow account to be confirmed.
* This method must be called within model instance context
*
* @param {sendConfirmation~callback} done callback that handles the response.
* @private
*/
Confirmable.prototype._sendConfirmationEmail = function(next) {
//this refer to model instance context
var confirmable = this;
//if already confirmed back-off
var isConfirmed =
(confirmable.confirmedAt && confirmable.confirmedAt != null);
if (isConfirmed) {
next(null, confirmable);
} else {
//send confirmation email
confirmable
.sendEmail(
require('sails-police').EMAIL_REGISTRATION_CONFIRMATON,
confirmable,
function done() {
//update confirmation send time
confirmable.confirmationSentAt = new Date();
confirmable.save(next);
});
}
};
//documentation for `next` callback of `sendConfirmation`
/**
* @description a callback to be called once sending confirmation email is done
* @callback sendConfirmation~callback
* @param {Object} error any error encountered during sending confirmation email
* @param {Object} confirmable a confirmable instance with `confirmationSentAt`
* updated and persisted
*/
/**
* @function
* @author lykmapipo
*
* @description confirm account creation.
* This method must be called within model static context
*
* @param {String} confirmationToken a valid confirmation token send during
* `sendConfirmationEmail`
*
*
* @param {confirm~callback} done callback that handles the response.
* @private
*/
Confirmable.prototype._confirm = function confirm(confirmationToken, done) {
//this refer to model static context
var Confirmable = this;
//TODO
//sanitize confirmationToken
//refactoring
async
.waterfall(
[
function(next) {
//find confirmable using confirmation token
Confirmable
.findOneByConfirmationToken(confirmationToken)
.exec(function(error, confirmable) {
next(error, confirmable);
});
},
function(confirmable, next) {
//any confirmable found?
if (_.isUndefined(confirmable)) {
next(new Error('Invalid confirmation token'));
} else {
next(null, confirmable);
}
},
function(confirmable, next) {
//check if confirmation token expired
var isTokenExpiry =
(!Utils.isAfter(new Date(), confirmable.confirmationTokenExpiryAt));
if (isTokenExpiry) {
//if expired
next(new Error('Confirmation token expired'));
} else {
//otherwise continue with token verification
next(null, confirmable);
}
},
function(confirmable, next) {
//verify confirmation token
var value =
confirmable.confirmationTokenExpiryAt.getTime().toString();
var tokenizer =
Utils.tokenizer(value);
if (!tokenizer.match(confirmationToken, confirmable.email)) {
next(new Error('Invalid confirmation token'));
} else {
//is valid token
next(null, confirmable);
}
},
function(confirmable, next) {
//update confirmation details
confirmable.confirmedAt = new Date();
confirmable.save(next);
}
],
function(error, confirmable) {
if (error) {
done(error);
} else {
done(null, confirmable);
}
});
};
//documentation for `done` callback of `confirm`
/**
* @description a callback to be called once confirmation is done
* @callback confirm~callback
* @param {Object} error any error encountered during account confirmation
* @param {Object} confirmable a confirmable instance with `confirmedAt`
* updated and persisted
*/
/**
* @function
* @author lykmapipo
*
* @description Compose confirmable confirmation
* by generate and send confirmation email details
* @param {Object} confirmable valid sails model morphed with sails-police
* @param {composeConfirmation~callback} done callback that handles the response.
* @public
*/
Confirmable.prototype.composeConfirmation = function(confirmable, done) {
//this context is of Confirmable
async
.waterfall(
[
function generateConfirmationToken(next) {
confirmable
.generateConfirmationToken(next);
},
function sendConfirmationEmail(confirmable, next) {
confirmable
.sendConfirmation(next);
}
],
function(error, confirmable) {
done(error, confirmable);
});
};
//documentation for `done` callback of `composeConfirmation`
/**
* @description a callback to be called once compose confirmation is done
* @callback composeConfirmation~callback
* @param {Object} error any error encountered during the process of composing
* confirmation
* @param {Object} confirmable a confirmable instance with `confirmationToken`,
* `confirmationTokenExpiryAt` and `confirmationSentAt`
* updated and persisted
*/
/**
* @function
* @author lykmapipo
*
* @description Check if confirmable is confirmed by using the below flow:
* 1. If is confirmed continue.
* 2. If not confirmed and confirmation token not expired throw `Account not confirmed`
* 3. If not confirmed and confirmation token expired
* `composeConfirmation` and throw
* `Confirmation token expired. Check your email for confirmation`
*
* @param {Object} confirmable valid sails model morphed with sails-police
* @callback checkConfirmation~callback done callback that handle response
* @public
*/
Confirmable.prototype.checkConfirmation = function(confirmable, done) {
//this context is of Confirmable
var self = this;
//check if already confirmed
var isConfirmed =
(confirmable.confirmedAt && confirmable.confirmedAt !== null);
//check if confirmation token expired
var isTokenExpired =
(!Utils.isAfter(new Date(), confirmable.confirmationTokenExpiryAt));
//is already confirmed
if (!isConfirmed && !isTokenExpired) {
done(new Error('Account not confirmed'));
}
//is not confirmed and
//confirmation token is expired
else if (!isConfirmed && isTokenExpired) {
//compose confirmation
self
.composeConfirmation(confirmable, function(error, confirmable) {
//is there any error during
//compose new confirmation?
if (error) {
done(error);
}
//new confirmation token is
// and send successfully
else {
done(new Error('Confirmation token expired. Check your email for confirmation.'));
}
});
}
//is confirmed
else {
done(null, confirmable);
}
};
//documentation for `done` callback of `checkConfirmation`
/**
* @description a callback to be called once check confirmation is done
* @callback checkConfirmation~callback
* @param {Object} error any error encountered during the process of checking
* confirmation
* @param {Object} confirmable a confirmable instance with `confirmationToken`,
* `confirmationTokenExpiryAt` and `confirmationSentAt`
* updated and persisted if confirmable was not confirmed
* and confirmation token was expired. Otherwise untouched
* confirmable instane.
*/
/**
* @description export singleton
* @type {Object}
*/
exports = module.exports = new Confirmable();