UNPKG

coins-logon-widget

Version:

COINS Logon Widget. Injectable utility to manage browser authorization with COINS

361 lines (301 loc) 11.4 kB
'use strict'; var auth = require('./lib/auth'); var createElement = require('virtual-dom/create-element'); var diff = require('virtual-dom/diff'); var EventEmitter = require('wolfy87-eventemitter'); var form = require('./components/form'); var merge = require('lodash/object/merge'); var messages = require('./lib/messages'); var patch = require('virtual-dom/patch'); var uniqueId = require('lodash/utility/uniqueId'); var utils = require('./lib/utils'); /** * Coins logon widget. * @class * @extends EventEmitter * * @param {object} options * @param {string} options.authCookieName * @param {string} options.baseUrl * @param {Node} options.el DOM node to append logon widget * @param {object} [options.messages] Override default messages by passing a * hash corresponding to account and password events. See {@link lib/messages} * for specifics. * @param {boolean} [options.redirect=false] * @param {string} [options.redirectUrl] * @param {boolean} [options.checkAuth=false] * @param {boolean} [options.horizontal=false] Whether to display the form * horizontally, with inputs displayed left-to-right */ function CoinsLogonWidget(options) { EventEmitter.call(this, options); var EVENTS = CoinsLogonWidget.EVENTS; var self = this; var authCookieName = utils.assertString( options.authCookieName, 'authCookieName' ); var baseUrl = utils.assertString(options.baseUrl, 'baseUrl'); this.element = utils.assertElement(options.el); this._options = { authCookieName: authCookieName, baseUrl: baseUrl, }; this._options.messages = options.messages ? merge({}, messages, options.messages) : messages; this._options.checkAuth = typeof options.checkAuth === 'boolean' ? options.checkAuth : false; this._options.redirect = typeof options.redirect === 'boolean' ? options.redirect : false; this._options.redirectUrl = typeof options.redirectUrl === 'string' ? options.redirectUrl : undefined; // Set initial state this._state = { horizontal: !!options.horizontal, isLoggedIn: false, onSubmit: this.login.bind(this), passwordProps: { id: uniqueId('coins-logon-widget-'), name: 'password', type: 'password', }, redirectUrl: this._options.redirectUrl, usernameProps: { id: uniqueId('coins-logon-widget-'), name: 'username', }, }; // Configure auth auth.setOptions({ authCookieName: authCookieName, baseUrl: baseUrl, }); // Wire up events this.on(EVENTS.LOGIN, this.login); this.on(EVENTS.LOGIN_ERROR, this.onError); this.on(EVENTS.LOGIN_ACCOUNT_EXPIRED, this.onAccountExpired); this.on(EVENTS.LOGIN_ACCOUNT_WILL_EXPIRE, this.onAccountWillExpire); this.on(EVENTS.LOGIN_PASSWORD_EXPIRED, this.onPasswordExpired); this.on(EVENTS.LOGIN_PASSWORD_WILL_EXPIRE, this.onPasswordWillExpire); this.on(EVENTS.LOGIN_SUCCESS, this.onLogin); this.on(EVENTS.LOGOUT, this.logout); this.on(EVENTS.LOGOUT_ERROR, this.onError); this.on(EVENTS.LOGOUT_SUCCESS, this.onLogout); this.init(); // Perform an auth check if the option is set if (this._options.checkAuth) { auth.isLoggedIn().then(function(isLoggedIn) { if (isLoggedIn) { self.emit(EVENTS.LOGIN_SUCCESS, { username: auth.getUsername(), }); } }); } } /** * Inherit from `EventEmitter`: * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain#Example} */ CoinsLogonWidget.prototype = Object.create(EventEmitter.prototype); CoinsLogonWidget.prototype.constructor = CoinsLogonWidget; /* ========================================================================== Actions ========================================================================== */ CoinsLogonWidget.prototype.destroy = function() { this.update(null); this.removeAllListeners(); }; CoinsLogonWidget.prototype.getFormData = function() { var password = this.element.querySelector('input[name=password]').value; var username = this.element.querySelector('input[name=username]').value; return { password: password, username: username, }; }; CoinsLogonWidget.prototype.init = function() { this._tree = form(this._state); this._rootNode = createElement(this._tree); this.element.className = this.element.className + ' coins-logon-widget'; this.element.appendChild(this._rootNode); }; CoinsLogonWidget.prototype.login = function() { var self = this; var EVENTS = CoinsLogonWidget.EVENTS; var formData = this.getFormData(); var password = formData.password; var username = formData.username; var state; // Validate input (make sure it isn't empty) if (!username || !password) { state = {}; if (!username) { state.usernameProps = { errorMessage: 'Missing username!', onKeyPress: this.onKeyPress.bind(this), }; this.emit(EVENTS.INVALID, { field: 'username' }); } if (!password) { state.passwordProps = { errorMessage: 'Missing password!', onKeyPress: this.onKeyPress.bind(this), }; this.emit(EVENTS.INVALID, { field: 'password' }); } return this.update(state); } this.update({ isLoading: true }); return auth.login(username, password) .then(function(response) { /** * Successful authentication also contains information regarding * a user's account status. If the user's password or account is * expired the widget needs to handle it here. */ var accountExpiration = Date.parse(response.user.acctExpDate); var DAY = CoinsLogonWidget.DAY; var now = Date.now(); var passwordExpiration = Date.parse(response.user.passwordExpDate); self.update({ isLoading: false }); if (accountExpiration - now < DAY * 10) { self.emit(EVENTS.LOGIN_ACCOUNT_WILL_EXPIRE, response); } else if (passwordExpiration - now < DAY * 10) { self.emit(EVENTS.LOGIN_PASSWORD_WILL_EXPIRE, response); } else { self.emit(EVENTS.LOGIN_SUCCESS, response); } return response; }, function(error) { self.update({ isLoading: false }); if (error === 'Password expired') { self.emit(EVENTS.LOGIN_PASSWORD_EXPIRED, error); } else if (error === 'Account expired') { self.emit(EVENTS.LOGIN_ACCOUNT_EXPIRED, error); } else { self.emit(EVENTS.LOGIN_ERROR, error); } return error; }); }; CoinsLogonWidget.prototype.logout = function() { var EVENTS = CoinsLogonWidget.EVENTS; var self = this; this.update({ isLoading: true }); return auth.logout() .then(function(response) { self.update({ isLoading: false }); self.emit(EVENTS.LOGOUT_SUCCESS, response); return response; }, function(error) { self.update({ isLoading: false }); self.emit(EVENTS.LOGOUT_ERROR, error); return error; }); }; CoinsLogonWidget.prototype.update = function(newState) { if (newState === this._state) { return; } this._state = merge({}, this._state, newState); // Persist fields' values if not logged in if (!this._state.isLoggedIn) { var formData = this.getFormData(); this._state.passwordProps.value = formData.password; this._state.usernameProps.value = formData.username; } else { this._state.passwordProps.value = ''; } var newTree = form(this._state); var patches = diff(this._tree, newTree); this._rootNode = patch(this._rootNode, patches); this._tree = newTree; }; /* ========================================================================== Built-in event listeners ========================================================================== */ CoinsLogonWidget.prototype.onAccountExpired = function() { var message = utils.callOrReturn(this._options.messages.accountExpired); this.update({ errorMessage: message }); }; CoinsLogonWidget.prototype.onAccountWillExpire = function(response) { var accountExpiration = Date.parse(response.user.acctExpDate); var now = Date.now(); var message = utils.callOrReturn( this._options.messages.accountWillExpire, [utils.formatDay((accountExpiration - now) / CoinsLogonWidget.DAY)] ); this.update({ errorMessage: message }); }; CoinsLogonWidget.prototype.onError = function(error) { var message; if (typeof error !== 'undefined') { message = error.message || error.toString(); } else { message = 'Unknown error!'; } this.update({ errorMessage: message }); console.error(error); }; CoinsLogonWidget.prototype.onKeyPress = function(event) { var clearErrorProps = { errorMessage: null, onKeyPress: null, }; if (event.target.name === 'username') { this.update({ usernameProps: clearErrorProps }); } if (event.target.name === 'password') { this.update({ passwordProps: clearErrorProps }); } }; CoinsLogonWidget.prototype.onLogin = function(response) { // Redirect if the option is set and there's a URL if (this._options.redirect && this._options.redirectUrl) { window.location = this._options.redirectUrl; } else { this.update({ isLoggedIn: true, username: response.username, }); } }; CoinsLogonWidget.prototype.onLogout = function() { this.update({ isLoggedIn: false }); }; CoinsLogonWidget.prototype.onPasswordExpired = function() { var message = utils.callOrReturn(this._options.messages.passwordExpired); this.update({ errorMessage: message }); }; CoinsLogonWidget.prototype.onPasswordWillExpire = function(response) { var passwordExpiration = Date.parse(response.user.passwordExpDate); var now = Date.now(); var message = utils.callOrReturn( this._options.messages.passwordWillExpire, [utils.formatDay((passwordExpiration - now) / CoinsLogonWidget.DAY)] ); this.update({ errorMessage: message }); }; /* ========================================================================== Static properties ========================================================================== */ CoinsLogonWidget.DAY = 24 * 60 * 60 * 1000; CoinsLogonWidget.EVENTS = { INVALID: 'invalid', LOGIN: 'login:init', LOGIN_ERROR: 'login:error', LOGIN_ACCOUNT_EXPIRED: 'login:accountExpired', LOGIN_ACCOUNT_WILL_EXPIRE: 'login:accountWillExpire', LOGIN_PASSWORD_EXPIRED: 'login:passwordExpired', LOGIN_PASSWORD_WILL_EXPIRE: 'login:passwordWillExpire', LOGIN_SUCCESS: 'login:success', LOGOUT: 'logout:init', LOGOUT_ERROR: 'logout:error', LOGOUT_SUCCESS: 'logout:success' }; module.exports = CoinsLogonWidget;