hunt
Version:
High level nodejs framework build on top of expressjs, mongoose, sequilize, socketio and passportjs
715 lines (664 loc) • 27.1 kB
JavaScript
;
var
winston = require('winston'),
storeReferer = require('./../passportStrategies/storeRefererHelper.js');
/**
* @class passport
* @extends config
* @classdesc
* Object that hold the passport.js configuration parameters
*
* @property {boolean} local - enable local strategy
* @property {boolean} signUpByEmail - user can signup by making POST /auth/signup with email and password, default is true, requires local strategy
* @property {boolean} verifyEmail - user have to follow link in email address to verify his account, default is true
* @property {string} verifyEmailTemplate - path to template for verifying email template
* @property {boolean} resetPassword - allow user to reset password for account, default is true
* @property {string} resetPasswordEmailTemplate - template for email used for reseting password
* @property {string} resetPasswordPageTemplate - template for page used for reseting password
*
* @property {boolean} steam - authorization by Steam openId - default is false - disabled.
*
* @property {number} sessionExpireAfterSeconds - time to live of cookie based http sessions, default is 180 seconds.
*
* @property {string} GOOGLE_CLIENT_KEY - used to authorize users by {@link https://developers.google.com/accounts/docs/OAuth2 | Google OAuth 2.0 strategy}, obtainable in {@link https://console.developers.google.com/ Google Developers' console }
* @property {string} GOOGLE_CLIENT_SECRET - used to authorize users by {@link https://developers.google.com/accounts/docs/OAuth2 | Google OAuth 2.0 strategy}, obtainable in {@link https://console.developers.google.com/ Google Developers' console }
* @property {Array.<string>} GOOGLE_SCOPES - OAuth 2.0 scopes required by application - see {@link https://developers.google.com/+/api/oauth#login-scopes}. Default value is `['https://www.googleapis.com/auth/userinfo.profile','https://www.googleapis.com/auth/userinfo.email']`
*
* @property {string} GITHUB_CLIENT_ID - used for github oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://github.com/settings/applications}
* @property {string} GITHUB_CLIENT_SECRET - used for github oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://github.com/settings/applications}
*
* @property {string} TWITTER_CONSUMER_KEY - used for twitter oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://dev.twitter.com/}
* @property {string} TWITTER_CONSUMER_SECRET - used for twitter oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://dev.twitter.com/}
*
* @property {string} VK_APP_ID - used for vk.com oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://vk.com/dev}
* @property {string} VK_APP_SECRET - used for vk.com oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://vk.com/dev}
* @property {Array.<string>} VK_SCOPE - array of scopes, that are required from user to share with application - used in vk.com authorization strategy
*
* @property {string} FACEBOOK_CLIENT_ID - used for facebook.com oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://developers.facebook.com/}
* @property {string} FACEBOOK_CLIENT_SECRET - used for facebook.com oauth, is populated automatically from enviroment value of the same name, obtainable here {@link https://developers.facebook.com/}
*/
module.exports = exports = function (core, router) {
var
strategies = [],
routes = [];
//Storing user in session, storage key is huntKey
core.passport.serializeUser(function (user, done) {
done(null, user.huntKey);
});
//Retrieving user from session
core.passport.deserializeUser(function (huntKey, done) {
core.model.User.findOneByHuntKey(huntKey, done);
});
//openid strategies
[
'steam'
// 'yahoo', 'intuit', 'paypal' - doesn't works with passportjs 0.3.x!
]
.map(function (strategyName) {
if (core.config.passport[strategyName] === true) {
strategies.push(require('./../passportStrategies/' + strategyName + '.openid.js'));
winston.silly('PassportManager: %s OpenID authorization enabled!', strategyName.toUpperCase());
}
});
//oauth strategies
if (core.config.passport.GOOGLE_CLIENT_ID && core.config.passport.GOOGLE_CLIENT_SECRET) {
strategies.push(require('./../passportStrategies/google.oauth2.js'));
winston.silly('PassportManager: Google OAuth 2.0 authorization enabled!');
}
if (core.config.passport.GITHUB_CLIENT_ID && core.config.passport.GITHUB_CLIENT_SECRET) {
strategies.push(require('./../passportStrategies/github.js'));
winston.silly('PassportManager: Github OAuth 2.0 authorization enabled!');
}
if (core.config.passport.TWITTER_CONSUMER_KEY && core.config.passport.TWITTER_CONSUMER_SECRET) {
strategies.push(require('./../passportStrategies/twitter.js'));
winston.silly('PassportManager: Twitter OAuth 2.0 authorization enabled!');
}
if (core.config.passport.VK_APP_ID && core.config.passport.VK_APP_SECRET) {
strategies.push(require('./../passportStrategies/vkcom.js'));
winston.silly('PassportManager: VK OAuth 2.0 authorization enabled!');
}
if (core.config.passport.FACEBOOK_CLIENT_ID && core.config.passport.FACEBOOK_CLIENT_SECRET) {
strategies.push(require('./../passportStrategies/facebook.js'));
winston.silly('PassportManager: Facebook OAuth 2.0 authorization enabled!');
}
//loading local strategy
if (core.config.passport.local === true) {
var
maxAge = core.config.passport.maxAge || 7 * 24 * 60 * 60 * 1000; //1 week
router.post('/login', storeReferer, function (request, response) {
var message = 'Invalid username, email or password!';
if (request.user) {
if (request.is('json')) {
core
.errorResponses
.doError(response, 400, 'You are already authorized!');
} else {
request.flash('error', 'You are already authorized!');
response.redirect('/auth/failure?strategy=local');
}
} else {
core.async.waterfall([
function (cb) {
request.model.User.signIn(request.body.username, request.body.password, cb);
},
function (userFound, cb) {
if (userFound) {
request.logIn(userFound, cb);
} else {
cb(new Error('UNABLE_TO_SIGNIN'));
}
}
], function (error) {
if (error) {
if (error.message === 'UNABLE_TO_SIGNIN') {
if (request.is('json')) {
core.errorResponses.doError(response, 403, message);
} else {
request.flash('error', message);
response.redirect('/auth/failure?strategy=local');
}
} else {
throw error;
}
} else {
if (!!request.body.remember) {
response.cookie('remember', request.user.huntKey, {'maxAge': maxAge, 'httpOnly': true, 'signed': true});
}
/**
* Event is emitted each time user signs in
*
* @event Hunt#user:signin:*
* @type {object}
* @property {string} type - type of event `auth`
* @property {object} info additional information
* @property {User} user - user, related to this event
* @tutorial authorization
* @see User
*
* @example
*
* Hunt.on('user:signin:*', function(payload){
* console.log('We catch '+this.event.join(':') + 'event, which means '+ payload.user.displayName + ' signed in!');
* });
*
*/
/**
* Event is emitted each time user signs in
*
* @event Hunt#user:signin:local
* @type {object}
* @property {string} type - type of event `auth`
* @property {object} info additional information
* @property {User} user - user, related to this event
* @tutorial authorization
* @see User
*
* @example
*
* Hunt.on('user:signin:local', function(payload){
* console.log('We catch '+this.event.join(':') + 'event, which means '+ payload.user.displayName + ' authorized by local strategy!');
* });
*
*/
core.emit(['user', 'signin', 'local'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': request.user
});
winston.info('user:signin:local', {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': request.user.id,
'userEmail': request.user.email,
'userName': request.user.username,
'user': request.user.toString()
});
if (request.is('json')) {
response
.status(200)
.json({
'status': 'Ok',
'code': 200,
'message': 'Welcome!',
'userId': request.user.id,
'huntKey': request.user.huntKey
});
} else {
response.redirect('/auth/success?strategy=local');
}
}
});
}
});
winston.silly('Local strategy authorization enabled!');
if (core.config.passport.signUpByEmail === true) {
//routes for signUp
router.post('/isBusy', function (request, response) {
request.model.User.findOneByEmailOrUsername(request.body.email, function (err, userFound) {
response.jsonp({
'isBusy': userFound ? true : false
});
});
});
router.post('/signup', storeReferer, function (request, response) {
var
username = request.body.username,
email = request.body.email,
password = request.body.password,
userAlreadySignedUp,
errors = [];
core.async.series(
[
function (cb) {
if (!request.body.email) {
errors.push('Email is required!');
}
if (!request.body.password) {
errors.push('Password is required!');
}
if (errors.length > 0) {
cb(new Error('MISSING_PARAMETERS'));
} else {
cb(null);
}
},
function (cb) {
if (core.validator.isEmail(request.body.email)) {
cb(null);
} else {
errors.push('Email is malformed!');
cb(new Error('MALFORMED_EMAIL'));
}
},
function (cb) {
request.model
.User.findOneByKeychain('email', request.body.email, function (error, userFound) {
if (error) {
cb(error);
} else {
if (userFound) {
if (userFound.password(password)) {
userAlreadySignedUp = userFound;
cb(null);
} else {
errors.push('Email is in use!');
cb(new Error('EMAIL_IN_USE'));
}
} else {
cb(null);
}
}
});
},
function (cb) {
if (userAlreadySignedUp) {
return cb(null);
}
if (username) {
request.model
.User.findOneByKeychain('username', username, function (error, userFound) {
if (error) {
cb(error);
} else {
if (userFound) {
if (userFound.password(password)) {
userAlreadySignedUp = userFound;
cb(null);
} else {
errors.push('Username is in use!');
cb(new Error('USERNAME_IN_USE'));
}
} else {
cb(null);
}
}
});
} else {
cb(null);
}
},
function (cb) {
if (userAlreadySignedUp) {
return request.login(userAlreadySignedUp, cb);
}
core.model.User.signUp(username, email, password, function (error, userSaved) {
if (error) {
cb(error);
} else {
request.login(userSaved, cb);
}
});
},
function (cb) {
if (request.user) {
if (userAlreadySignedUp) {
request.flash('success', 'Welcome to our site, ' + request.user.toString() + '!');
} else {
if (core.config.passport.verifyEmail) {
request.flash('success', 'Thanks for signing up! Please, verify your email by following link in it!');
request.user.notifyByEmail({
'subject': 'Email address verification',
'template': core.config.passport.verifyEmailTemplate,
'verifyUrl': core.config.hostUrl + 'auth/confirm/' + request.user.keychain.welcomeLink
});
} else {
request.flash('success', 'Thanks for signing up!');
}
}
core.emit(['user', 'signup', 'local'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': request.user
});
winston.info('user:signup:local:%s', request.user.id, {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': request.user.id,
'userEmail': request.user.email,
'userName': request.user.username,
'user': request.user.toString()
});
cb();
} else {
cb(new Error('Unable to sign up user!'));
}
}
],
function (error) {
if (error) {
switch (error.message) {
case 'MISSING_PARAMETERS':
break;
case 'MALFORMED_EMAIL':
break;
case 'EMAIL_IN_USE':
break;
case 'USERNAME_IN_USE':
break;
default:
throw error;
}
}
if (request.is('json')) {
if (errors.length > 0) {
response.status(400).json({
'code': 400,
'status': 'Error',
'message': 'Unable to create account!',
'validationErrors': errors
});
} else {
response
.status(201)
.json({
'status': 'Ok',
'code': 201,
'message': 'Account created!',
'userId': request.user.id,
'huntKey': request.user.huntKey
});
}
} else {
errors.map(function (e) {
request.flash('error', 'Error creating account: ' + e);
});
response.redirect('back');
}
}
);
});
if (core.config.passport.verifyEmail === true) {
winston.silly('Email verification enabled!');
router.get('/confirm/:hash', function (request, response) {
var welcomeLink = request.params.hash;
if (request.user) {
if (request.user.keychain && request.user.keychain.welcomeLink === welcomeLink) {
delete request.user.keychain.welcomeLink;
request.user.markModified('keychain');
request.user.accountVerified = true;
request.user.save(function (error) {
if (error) {
throw error;
}
core.emit(['user', 'confirm'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': request.user
});
winston.info('user:confirm:%s', request.user.id, {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': request.user.id,
'userEmail': request.user.email,
'userName': request.user.username,
'user': request.user.toString()
});
response.redirect('/auth/success?strategy=confirm');
});
} else {
response.redirect('/auth/failure?strategy=confirm');
}
} else {
request.model.User.findOneByWelcomeLinkAndVerifyEmail(welcomeLink, function (error, userFound) {
if (error) {
throw error;
}
if (userFound) {
request.login(userFound, function (error) {
if (error) {
throw error;
}
core.emit(['user', 'confirm'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': request.user
});
winston.info('user:confirm:%s', request.user.id, {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': request.user.id,
'userEmail': request.user.email,
'userName': request.user.username,
'user': request.user.toString()
});
response.redirect('/auth/success?strategy=confirm');
});
} else {
response.redirect('/auth/failure?strategy=confirm');
}
});
}
});
}
}
if (core.config.passport.resetPassword) {
//route to ask for password reset email
winston.silly('PassportManager: reset password enabled!');
router.post('/restoreAccount', function (request, response) {
var message = 'Check your email address, we have send you instructions for restoring account';
if (request.body.email && core.validator.isEmail(request.body.email)) {
request.model.User.findOneByEmailOrUsername(request.body.email, function (err, userFound) {
if (err) {
throw err;
}
if (userFound) {
userFound.keychain = userFound.keychain || {};
userFound.keychain.welcomeLink = core.rack();
userFound.invalidateSession(function (err) {
if (err) {
throw err;
}
userFound.notifyByEmail({
'subject': 'Reset password',
'template': core.config.passport.resetPasswordEmailTemplate,
'resetUrl': core.config.hostUrl + 'auth/reset/' + userFound.keychain.welcomeLink
});
});
core.emit(['user', 'resetpassword', 'require'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': userFound
});
winston.info('user:resetpassword:require:%s', userFound.id, {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': userFound.id,
'userEmail': userFound.email,
'userName': userFound.username,
'user': userFound.toString()
});
}
if (request.is('json')) {
response
.status(201)
.json({
'status': 'Created',
'code': 201,
'message': message
});
} else {
request.flash('success', message); //haha)
response.redirect('back');
}
});
} else {
if (request.is('json')) {
response
.status(400)
.json({
'status': 'Bad request',
'code': 400,
'message': 'Malformed email address'
});
} else {
request.flash('error', 'Malformed email address');
response.redirect('back');
}
}
});
//route to render page with form to reset password
router.get('/reset/:resetLink', function (request, response, next) {
if (request.user) {
response.redirect('/');
} else {
request.model.User.findOneByKeychain('welcomeLink', request.params.resetLink, function (err, userFound) {
if (err) {
throw err;
}
if (userFound) {
core.emit(['user', 'resetpassword', 'render'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': userFound
});
winston.info('user:resetpassword:render:%s', userFound.id, {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': userFound.id,
'userEmail': userFound.email,
'userName': userFound.username,
'user': userFound.toString()
});
response.render('' + core.config.passport.resetPasswordPageTemplate, {
'title': 'Reset your password',
'description': 'Welcome back!',
'welcomeLink': userFound.keychain.welcomeLink
});
} else {
next();
}
});
}
});
//route to reset password
router.post('/resetPassword', function (request, response) {
var message = 'Thanks! Your password is reset!';
if (request.body.reset && request.body.password) {
request.model.User.findOneByKeychain('welcomeLink', request.body.reset, function (err, userFound) {
if (err) {
throw err;
}
if (userFound) {
core.async.series([
function (cb) {
userFound.password = request.body.password;
userFound.save(cb);
},
function (cb) {
request.logIn(userFound, cb);
}
], function (error) {
if (error) {
throw error;
}
if (request.is('json')) {
core.emit(['user', 'resetpassword', 'perform'], {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'user': request.user
});
winston.info('user:resetpassword:perform:%s', userFound.id, {
'ip': request.ip,
'ips': request.ips,
'userAgent': request.headers['user-agent'],
'userId': request.user.id,
'userEmail': request.user.email,
'userName': request.user.username,
'user': request.user.toString()
});
} else {
request.flash('success', message);
response.redirect('back');
}
});
} else {
request.flash('success', message); // ;-)
response.redirect('back');
}
});
} else {
if (request.is('json')) {
response
.status(400)
.json({
'status': 'Bad request',
'code': 400,
'message': 'Body parameters of `welcomeLink` or `password` are missing!'
});
} else {
request.flash('error', 'Parameters of `welcomeLink` or `password` are missing!');
response.redirect('back');
}
}
});
}
}
//adding some always working routes for authorization
//they can be overloaded in application by Hunt.extendRoutes(function(core){});
router.get('/success', function (req, res) {
if (req.user) {
req.flash('success', 'Welcome to our site, ' + req.user.toString() + '!');
}
var ref = req.session.ref;
if (ref) {
delete req.session.ref;
res.redirect(ref);
} else {
res.redirect('/');
}
});
router.get('/failure', function (req, res) {
if (!req.user) {
req.flash('error', 'Authorization failed!');
}
var ref = req.session.ref;
if (ref) {
delete req.session.ref;
res.redirect(ref);
} else {
res.redirect('/');
}
});
router.all('/logout', function (req, res) {
if (req.user) {
var farewell = 'Goodbye, ' + req.user.toString() + '!';
req.flash('success', farewell);
req.logout();
if (req.is('json')) {
res
.status(200)
.json({'code': 200, 'status': 'Ok', 'message': farewell});
} else {
res.redirect('back');
}
} else {
if (req.is('json')) {
core.errorResponses.error401(res);
} else {
res.redirect('back');
}
}
});
//initializing strategies defined by Hunt.extendStrategy
strategies.map(function (strategy) {
core.passport.use(strategy.strategy(core));
routes.push(strategy.routes);
});
//adding routes specific to stategies
routes.map(function (r) {
r(core, router);
});
};