Home Reference Source

js/models/Auth.js

import User from '~/models/User';
import Data, { Key } from '~/models/Data';
import { Bugsnag } from '~/helpers/Bugsnag';
import axios from 'axios/dist/axios.min.js';

import ModalController from '~/controllers/ModalController';
import AuthModalTemplate from '~/template/login/AuthModalTemplate';

import { from, fromEvent } from 'rxjs/index';
import { filter, first, map, tap } from 'rxjs/operators';

const oauthData = Data.shared.encodedJSONForKey(Key.loginData);

/**
 * @typedef {Object} AuthConfig
 * @property {boolean} [append=false] - If logged in, append to current user.
 */

/**
 * Manages authorization use `Auth.shared()` to get global instance
 */
class Auth {
    static Unauthorized = Symbol('Auth.Unauthorized');

    /**
     * OAuth endpoint
     * @param {string} site - Site ID
     * @param {Object} options
     * @param {boolean} [options.clientOnly=false] - A boolean specifying it to use client flow.
     * @param {AuthConfig} [options.authConfig={}] - authorization config
     * @return {string} URL to put into link
     */
    static oauthEndpointForSite(site, { clientOnly = false, authConfig = {} } = {}) {
        const siteData = oauthData.sites[site];
        const options = {
            provider: site,
            redirect: location.href,
            clientOnly: clientOnly,
            authConfig: authConfig
        };

        return `${siteData.authorize}?${Object.entries({
            client_id: siteData.client,
            scope: siteData.scopes.join(" "),
            state: Buffer.from(JSON.stringify(options)).toString('base64'),
            redirect_uri: oauthData.redirect_uri,
            response_type: 'code'
        }).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')}`;
    }

    /**
     * Authorizes for an OAuth site with auth config using CLIENT flow.
     * @param {string} site - site ID
     * @param {Object} [options] - Options for {@link Auth.oauthEndpointForSite}
     * @return {Promise<string>} Resolves to auth key.
     * @async
     */
    authorizeOAuth(site, options = {}) {
        return new Promise((resolve, reject) => {
            const endpoint = Auth.oauthEndpointForSite(site, options);

            // If we're using a server side flow then we'll stick to that
            if (!options.clientOnly) {
                window.location.href = endpoint;
                return;
            }

            const authWindow = window.open(endpoint, `Login to Axtell`, 'width=800,height=800,toolbar=0,menubar=0');

            fromEvent(window, 'message')
                .pipe(
                    filter(event => event.source === authWindow),
                    map(event => event.data || {}),
                    filter(data => data.oauth),
                    first(),
                    map(data => data.success),
                    tap(() => authWindow.postMessage({ received: true }, '*')))
                .subscribe(isAuthorizationSuccess => {
                    console.log(isAuthorizationSuccess);
                    if (isAuthorizationSuccess) {
                        resolve();
                    } else {
                        reject();
                    }
                });
        });
    }

    /**
     * Don't use this. Use `Auth.shared()`
     */
    constructor() {
        this._user = null;

        this._isAuthorized = null;
    }

    /**
     * Returns global instance of `Auth`
     * @type {Promise<Auth>}
     */
    static get shared() {
        if (Auth._shared !== null)
            return Auth._shared;

        const auth = new Auth();
        const user = auth.user;
        Auth._shared = auth;

        // Since now that the auth it setup, we'll setup bugsnag user info
        if (Bugsnag) {
            if (user !== Auth.Unauthorized) {
                Bugsnag.user = {
                    authorized: true,
                    id: user.id,
                    name: user.name
                };
            } else {
                Bugsnag.user = {
                    authorized: false
                };
            }
        }

        return auth;
    }

    /**
     * Determines if user is authorized at the moment of call.
     * @return {Boolean} this is sync
     */
    get isAuthorized() {
        return Data.shared.hasKey('me');
    }

    /**
     * Gets the current user.
     *
     * @type {?User} the current logged in user. Resolves to `Unauthorized` if
     *               not logged in.
     */
    get user() {
        if (this.isAuthorized) {
            const value = Data.shared.encodedJSONForKey('me');
            return User.fromJSON(value);
        } else {
            return Auth.Unauthorized;
        }
    }

    /**
     * Logs the given user out. You must reload the pages for changes.
     */
    async logout() {
        await axios.post('/user/logout');
    }

    /**
     * Ensures the user is logged in.
     * @param {?string} reason Why this is being ensured.
     * @return {boolean} `false` is user refused to login and is not logged in.
     */
    async ensureLoggedIn(reason = null) {
        if (this.isAuthorized) return true;

        alert('You must be logged in to continue using this feature. (note: this is a temporary alert, will be replaced later)');
        return false;
    }

    /**
     * Logs into a code-golf user using a JWT authorization key.
     * @param {AuthJWTToken} authData - Authorization data
     * @return {Promise} resolves to a {@link User} of the logged in user.
     */
    async loginJWT(authData) {
        await axios.post(
            '/auth/login/jwt',
            authData.json
        );
    }
}

Auth._shared = null;

/**
 * @typedef {Object} AuthProfile
 * @property {string} email
 * @property {string} name
 * @property {?string} avatar
 */

/**
 * Manages data for a login authorization instance
 * @implements {JSONConvertable}
 */
export class AuthJWTToken {
    /**
     * @param {string} jwt - Base-64 JWT representing a OpenID auth JSON.
     * @param {AuthProfile} profile - Profile information
     * @param {Object} options - auth options
     */
    constructor(authToken, profile, options) {
        this._authToken = authToken;
        this._profile = profile;
        this._options = options;
    }

    /** @override */
    get json() {
        return {
            token: this._authToken,
            profile: this._profile,
            authConfig: this._options
        };
    }
}

export default Auth;