auth/model/User.js

const Model = require('flitter-orm/src/model/Model')

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

const uuid = require('uuid/v4')

/**
 * Base class for the flitter-auth User model.
 * @extends module:flitter-orm/src/model/Model~Model
 */
class BaseUser extends Model {
    /**
     * Defines the services required by this model.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'configs', 'auth']
    }

    /**
     * Defines the schema for this model.
     * @returns {object}
     */
    static get schema() {
        return {
            uid: String,
            provider: { type: String, default: 'flitter' },
            data: { type: String, default: '{}' },
            uuid: { type: String, default: uuid },
            roles: [String],
            permissions: [String],
            last_auth: Date,
            block_login: Boolean,

            // Fields needed by the flitter provider:
            password: String,
        }
    }

    static async authFilter() {
        const filter = (await this.filter()).field('block_login')
            .not().equal(true).end()
            .end()

        return filter
    }

    static async lookup(params) {
        const filter = await this.authFilter()
        filter.absorb(params)
        return filter.end().findOne()
    }

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

    /**
     * Stores a value in the user's serialized JSON.
     * @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)
    }

    /**
     * Checks if an array of permissions contains a permission that
     * permits the passed in permission.
     * @example
     * _array_allow_permission(['foo:bar', 'baz'], 'foo') // false
     * @example
     * _array_allow_permission(['foo:bar', 'baz'], 'foo:bar') // true
     * @example
     * _array_allow_permission(['foo:bar', 'baz'], 'foo:bar:view') // true
     * @param {Array<string>} array_of_permissions
     * @param {string} permission
     * @returns {boolean} - true if the permission check passed
     */
    _array_allow_permission(array_of_permissions, permission) {
        const permission_parts = permission.split(':')

        for ( let i = permission_parts.length; i > 0; i-- ) {
            const permission_string = permission_parts.slice(0, i).join(':')
            if ( array_of_permissions.includes(permission_string) ) return true
        }

        return false
    }

    /**
     * Checks if the specified role name contains the specified permission.
     * Accounts for permissions with arguments. (e.g. view_image:2234)
     * @param {string} role - the name of the role
     * @param {string} permission - the name of the permission
     * @returns {boolean} - true if the role contains the permission
     */
    role_has_permission(role, permission){
        const role_config = this.configs.get('auth.roles')[role]
        if ( !role_config ) return false

        return this._array_allow_permission(role_config, permission)
    }

    /**
     * Checks if a user has the specified permission.
     * Permissions may include an argument specified like so:
     *      view_image:3348
     * If no argument is provided, it is assumed that the user
     * has permission regardless of the argument.
     *
     * @param {String} permission
     * @returns {Boolean} - true if the user has permission
     */
    can(permission){
        // Check roles first:
        for ( let role_key in this.roles ){
            if ( !this.roles.hasOwnProperty(role_key) ) continue
            if ( this.role_has_permission(this.roles[role_key], permission) ) return true
        }

        // Check user permissions:
        return this._array_allow_permission(this.permissions, permission)
    }

    /**
     * Applies the specified role to the user, if they do not already have it.
     * @param {string} role
     */
    promote(role){
        if ( !this.roles.includes(role) ) this.roles.push(role)
    }

    /**
     * Strips the specified role from the user, if they have it.
     * @param {string} role
     */
    demote(role){
        if ( this.roles.includes(role) ) this.roles = this.roles.filter(r => r !== role)
    }

    /**
     * Applies the specified permission to the user if their current permission set
     * doesn't already allow them to access it, or if the force flag is set.
     * @param {string} permission
     * @param {Boolean} [force = false] - if true, add the permission to the user even if it is covered by a generic permission or role
     */
    allow(permission, force = false){
        if ( !this.can(permission) || (force && !this.permissions.includes(permission)) ) this.permissions.push(permission)
    }

    /**
     * Strips the permission from the user, if they have it.
     * @param {string} permission
     */
    disallow(permission){
        if ( this.permissions.includes(permission) ) this.permissions = this.permissions.filter(p => p !== permission)
    }

    /**
     * Get the auth provider this user is managed through.
     * @returns {module:flitter-auth/Provider~Provider}
     */
    get_provider() {
        return this.auth.get_provider(this.provider)
    }
}

module.exports = exports = BaseUser