diff --git a/src/controllers/api/channel/banController.js b/src/controllers/api/channel/banController.js new file mode 100644 index 0000000..106c58e --- /dev/null +++ b/src/controllers/api/channel/banController.js @@ -0,0 +1,99 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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 {validationResult, matchedData} = require('express-validator'); + +//local imports +const {exceptionHandler} = require('../../../utils/loggerUtils.js'); +const {userModel} = require('../../../schemas/userSchema.js'); +const channelModel = require('../../../schemas/channel/channelSchema'); + +//api account functions +module.exports.get = async function(req, res){ + try{ + //Get validation result + const validResult = validationResult(req); + + //If we data is valid + if(validResult.isEmpty()){ + //Set channel object from sanatized/validated data, and get user document from session data + const {chanName} = matchedData(req); + const chanDB = await channelModel.findOne({name: chanName}); + + res.status(200); + return res.send(await chanDB.getChanBans()); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} + +module.exports.post = async function(req, res){ + try{ + //Get validation result + const validResult = validationResult(req); + + //If we data is valid + if(validResult.isEmpty()){ + //Set channel object from sanatized/validated data, and get user document from session data + const {chanName, user, expirationDays, banAlts} = matchedData(req); + const userDB = await userModel.findOne({user}); + const chanDB = await channelModel.findOne({name: chanName}); + + await chanDB.banByUserDoc(userDB, expirationDays, banAlts); + + res.status(200); + return res.send(await chanDB.getChanBans()); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} + +module.exports.delete = async function(req, res){ + try{ + //Get validation result + const validResult = validationResult(req); + + //If we data is valid + if(validResult.isEmpty()){ + //Set channel object from sanatized/validated data, and get user document from session data + const {chanName, user} = matchedData(req); + const userDB = await userModel.findOne({user}); + const chanDB = await channelModel.findOne({name: chanName}); + + await chanDB.unbanByUserDoc(userDB); + + res.status(200); + return res.send(await chanDB.getChanBans()); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} \ No newline at end of file diff --git a/src/routers/api/channelRouter.js b/src/routers/api/channelRouter.js index 6c70bca..edb10cb 100644 --- a/src/routers/api/channelRouter.js +++ b/src/routers/api/channelRouter.js @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //npm imports +const { body } = require('express-validator'); const { Router } = require('express'); //local imports @@ -29,6 +30,7 @@ const settingsController = require("../../controllers/api/channel/settingsContro const permissionsController = require("../../controllers/api/channel/permissionsController") const rankController = require("../../controllers/api/channel/rankController"); const deleteController = require("../../controllers/api/channel/deleteController"); +const banController = require("../../controllers/api/channel/banController"); //globals const router = Router(); @@ -38,16 +40,28 @@ router.use("/register",permissionSchema.reqPermCheck("registerChannel")); 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")); +router.use("/delete", channelValidator.name('chanName'), channelModel.reqPermCheck("deleteChannel")); +router.use("/ban", channelValidator.name('chanName'), channelModel.reqPermCheck("manageChannel")); //routing functions +//register router.post('/register', channelValidator.name(), channelValidator.description(), channelValidator.thumbnail(), registerController.post); +//list router.get('/list', listController.get); +//settings router.get('/settings', settingsController.get); router.post('/settings', channelValidator.settingsMap(), settingsController.post); +//permissions router.get('/permissions', permissionsController.get); router.post('/permissions', channelPermissionValidator.channelPermissionsMap(), permissionsController.post); +//rank 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); +//delete +router.post('/delete', channelValidator.name('confirm'), deleteController.post); +//ban +router.get('/ban', banController.get); +router.post('/ban', accountValidator.user(), body("banAlts").isBoolean(), body("expirationDays").isInt(), banController.post); +router.delete('/ban', accountValidator.user(), banController.delete); module.exports = router; \ No newline at end of file diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 089b441..2d35ac0 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -67,6 +67,29 @@ const channelSchema = new mongoose.Schema({ required: true, enum: permissionModel.rankEnum } + }], + //Thankfully we don't have to keep track of alts, ips, or deleted users so this should be a little easier :P + banList: [{ + user: { + type: mongoose.SchemaTypes.ObjectID, + required: true, + ref: "user" + }, + banDate: { + type: mongoose.SchemaTypes.Date, + required: true, + default: new Date() + }, + expirationDays: { + type: mongoose.SchemaTypes.Number, + required: true, + default: 14 + }, + banAlts: { + type: mongoose.SchemaTypes.Boolean, + required: true, + default: false + } }] }); @@ -323,6 +346,113 @@ channelSchema.methods.permCheck = async function (user, perm){ return await this.permCheckByUserDoc(userDB, perm) } +channelSchema.methods.checkBanByUserDoc = async function(userDB){ + var foundBan = null; + + this.banList.forEach((ban) => { + if(ban.user != null){ + if(ban.user.toString() == userDB._id.toString()){ + foundBan = ban; + } + } + }); + + return foundBan; +} + +channelSchema.methods.getChanBans = async function(){ + //Create an empty list to hold our found bans + var banList = []; + //Populate the users in the banList + await this.populate('banList.user'); + + //Crawl through known bans + this.banList.forEach((ban) => { + + var banObj = { + banDate: ban.banDate, + expirationDays: ban.expirationDays, + } + + //Check if the ban was permanent (expiration set before ban date) + if(ban.expirationDays > 0){ + //if not calculate expiration date + var expirationDate = new Date(ban.banDate); + expirationDate.setDate(expirationDate.getDate() + ban.expirationDays); + + //Set calculated expiration date + banObj.expirationDate = expirationDate; + } + + //Setup user object (Do this last to keep it at bottom for human-readibility of json :P) + banObj.user = { + id: ban.user.id, + user: ban.user.user, + img: ban.user.img, + date: ban.user.date + } + + banList.push(banObj); + }); + + return banList; +} + +channelSchema.methods.banByUserDoc = async function(userDB, expirationDays, banAlts){ + //Throw a shitfit if the user doesn't exist + if(userDB == null){ + throw new Error("Cannot ban non-existant user!"); + } + + const foundBan = await this.checkBanByUserDoc(userDB); + + if(foundBan != null){ + throw new Error("User already banned!"); + } + + //Create a new ban document based on input + const banDoc = { + user: userDB._id, + expirationDays, + banAlts + } + + //Push the ban to the list + this.banList.push(banDoc); + + await this.save(); +} + +channelSchema.methods.ban = async function(user, expirationDays, banAlts){ + const userDB = await userModel.find({user}); + return await this.banByUserDoc(userDB, expirationDays, banAlts); +} + +channelSchema.methods.unbanByUserDoc = async function(userDB){ + //Throw a shitfit if the user doesn't exist + if(userDB == null){ + throw new Error("Cannot ban non-existant user!"); + } + + const foundBan = await this.checkBanByUserDoc(userDB); + + if(foundBan == null){ + throw new Error("User already unbanned!"); + } + + //You know I can't help but feel like an asshole for looking for the index of something I just pulled out of an array using forEach... + //Then again this is such an un-used function that the issue of code re-use overshadows performance + //I mean how often are we REALLY going to be un-banning users from channels? + const banIndex = this.banList.indexOf(foundBan); + this.banList.splice(banIndex,1); + return await this.save(); +} + +channelSchema.methods.unban = async function(user){ + const userDB = await userModel.find({user}); + return await this.unbanByUserDoc(userDB); +} + channelSchema.methods.nuke = async function(confirm){ if(confirm == "" || confirm == null){ throw new Error("Empty Confirmation String!"); diff --git a/www/js/utils.js b/www/js/utils.js index 9b44e2d..eece7af 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -324,8 +324,8 @@ class canopyAjaxUtils{ } } - async getChannelRank(channel){ - var response = await fetch(`/api/channel/rank?chanName=${channel}`,{ + async getChannelRank(chanName){ + var response = await fetch(`/api/channel/rank?chanName=${chanName}`,{ method: "GET" }); @@ -380,6 +380,53 @@ class canopyAjaxUtils{ utils.ux.displayResponseError(await response.json()); } } + + async getChanBans(chanName){ + var response = await fetch(`/api/channel/ban?chanName=${chanName}`,{ + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + + async chanBan(chanName, user, expirationDays, banAlts){ + var response = await fetch(`/api/channel/ban`,{ + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({chanName, user, expirationDays, banAlts}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + + async chanUnban(chanName, user){ + var response = await fetch(`/api/channel/ban`,{ + method: "DELETE", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({chanName, user}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } } const utils = new canopyUtils() \ No newline at end of file