/**
* @module auth
* @author Flavio De Stefano <flavio.destefano@caffeinalab.com>
*/
/**
* @property config
* @property {String} [config.loginUrl="/login"] The URL called by login().
* @property {String} [config.logoutUrl="/logout"] The URL called by logout().
* @property {String} [config.modelId="me"] The id for the user model.
* @property {Boolean} [config.ignoreServerModelId=false] Force the module to use the configured modelId to fetch and store the user model.
* @property {Boolean} [config.useOAuth=false] Use OAuth method to authenticate.
* @property {String} [config.oAuthAccessTokenURL="/oauth/access_token"] OAuth endpoint to retrieve access token.
* @property {String} [config.oAuthClientID="app"] OAuth client ID
* @property {String} [config.oAuthClientSecret="secret"] OAuth client secret.
* @property {Boolean} [config.useTouchID=false] Use TouchID to protect stored/offline login.
* @property {Boolean} [config.enforceTouchID=false] If true, disable the stored/offline login when TouchID is disabled or not supported.
* @property {Boolean} [config.useTouchIDPromptConfirmation=false] Ask the user if he wants to use the TouchID protection after the first signup. If false, the TouchID protection is used without prompts.
*/
exports.config = _.extend({
loginUrl: '/login',
logoutUrl: '/logout',
modelId: 'me',
ignoreServerModelId: false,
useOAuth: false,
oAuthAccessTokenURL: '/oauth/access_token',
oAuthClientID: 'app',
oAuthClientSecret: 'secret',
useTouchID: false,
enforceTouchID: false,
useTouchIDPromptConfirmation: false,
}, Alloy.CFG.T ? Alloy.CFG.T.auth : {});
var MODULE_NAME = 'auth';
var Q = require('T/ext/q');
var HTTP = require('T/http');
var Event = require('T/event');
var Cache = require('T/cache');
var Util = require('T/util');
var Dialog = require('T/dialog');
var Prop = require('T/prop');
var TouchID = Util.requireOrNull("ti.touchid");
if (OS_IOS && exports.config.useTouchID == true && TouchID != null) {
TouchID.setAuthenticationPolicy(TouchID.AUTHENTICATION_POLICY_BIOMETRICS);
}
var currentUser = null;
/**
* OAuth object instance of oauth module
* @see support/oauth
* @type {Object}
*/
exports.OAuth = require('T/support/oauth');
exports.OAuth.__setParent(module.exports);
////////////
// Driver //
////////////
function getStoredDriverString() {
var hasDriver = Prop.hasProperty('auth.driver');
var hasMe = Prop.hasProperty('auth.me');
if (hasDriver && hasMe) {
return Prop.getString('auth.driver');
}
}
function driverLogin(opt) {
var driver = exports.loadDriver(opt.driver);
var method = opt.stored === true ? 'storedLogin' : 'login';
return Q.promise(function(resolve, reject) {
driver[ method ]({
data: opt.data,
success: resolve,
error: reject
});
});
}
///////////////////////
// Server side login //
///////////////////////
function serverLoginWithOAuth(opt, dataFromDriver) {
var oAuthPostData = {
client_id: exports.OAuth.getClientID(),
client_secret: exports.OAuth.getClientSecret(),
grant_type: 'password',
username: '-',
password: '-'
};
return Q.promise(function(resolve, reject) {
HTTP.send({
url: exports.config.oAuthAccessTokenURL,
method: 'POST',
data: _.extend({}, oAuthPostData, dataFromDriver),
suppressFilters: ['oauth'],
success: function(data) {
exports.OAuth.storeCredentials(data);
resolve(data);
},
error: reject,
});
});
}
function serverLoginWithCookie(opt, dataFromDriver) {
return Q.promise(function(resolve, reject) {
HTTP.send({
url: opt.loginUrl,
method: 'POST',
data: dataFromDriver,
success: function(data) {
HTTP.exportCookiesToSystem();
resolve(data);
},
error: reject,
});
});
}
function apiLogin(opt, dataFromDriver) {
var driver = exports.loadDriver(opt.driver);
opt.loginUrl = driver.config.loginUrl || exports.config.loginUrl;
if (exports.config.useOAuth == true) {
return serverLoginWithOAuth(opt, dataFromDriver);
} else {
return serverLoginWithCookie(opt, dataFromDriver);
}
}
//////////////////////
// Fetch user model //
//////////////////////
function fetchUserModel(opt, dataFromServer) {
dataFromServer = dataFromServer || {};
return Q.promise(function(resolve, reject) {
var id = exports.config.modelId;
if (exports.config.ignoreServerModelId == false && dataFromServer.id != null) {
id = dataFromServer.id;
}
currentUser = Alloy.createModel('user', { id: id });
currentUser.fetch({
http: {
refresh: true,
cache: false,
},
success: function() {
Prop.setObject('auth.me', currentUser.toJSON());
resolve();
},
error: function(model, err) {
reject(err);
}
});
});
}
/**
* Load a driver
* @return {Object}
*/
exports.loadDriver = function(name) {
var driver = Alloy.Globals.Trimethyl.loadDriver('auth', name, {
login: function() {},
storedLogin: function() {},
isStoredLoginAvailable: function() {},
logout: function() {}
});
driver.__setParent(module.exports);
return driver;
};
/**
* Add an event to current module
*/
exports.event = function(name, cb) {
Event.on(MODULE_NAME + '.' + name, cb);
};
//////////////
// Touch ID //
//////////////
/**
* Check if the TouchID is enabled and supported on the device and configuration.
* @return {Boolean}
*/
exports.isTouchIDSupported = function() {
return exports.config.useTouchID == true && TouchID != null && TouchID.isSupported();
};
/**
* Authenticately via TouchID.
* @param {Function} success The callback to call on success.
* @param {Function} error The callback to call on error.
*/
exports.authenticateViaTouchID = function(opt) {
opt = _.defaults(opt || {}, {
success: Alloy.Globals.noop,
error: Alloy.Globals.noop
});
clearTimeout(exports.authenticateViaTouchID.timeout);
if (exports.isTouchIDSupported() && exports.userWantsToUseTouchID()) {
if (opt.timeout != null) {
exports.authenticateViaTouchID.timeout = setTimeout(function() {
TouchID.invalidate();
}, opt.timeout);
}
return TouchID.authenticate({
reason: L('auth_touchid_reason'),
callback: function(e) {
setTimeout(function(){
if (e.success) {
clearTimeout(exports.authenticateViaTouchID.timeout);
opt.success({ touchID: true });
} else {
opt.error();
}
}, 0);
}
});
}
if (exports.config.enforceTouchID == true) {
Ti.API.warn(MODULE_NAME + ": the user has denied access to TouchID or device doesn't support TouchID, but current configuration is enforcing TouchID usage");
opt.error();
} else {
opt.success({ touchID: false });
}
};
/**
* Set or get the TouchID use property.
* @param {Boolean} val
* @return {Boolean}
*/
exports.userWantsToUseTouchID = function(val) {
if (val !== undefined) {
Prop.setBool('auth.touchid.use', val);
} else {
return Prop.getBool('auth.touchid.use', false);
}
};
/**
* Get current User model
* @return {Backbone.Model}
*/
exports.getUser = function(){
return currentUser;
};
/**
* Check if the user is logged in
* @return {Boolean}
*/
exports.isLoggedIn = function() {
return currentUser !== null;
};
/**
* Get current User ID
* Return 0 if no user is logged in
* @return {Number}
*/
exports.getUserID = function(){
if (currentUser === null) return 0;
return currentUser.id;
};
/**
* Login using selected driver
* @param {Object} opt
* @param {Boolean} [opt.silent=false] Silence all global events
* @param {String} [opt.driver="bypass"] The driver to use as string
* @param {Function} [opt.success=null] The success callback to invoke
* @param {Function} [opt.error=null] The error callback to invoke
*/
exports.login = function(opt) {
opt = _.defaults(opt || {}, {
success: Alloy.Globals.noop,
error: Alloy.Globals.noop,
silent: false,
driver: 'bypass'
});
driverLogin(opt)
.then(function(dataFromDriver) {
return apiLogin(opt, _.extend({}, dataFromDriver, {
method: opt.driver
}));
})
.then(function(dataFromServer) {
return fetchUserModel(opt, dataFromServer);
})
.then(function() {
Prop.setString('auth.driver', opt.driver);
})
.then(function() {
return Q.promise(function(resolve, reject) {
if (exports.config.useTouchIDPromptConfirmation == true && !opt.stored && exports.isTouchIDSupported()) {
Dialog.confirm("Touch ID", L("auth_touchid_confirmation_message"), [
{
title: L('yes', 'Yes'),
preferred: true,
callback: function() {
exports.userWantsToUseTouchID(true);
resolve();
}
},
{
title: L('no', 'No'),
callback: function() {
exports.userWantsToUseTouchID(false);
resolve();
}
}
]);
} else {
resolve();
}
});
})
.then(function() {
var payload = { id: currentUser.id };
opt.success(payload);
if (opt.silent !== true) {
Event.trigger('auth.success', payload);
}
})
.fail(function(err) {
Event.trigger('auth.error', err);
opt.error(err);
});
};
/**
* Check if the stored login feature is available
* Stored login indicate if the auth can be completed using stored credentials on the device
* but require an Internet connection anyway
* @return {Boolean}
*/
exports.isStoredLoginAvailable = function() {
var driver = getStoredDriverString();
if (driver == null) return false;
return exports.loadDriver(driver).isStoredLoginAvailable();
};
/**
* Login using stored credentials on the device
* @param {Object} opt
* @param {Boolean} [opt.silent=false] Silence all global events
* @param {Function} [opt.success=null] The success callback to invoke
* @param {Function} [opt.error=null] The error callback to invoke
*/
exports.storedLogin = function(opt) {
opt = _.defaults(opt || {}, {
success: Alloy.Globals.noop,
error: Alloy.Globals.noop
});
if (exports.isStoredLoginAvailable()) {
exports.authenticateViaTouchID({
timeout: opt.timeout,
success: function() {
exports.login(_.extend(opt || {}, {
stored: true,
driver: getStoredDriverString()
}));
},
error: opt.error
});
} else {
opt.error();
}
};
/**
* Check if an offline login is available
* @return {Boolean}
*/
exports.isOfflineLoginAvailable = function() {
return Prop.hasProperty('auth.me');
};
/**
* Login using offline properties
* This method doesn't require an internet connection
* @param {Object} opt
* @param {Boolean} [opt.silent=false] Silence all global events
* @param {Function} [opt.success=null] The success callback to invoke
* @param {Function} [opt.error=null] The error callback to invoke
*/
exports.offlineLogin = function(opt) {
opt = _.defaults(opt || {}, {
silent: false,
success: Alloy.Globals.noop,
error: Alloy.Globals.noop
});
if (exports.isOfflineLoginAvailable()) {
exports.authenticateViaTouchID({
timeout: opt.timeout,
success: function() {
currentUser = Alloy.createModel('user', Prop.getObject('auth.me'));
var payload = {
id: currentUser.id,
offline: true
};
opt.success(payload);
if (opt.silent !== true) {
Event.trigger('auth.success', payload);
}
},
error: opt.error
});
} else {
opt.error();
}
};
/**
* This method will select the best behaviour and will login the user
* @param {Object} opt
* @param {Object} opt
* @param {Boolean} [opt.silent=false] Silence all global events
* @param {Function} [opt.success=null] The success callback to invoke
* @param {Function} [opt.error=null] The error callback to invoke
* @param {Function} [opt.timeout=10000] Timeout after the auto login will cause an error
*/
exports.autoLogin = function(opt) {
opt = _.defaults(opt || {}, {
success: Alloy.Globals.noop,
error: Alloy.Globals.noop,
timeout: 10000,
silent: false
});
var success = opt.success;
var error = opt.error;
var timeouted = false;
var errorTimeout = setTimeout(function() {
timeouted = true;
opt.error();
}, opt.timeout);
opt.success = function() {
clearTimeout(errorTimeout);
success.apply(null, arguments);
};
opt.error = function() {
clearTimeout(errorTimeout);
success = Alloy.Globals.noop;
error.apply(null, arguments);
};
if (Ti.Network.online) {
var driver = getStoredDriverString();
if (exports.config.useOAuth == true && driver === 'bypass') {
if (exports.OAuth.getAccessToken() != null) {
exports.authenticateViaTouchID({
timeout: opt.timeout,
success: function() {
fetchUserModel()
.then(function() {
if (timeouted) return;
var payload = {
id: currentUser.id,
oauth: true
};
opt.success(payload);
if (opt.silent !== true) {
Event.trigger('auth.success', payload);
}
})
.fail(function(err) {
opt.error(err);
if (opt.silent != true) {
Event.trigger('auth.error', err);
}
});
},
error: function() {
// Not a real error, no object passing
opt.error();
}
});
} else {
// Not a real error, no object passing
opt.error();
}
} else {
if (exports.isStoredLoginAvailable()) {
exports.storedLogin({
timeout: opt.timeout,
success: function(payload) {
if (timeouted) return;
opt.success(payload);
if (opt.silent != true) {
Event.trigger('auth.success', payload);
}
},
error: opt.error,
silent: true // manage internally
});
} else {
// Not a real error, no object passing
opt.error();
}
}
} else /* is offline */ {
if (exports.isOfflineLoginAvailable()) {
exports.offlineLogin({
timeout: opt.timeout,
success: function(payload) {
if (timeouted) return;
opt.success(payload);
if (opt.silent != true) {
Event.trigger('auth.success', payload);
}
},
error: opt.error,
silent: true // manage internally
});
} else {
// Not a real error, no object passing
opt.error();
}
}
};
/**
* Logout the user
* @param {Function} callback Callback to invoke on completion
*/
exports.logout = function(callback) {
Event.trigger('auth.logout', {
id: exports.getUserID()
});
var driver = getStoredDriverString();
if (driver != null) {
exports.loadDriver(driver).logout();
}
var logoutUrl = (driver && driver.config ? driver.config.logoutUrl : null) || exports.config.logoutUrl;
HTTP.send({
url: logoutUrl,
method: 'POST',
timeout: 3000,
complete: function() {
currentUser = null;
Prop.removeProperty('auth.me');
Cache.purge();
if (exports.config.useOAuth == true) {
exports.OAuth.resetCredentials();
} else {
Ti.Network.removeHTTPCookiesForDomain(Util.getDomainFromURL(HTTP.config.base));
}
if (_.isFunction(callback)) callback();
}
});
};
//////////
// Init //
//////////
if (exports.config.useOAuth == true) {
HTTP.addFilter('oauth', exports.OAuth.httpFilter);
}