/* global confirm, document, navigator, location, window */
var utility = require('../utility/utility.js'),
Connection = require('../Connection.js'),
_ = require('underscore');
//--------------------- Initialization ----------//
/**
* @class Auth: handling Pryv authentication through browser popup
* */
var Auth = function () {};
_.extend(Auth.prototype, {
connection: null, // actual connection managed by Auth
config: {
// TODO: clean up this hard-coded mess and rely on the one and only Pryv URL domains reference
registerURL: {ssl: true, host: 'reg.pryv.me'},
sdkFullPath: 'https://api.pryv.com/lib-javascript/latest'
},
state: null, // actual state
window: null, // popup window reference (if any)
spanButton: null, // an element on the app web page that can be controlled
buttonHTML: '',
onClick: {}, // functions called when button is clicked
settings: null,
pollingID: false,
pollingIsOn: true, // may be turned off if we can communicate between windows
cookieEnabled: false,
ignoreStateFromURL: false // turned to true in case of loggout
});
// Initialize style sheet and supported languages
utility.loadExternalFiles(Auth.prototype.config.sdkFullPath +
'/assets/buttonSigninPryv.css', 'css');
Auth.prototype.uiSupportedLanguages = ['en', 'fr'];
/**
* Setup the authentication process
* //TODO check settings
* @param settings: initialization settings
* @returns {Connection}: the connection managed by Auth.
* A new one is created each time setup is called.
*/
Auth.prototype.setup = function (settings) {
this.state = null;
this._checkCookies();
settings.languageCode =
utility.getPreferredLanguage(this.uiSupportedLanguages, settings.languageCode);
// ReturnURL
settings.returnURL = settings.returnURL || 'auto#';
if (settings.returnURL) {
// Check the trailer
var trailer = settings.returnURL.charAt(settings.returnURL.length - 1);
if ('#&?'.indexOf(trailer) < 0) {
throw new Error('Pryv access: Last character of --returnURL setting-- is not ' +
'"?", "&" or "#": ' + settings.returnURL);
}
// Set self as return url?
if((settings.returnURL.indexOf('auto') === 0 && utility.browserIsMobileOrTablet()) ||
(settings.returnURL.indexOf('self') === 0)) {
var myParams = settings.returnURL.substring(4);
// Eventually clean-up current url from previous pryv returnURL
settings.returnURL = this._cleanStatusFromURL() + myParams;
} else if(settings.returnURL.indexOf('auto') === 0 && !utility.browserIsMobileOrTablet()) {
settings.returnURL = false;
}
if (settings.returnURL && settings.returnURL.indexOf('http') < 0) {
throw new Error('Pryv access: --returnURL setting-- does not start with http: ' +
settings.returnURL);
}
}
this.settings = settings;
// TODO: Clean up this hard-coded mess and rely on the one and only Pryv URL domains reference
var z = this.config.registerURL.host;
this.settings.domain = z.substring(z.indexOf('.') + 1);
var params = {
requestingAppId : settings.requestingAppId,
requestedPermissions : settings.requestedPermissions,
languageCode : settings.languageCode,
returnURL : settings.returnURL
};
// Advanced dev. option for oauth
if (settings.oauthState) {
params.oauthState = settings.oauthState;
}
// Advanced dev. option for local testing with rec-la
if (this.config.reclaDevel) {
// Return url will be forced to https://se.rec.la + reclaDevel
params.reclaDevel = this.config.reclaDevel;
}
this.stateInitialization();
this.connection = new Connection(null, null, {ssl: true, domain: this.settings.domain});
// Look if we have a returning user (document.cookie)
var cookieUserName = this.cookieEnabled ?
utility.docCookies.getItem('access_username' + this.settings.domain) : false;
var cookieToken = this.cookieEnabled ?
utility.docCookies.getItem('access_token' + this.settings.domain) : false;
// Look in the URL if we are returning from a login process
var stateFromURL = this._getStatusFromURL();
if (stateFromURL && (! this.ignoreStateFromURL)) {
this.stateChanged(stateFromURL);
} else if (cookieToken && cookieUserName) {
this.stateChanged({status: 'ACCEPTED', username: cookieUserName,
token: cookieToken, domain: this.settings.domain});
} else {
// Launch process
var pack = {
path : '/access',
params : params,
success : function (data) {
if (data.status && data.status !== 'ERROR') {
this.stateChanged(data);
} else {
// TODO call shouldn't failed
this.internalError('/access Invalid data: ', data);
}
}.bind(this),
error : function (jsonError) {
this.internalError('/access ajax call failed: ', jsonError);
}.bind(this)
};
utility.request(_.extend(pack, this.config.registerURL));
}
return this.connection;
};
//--------------------- UI Content -----------//
/**
* Generate Pryv login button
* @param onClick: id of the event to trigger when button is clicked
* @param buttonText: button label
* @returns {string}: html code of the generated Pryv button
*/
Auth.prototype.uiButton = function (onClick, buttonText) {
if (utility.supportCSS3()) {
return '<div id="pryv-access-btn" class="pryv-access-btn-signin" data-onclick-action="' +
onClick + '">' +
'<a class="pryv-access-btn pryv-access-btn-pryv-access-color" href="#">' +
'<span class="logoSignin">Y</span></a>' +
'<a class="pryv-access-btn pryv-access-btn-pryv-access-color" href="#"><span>' +
buttonText + '</span></a></div>';
} else {
return '<a href="#" id ="pryv-access-btn" data-onclick-action="' + onClick +
'" class="pryv-access-btn-signinImage" ' +
'src="' + this.config.sdkFullPath + '/assets/btnSignIn.png" >' + buttonText + '</a>';
}
};
/**
* Error button
*/
Auth.prototype.uiErrorButton = function () {
var strs = {
'en': { 'msg': 'Error :(' },
'fr': { 'msg': 'Erreur :('}
}[this.settings.languageCode];
this.onClick.Error = function () {
this.logout();
return false;
}.bind(this);
return this.uiButton('Error', strs.msg);
};
/**
* Loading button
*/
Auth.prototype.uiLoadingButton = function () {
var strs = {
'en': { 'msg': 'Loading...' },
'fr': { 'msg': 'Chargement...'}
}[this.settings.languageCode];
this.onClick.Loading = function () {
return false;
};
return this.uiButton('Loading', strs.msg);
};
/**
* Signin Button
*/
Auth.prototype.uiSigninButton = function () {
var strs = {
'en': { 'msg': 'Sign in' },
'fr': { 'msg': 'S\'identifier' }
}[this.settings.languageCode];
this.onClick.Signin = function () {
this.popupLogin();
return false;
}.bind(this);
return this.uiButton('Signin', strs.msg);
};
/**
* Signout Button
*/
Auth.prototype.uiConfirmLogout = function () {
var strs = {
'en': { 'logout': 'Sign out?'},
'fr': { 'logout': 'Se déconnecter?'}
}[this.settings.languageCode];
if (confirm(strs.logout)) {
this.logout();
}
};
/**
* Confirm logout Button
* @param username: user to be logout
*/
Auth.prototype.uiInButton = function (username) {
this.onClick.In = function () {
this.uiConfirmLogout();
return false;
}.bind(this);
return this.uiButton('In', username);
};
/**
* Access refused Button
* @param message: reason for refusal
*/
Auth.prototype.uiRefusedButton = function (message) {
console.log('Pryv access [REFUSED]' + message);
var strs = {
'en': { 'msg': 'access refused'},
'fr': { 'msg': 'Accès refusé'}
}[this.settings.languageCode];
this.onClick.Refused = function () {
this.logout();
return false;
}.bind(this);
return this.uiButton('Refused', strs.msg);
};
/**
* Update Pryv button included in the webpage
* @param html: html code of the Pryv button
*/
Auth.prototype.updateButton = function (html) {
this.buttonHTML = html;
if (this.settings.spanButtonID) {
utility.domReady(function () {
if (!this.spanButton) {
var element = document.getElementById(this.settings.spanButtonID);
if (typeof(element) === 'undefined' || element === null) {
throw new Error('access-SDK cannot find span ID: "' +
this.settings.spanButtonID + '"');
} else {
this.spanButton = element;
}
}
this.spanButton.innerHTML = this.buttonHTML;
this.spanButton.onclick = function (e) {
e.preventDefault();
var element = document.getElementById('pryv-access-btn');
console.log('onClick', this.spanButton,
element.getAttribute('data-onclick-action'));
this.onClick[element.getAttribute('data-onclick-action')]();
}.bind(this);
}.bind(this));
}
};
//--------------- State Management ------------------//
/**
* Handles state changes
* @param data: the new state data
*/
Auth.prototype.stateChanged = function (data) {
if (data.id) { // error
if (this.settings.callbacks.error) {
this.settings.callbacks.error(data.id, data.message);
}
this.updateButton(this.uiErrorButton());
// this.logout(); Why should I retry if it failed already once?
}
if(!(data.status === this.state.status ||
data.status === 'LOADED' ||
data.status === 'POPUPINIT')) {
this.state = data;
switch(this.state.status) {
case 'NEED_SIGNIN':
this.stateNeedSignin();
break;
case 'REFUSED':
this.stateRefused();
break;
case 'ACCEPTED':
this.stateAccepted();
}
}
};
/**
* State 0: Initialization
* Pryv button is loading
*/
Auth.prototype.stateInitialization = function () {
this.state = {status : 'initialization'};
this.updateButton(this.uiLoadingButton());
if (this.settings.callbacks.initialization) {
this.settings.callbacks.initialization();
}
};
/**
* State 0: Need Signin
* Wait the user to sign in
*/
Auth.prototype.stateNeedSignin = function () {
this.updateButton(this.uiSigninButton());
if (this.settings.callbacks.needSignin) {
this.settings.callbacks.needSignin(this.state.url, this.state.poll,
this.state.poll_rate_ms);
}
};
/**
* State 2: Accepted
* The user is logged in and authorized, saves the credentials
*/
Auth.prototype.stateAccepted = function () {
if (this.cookieEnabled) {
utility.docCookies.setItem('access_username' + this.settings.domain, this.state.username, 3600);
utility.docCookies.setItem('access_token' + this.settings.domain, this.state.token, 3600);
}
this.updateButton(this.uiInButton(this.state.username));
this.connection.username = this.state.username;
this.connection.auth = this.state.token;
this.connection.domain = this.settings.domain;
if (this.settings.callbacks.accepted) {
this.settings.callbacks.accepted(this.state.username, this.state.token, this.state.lang);
}
if (this.settings.callbacks.signedIn) {
this.settings.callbacks.signedIn(this.connection, this.state.lang);
}
};
/**
* State 3: User refused
* The user is notified about refused access
*/
Auth.prototype.stateRefused = function () {
this.updateButton(this.uiRefusedButton(this.state.message));
if (this.settings.callbacks.refused) {
this.settings.callbacks.refused('refused:' + this.state.message);
}
};
/**
* Throw an internal error
* @param message: error message
* @param jsonData: error data
*/
Auth.prototype.internalError = function (message, jsonData) {
this.stateChanged({id: 'INTERNAL_ERROR', message: message, data: jsonData});
};
//--------------- Connection Management ------------------//
/**
* Login the user and save references
* TODO: discuss whether signature should be `(settings, callback)`
* @param settings: authentication settings
*/
Auth.prototype.login = function (settings) {
this._checkCookies();
var defaultDomain = utility.urls.defaultDomain;
this.settings = settings = _.defaults(settings, {
ssl: true,
domain: defaultDomain
});
Connection.login(settings, function(err, conn, res) {
if((err || !res.token) && typeof(this.settings.callbacks.error) === 'function') {
return this.settings.callbacks.error(err || res);
}
this.connection = conn;
if (this.cookieEnabled && settings.rememberMe) {
utility.docCookies.setItem('access_username' + this.settings.domain,
settings.username, 3600);
utility.docCookies.setItem('access_token' + this.settings.domain,
res.token, 3600);
utility.docCookies.setItem('access_preferredLanguage' + this.settings.domain,
res.preferredLanguage, 3600);
}
if (typeof(this.settings.callbacks.signedIn) === 'function') {
this.settings.callbacks.signedIn(this.connection);
}
}.bind(this));
};
/**
* Logout the user and clear all references
*/
Auth.prototype.logout = function () {
this.ignoreStateFromURL = true;
if (this.cookieEnabled) {
utility.docCookies.removeItem('access_username' + this.settings.domain);
utility.docCookies.removeItem('access_token' + this.settings.domain);
}
this.state = null;
if (this.settings.callbacks.accepted) {
this.settings.callbacks.accepted(false, false, false);
}
if (this.settings.callbacks.signedOut) {
this.settings.callbacks.signedOut(this.connection);
}
this.connection = null;
this.setup(this.settings);
};
/**
* Trusted logout with Pryv API call
* TODO: belong elsewhere, useful? (e.g. static method of Connection)
*/
Auth.prototype.trustedLogout = function () {
if (this.connection) {
this.connection.trustedLogout(this.settings.callbacks);
}
};
/**
* Request for access information
* TODO: belong elsewhere? (e.g. static method of Connection)
* @param settings: request settings
*/
Auth.prototype.whoAmI = function (settings) {
var defaultDomain = utility.urls.defaultDomain;
this.settings = settings = _.defaults(settings, {
ssl: true,
domain: defaultDomain
});
this.connection = new Connection({
ssl: settings.ssl,
domain: settings.domain
});
var pack = {
ssl: settings.ssl,
host: settings.username + '.' + settings.domain,
path : '/auth/who-am-i',
method: 'GET',
success : function (data) {
if (data.token) {
this.connection.username = data.username;
this.connection.auth = data.token;
var conn = new Connection(data.username, data.token, {
ssl: settings.ssl,
domain: settings.domain
});
conn.accessInfo(function (error) {
console.log('after access info', this.connection);
if(error && (typeof(this.settings.callbacks.error) === 'function')) {
this.settings.callbacks.error(error);
} else if(!error && typeof(this.settings.callbacks.signedIn) === 'function') {
this.settings.callbacks.signedIn(this.connection);
}
}.bind(this));
} else if (typeof(this.settings.callbacks.error) === 'function') {
this.settings.callbacks.error(data);
}
}.bind(this),
error : function (jsonError) {
if (typeof(this.settings.callbacks.error) === 'function') {
this.settings.callbacks.error(jsonError);
}
}.bind(this)
};
utility.request(pack);
};
/**
* Login the user using stored cookies
* TODO: belong elsewhere, merge with standard login? (e.g. member method of Connection)
* @param settings: authentication settings
* @returns {*}: a successful connection or false
*/
Auth.prototype.loginWithCookie = function (settings) {
var defaultDomain = utility.urls.defaultDomain;
this.settings = settings = _.defaults(settings, {
ssl: true,
domain: defaultDomain
});
this.connection = new Connection({
ssl: settings.ssl,
domain: settings.domain
});
this._checkCookies();
var cookieUserName = this.cookieEnabled ?
utility.docCookies.getItem('access_username' + this.settings.domain) : false;
var cookieToken = this.cookieEnabled ?
utility.docCookies.getItem('access_token' + this.settings.domain) : false;
if (cookieUserName && cookieToken) {
this.connection.username = cookieUserName;
this.connection.domain = this.settings.domain;
this.connection.auth = cookieToken;
if (typeof(this.settings.callbacks.signedIn) === 'function') {
this.settings.callbacks.signedIn(this.connection);
}
return this.connection;
}
return false;
};
//--------------- Popup Management ------------------//
/**
* Read the polling route using a polling key
*/
Auth.prototype.poll = function poll() {
if (this.pollingIsOn && this.state.poll_rate_ms) {
// Remove eventually pending poll
if (this.pollingID) {
clearTimeout(this.pollingID);
}
var pack = {
path : '/access/' + this.state.key,
method : 'GET',
success : function (data) {
this.stateChanged(data);
}.bind(this),
error : function (jsonError) {
this.internalError('poll failed: ', jsonError);
}.bind(this)
};
utility.request(_.extend(pack, this.config.registerURL));
this.pollingID = setTimeout(this.poll.bind(this), this.state.poll_rate_ms);
} else {
console.log('stopped polling: on=' + this.pollingIsOn + ' rate:' + this.state.poll_rate_ms);
}
};
/**
* Messaging between browser window and window.opener
* @param event: message from browser
* @returns {boolean}: false in case of error
*/
Auth.prototype.popupCallBack = function (event) {
// Do not use 'this' here !
if (!this.settings.forcePolling) {
if (event.source !== this.window) {
console.log('popupCallBack event.source does not match Auth.window');
return false;
}
console.log('from popup >>> ' + JSON.stringify(event.data));
this.pollingIsOn = false; // If we can receive messages we stop polling
this.stateChanged(event.data);
}
};
/**
* Display the login popup
* @returns {boolean}: false in case of error
*/
Auth.prototype.popupLogin = function popupLogin() {
if (!this.state || !this.state.url) {
throw new Error('Pryv Sign-In Error: NO SETUP. Please call Auth.setup() first.');
}
if (this.settings.returnURL) {
location.href = this.state.url;
} else {
// Start polling
setTimeout(this.poll(), 1000);
var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft,
screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop,
outerWidth = typeof window.outerWidth !== 'undefined' ?
window.outerWidth : document.body.clientWidth,
outerHeight = typeof window.outerHeight !== 'undefined' ?
window.outerHeight : (document.body.clientHeight - 22),
width = 270,
height = 420,
left = parseInt(screenX + ((outerWidth - width) / 2), 10),
top = parseInt(screenY + ((outerHeight - height) / 2.5), 10),
features = (
'width=' + width +
',height=' + height +
',left=' + left +
',top=' + top +
',scrollbars=yes'
);
window.addEventListener('message', this.popupCallBack.bind(this), false);
this.window = window.open(this.state.url, 'prYv Sign-in', features);
if (!this.window) {
// TODO try to fall back on access
console.log('FAILED_TO_OPEN_WINDOW');
} else if(window.focus) {
this.window.focus();
}
return false;
}
};
//--------------------- Utils ----------//
// Regular expression filtering url parameters
var statusRegexp = /[?#&]+prYv([^=&]+)=([^&]*)/gi;
/**
* Grab status parameter from url query string
* @returns {*}: status parameter if present or false
* @private
*/
Auth.prototype._getStatusFromURL = function () {
var vars = {};
window.location.href.replace(statusRegexp,
function (m, key, value) {
vars[key] = value;
});
//TODO check validity of status
return (vars.status) ? vars : false;
};
/**
* Remove url parameters from url query string
* @returns {string}: original url without parameters
* @private
*/
Auth.prototype._cleanStatusFromURL = function () {
return window.location.href.replace(statusRegexp, '');
};
/**
* Check if cookies are supported and save this information as boolean
* @private
*/
Auth.prototype._checkCookies = function () {
this.cookieEnabled = (navigator.cookieEnabled);
if (typeof navigator.cookieEnabled === 'undefined' && !this.cookieEnabled) { //if not IE4+ NS6+
document.cookie = 'testcookie';
this.cookieEnabled = (document.cookie.indexOf('testcookie') !== -1);
}
};
module.exports = new Auth();