Home Reference Source Repository

js/Controllers/ControllerAuthentication.js

import BaseController from 'js/Controllers/BaseController';
import Configuration from 'js/Configuration';
import Cookie from 'js/Shared/Cookie';
import RODAN_EVENTS from 'js/Shared/RODAN_EVENTS';
import Radio from 'backbone.radio';
import User from 'js/Models/User';

/**
 * Controls authentication.
 */
export default class ControllerAuthentication extends BaseController
{
///////////////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS
///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Initializes the instance.
     *
     * How authentication behaves depends on Configuration.SERVER_AUTHENTICATION_TYPE.
     */
    initialize()
    {
        this._user = null;
        if (Configuration.SERVER_AUTHENTICATION_TYPE === 'session')
        {
            this._token = new Cookie('csrftoken');
        }
        else if (Configuration.SERVER_AUTHENTICATION_TYPE === 'token')
        {
            this._token = new Cookie('token');
        }
        else
        {
            /** @todo throw error if bad authentication type. */
        }
    }

    /**
     * AJAX prefilter associated with authentication.
     *
     * This will make sure that the appropriate request headers for authentication are set on all AJAX requests to the server.
     *
     * @param {object} options object.beforeSend (optional) is a function that takes in the XmlHTTPRequest before sending; this may be useful for doing pre-processing of AJAX requests
     */
    ajaxPrefilter(options)
    {
        var that = this;
        var oldOnBeforeSend = options.beforeSend;
        if (Configuration.SERVER_AUTHENTICATION_TYPE === 'session' && !options.beforeSend) 
        {
            options.xhrFields = { withCredentials: true };
            options.beforeSend = function (xhr) 
            {
                if (oldOnBeforeSend)
                {
                    oldOnBeforeSend(xhr);
                }
                xhr.setRequestHeader('X-CSRFToken', that._token.value);
            };
        }
        else if(Configuration.SERVER_AUTHENTICATION_TYPE === 'token')
        {
            options.beforeSend = function (xhr)
            {
                if (oldOnBeforeSend)
                {
                    oldOnBeforeSend(xhr);
                }
                xhr.setRequestHeader('Authorization', 'Token ' + that._token.value);
            };
        }
    }

///////////////////////////////////////////////////////////////////////////////////////
// PRIVATE METHODS
///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Initialize radio.
     */
    _initializeRadio()
    {
        Radio.channel('rodan').reply(RODAN_EVENTS.REQUEST__AUTHENTICATION_USER, () => this._handleRequestUser());
        Radio.channel('rodan').reply(RODAN_EVENTS.REQUEST__AUTHENTICATION_LOGIN, options => this._login(options));
        Radio.channel('rodan').reply(RODAN_EVENTS.REQUEST__AUTHENTICATION_CHECK, () => this._checkAuthenticationStatus());
        Radio.channel('rodan').reply(RODAN_EVENTS.REQUEST__AUTHENTICATION_LOGOUT, () => this._logout());
    }

    /**
     * Handle authentication response.
     */
    _handleAuthenticationResponse(event)
    {
        var request = event.currentTarget;
        if (request.responseText === null)
        {
            Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_ERROR_NULL);
        }
        
        switch (request.status)
        {
            case 200:
                var parsed = JSON.parse(request.responseText);
                this._user = new User(parsed);
                this._processAuthenticationData();
                Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGIN_SUCCESS, {user: this._user});
                break;
            case 400:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGINREQUIRED);
                break;
            case 401:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request,
                                                                           message: 'Incorrect username/password.'});
                Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGINREQUIRED);
                break;
            case 403:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGINREQUIRED);
                break;
            default:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                break;
        }
    }

    /**
     * Handle deauthentication response.
     */
    _handleDeauthenticationResponse(event)
    {
        var request = event.currentTarget;
        if (request.responseText === null)
        {
            Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_ERROR_NULL);
        }

        switch (request.status)
        {
            case 200:
                Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGOUT_SUCCESS);
                break;
            case 400:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                break;
            case 401:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                break;
            case 403:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                break;
            default:
                Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request});
                break;
        }
    }

    /**
     * Handle timeout.
     */
    _handleTimeout(event)
    {
        Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__SERVER_WENTAWAY, {event: event});
    }

    /**
     * Sends request to check authentication.
     */
    _checkAuthenticationStatus()
    {
        // First, check if we have the appropriate authentication data. If we do, check it.
        // If we don't, trigger an event to inform of login require.
        if (this._token.value === '')
        {
            Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGINREQUIRED);
        }
        else
        {
            var authRoute = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, 'auth-me');
            var request = new XMLHttpRequest();
            request.onload = (event) => this._handleAuthenticationResponse(event);
            request.ontimeout = (event) => this._handleTimeout(event);
            request.open('GET', authRoute, true);
            request.setRequestHeader('Accept', 'application/json');
            this._setAuthenticationData(request);
            request.send();
        }
    }

    /**
     * Login.
     */
    _login(options)
    {
        var authRoute = this._getAuthenticationRoute();
        var authType = Configuration.SERVER_AUTHENTICATION_TYPE;
        var request = new XMLHttpRequest();
        request.onload = (event) => this._handleAuthenticationResponse(event);
        request.ontimeout = (event) => this._handleTimeout(event);
        request.open('POST', authRoute, true);
        if (authType === 'session')
        {
            request.withCredentials = true;
        }
        request.setRequestHeader('Accept', 'application/json');
        request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        request.send('username=' + options.username + '&password=' + options.password);
    }

    /**
     * Logout.
     */
    _logout()
    {
        var authRoute = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, 'auth-reset-token');
        var request = new XMLHttpRequest();
        request.onload = (event) => this._handleDeauthenticationResponse(event);
        request.ontimeout = (event) => this._handleTimeout(event);
        request.open('POST', authRoute, true);
        request.setRequestHeader('Accept', 'application/json');
        this._setAuthenticationData(request);
        this._deleteAuthenticationData();
        request.send();
        this._user = null;
    }

    /**
     * Sets the appropriate authentication data to the request.
     */
    _setAuthenticationData(request)
    {
        if (Configuration.SERVER_AUTHENTICATION_TYPE === 'token')
        {
            request.setRequestHeader('Authorization', 'Token ' + this._token.value);
        }
        else if (Configuration.SERVER_AUTHENTICATION_TYPE === 'session')
        {
            request.withCredentials = true;
            request.setRequestHeader('X-CSRFToken', this._token.value);
        }   
    }

    /**
     * Deletes authentication data.
     */
    _deleteAuthenticationData()
    {
        // Only need to worry about token authentication.
        if (Configuration.SERVER_AUTHENTICATION_TYPE === 'token')
        {
            Cookie.saveCookie('token', '', 0);
            this._token = new Cookie('token');
        }
    }

    /** 
     * Save authentication data.
     */
    _processAuthenticationData()
    {
        if (Configuration.SERVER_AUTHENTICATION_TYPE === 'token' && this._user.has('token'))
        {
            Cookie.saveCookie('token', this._user.get('token'), 365);
            this._token = new Cookie('token');
        }
        else if (Configuration.SERVER_AUTHENTICATION_TYPE === 'session')
        {
            this._token = new Cookie('csrftoken');
        }
    }

    /**
     * Send out active user.
     */
    _handleRequestUser()
    {
        return this._user;
    }

    /**
     * Returns authentication route.
     */
    _getAuthenticationRoute()
    {
        switch (Configuration.SERVER_AUTHENTICATION_TYPE)
        {
            case 'session':
            {
                return Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, 'session-auth');
            }

            case 'token':
            {
                return Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, 'auth-token');
            }

            default:
            {
                console.error('An acceptable Authentication Type was not provided');
                break;
            }
        }
    }
}