Home Reference Source

src/auth.js

/*
 * OS.js - JavaScript Cloud/Web Desktop Platform
 *
 * Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @license Simplified BSD License
 */
import Cookies from 'js-cookie';
import Login from './login';
import serverAuth  from './adapters/auth/server';
import localStorageAuth from './adapters/auth/localstorage';
import logger from './logger';

const defaultAdapters = {
  server: serverAuth,
  localStorage: localStorageAuth
};

const createUi = (core, options) => {
  const defaultUi = core.config('auth.ui', {});

  return options.login
    ? options.login(core, options.config || {})
    : new Login(core, options.ui || defaultUi);
};

const createAdapter = (core, options) => {
  const adapter = core.config('standalone')
    ? localStorageAuth
    : typeof options.adapter === 'function'
      ? options.adapter
      : defaultAdapters[options.adapter || 'server'];

  return {
    login: () => Promise.reject(new Error('Not implemented')),
    logout: () => Promise.reject(new Error('Not implemented')),
    register: () => Promise.reject(new Error('Not implemented')),
    init: () => Promise.resolve(true),
    destroy: () => {},
    ...adapter(core, options.config || {})
  };
};

/**
 * TODO: typedef
 * @typedef {Object} AuthAdapter
 */

/**
 * TODO: typedef
 * @typedef {Object} AuthAdapterConfig
 */

/**
 * @typedef {Object} AuthForm
 * @property {string} [username]
 * @property {string} [password]
 */

/**
 * @callback AuthAdapterCallback
 * @param {Core} core
 * @return {AuthAdapter}
 */

/**
 * @callback LoginAdapterCallback
 * @param {Core} core
 * @return {Login}
 */

/**
 * @callback AuthCallback
 * @param {AuthForm} data
 * @return {boolean}
 */

/**
 * @typedef {Object} AuthSettings
 * @property {AuthAdapterCallback|AuthAdapter} [adapter] Adapter to use
 * @property {LoginAdapterCallback|Login} [login] Login Adapter to use
 * @property {AuthAdapterConfig} [config] Adapter configuration
 */

/**
 * Handles Authentication
 */
export default class Auth {

  /**
   * @param {Core} core OS.js Core instance reference
   * @param {AuthSettings} [options={}] Auth Options
   */
  constructor(core, options = {}) {
    /**
     * Authentication UI
     * @type {Login}
     * @readonly
     */
    this.ui = createUi(core, options);

    /**
     * Authentication adapter
     * @type {AuthAdapter}
     * @readonly
     */
    this.adapter = createAdapter(core, options);

    /**
     * Authentication callback function
     * @type {AuthCallback}
     * @readonly
     */
    this.callback = function() {};

    /**
     * Core instance reference
     * @type {Core}
     * @readonly
     */
    this.core = core;
  }

  /**
   * Initializes authentication handler
   */
  init() {
    this.ui.on('login:post', values => this.login(values));
    this.ui.on('register:post', values => this.register(values));

    return this.adapter.init();
  }

  /**
   * Destroy authentication handler
   */
  destroy() {
    this.ui.destroy();
  }

  /**
   * Run the shutdown procedure
   * @param {boolean} [reload] Reload afterwards
   */
  shutdown(reload) {
    try {
      this.core.destroy();
    } catch (e) {
      logger.warn('Exception on destruction', e);
    }

    this.core.emit('osjs/core:logged-out');

    if (reload) {
      setTimeout(() => {
        window.location.reload();
        // FIXME Reload, not refresh
        // this.core.boot();
      }, 1);
    }
  }

  /**
   * Shows Login UI
   * @param {AuthCallback} cb Authentication callback
   * @return {Promise<boolean>}
   */
  show(cb) {
    const login = this.core.config('auth.login', {});
    const autologin = login.username && login.password;
    const settings = this.core.config('auth.cookie');

    this.callback = cb;
    this.ui.init(autologin);

    if (autologin) {
      return this.login(login);
    } else if (settings.enabled) {
      const cookie = Cookies.get(settings.name);
      console.warn(cookie);
      if (cookie) {
        return this.login(JSON.parse(cookie));
      }
    }

    return Promise.resolve(true);
  }

  /**
   * Performs a login
   * @param {AuthForm} values Form values as JSON
   * @return {Promise<boolean>}
   */
  login(values) {
    this.ui.emit('login:start');

    return this.adapter
      .login(values)
      .then(response => {
        if (response) {
          const settings = this.core.config('auth.cookie');
          if (settings.enabled) {
            Cookies.set(settings.name, JSON.stringify(values), {
              expires: settings.expires,
              sameSite: 'strict'
            });
          } else {
            Cookies.remove(settings.name);
          }

          this.ui.destroy();
          this.callback(response);

          this.core.emit('osjs/core:logged-in');
          this.ui.emit('login:stop', response);

          return true;
        }

        return false;
      })
      .catch(e => {
        if (this.core.config('development')) {
          logger.warn('Exception on login', e);
        }

        this.ui.emit('login:error', 'Login failed', e);
        this.ui.emit('login:stop');

        return false;
      });
  }

  /**
   * Performs a logout
   * @param {boolean} [reload=true] Reload client afterwards
   * @return {Promise<boolean>}
   */
  logout(reload = true) {
    return this.adapter.logout(reload)
      .then(response => {
        if (!response) {
          return false;
        }

        const settings = this.core.config('auth.cookie');
        Cookies.remove(settings.name);

        this.shutdown(reload);

        return true;
      });
  }

  /**
   * Performs a register call
   * @param {AuthForm} values Form values as JSON
   * @return {Promise<*>}
   */
  register(values) {
    this.ui.emit('register:start');

    return this.adapter
      .register(values)
      .then(response => {
        if (response) {
          this.ui.emit('register:stop', response);

          return response;
        }

        return false;
      })
      .catch(e => {
        if (this.core.config('development')) {
          logger.warn('Exception on registration', e);
        }

        this.ui.emit('register:error', 'Registration failed');
        this.ui.emit('register:stop');

        return false;
      });
  }
}