# JSDoc: Source: schemas/permissionSchema.js

# Source: schemas/permissionSchema.js

    /*Canopy - The next generation of stoner streaming software
    Copyright (C) 2024-2025 Rainbownapkin and the TTN Community

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.*/

    //NPM Imports
    const {mongoose} = require('mongoose');

    //Local Imports
    const userModel = require('./user/userSchema');
    const channelPermissionSchema = require('./channel/channelPermissionSchema');
    const {errorHandler} = require('../utils/loggerUtils');

    //This originally belonged to the permissionSchema, but this avoids circular dependencies.
    //We could update all references but quite honestly I that would be uglier, this should have a copy too...
    const rankEnum = channelPermissionSchema.statics.rankEnum;

    /**
     * DB Schema for the singular site-wide permission document
     */
    const permissionSchema = new mongoose.Schema({
        adminPanel: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        changeRank: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        changePerms: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        announce: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        resetToke: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        editTokeCommands: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        banUser: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        nukeUser: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        genPasswordReset: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        registerChannel: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        editEmotes: {
            type: mongoose.SchemaTypes.String,
            enum: rankEnum,
            default: "admin",
            required: true
        },
        channelOverrides: {
            type: channelPermissionSchema,
            default: () => ({})
        },
    });

    //Statics
    permissionSchema.statics.rankEnum = rankEnum;

    /**
     * Returns the server's singular permission document from the DB
     * @returns {Mongoose.Document} - The server's singular permission document
     */
    permissionSchema.statics.getPerms = async function(){
        //Not sure why 'this' didn't work from here when calling this, I'm assuming it's because I'm doing it from middleware
        //which is probably binding shit to this function, either way this works :P

        //Get the first document we find
        var perms = await module.exports.findOne({});

        if(perms){
            //If we found something then the permissions document exist and this is it,
            //So long as no one else has fucked with the database it should be the only one. (is this forshadowing for a future bug?)
            return perms;
        }else{
            //Otherwise this is the first launch of the install, say hello
            console.log("First launch detected! Initializing permissions document in Database!");

            //create and save the permissions document
            perms = await module.exports.create({});
            await perms.save();

            //live up to the name of the function
            return perms;
        }
    }

    /**
     * Converts rank name to number
     * @param {String} rank - rank to check
     * @returns {Number} Rank level
     */
    permissionSchema.statics.rankToNum = function(rank){
        return rankEnum.indexOf(rank);
    }

    /**
     * Check users rank against a given permission by username
     * @param {String} user - Username of the user to check against a perm
     * @param {String} perm - Permission to check user against
     * @returns {Boolean} Whether or not the user is authorized for the permission in question
     */
    permissionSchema.statics.permCheck = async function(user, perm){
        //Check if the user is null
        if(user != null){
            //This specific call is why we export the userModel the way we do
            //Someone will yell at me for circular dependencies but the fucking interpreter isn't :P
            const userDB = await userModel.userModel.findOne({user: user.user});
            return await this.permCheckByUserDoc(userDB, perm);
        }else{
            return await this.permCheckByUserDoc(null, perm);
        }
    }

    /**
     * Syntatic sugar for perms.CheckByUserDoc so we don't have to get the document ourselves
     * @param {Mongoose.Document} user - User document to check perms against
     * @param {String} perm - Permission to check user against
     * @returns {Boolean} Whether or not the user is authorized for the permission in question
     */
    permissionSchema.statics.permCheckByUserDoc = async function(user, perm){
        //Get permission list
        const perms = await this.getPerms();
        //Call the perm check method
        return perms.permCheckByUserDoc(user, perm);
    }

    /**
     * Check users rank by a given permission by username
     * @param {String} user - Username of the user to check against a perm
     * @param {String} perm - Permission to check user against
     * @returns {Boolean} Whether or not the user is authorized for the permission in question
     */
    permissionSchema.statics.overrideCheck = async function(user, perm){
        //Check if the user is null
        if(user != null){
            //This specific call is why we export the userModel the way we do
            //Someone will yell at me for circular dependencies but the fucking interpreter isn't :P
            const userDB = await userModel.userModel.findOne({user: user.user});
            return await this.overrideCheckByUserDoc(userDB, perm);
        }else{
            return await this.overrideCheckByUserDoc(null, perm);
        }
    }

    /**
     * Syntatic sugar for perms.overrideCheckByUSerDoc so we don't have to seperately get the perm doc
     * Checks channel perm override against a given user by username
     * @param {String} user - Username of the user to check against a perm
     * @param {String} perm - Permission to check user against
     * @returns {Boolean} Whether or not the user is authorized for the permission in question
     */
    permissionSchema.statics.overrideCheckByUserDoc = async function(user, perm){
        //Get permission list
        const perms = await this.getPerms();
        //Call the perm check method
        return perms.overrideCheckByUserDoc(user, perm);
    }

    //Middleware for rank checks
    /**
     * Configurable express middleware which checks user's request against a given permission
     * @param {String} perm - Permission to check
     * @returns {Function} Express middlewhere function with given permission injected into it
     */
    permissionSchema.statics.reqPermCheck = function(perm){
        return (req, res, next)=>{
            permissionSchema.statics.permCheck(req.session.user, perm).then((access) => {
                if(access){
                    next();
                }else{
                    return errorHandler(res, "You do not have a high enough rank to access this resource.", 'Unauthorized', 401);
                }
            });
        }
    }

    //methods
    //these are good to have even for single-doc collections since we can loop through them without finding them in the database each time
    /**
     * Checks permission against a single user by document
     * @param {Mongoose.Document} userDB - User doc to rank check against
     * @param {String} perm - Permission to check user doc against
     * @returns {Boolean} True if authorized
     */
    permissionSchema.methods.permCheckByUserDoc = function(userDB, perm){
        //Set user to anon rank if no rank was found for the given user
        if(userDB == null || userDB.rank == null){
            userDB ={
                rank: "anon"
            };
        }

        //Check if this permission exists
        if(this[perm] != null){
            //if so get required rank as a number
            requiredRank = this.model().rankToNum(this[perm])
            //if so get user rank as a number
            userRank = userDB ? this.model().rankToNum(userDB.rank) : 0;
            //return whether or not the user is equal to or higher than the required rank for this permission
            return (userRank >= requiredRank);
        }else{
            //if not scream and shout
            throw loggerUtils.exceptionSmith(`Permission check '${perm}' not found!`, "Validation");
        }
    }

    /**
     * Checks channel override permission against a single user by document
     * @param {Mongoose.Document} userDB - User doc to rank check against
     * @param {String} perm - Channel Override Permission to check user doc against
     * @returns {Boolean} True if authorized
     */
    permissionSchema.methods.overrideCheckByUserDoc = function(userDB, perm){
        //Set user to anon rank if no rank was found for the given user
        if(userDB == null || userDB.rank == null){
            userDB ={
                rank: "anon"
            };
        }

        //Check if this permission exists
        if(this.channelOverrides[perm] != null){
            //if so get required rank as a number
            requiredRank = this.model().rankToNum(this.channelOverrides[perm])
            //if so get user rank as a number
            userRank = userDB ? this.model().rankToNum(userDB.rank) : 0;
            //return whether or not the user is equal to or higher than the required rank for this permission
            return (userRank >= requiredRank);
        }else{
            //if not scream and shout
            throw loggerUtils.exceptionSmith(`Permission check '${perm}' not found!`, "validation");
        }
    }

    /**
     * Returns entire permission map marked with booleans
     * @param {Mongoose.Document} userDB - User Doc to generate perm map against
     * @returns {Map} Permission map containing booleans for each permission's authorization for a given user doc
     */
    permissionSchema.methods.getPermMapByUserDoc = function(userDB){
        //Pull permissions keys
        let permTree = this.schema.tree;
        let overrideTree = channelPermissionSchema.tree;
        let permMap = new Map();
        let overrideMap = new Map();

        //For each object in the temporary permissions object
        for(let perm of Object.keys(permTree)){
            //Check the current permission
            permMap.set(perm, this.permCheckByUserDoc(userDB, perm));
        }

        //For each object in the temporary permissions object
        for(let perm of Object.keys(overrideTree)){
            //Check the current permission
            overrideMap.set(perm, this.overrideCheckByUserDoc(userDB, perm));
        }

        //return the auto-generated schema
        return {
            site: permMap,
            channelOverrides: overrideMap
        }
    }

    module.exports = mongoose.model("permissions", permissionSchema);