/*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; 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; 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; } } permissionSchema.statics.rankToNum = function(rank){ return rankEnum.indexOf(rank); } 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); } } 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); } 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); } } 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 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 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 new Error(`Permission check '${perm}' not found!`); } } 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 new Error(`Permission check '${perm}' not found!`); } } 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);