e-ucm/rage-analytics-backend

View on GitHub
lib/auth-tokens.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

/**
 * Manages the gameplays stored. A new gameplay collection is created for each game version
 * ('gameplays_<versionId>') with:
 *
 *      {
 *          playerId: <String>,
 *          sessions: <Integer>,    - This value increases each time we invoke the 'start' method.
 *          started: <Date>         - The moment this gameplay was started. Either when we've received a
 *                                     '/api/collector/start/:trackingCode' request or when we've received
 *                                     a Statement with a special(reserved) verb id (more info. at 'traces.js')
 *      }
 *
 * Manages a collection of authorization tokens, named 'authtokens', with the following documents:
 *
 *     {
 *          authToken: <String>,    - version._id + gameplay._id + <10_random_string_characters> + gameplay.sessions.
 *          gameplayId: <String>,
 *          versionId: <String>,
 *          playerId: <String>,
 *          lastAccessed: <Date>    - Updated each time new data is sent to be tracked.
 *     }
 */

module.exports = (function () {
    var Q = require('q');
    var Collection = require('easy-collections');
    var authTokens = new Collection(require('./db'), 'authtokens');
    var gameplays = require('./gameplays');

    var Validator = require('jsonschema').Validator;
    var v = new Validator();

    /*
     {
     "_id" : ObjectId("5a7b19f223795a0081bc8328"),
     "authToken" : "5a7b195623795a0081bc83255a7b19f223795a0081bc83270976801521",
     "gameplayId" : ObjectId("5a7b19f223795a0081bc8327"),
     "versionId" : ObjectId("5a7b195623795a0081bc8325"),
     "session" : 1,
     "playerId" : ObjectId("5a7b19f223795a0081bc8326"),
     "lastAccessed" : ISODate("2018-02-07T15:24:11.124Z"),
     "firstSessionStarted" : ISODate("2018-02-07T15:23:30.750Z"),
     "currentSessionStarted" : ISODate("2018-02-07T15:23:30.750Z")
     }
     */

    var authTokenSchema = {
        id: '/AuthTokenSchema',
        type: 'object',
        properties: {
            authToken: { type: 'string'},
            gameplayId: { type: 'ID'},
            versionId: { type: 'ID'},
            activityId: { type: 'ID'},
            session: { type: 'number'},
            playerId: { type: 'ID'},
            lastAccessed: { type: 'date'},
            firstSessionStarted: { type: 'date'},
            currentSessionStarted: { type: 'date'}
        },
        required: ['authToken','gameplayId','versionId','session','playerId','lastAccessed','firstSessionStarted','currentSessionStarted'],
        additionalProperties: false
    };
    v.addSchema(authTokenSchema, '/AuthTokenSchema');

    var authTokenSchemaPut = {
        id: '/AuthTokenPutSchema',
        type: 'object',
        properties: {
            lastAccessed: { type: 'date'}
        },
        additionalProperties: false,
        minProperties: 1,
        maxProperties: 1
    };
    v.addSchema(authTokenSchemaPut, '/AuthTokenPutSchema');

    var token = function () {
        return Math.random().toString(10).substr(10);
    };

    /**
     * If there is an existing gameplay with the given 'version._id' for the given 'playerId' it increases its
     * 'sessions' counter by one.
     * Otherwise will create a new gameplay for that 'playerId' with the 'sessions' attribute initialized to 1 and
     * the 'started' attribute set to the current date.
     *
     * Note that if a new gameplay is created it will be created with 1 session directly.
     *
     * Afterwards creates a new authorization document with the following information:
     *
     *      {
     *          authToken: authToken,       - version._id + gameplay._id + <10_random_string_characters> + gameplay.sessions.
     *          gameplayId: gameplay._id,   - The ID of the gameplay that was recently created or whose 'sessions'
     *                                         attribute has just been increased by 1.
     *          versionId: version._id,
     *          playerId: playerId,
     *          lastAccessed: <currentDate>
     *      }
     *
     * This method is invoked when we receive an '/api/collector/start/:trackingCode' request, right after
     * ensuring that the 'trackingCode' is correct and querying the player depending of an 'Authorization' header.
     * More info. can be found at 'collector.js'@start function.
     *
     * @param playerId
     * @param trackingCode
     * @param version
     * @returns the newly created authorization token document.
     */
    authTokens.start = function (player, version, activity) {
        var id = activity ? activity._id : version._id;

        return gameplays.find(id, player)
            .then(function (gameplay) {
                // To assure uniqueness in the authToken, but also randomness
                var authorization = version._id + gameplay._id + token() + gameplay.sessions;

                return gameplays.startAttempt(id, player, authorization).then(function (attempt) {
                    // If it's the first session 'currentSessionStarted' and 'firstSessionStarted' must be the same
                    var currentTime = new Date();
                    var authToken = {
                        authToken: authorization,
                        gameplayId: gameplay._id,
                        versionId: version._id,
                        session: attempt.number,
                        playerId: player._id,
                        lastAccessed: currentTime,
                        firstSessionStarted: gameplay.firstSessionStarted,
                        currentSessionStarted: attempt.number === 1 ? gameplay.firstSessionStarted : currentTime
                    };

                    if (activity) {
                        authToken.activityId = activity._id;
                    }

                    var validationObj = v.validate(authToken, authTokenSchema);
                    if (validationObj.errors && validationObj.errors.length > 0) {
                        throw {
                            message: 'Course bad format: ' + validationObj.errors[0],
                            status: 400
                        };
                    }

                    return authTokens.insert(authToken)
                        .then(function (authToken) {
                            // First we create a new attempt
                            return {
                                authToken: authToken.authToken,
                                session: authToken.session,
                                firstSessionStarted: authToken.firstSessionStarted,
                                currentSessionStarted: authToken.currentSessionStarted
                            };
                        });
                });
            });
    };

    /**
     * Ensures that a document with the given 'authorization' exists and updates its 'lastAccessed' attribute.
     *
     * @param authorization
     * @returns 401 - Meaning that that 'authorization' is invalid.
     */
    authTokens.track = function (authorization) {
        var deferred = Q.defer();
        var set = {
            lastAccessed: new Date()
        };

        // Local validation (possibly unnecesary)
        var validationObj = v.validate(set, authTokenSchemaPut);
        if (validationObj.errors && validationObj.errors.length > 0) {
            throw {
                message: 'AuthToken bad format: ' + validationObj.errors[0],
                status: 400
            };
        }
        authTokens.collection().findOneAndUpdate({
            authToken: authorization
        }, { $set: set }, {
            returnOriginal: false,
            sort: {
                _id: 1
            }
        }).then(function (authToken) {
            if (!authToken.ok) {
                deferred.reject({ status: 401 });
                return;
            }
            deferred.resolve(authToken.value);
        }).catch(function (err) {
            deferred.reject(err);
        });

        return deferred.promise;
    };

    /**
     * Ends the attempt for an 'authorization'
     *
     * @param authorization
     * @returns 401 - Meaning that that 'authorization' is invalid.
     */
    authTokens.end = function (authorization) {
        return authTokens.track(authorization, true)
            .then(function (authToken) {
                var id = authToken.activityId ? authToken.activityId : authToken.versionId;
                return gameplays.endAttempt(id, authToken.authToken);
            });
    };

    return authTokens;
})();