auth/model/KeyAction.js

/**
 * @module flitter-auth/model/KeyAction
 */

const Model = require('flitter-orm/src/model/Model')
const uuid = require('uuid/v4')
const { ObjectId } = require('mongodb')

/**
 * Represents a single available key action. Key actions
 * are one-time use links that directly call a method on
 * a controller. These actions:
 *
 * - Can pass along context
 * - Have expiration dates
 * - Are single-use only
 * - Can automatically log in a user during the request lifecycle
 * @extends module:flitter-orm/src/model/Model~Model
 */
class KeyAction extends Model {
    /**
     * Defines the services required by this model.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'models', 'configs']
    }

    /**
     * Gets the schema for this model. Provides the following fields:
     *  - key: String [uuid]
     *  - secret: String [uuid]
     *  - user_id: ObjectId
     *  - handler: String
     *  - created: Date [now]
     *  - expires: Date [now +1 day]
     *  - used: Boolean
     *  - auto_login: Boolean
     *  - no_auto_logout: Boolean
     *  - did_auto_login: Boolean
     *  - data: String ['{}']
     * @type {object}
     */
    static get schema() {
        return {
            key: { type: String, default: uuid },
            secret: { type: String, default: uuid },
            user_id: ObjectId,
            handler: String,
            created: { type: Date, default: () => new Date },
            expires: {
                type: Date,
                default: () => {
                    const date = new Date
                    date.setDate(date.getDate() + 1)
                    return date
                }
            },
            used: Boolean,
            auto_login: Boolean,
            no_auto_logout: Boolean,
            did_auto_login: Boolean,
            data: { type: String, default: '{}' },
        }
    }

    /**
     * Generate a filter object for this model that restricts
     * the result set to unused, unexpired key actions.
     * @returns {Promise<module:flitter-orm/src/filter/Filter~Filter>}
     */
    static async availableFilter() {
        const filter = await this.filter()
        filter.field('expires').greater_than(new Date).end()
        filter.field('used').not().equal(true).end().end()
        return filter
    }

    /**
     * Lookup a single key action based on the passed in filter parameters.
     * Automatically restricts the result set to unused, unexpired key actions.
     * @param {object} params
     * @returns {Promise<module:flitter-auth/model/KeyAction~KeyAction>}
     */
    static async lookup(params) {
        const filter = await this.availableFilter()
        filter.absorb(params)
        return filter.end().findOne()
    }

    /**
     * Get a value from the action's metadata.
     * @param {string} key
     * @returns {*}
     */
    data_get(key) {
        return JSON.parse(this.data ? this.data : '{}')[key]
    }

    /**
     * Set a value in the action's metadata.
     * @param {string} key
     * @param {*} value
     */
    data_set(key, value) {
        const data = JSON.parse(this.data ? this.data : '{}')
        data[key] = value
        this.data = JSON.stringify(data)
    }

    /**
     * Get the URL path for this key action. Uses the 'app.url' config.
     * @returns {string}
     */
    url() {
        const config_app_url = this.configs.get('app.url')
        const base_url = config_app_url.endsWith('/') ? config_app_url : `${config_app_url}/`
        return `${base_url}auth/action/${this.key}`
    }

    /**
     * Fetch the associated user for this action.
     * @returns {Promise<module:flitter-auth/model/User~BaseUser|void>}
     */
    async user() {
        const User = this.models.get('auth:User')
        return this.has_one(User, 'user_id', '_id')
    }

    /**
     * Close out the key action. If did_auto_login is set and no_auto_logout is not set,
     * log out the user and remove the key action from the session for the provided request.
     * @param {express/request} request - the request
     * @returns {Promise<void>}
     */
    async close(request) {
        if ( request.key_action && request.key_action.did_auto_login && !request.key_action.no_auto_logout ) {
            const action = request.key_action
            delete request.key_action
            delete request.session.key_action_key

            if ( action.user_id && action.auto_login && request.is_auth ) {
                const provider = await request.security.provider()
                await provider.logout(request)
                await request.session.save()
            }
        }
    }
}

module.exports = exports = KeyAction