From 61fab57a6dfcaab5781d16d79b57dd7db6b26eca Mon Sep 17 00:00:00 2001 From: rainbownapkin Date: Mon, 25 Nov 2024 00:44:07 -0500 Subject: [PATCH] Channel Rank/Auth base backend functional --- src/app/channel/activeChannel.js | 8 +- src/app/channel/channelManager.js | 13 +- src/app/channel/connectedUser.js | 3 +- .../api/channel/permissionsController.js | 35 +++- src/controllers/api/channel/rankController.js | 79 +++++-- src/routers/api/channelRouter.js | 20 +- src/routers/channelRouter.js | 4 +- src/schemas/channel/channelSchema.js | 196 ++++++++++++++++-- src/schemas/permissionSchema.js | 12 -- src/validators/channelValidator.js | 2 + src/validators/permissionsValidator.js | 12 -- www/js/utils.js | 17 ++ 12 files changed, 318 insertions(+), 83 deletions(-) diff --git a/src/app/channel/activeChannel.js b/src/app/channel/activeChannel.js index c278df1..ee348f8 100644 --- a/src/app/channel/activeChannel.js +++ b/src/app/channel/activeChannel.js @@ -27,16 +27,18 @@ module.exports = class{ this.userList = new Map(); } - async handleConnection(userDB, socket){ + async handleConnection(userDB, chanDB, socket){ //get current user object from the userlist var userObj = this.userList.get(userDB.user); + //get channel rank for current user + const chanRank = await chanDB.getChannelRankByUserDoc(userDB); //If user is already connected if(userObj){ //Add this socket on to the userobject userObj.sockets.push(socket.id); }else{ - userObj = new connectedUser(userDB.user, userDB.id, userDB.rank, userDB.flair, this, socket); + userObj = new connectedUser(userDB.user, userDB.id, userDB.rank, chanRank, userDB.flair, this, socket); } //Set user entry in userlist @@ -49,6 +51,8 @@ module.exports = class{ //await this.sendClientMetadata(userDB, socket); await userObj.sendClientMetadata(); + console.log(userObj); + //Send out the userlist this.broadcastUserList(socket.chan); } diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 5101d97..042f286 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -45,14 +45,15 @@ module.exports = class{ const userDB = await this.authSocket(socket); //Get the active channel based on the socket - var activeChan = await this.getActiveChan(socket); + var {activeChan, chanDB} = await this.getActiveChan(socket); //Define listeners this.defineListeners(socket); this.chatHandler.defineListeners(socket); //Connect the socket to it's given channel - activeChan.handleConnection(userDB, socket); + //Lil' hacky to pass chanDB like that, but why double up on DB calls? + activeChan.handleConnection(userDB, chanDB, socket); }catch(err){ //Flip a table if something fucks up return loggerUtils.socketCriticalExceptionHandler(socket, err); @@ -83,10 +84,11 @@ module.exports = class{ async getActiveChan(socket){ socket.chan = socket.handshake.headers.referer.split('/c/')[1]; + const chanDB = (await channelModel.findOne({name: socket.chan})) //Check if channel exists - if(await channelModel.findOne({name: socket.chan}) == null){ - throw new Error("Channel not found!") + if(chanDB == null){ + throw new Error("Channel not found!"); } //Check if current channel is active @@ -99,7 +101,8 @@ module.exports = class{ } //Return whatever the active channel is (new or old) - return activeChan; + return {activeChan, chanDB}; + //return activeChan; } defineListeners(socket){ diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 4b31432..31fcadd 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -19,10 +19,11 @@ const flairModel = require('../../schemas/flairSchema'); const permissionModel = require('../../schemas/permissionSchema'); module.exports = class{ - constructor(id, name, rank, flair, channel, socket){ + constructor(id, name, rank, chanRank, flair, channel, socket){ this.id = id; this.name = name; this.rank = rank; + this.chanRank = chanRank; this.flair = flair; this.channel = channel; this.sockets = [socket.id]; diff --git a/src/controllers/api/channel/permissionsController.js b/src/controllers/api/channel/permissionsController.js index f115408..8bd8eed 100644 --- a/src/controllers/api/channel/permissionsController.js +++ b/src/controllers/api/channel/permissionsController.js @@ -20,6 +20,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {exceptionHandler} = require('../../../utils/loggerUtils.js'); const channelModel = require('../../../schemas/channel/channelSchema.js'); +const permissionModel = require('../../../schemas/permissionSchema.js'); //api account functions module.exports.get = async function(req, res){ @@ -53,15 +54,41 @@ module.exports.post = async function(req, res){ if(validResult.isEmpty()){ const data = matchedData(req); - const channel = await channelModel.findOne({name: data.chanName}); - const permissionsMap = new Map(Object.entries(data.channelPermissionsMap)); + //get channel document based on sanatized/validated input + const chanDB = await channelModel.findOne({name: data.chanName}); + //get permissions map based on sanatized/validated input + const permissionsMap = data.channelPermissionsMap; + //get chanRank off off session user + const chanRank = await chanDB.getChannelRank(req.session.user); + //setup flag for permissions errors + var permError = null; - if(channel == null){ + if(chanDB == null){ throw new Error("Channel not found."); } + //For each permission submitted + Object.keys(permissionsMap).forEach((perm) => { + //Check to make sure no one is jumping perms (this should be admins only, but just in-case) + //Setting a boolean inside of an if statement seems fucked, until you realize it won't set it back false on the next loop :P + if(permissionModel.rankToNum(chanDB.permissions[perm]) > permissionModel.rankToNum(chanRank) || permissionModel.rankToNum(permissionsMap[perm]) > permissionModel.rankToNum(chanRank)){ + permError = true; + } + + //Set permissions in the permissions model + chanDB.permissions[perm] = permissionsMap[perm]; + }); + + //Flip our shit if something's wrong. + if(permError){ + res.status(401); + return res.send({errors:[{type: "Unauthorized", msg: "New rank must be equal to or below that of the user changing it.", date: new Date()}]}); + } + + await chanDB.save(); + res.status(200); - return res.send(await channel.updateChannelPerms(permissionsMap)); + return res.send(chanDB.permissions); }else{ res.status(400); res.send({errors: validResult.array()}) diff --git a/src/controllers/api/channel/rankController.js b/src/controllers/api/channel/rankController.js index 7c725bc..e85ef32 100644 --- a/src/controllers/api/channel/rankController.js +++ b/src/controllers/api/channel/rankController.js @@ -34,24 +34,19 @@ module.exports.get = async function(req, res){ //Get channel document from validated/sanatized chanName querystring const data = matchedData(req); const chanDB = await channelModel.findOne({name: data.chanName}); + + //get userDB from session + if(req.session.user != null){ + var userDB = await userModel.findOne({user: req.session.user.user}); + } + + //If for some reason there isn't any user found + if(userDB == null){ + var userDB = {rank: "anon"}; + } + //Setup empty array for our return data - const userList = []; - - //Populate the user objects in our ranklist based off of their DB ID's - await chanDB.populate('rankList.user'); - - //For each rank object in the rank list - chanDB.rankList.forEach(async (rankObj) => { - //Create a new user object from rank object data - const userObj = { - id: rankObj.user.id, - user: rankObj.user.user, - rank: rankObj.rank - } - - //Add our user object to the list - userList.push(userObj); - }); + const userList = await chanDB.getRankList(); //Send out the userlist we created res.status(200); @@ -66,3 +61,53 @@ module.exports.get = async function(req, res){ } } +module.exports.post = async function(req, res){ + try{ + //Get validation results + const validResult = validationResult(req); + + //If we don't have any validation errors + if(validResult.isEmpty()){ + const data = matchedData(req); + //Get channel document from sanatized/validated data + const chanDB = await channelModel.findOne({name: data.chanName}); + //Get user document from sanatized/validated data + const userDB = await userModel.findOne({user: data.user}); + //Get requesting user rank from sanatized/validated data + const chanRank = await chanDB.getChannelRank(req.session.user); + //Get target user rank from sanatized/validated data + const targetChanRank = await chanDB.getChannelRankByUserDoc(userDB); + + if(data.user == null || userDB == null){ + //If the user is null, scream and shout + res.status(400); + return res.send({errors:[{type: "Bad Query", msg: "User not found.", date: new Date()}]}); + }else if(data.user == req.session.user.user){ + //If some smart-ass is trying self-privelege escalation + res.status(401); + return res.send({errors:[{type: "Unauthorized", msg: "No, you can't change your own rank. Fuck off.", date: new Date()}]}); + }else if(permissionModel.rankToNum(data.rank) >= permissionModel.rankToNum(chanRank)){ + //If the user is below the new rank of the user they're setting, scream and shout + res.status(401); + return res.send({errors:[{type: "Unauthorized", msg: "New rank must be below that of the user changing it.", date: new Date()}]}); + }else if(permissionModel.rankToNum(targetChanRank) >= permissionModel.rankToNum(chanRank)){ + //If the user is below the original rank of the user they're setting, scream and shout + res.status(401); + return res.send({errors:[{type: "Unauthorized", msg: "You cannot promote/demote peer/outranking users.", date: new Date()}]}); + } + + //Set rank + var rankList = await chanDB.setRank(userDB, data.rank); + + res.status(200); + res.send(rankList); + }else{ + //If we received bad input, we have only one action: bitch, moan, and complain! + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + console.log(err); + return exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/routers/api/channelRouter.js b/src/routers/api/channelRouter.js index ee6325b..6c70bca 100644 --- a/src/routers/api/channelRouter.js +++ b/src/routers/api/channelRouter.js @@ -19,7 +19,9 @@ const { Router } = require('express'); //local imports const permissionSchema = require("../../schemas/permissionSchema"); +const channelModel = require("../../schemas/channel/channelSchema"); const {channelValidator} = require("../../validators/channelValidator"); +const {accountValidator} = require("../../validators/accountValidator"); const {channelPermissionValidator} = require("../../validators/permissionsValidator"); const registerController = require("../../controllers/api/channel/registerController"); const listController = require("../../controllers/api/channel/listController"); @@ -33,17 +35,19 @@ const router = Router(); //user authentication middleware router.use("/register",permissionSchema.reqPermCheck("registerChannel")); -router.use("/delete",permissionSchema.reqPermCheck("deleteChannel")); -router.use("/settings",permissionSchema.reqPermCheck("manageChannel")); +router.use("/settings", channelValidator.name('chanName'), channelModel.reqPermCheck("manageChannel")); +router.use("/permissions", channelValidator.name('chanName'), channelModel.reqPermCheck("manageChannel")); +router.use("/rank", channelValidator.name('chanName'), channelModel.reqPermCheck("manageChannel")); //routing functions router.post('/register', channelValidator.name(), channelValidator.description(), channelValidator.thumbnail(), registerController.post); router.get('/list', listController.get); -router.get('/settings', channelValidator.name('chanName'), settingsController.get); -router.post('/settings', channelValidator.name('chanName'), channelValidator.settingsMap(), settingsController.post); -router.get('/permissions', channelValidator.name('chanName'), permissionsController.get); -router.post('/permissions', channelValidator.name('chanName'), channelPermissionValidator.channelPermissionsMap(), permissionsController.post); -router.get('/rank', channelValidator.name('chanName'), rankController.get); -router.post('/delete', channelValidator.name('chanName'), channelValidator.name('confirm'),deleteController.post); +router.get('/settings', settingsController.get); +router.post('/settings', channelValidator.settingsMap(), settingsController.post); +router.get('/permissions', permissionsController.get); +router.post('/permissions', channelPermissionValidator.channelPermissionsMap(), permissionsController.post); +router.get('/rank', rankController.get); +router.post('/rank', accountValidator.user(), channelValidator.rank(), rankController.post); +router.post('/delete', channelValidator.name('chanName'), channelValidator.name('confirm'), channelModel.reqPermCheck("deleteChannel"), deleteController.post); module.exports = router; \ No newline at end of file diff --git a/src/routers/channelRouter.js b/src/routers/channelRouter.js index 223559d..20251fa 100644 --- a/src/routers/channelRouter.js +++ b/src/routers/channelRouter.js @@ -19,7 +19,7 @@ const { Router } = require('express'); //local imports -const permissionSchema = require("../schemas/permissionSchema"); +const channelModel = require("../schemas/channel/channelSchema"); const channelController = require("../controllers/channelController"); const channelSettingsController = require("../controllers/channelSettingsController"); @@ -27,7 +27,7 @@ const channelSettingsController = require("../controllers/channelSettingsControl const router = Router(); //User authentication middleware -router.use("/*/settings",permissionSchema.reqPermCheck("manageChannel")); +router.use("/*/settings",channelModel.reqPermCheck("manageChannel","/c/")); //routing functions router.get('/*/settings', channelSettingsController.get); diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 8c4780f..96a20e2 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -16,11 +16,14 @@ along with this program. If not, see .*/ //NPM Imports const {mongoose} = require('mongoose'); +const {validationResult, matchedData} = require('express-validator'); //Local Imports const statModel = require('../statSchema.js'); +const userModel = require('../userSchema.js'); const permissionModel = require('../permissionSchema.js'); const channelPermissionSchema = require('./channelPermissionSchema.js'); +const { exceptionHandler } = require('../../utils/loggerUtils.js'); const channelSchema = new mongoose.Schema({ id: { @@ -118,6 +121,56 @@ channelSchema.statics.getChannelList = async function(includeHidden = false){ return chanGuide; } +//Middleware for rank checks +//Man, it would be really nice if express middleware actually supported async functions, you know, as if it where't still 2015 >:( +//Also holy shit, sharing a function between two middleware functions is a nightmare +//I'd rather just have this check chanField for '/c/' to handle channels in URL, fuck me this was obnoxious to write +channelSchema.statics.reqPermCheck = function(perm, chanField = "chanName"){ + return (req, res, next)=>{ + try{ + //Check validation result + const validResult = validationResult(req); + + //if our chan field is set to '/c/', telling us to check the URL + if(chanField == '/c/'){ + //Rip the chan name out of the URL + var chanName = (req.originalUrl.split('/c/')[1].replace('/settings','')); + }else if(validResult.isEmpty()){ + //otherwise if our input is valid, use that + var chanName = matchedData(req)[chanField]; + }else{ + //We didn't get /c/ and we got a bad input, time for shit to hit the fan! + res.status(400); + return res.send({errors: validResult.array()}) + } + + //Find the related channel document, and handle it using a then() block + this.findOne({name: chanName}).then((chanDB) => { + //If we didnt find a channel + if(chanDB == null){ + //FUCK + res.status(401); + return res.send({error:`Cannot perm check non-existant channel!.`}); + } + + //Run a perm check against the current user and permission + chanDB.permCheck(req.session.user, perm).then((permitted) => { + if(permitted){ + //if we're permitted, go on to fulfill the request + next(); + }else{ + //If not, prevent the request from going through and tell them why + res.status(401); + return res.send({error:`You do not have a high enough rank to access this resource.`}); + } + }); + }); + }catch(err){ + return exceptionHandler(res, err); + } + } +} + //methods channelSchema.methods.updateSettings = async function(settingsMap){ settingsMap.forEach((value, key) => { @@ -133,37 +186,140 @@ channelSchema.methods.updateSettings = async function(settingsMap){ return this.settings; } -channelSchema.methods.updateChannelPerms = async function(permissionsMap){ - permissionsMap.forEach((value, key) => { - if(this.permissions[key] == null){ - throw new Error("Invalid channel permission."); +channelSchema.methods.rankCrawl = async function(userDB,cb){ + //Crawl through channel rank list + //TODO: replace this with rank check function shared with setRank + this.rankList.forEach(async (rankObj, rankIndex) => { + //check against user ID to speed things up + if(rankObj.user.user == null){ + if(rankObj.user.toString() == userDB._id.toString()){ + //If we found a match, call back + cb(rankObj, rankIndex); + } + }else{ + //in case someone populated the users + if(rankObj.user.user == userDB.user){ + //If we found a match, call back + cb(rankObj, rankIndex); + } } - - this.permissions[key] = value; - }) - - await this.save(); - - return this.permissions; + }); } -channelSchema.methods.setUserRank = async function(userDB,rank){ - //Create rank object based on input - const rankObj = { - user: userDB._id, - rank: rank +channelSchema.methods.setRank = async function(userDB,rank){ + //Create variable to store found ranks + var foundRankIndex = null; + + //Crawl through ranks to find matching index + this.rankCrawl(userDB,(rankObj, rankIndex)=>{foundRankIndex = rankIndex}); + + //If we found an existing rank object + if(foundRankIndex != null){ + if(rank == "user"){ + this.rankList.splice(foundRankIndex,1); + }else{ + //otherwise, set the users rank + this.rankList[foundRankIndex].rank = rank; + } + }else if(rank != "user"){ + //if the user rank object doesn't exist, and we're not setting to user + //Create rank object based on input + const rankObj = { + user: userDB._id, + rank: rank + } + + //Add it to rank list + this.rankList.push(rankObj); } - //Add it to rank list - this.rankList.push(rankObj); + + //Save our channel and return rankList await this.save(); return this.rankList; } -channelSchema.methods.getChannelRankFromUser = async function(userDB){ +channelSchema.methods.getRankList = async function(){ + //Create an empty array to hold the user list + const rankList = []; + + //Populate the user objects in our ranklist based off of their DB ID's + await this.populate('rankList.user'); + + //For each rank object in the rank list + this.rankList.forEach(async (rankObj) => { + //Create a new user object from rank object data + const userObj = { + id: rankObj.user.id, + user: rankObj.user.user, + rank: rankObj.rank + } + + //Add our user object to the list + rankList.push(userObj); + }); + + //return userList + return rankList; } -channelSchema.methods.channelPermCheck = async function(user, perm){ +channelSchema.methods.getChannelRankByUserDoc = async function(userDB = null){ + var foundRank = null; + + //Check to make sure userDB exists before going forward + if(userDB == null){ + //If so this user is probably not signed in + return "anon" + } + + //Crawl through ranks to find matching rank + this.rankCrawl(userDB,(rankObj)=>{foundRank = rankObj}); + + //If we found an existing rank object + if(foundRank != null){ + //return rank + return foundRank.rank; + }else{ + //default to "user" for registered users, and "anon" for anonymous + if(userDB.rank == "anon"){ + return "anon"; + }else{ + return "user"; + } + } +} + +channelSchema.methods.getChannelRank = async function(user){ + const userDB = await userModel.findOne({user: user.user}); + return await this.getChannelRankByUserDoc(userDB); +} + +channelSchema.methods.permCheckByUserDoc = async function(userDB, perm){ + //Get site-wide rank as number, default to anon for anonymous users + const rank = userDB ? permissionModel.rankToNum(userDB.rank) : permissionModel.rankToNum("anon"); + //Get channel rank as number + const chanRank = permissionModel.rankToNum(await this.getChannelRankByUserDoc(userDB)); + //Get channel permission rank requirement as number + const permRank = permissionModel.rankToNum(this.permissions[perm]); + //Get site-wide rank requirement to override as number + const overrideRank = permissionModel.rankToNum((await permissionModel.getPerms()).channelOverrides[perm]); + //Get channel perm check result + const permCheck = (chanRank >= permRank); + //Get site-wide override perm check result + const overrideCheck = (rank >= overrideRank); + + return (permCheck || overrideCheck); +} + +channelSchema.methods.permCheck = async function (user, perm){ + //Set userDB to null if we wheren't passed a real user + if(user != null){ + var userDB = await userModel.findOne({user: user.user}); + }else{ + var userDB = null; + } + + return await this.permCheckByUserDoc(userDB, perm) } channelSchema.methods.nuke = async function(confirm){ diff --git a/src/schemas/permissionSchema.js b/src/schemas/permissionSchema.js index aa12779..7cc25da 100644 --- a/src/schemas/permissionSchema.js +++ b/src/schemas/permissionSchema.js @@ -43,18 +43,6 @@ const permissionSchema = new mongoose.Schema({ default: "admin", required: true }, - manageChannel: { - type: mongoose.SchemaTypes.String, - enum: rankEnum, - default: "admin", - required: true - }, - deleteChannel: { - type: mongoose.SchemaTypes.String, - enum: rankEnum, - default: "admin", - required: true - }, channelOverrides: { type: channelPermissionSchema, default: () => ({}) diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index b965766..d4baafc 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -27,6 +27,8 @@ module.exports.channelValidator = { thumbnail: (field = 'thumbnail') => accountValidator.img(field), + rank: (field = 'rank') => accountValidator.rank(field), + settingsMap: () => checkExact(checkSchema({ 'settingsMap.hidden': { optional: true, diff --git a/src/validators/permissionsValidator.js b/src/validators/permissionsValidator.js index af8108d..24b8b8f 100644 --- a/src/validators/permissionsValidator.js +++ b/src/validators/permissionsValidator.js @@ -45,18 +45,6 @@ module.exports.permissionsValidator = { custom: { options: module.exports.isRank }, - }, - 'permissionsMap.manageChannel': { - optional: true, - custom: { - options: module.exports.isRank - }, - }, - 'permissionsMap.deleteChannel': { - optional: true, - custom: { - options: module.exports.isRank - }, } }) } diff --git a/www/js/utils.js b/www/js/utils.js index f564c4a..c473d13 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -253,6 +253,23 @@ class canopyAjaxUtils{ } } + async setChannelRank(chanName, user, rank){ + var response = await fetch(`/api/channel/rank`,{ + method: "POST", + headers: { + "Content-Type": "application/json" + }, + //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... + body: JSON.stringify({chanName, user, rank}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + async deleteChannel(chanName, confirm){ var response = await fetch(`/api/channel/delete`,{ method: "POST",