controllers/user.js
var express = require('express');
var router = express.Router();
var FB = require('facebook-node');
var Twitter = require('twitter');
var async = require('async');
var Models = require('telepat-models');
var security = require('./security');
var jwt = require('jsonwebtoken');
var microtime = require('microtime-nodejs');
var crypto = require('crypto');
var guid = require('uuid');
var mandrill = require('mandrill-api');
var sendgridHelper = require('sendgrid').mail;
var unless = function(paths, middleware) {
return function(req, res, next) {
var excluded = false;
for (var i=0; i<paths.length; i++) {
if (paths[i] === req.path) {
excluded = true;
}
}
if (excluded) {
return next();
} else {
return middleware(req, res, next);
}
};
};
var isMobileBrowser = function(userAgent) {
return userAgent.match(/(iPad|iPhone|iPod|Android|Windows Phone)/g) ? true : false;
};
router.use(unless(['/refresh_token', '/confirm', '/request_password_reset', '/metadata', '/update_metadata', '/reset_password_intermediate'], security.deviceIdValidation));
router.use(unless(['/refresh_token', '/confirm', '/metadata', '/update_metadata', '/reset_password_intermediate'], security.applicationIdValidation));
router.use(unless(['/refresh_token', '/confirm', '/metadata', '/update_metadata', '/reset_password_intermediate'], security.apiKeyValidation));
router.use(['/logout', '/me', '/update', '/update_immediate', '/delete', '/metadata', '/update_metadata'],
security.tokenValidation);
/**
* @api {post} /user/login-password Password login
* @apiDescription Logs in the user with a password
* @apiName UserLoginPassword
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
* @apiHeader {String} X-BLGREQ-UDID Custom header containing the device ID (obtained from device/register)
*
* @apiParam {String} password The password
* @apiParam {String} username Username
*
* @apiExample {json} Client Request
* {
* "username": "user@example.com",
* "password": "magic-password1337"
* }
*
* @apiSuccessExample {json} Success Response
* {
* "content": {
* "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImdhYmlAYXBwc2NlbmQuY29tIiwiaXNBZG1pbiI6dHJ1ZSwi
* aWF0IjoxNDMyOTA2ODQwLCJleHAiOjE0MzI5MTA0NDB9.knhPevsK4cWewnx0LpSLrMg3Tk_OpchKu6it7FK9C2Q"
* "user": {
* "id": 31,
* "type": "user",
* "username": "abcd@appscend.com",
* "devices": [
* "466fa519-acb4-424b-8736-fc6f35d6b6cc"
* ],
* "password": "acb8a9cbb479b6079f59eabbb50780087859aba2e8c0c397097007444bba07c0"
* }
* }
* }
*
* @apiError 401 [031]UserBadLogin User email and password did not match
*
*/
router.post(['/login-password', '/login_password'], function(req, res, next) {
if (!req.body.username)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['username']));
if (!req.body.password)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['password']));
var userProfile = null;
var username = req.body.username;
var password = req.body.password.toString();
var deviceId = req._telepat.device_id;
var appId = req._telepat.applicationId;
var requiresConfirmation = Models.Application.loadedAppModels[appId].email_confirmation;
var hashedPassword = null;
async.series([
function(callback) {
//try and get user profile from DB
Models.User({username: username}, appId, function(err, result) {
if (err && err.status == 404) {
callback(new Models.TelepatError(Models.TelepatError.errors.UserNotFound));
}
else if (err)
callback(err);
else {
if (!requiresConfirmation || result.confirmed) {
userProfile = result;
callback();
} else {
return callback(new Models.TelepatError(Models.TelepatError.errors.UnconfirmedAccount));
}
}
});
},
function(callback) {
var patches = [];
patches.push(Models.Delta.formPatch(userProfile, 'append', {devices: deviceId}));
if (userProfile.devices) {
var idx = userProfile.devices.indexOf(deviceId);
if (idx === -1) {
Models.User.update(patches, callback);
} else
callback();
} else {
Models.User.update(patches, callback);
}
},
function(callback) {
security.encryptPassword(req.body.password, function(err, hash) {
if (err)
return callback(err);
hashedPassword = hash;
callback();
});
}
], function(err) {
if (err)
return next(err);
if (hashedPassword != userProfile.password) {
return next(new Models.TelepatError(Models.TelepatError.errors.UserBadLogin));
}
delete userProfile.password;
var token = security.createToken({username: username, id: userProfile.id});
res.status(200).json({status: 200, content: {user: userProfile, token: token }});
});
});
/**
* @api {post} /user/login-{s} Login
* @apiDescription Log in the user through Facebook or Twitter.
* @apiName UserLogin
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
* @apiHeader {String} X-BLGREQ-UDID Custom header containing the device ID (obtained from device/register)
*
* @apiParam {String} s GET param for the login provider
* @apiParam {String} access_token Facebook access token.
*
* @apiExample {json} Facebook login
* {
* "access_token": "fb access token"
* }
*
* @apiExample {json} Twitter login
* {
* "oauth_token": "oauth token",
* "oauth_token_secret": "oauth token secret"
* }
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": {
* "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImdhYmlAYXBwc2NlbmQuY29tIiwiaXNBZG1pbiI6dHJ1ZSwi
* aWF0IjoxNDMyOTA2ODQwLCJleHAiOjE0MzI5MTA0NDB9.knhPevsK4cWewnx0LpSLrMg3Tk_OpchKu6it7FK9C2Q"
* "user": {
* "id": 31,
* "type": "user",
* "username": "abcd@appscend.com",
* "devices": [
* "466fa519-acb4-424b-8736-fc6f35d6b6cc"
* ],
* "password": "acb8a9cbb479b6079f59eabbb50780087859aba2e8c0c397097007444bba07c0"
* }
* }
* }
*
* @apiError 400 [028]InsufficientFacebookPermissions User email is not publicly available
* (insufficient Facebook permissions)
* @apiError 404 [023]UserNotFound User not found
*
*/
router.post('/login-:s', function(req, res, next) {
if (Object.getOwnPropertyNames(req.body).length === 0)
return next(new Models.TelepatError(Models.TelepatError.errors.RequestBodyEmpty));
var loginProvider = req.params.s;
if (loginProvider == 'facebook') {
if (!req.body.access_token)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['access_token']));
if (!app.telepatConfig.config.login_providers || !app.telepatConfig.config.login_providers.facebook)
return next(new Models.TelepatError(Models.TelepatError.errors.ServerNotConfigured,
['facebook login provider']));
else
FB.options(app.telepatConfig.config.login_providers.facebook);
} else if (loginProvider == 'twitter') {
if (!req.body.oauth_token)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['oauth_token']));
if (!req.body.oauth_token_secret)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['oauth_token_secret']));
if (!app.telepatConfig.config.login_providers || !app.telepatConfig.config.login_providers.twitter)
return next(new Models.TelepatError(Models.TelepatError.errors.ServerNotConfigured,
['twitter login provider']));
} else {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidLoginProvider, ['facebook, twitter']));
}
var accessToken = req.body.access_token;
var username = null;
var userProfile = null;
var socialProfile = null;
var deviceId = req._telepat.device_id;
var appId = req._telepat.applicationId;
async.series([
//Retrieve facebook information
function(callback) {
if (loginProvider == 'facebook') {
FB.napi('/me?fields=name,email,id,gender,picture', {access_token: accessToken}, function(err, result) {
if (err) return callback(err);
username = result.email || result.id;
socialProfile = result;
callback();
});
} else if (loginProvider == 'twitter') {
var options = {
access_token_key: req.body.oauth_token,
access_token_secret: req.body.oauth_token_secret
};
options.consumer_key = app.telepatConfig.config.login_providers.twitter.consumer_key;
options.consumer_secret = app.telepatConfig.config.login_providers.twitter.consumer_secret;
var twitterClient = new Twitter(options);
twitterClient.get('account/settings', {}, function(err, result) {
if (err)
return callback(err);
twitterClient.get('users/show', {screen_name: result.screen_name}, function(err1, result1) {
if (err1)
return callback(err1);
username = result.screen_name;
socialProfile = result1;
callback();
});
});
}
},
function(callback) {
//try and get user profile from DB
if (req.body.username && loginProvider == 'facebook') {
async.series([
function(callback1) {
Models.User({username: username}, appId, function(err, result) {
if (err && err.status == 404) {
callback1();
}
else if (err)
callback1(err);
else if (!result.fid) {
callback1();
} else {
callback1(new Models.TelepatError(Models.TelepatError.errors.UserAlreadyExists));
}
});
},
function(callback1) {
Models.User({username: req.body.username}, appId, function(err, result) {
if (!err) {
var patches = [];
patches.push(Models.Delta.formPatch(result, 'replace', {username: username}));
patches.push(Models.Delta.formPatch(result, 'replace', {picture: socialProfile.picture.data.url}));
patches.push(Models.Delta.formPatch(result, 'replace', {fid: socialProfile.id}));
patches.push(Models.Delta.formPatch(result, 'replace', {name: socialProfile.name}));
Models.User.update(patches, function(err, modifiedUser) {
if (err) return callback1(err);
userProfile = modifiedUser;
callback1();
});
} else if (err && err.status != 404)
callback1(err);
else {
callback1(new Models.TelepatError(Models.TelepatError.errors.UserNotFound));
}
});
}
], callback);
} else {
Models.User({username: username}, appId, function(err, result) {
if (err && err.status == 404) {
callback(new Models.TelepatError(Models.TelepatError.errors.UserNotFound));
}
else if (err)
callback(err);
else {
userProfile = result;
callback();
}
});
}
},
//update user with deviceID if it already exists
function(callback) {
//if linking account with fb, user updating again is not necessary
if (req.body.username && loginProvider == 'facebook')
return callback();
if (userProfile.devices) {
var idx = userProfile.devices.indexOf(deviceId);
if (idx === -1)
userProfile.devices.push(deviceId);
} else {
userProfile.devices = [deviceId];
}
var patches = [];
patches.push(Models.Delta.formPatch(userProfile, 'replace', {devices: userProfile.devices}));
if (loginProvider == 'facebook') {
if (userProfile.name != socialProfile.name)
patches.push(Models.Delta.formPatch(userProfile, 'replace', {name: socialProfile.name}));
if (userProfile.gender != socialProfile.gender)
patches.push(Models.Delta.formPatch(userProfile, 'replace', {gender: socialProfile.gender}));
if (userProfile.picture != socialProfile.picture.data.url)
patches.push(Models.Delta.formPatch(userProfile, 'replace', {picture: socialProfile.picture.data.url}));
} else if (loginProvider == 'twitter') {
if (userProfile.name != socialProfile.name)
patches.push(Models.Delta.formPatch(userProfile, 'replace', {name: socialProfile.name}));
if (userProfile.picture != socialProfile.profile_image_url_https)
patches.push(Models.Delta.formPatch(userProfile, 'replace', {picture: socialProfile.picture}));
}
Models.User.update(patches, callback);
}
//final step: send authentification token
], function(err) {
if(err && err.code == '023') {
return next(err);
}
if(loginProvider == 'facebook' && err && err.response && err.response.error.code == 190) {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidAuthorization, ['Facebook access token has expired']));
}
if (err && err[0] && err[0].code == 89) {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidAuthorization, ['Twitter access token has expired']));
}
if (err)
return next(err);
else {
var token = security.createToken({username: username, id: userProfile.id});
delete userProfile.password;
res.json({status: 200, content: {token: token, user: userProfile}});
}
});
});
/**
* @api {post} /user/register-{s} Register
* @apiDescription Registers a new user using a Facebook token or directly with an email and password. User is not created
* immediately.
* @apiName UserRegister
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
* @apiHeader {String} X-BLGREQ-UDID Custom header containing the device ID (obtained from device/register)
*
* @apiParam {String} s GET param for the login provider (can be "username" for registering without a 3rd party)
* @apiParam {String} access_token Facebook access token.
*
* @apiExample {json} Username
*
* {
* "username": "example@appscend.com",
* "password": "secure_password1337",
* "name": "John Smith"
* }
*
* @apiExample {json} Facebook Request
* {
* "access_token": "fb access token"
* }
*
* @apiExample {json} Twitter request
* {
* "oauth_token": "oauth token",
* "oauth_token_secret": "oauth token secret"
* }
*
* @apiSuccessExample {json} Success Response
* {
* "status": 202,
* "content": "User created"
* }
*
* @apiError 400 [028]InsufficientFacebookPermissions User email is not publicly available
* (insufficient facebook permissions)
* @apiError 409 [029]UserAlreadyExists User with that email address already exists
*
*/
router.post('/register-:s', function(req, res, next) {
if (Object.getOwnPropertyNames(req.body).length === 0) {
return next(new Models.TelepatError(Models.TelepatError.errors.RequestBodyEmpty));
}
var loginProvider = req.params.s;
if (loginProvider == 'facebook') {
if (!req.body.access_token)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['access_token']));
if (!app.telepatConfig.config.login_providers || !app.telepatConfig.config.login_providers.facebook)
return next(new Models.TelepatError(Models.TelepatError.errors.ServerNotConfigured,
['facebook login handler']));
else
FB.options(app.telepatConfig.config.login_providers.facebook);
} else if (loginProvider == 'twitter') {
if (!req.body.oauth_token)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['oauth_token']));
if (!req.body.oauth_token_secret)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['oauth_token_secret']));
if (!app.telepatConfig.config.login_providers || !app.telepatConfig.config.login_providers.twitter)
return next(new Models.TelepatError(Models.TelepatError.errors.ServerNotConfigured,
['twitter login provider']));
} else if (loginProvider == 'username') {
if (!req.body.username)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['username']));
if (!req.body.password)
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['password']));
} else {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidLoginProvider, ['facebook, twitter, username']));
}
var userProfile = req.body;
var accessToken = req.body.access_token;
var fbFriends = [];
var deviceId = req._telepat.device_id;
var appId = req._telepat.applicationId;
var requiresConfirmation = Models.Application.loadedAppModels[appId].email_confirmation;
if (loginProvider == 'username' && requiresConfirmation && !req.body.email) {
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField, ['email']));
}
var timestamp = microtime.now();
async.waterfall([
function(callback) {
if (loginProvider == 'facebook') {
FB.napi('/me?fields=name,email,id,gender,picture', {access_token: accessToken}, function(err, result) {
if (err) {
return callback(err);
}
var picture = result.picture.data.url;
delete result.picture;
userProfile = result;
userProfile.picture = picture;
userProfile.username = result.email || result.id;
callback();
});
} else if (loginProvider == 'twitter') {
var options = {
access_token_key: req.body.oauth_token,
access_token_secret: req.body.oauth_token_secret
};
options.consumer_key = app.telepatConfig.config.login_providers.twitter.consumer_key;
options.consumer_secret = app.telepatConfig.config.login_providers.twitter.consumer_secret;
var twitterClient = new Twitter(options);
twitterClient.get('account/settings', {}, function(err, result) {
if (err)
return callback(err);
twitterClient.get('users/show', {screen_name: result.screen_name}, function(err1, result1) {
if (err1)
return callback(err1);
userProfile = {};
userProfile.name = result1.screen_name;
userProfile.username = result.screen_name;
userProfile.picture = result1.profile_image_url_https;
callback();
});
});
} else {
callback();
}
},
function(callback) {
//get his/her friends
if (loginProvider == 'facebook') {
FB.napi('/me/friends', {access_token: accessToken}, function(err, result) {
if (err) return callback(err);
for(var f in result.data) {
fbFriends.push(result.data[f].id);
}
callback();
});
} else
callback();
},
function(callback) {
if (!userProfile.username) {
return callback(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField,
['username']));
}
Models.User({username: userProfile.username}, appId, function(err, result) {
if (!err) {
callback(new Models.TelepatError(Models.TelepatError.errors.UserAlreadyExists));
}
else if (err && err.status != 404)
callback(err);
else {
callback();
}
});
},
//send message to kafka if user doesn't exist in order to create it
function(callback) {
if (fbFriends.length)
userProfile.friends = fbFriends;
userProfile.type = 'user';
userProfile.devices = [deviceId];
if (userProfile.password)
security.encryptPassword(userProfile.password, callback);
else
callback(null, false);
}, function(hash, callback) {
if (hash !== false)
userProfile.password = hash;
//request came from facebook
if (accessToken) {
userProfile.fid = userProfile.id;
delete userProfile.id;
}
if (requiresConfirmation && loginProvider == 'username') {
var mandrill = app.telepatConfig.config.mandrill && app.telepatConfig.config.mandrill.api_key;
var sendgrid = app.telepatConfig.config.sendgrid && app.telepatConfig.config.sendgrid.api_key;
if (!mandrill && !sendgrid) {
Models.Application.logger.warning('Mandrill API key is missing, user email address will be ' +
'automatically confirmed');
userProfile.confirmed = true;
} else if (!Models.Application.loadedAppModels[appId].from_email) {
Models.Application.logger.warning('"from_email" config missing, user email address will be ' +
'automatically confirmed');
userProfile.confirmed = true;
} else {
var messageContent = '';
var emailProvider = app.telepatConfig.config.mandrill ? 'mandrill' : 'sendgrid';
var apiKey = {};
apiKey[emailProvider] = app.telepatConfig.config[emailProvider].api_key;
userProfile.confirmed = false;
userProfile.confirmationHash = crypto.createHash('md5').update(guid.v4()).digest('hex').toLowerCase();
var url = 'http://'+req.headers.host + '/user/confirm?username='+
encodeURIComponent(userProfile.username)+'&hash='+userProfile.confirmationHash+'&app_id='+appId;
if (req.body.callbackUrl)
url += '&redirect_url='+encodeURIComponent(req.body.callbackUrl);
if (Models.Application.loadedAppModels[appId].email_templates &&
Models.Application.loadedAppModels[appId].email_templates.confirm_account) {
messageContent = Models.Application.loadedAppModels[appId].email_templates.confirm_account.
replace('{CONFIRM_LINK}', url);
} else {
messageContent = 'In order to be able to use and log in to the "'+Models.Application.loadedAppModels[appId].name+
'" app click this link: <a href="'+url+'">Confirm</a>';
}
if (Models.Application.loadedAppModels[appId].email_templates &&
Models.Application.loadedAppModels[appId].email_templates.confirm_account) {
messageContent = Models.Application.loadedAppModels[appId].email_templates.confirm_account.
replace(/\{CONFIRM_LINK}/g, url);
} else {
messageContent = 'In order to be able to use and log in to the "'+Models.Application.loadedAppModels[appId].name+
'" app click this link: <a href="'+url+'">Confirm</a>';
}
sendEmail(apiKey,
{
email: Models.Application.loadedAppModels[appId].from_email,
name: Models.Application.loadedAppModels[appId].name
},
userProfile.email,
'Account confirmation for "'+Models.Application.loadedAppModels[appId].name+'"',
messageContent
);
}
}
userProfile.application_id = req._telepat.applicationId;
delete userProfile.access_token;
delete userProfile.callbackUrl;
app.messagingClient.send([JSON.stringify({
op: 'create',
object: userProfile,
application_id: req._telepat.applicationId,
timestamp: timestamp
})], 'aggregation', callback);
}
], function(err) {
if (err && err.message == 'Invalid OAuth access token.' && loginProvider == 'facebook') {
return next(new Models.TelepatError(Models.TelepatError.errors.ServerConfigurationFailure, 'Facebook invalid OAuth access token'));
}
if (err) return next(err);
res.status(202).json({status: 202, content: 'User created'});
});
});
/**
* @api {get} /user/confirm ConfirmEmailAddress
* @apiDescription Confirms the email address for the user
* @apiName ConfirmEmailAddress
* @apiGroup User
* @apiVersion 0.4.3
*
* @apiHeader {String} Content-type application/json
*
* @apiParam {String} username The username
* @apiParam {String} hash The confirmation hash
* @apiParam {String} app_id The application ID
* @apiParam {String} callbackUrl The app deeplink url to redirect the user to
*
* @apiSuccessExample {json} Success Response
* {
* "content": {
* "id": 31,
* "type": "user",
* "username": "abcd@appscend.com",
* "devices": [
* "466fa519-acb4-424b-8736-fc6f35d6b6cc"
* ]
* }
* }
*
*/
router.get('/confirm', function(req, res, next) {
var username = req.body.username;
var hash = req.body.hash;
var appId = req.body.app_id;
var user = null;
var redirectUrl = req.body.redirect_url;
async.series([
function(callback) {
Models.User({username: username}, appId, function(err, result) {
if (err) return callback(err);
user = result;
callback();
});
},
function(callback) {
if (hash != user.confirmationHash) {
return callback(new Models.TelepatError(Models.TelepatError.errors.ClientBadRequest, ['invalid hash']));
}
var patches = [];
patches.push(Models.Delta.formPatch(user, 'replace', {confirmed: true}));
Models.User.update(patches, callback);
}
], function(err) {
if (err)
return next(err);
if (redirectUrl && app.telepatConfig.config.redirect_url) {
res.redirect(app.telepatConfig.config.redirect_url+'?url='+encodeURIComponent(redirectUrl));
res.end();
} else if (Models.Application.loadedAppModels[appId].email_templates &&
Models.Application.loadedAppModels[appId].email_templates.after_confirm) {
res.status(200);
res.set('Content-Type', 'text/html');
res.send(Models.Application.loadedAppModels[appId].email_templates.after_confirm);
} else {
res.status(200).json({status: 200, content: 'Account confirmed'});
}
});
});
/**
* @api {get} /user/me Me
* @apiDescription Info about logged user
* @apiName UserMe
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} Authorization The authorization token obtained in the login endpoint.
* Should have the format: <i>Bearer $TOKEN</i>
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
* @apiHeader {String} X-BLGREQ-UDID Custom header containing the device ID (obtained from device/register)
*
* @apiParam {String} password The password
* @apiParam {String} email The email
*
* @apiSuccessExample {json} Success Response
* {
* "content": {
* "id": 31,
* "type": "user",
* "username": "abcd@appscend.com",
* "devices": [
* "466fa519-acb4-424b-8736-fc6f35d6b6cc"
* ]
* }
* }
*
*/
router.get('/me', function(req, res, next) {
Models.User({id: req.user.id}, req._telepat.applicationId, function(err, result) {
if (err && err.status == 404) {
return next(new Models.TelepatError(Models.TelepatError.errors.UserNotFound));
}
else if (err)
next(err);
else
delete result.password;
res.status(200).json({status: 200, content: result});
});
});
/**
* @api {get} /user/get getUser
* @apiDescription Info about an user, based on their ID
* @apiName UserGet
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} Authorization The authorization token obtained in the login endpoint.
* Should have the format: <i>Bearer $TOKEN</i>
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
* @apiHeader {String} X-BLGREQ-UDID Custom header containing the device ID (obtained from device/register)
*
* @apiParam {String} user_id The ID of the desired user
*
* @apiSuccessExample {json} Success Response
* {
* "content": {
* "id": 31,
* "type": "user",
* "username": "abcd@appscend.com",
* "devices": [
* "466fa519-acb4-424b-8736-fc6f35d6b6cc"
* ]
* }
* }
*
*/
router.get('/get', function(req, res, next) {
Models.User({id: req.body.user_id}, req._telepat.applicationId, function(err, result) {
if (err && err.status == 404) {
return next(new Models.TelepatError(Models.TelepatError.errors.UserNotFound));
}
else if (err)
next(err);
else
delete result.password;
res.status(200).json({status: 200, content: result});
});
});
/**
* @api {get} /user/logout Logout
* @apiDescription Logs out the user removing the device from his array of devices.
* @apiName UserLogout
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
* @apiHeader {String} X-BLGREQ-UDID Custom header containing the device ID (obtained from device/register)
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": "Logged out of device"
* }
*/
router.get('/logout', function(req, res, next) {
var deviceId = req._telepat.device_id;
var appID = req._telepat.applicationId;
async.waterfall([
function(callback) {
Models.User({id: req.user.id}, appID, callback);
},
function(user, callback) {
if (user.devices) {
var idx = user.devices.indexOf(deviceId);
if (idx >= 0)
user.devices.splice(idx, 1);
Models.User.update([
{
"op": "replace",
"path": "user/"+req.user.id+"/devices",
"value": user.devices
}
], callback);
} else {
callback();
}
}
], function(err) {
if (err) return next(err);
res.status(200).json({status: 200, content: "Logged out of device"});
});
});
/**
* @api {get} /user/refresh_token Refresh Token
* @apiDescription Sends a new authentication token to the user. The old token must be provide (and it may or not
* may not be already expired).
* @apiName RefreshToken
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} Authorization The authorization token obtained in the login endpoint.
* Should have the format: <i>Bearer $TOKEN</i>
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": {
* token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImdhYmlAYXBwc2NlbmQuY29tIiwiaXNBZG1pbiI6dHJ1ZSwiaW
* F0IjoxNDMyOTA2ODQwLCJleHAiOjE0MzI5MTA0NDB9.knhPevsK4cWewnx0LpSLrMg3Tk_OpchKu6it7FK9C2Q"
* }
* }
*
* @apiError 400 [013]AuthorizationMissing If authorization header is missing
* @apiError 400 [039]ClientBadRequest Error decoding auth token
* @apiError 400 [040]MalformedAuthorizationToken Authorization token is malformed
* @apiError 400 [014]InvalidAuthorization Authorization header is invalid
*/
router.get('/refresh_token', function(req, res, next) {
if (!req.get('Authorization')) {
return next(new Models.TelepatError(Models.TelepatError.errors.AuthorizationMissing));
}
var authHeader = req.get('Authorization').split(' ');
if (authHeader[0] == 'Bearer' && authHeader[1]) {
try {
var decoded = jwt.decode(authHeader[1]);
} catch (e) {
return next(new Models.TelepatError(Models.TelepatError.errors.ClientBadRequest, [e.message]));
}
if (!decoded) {
return next(new Models.TelepatError(Models.TelepatError.errors.MalformedAuthorizationToken));
}
var newToken = security.createToken(decoded);
return res.status(200).json({status: 200, content: {token: newToken}});
} else {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidAuthorization, ['header invalid']));
}
});
/**
* @api {post} /user/update Update
* @apiDescription Updates the user information. This operation is not immediate.
* @apiName UserUpdate
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiParam {Object[]} patches Array of patches that describe the modifications
*
* @apiExample {json} Client Request
* {
* "patches": [
* {
* "op": "replace",
* "path": "user/user_id/field_name",
* "value": "new value
* }
* ]
* }
*
*
* @apiSuccessExample {json} Success Response
* {
* "status": 202,
* "content": "User updated"
* }
*
* @apiError [042]400 InvalidPatch Invalid patch supplied
*
*/
router.post('/update', function(req, res, next) {
if (Object.getOwnPropertyNames(req.body).length === 0) {
return next(new Models.TelepatError(Models.TelepatError.errors.RequestBodyEmpty));
} else if (!Array.isArray(req.body.patches)) {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidFieldValue,
['"patches" is not an array']));
} else if (req.body.patches.length == 0) {
return next(new Models.TelepatError(Models.TelepatError.errors.InvalidFieldValue,
['"patches" array is empty']));
}
var patches = req.body.patches;
var id = req.user.id;
var username = req.user.username;
var modifiedMicrotime = microtime.now();
var i = 0;
async.eachSeries(patches, function(p, c) {
patches[i].username = username;
if (patches[i].path.split('/')[2] == 'password') {
security.encryptPassword(patches[i].value, function(err, hash) {
patches[i].value = hash;
i++;
c();
});
} else {
i++;
c();
}
}, function() {
async.eachSeries(patches, function(patch, c) {
var patchUserId = patch.path.split('/')[1];
if (patchUserId != id) {
return c(new Models.TelepatError(Models.TelepatError.errors.InvalidPatch,
['Invalid ID in one of the patches']));
}
c();
}, function(err) {
if (err) return next(err);
app.messagingClient.send([JSON.stringify({
op: 'update',
patches: patches,
application_id: req._telepat.applicationId,
timestamp: modifiedMicrotime
})], 'aggregation', function(err) {
if (err)
return next(err);
res.status(202).json({status: 202, content: "User updated"});
});
});
});
});
/**
* @api {delete} /user/delete Delete
* @apiDescription Deletes a user
* @apiName UserDelete
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} Authorization The authorization token obtained in the login endpoint. Should have the format: <i>Bearer $TOKEN</i>
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
*
* @apiSuccessExample {json} Success Response
* {
* "status": 202,
* "content": "User deleted"
* }
*
*/
router.delete('/delete', function(req, res, next) {
var timestamp = microtime.now();
app.messagingClient.send([JSON.stringify({
op: 'delete',
object: {id: req.user.id, model: 'user'},
application_id: req._telepat.applicationId,
timestamp: timestamp
})], 'aggregation', function(err) {
if (err) return next(err);
res.status(202).json({status: 202, content: "User deleted"});
});
});
/**
* @api {delete} /user/request_password_reset Request Password Reset
* @apiDescription Requests a password reset for the user, an email is sent to its email address
* @apiName UserRequestPasswordReset
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
*
* @apiParam {string} link An application deep link to redirect the user when clicking the link in the email sent
* @apiParam {string} username The username which password we want to reset
*
* @apiExample {json} Client Request
* {
* "link": "app://callback-url",
* "username": "email@example.com"
* }
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": "Password reset email sent"
* }
*
*/
router.post('/request_password_reset', function(req, res, next) {
var link = req.body.callbackUrl; // either 'browser' or 'app'
var appId = req._telepat.applicationId;
var username = req.body.username;
var token = crypto.createHash('md5').update(guid.v4()).digest('hex').toLowerCase();
var user = null;
var mandrill = app.telepatConfig.config.mandrill && app.telepatConfig.config.mandrill.api_key;
var sendgrid = app.telepatConfig.config.sendgrid && app.telepatConfig.config.sendgrid.api_key;
if (!mandrill && !sendgrid) {
return next(new Models.TelepatError(Models.TelepatError.errors.ServerNotConfigured, ['mandrill/sendgrid API keys missing']));
}
async.series([
function(callback) {
Models.User({username: username}, appId, function(err, result) {
if (err) return callback(err);
if (!result.email)
return callback(new Models.TelepatError(Models.TelepatError.errors.ClientBadRequest,
['user has no email address']));
user = result;
callback();
});
},
function(callback) {
var messageContent = '';
var emailProvider = app.telepatConfig.config.mandrill ? 'mandrill' : 'sendgrid';
var apiKey = {};
apiKey[emailProvider] = app.telepatConfig.config[emailProvider].api_key;
link += '?token='+token+'&user_id='+user.id;
var redirectUrl = 'http://'+req.headers.host+'/user/reset_password_intermediate?url='+encodeURIComponent(link)+
'&app_id='+appId;
if (Models.Application.loadedAppModels[appId].email_templates &&
Models.Application.loadedAppModels[appId].email_templates.reset_password) {
messageContent = Models.Application.loadedAppModels[appId].email_templates.reset_password.
replace(/\{CONFIRM_LINK}/g, redirectUrl);
} else {
messageContent = 'Password reset request from the "'+Models.Application.loadedAppModels[appId].name+
'" app. Click this URL to reset password: <a href="'+redirectUrl+'">Reset</a>';
}
sendEmail(apiKey,
{
email: Models.Application.loadedAppModels[appId].from_email,
name: Models.Application.loadedAppModels[appId].name
},
user.email,
'Reset account password for "'+username+'"',
messageContent
);
var patches = [];
patches.push(Models.Delta.formPatch(user, 'replace', {password_reset_token: token}));
Models.User.update(patches, callback);
}
], function(err) {
if (err)
return next(err);
res.status(200).json({status: 200, content: "Password reset email sent"});
});
});
router.get('/reset_password_intermediate', function(req, res, next) {
var appId = req.query.app_id;
if (!Models.Application.loadedAppModels[appId])
return next(new Models.TelepatError(Models.TelepatError.errors.ApplicationNotFound, [appId]));
if (!isMobileBrowser(req.get('User-Agent'))) {
if (Models.Application.loadedAppModels[appId].email_templates &&
Models.Application.loadedAppModels[appId].email_templates.weblink) {
res.status(200);
res.type('html');
res.send(Models.Application.loadedAppModels[appId].email_templates.weblink);
res.end();
}
} else {
res.redirect(decodeURIComponent(req.query.url));
}
});
/**
* @api {delete} /user/password_reset Password Reset
* @apiDescription Resets the password of the user based on a token
* @apiName UserPasswordReset
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} X-BLGREQ-APPID Custom header which contains the application ID
* @apiHeader {String} X-BLGREQ-SIGN Custom header containing the SHA256-ed API key of the application
*
* @apiParam {String} token The token received from the query params in the app deeplink callback url
* @apiParam {String} user_id The user_id received from the query params in the app deeplink callback url
* @apiParam {String} password The new password
*
* @apiExample {json} Client Request
* {
* "token": "password_reset_token",
* "user_id": "user_id",
* "password": "new passowrd"
* }
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": "new passowrd"
* }
*
*/
router.post('/password_reset', function(req, res, next) {
var token = req.body.token;
var userId = req.body.user_id;
var newPassword = req.body.password;
var appId = req._telepat.applicationId;
var user = null;
async.series([
function(callback) {
Models.User({id: userId}, appId, function(err, result) {
if (err) return callback(err);
if (result.password_reset_token == null ||
result.password_reset_token == undefined ||
result.password_reset_token != token) {
return callback(new Models.TelepatError(Models.TelepatError.errors.ClientBadRequest,
['invalid token']));
}
user = result;
callback();
})
},
function(callback) {
security.encryptPassword(newPassword, function(err, hashedPassword) {
if (err) return callback(err);
var patches = [];
patches.push(Models.Delta.formPatch(user, 'replace', {password: hashedPassword}));
patches.push(Models.Delta.formPatch(user, 'replace', {password_reset_token: null}));
Models.User.update(patches, callback);
});
}
], function(err) {
if (err)
return next(err);
res.status(200).json({status: 200, content: newPassword});
})
});
/**
* @api {get} /user/metadata Get Metadata
* @apiDescription Gets user metadata (private info)
* @apiName UserGetMetadata
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} Authorization The authorization token obtained in the login endpoint. Should have the format: <i>Bearer $TOKEN</i>
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": {
* "id": "9fa7751a-d733-404a-a269-c8b64817dfd5",
* "user_id": "15f76424-d4bd-48d4-b812-c4ebc09782f1",
* "points": 100,
* }
* }
*
*/
router.get('/metadata', function(req, res, next) {
var userId = req.user.id;
Models.User.getMetadata(userId, function(err, result) {
if (err) return next(err);
res.status(200).json({status: 200, content: result});
});
});
/**
* @api {post} /user/update_metadata Update Metadata
* @apiDescription Updates user metadata
* @apiName UserUpdateMetadata
* @apiGroup User
* @apiVersion 0.4.0
*
* @apiHeader {String} Content-type application/json
* @apiHeader {String} Authorization The authorization token obtained in the login endpoint. Should have the format: <i>Bearer $TOKEN</i>
*
* @apiParam {Object[]} patches Array of patches that describe the modifications
*
* @apiExample {json} Client Request
* {
* "patches": [
* {
* "op": "replace",
* "path": "user_metadata/metadata_id/field_name",
* "value": "new value
* }
* ]
* }
*
*
* @apiSuccessExample {json} Success Response
* {
* "status": 200,
* "content": "Metadata updated successfully"
* }
*
* @apiError [042]400 InvalidPatch Invalid patch supplied
*
*/
router.post('/update_metadata', function(req, res, next) {
var userId = req.user.id;
var patches = req.body.patches;
if (!Array.isArray(patches) || patches.length == 0) {
return next(new Models.TelepatError(Models.TelepatError.errors.MissingRequiredField,
['patches must be a non-empty array']));
}
Models.User.updateMetadata(userId, patches, function(err) {
if (err) return next(err);
res.status(200).json({status: 200, content: "Metadata updated successfully"});
});
});
function sendEmail(provider, from, to, subject, content) {
var emailService = Object.keys(provider)[0];
var apiKey = provider[emailService];
if (emailService == 'mandrill') {
var mandrillClient = new mandrill.Mandrill(apiKey);
var message = {
html: content,
subject: subject,
from_email: from.email,
from_name: from.name,
to: [
{
email: to,
type: 'to'
}
]
};
mandrillClient.messages.send({message: message, async: "async"}, function() {}, function(err) {
Models.Application.logger.warning('Unable to send Mandrill mail: ' + err.name + ' - '
+ err.message);
});
} else if (emailService == 'sendgrid') {
var from_email = new sendgridHelper.Email(from.email, from.name);
var to_email = new sendgridHelper.Email(to);
var mail = new sendgridHelper.Mail(from_email, subject, to_email, new sendgridHelper.Content('text/html', content));
var sg = require('sendgrid')(apiKey);
var req = sg.emptyRequest({
method: 'POST',
path: '/v3/mail/send',
body: mail.toJSON()
});
sg.API(req, function(err, response) {
if (err) {
Models.Application.logger.warning('Unable to send Sendgrid amail: ' + err.name + ' - '
+ err.message);
}
else if (response.statusCode >= 400) {
var error = JSON.parse(response.body);
Models.Application.logger.warning('Unable to send Sendgrid amail: ' + error.errors[0].message);
}
});
}
}
module.exports = router;