/*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 .*/ //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: () => ({}) }, debug: { type: mongoose.SchemaTypes.String, enum: rankEnum, default: "admin", required: true }, }); //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);