diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js index 0fb2797..88ccce5 100644 --- a/src/app/channel/channelManager.js +++ b/src/app/channel/channelManager.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //Local Imports -const channelModel = require('../../schemas/channelSchema'); +const channelModel = require('../../schemas/channel/channelSchema'); const flairModel = require('../../schemas/flairSchema'); const userModel = require('../../schemas/userSchema'); const loggerUtils = require('../../utils/loggerUtils'); diff --git a/src/controllers/adminPanelController.js b/src/controllers/adminPanelController.js index b17641f..f4287b6 100644 --- a/src/controllers/adminPanelController.js +++ b/src/controllers/adminPanelController.js @@ -18,7 +18,7 @@ along with this program. If not, see .*/ const config = require('../../config.json'); const userModel = require('../schemas/userSchema'); const permissionModel = require('../schemas/permissionSchema'); -const channelModel = require('../schemas/channelSchema'); +const channelModel = require('../schemas/channel/channelSchema'); const {exceptionHandler} = require("../utils/loggerUtils"); //register page functions diff --git a/src/controllers/api/admin/listChannelsController.js b/src/controllers/api/admin/listChannelsController.js index ed3f943..089a378 100644 --- a/src/controllers/api/admin/listChannelsController.js +++ b/src/controllers/api/admin/listChannelsController.js @@ -16,7 +16,7 @@ along with this program. If not, see .*/ //local imports const {exceptionHandler} = require('../../../utils/loggerUtils.js'); -const channelModel = require('../../../schemas/channelSchema.js'); +const channelModel = require('../../../schemas/channel/channelSchema.js'); //api list channel functions module.exports.get = async function(req, res){ diff --git a/src/controllers/api/channel/deleteController.js b/src/controllers/api/channel/deleteController.js index e97e641..3d96fbb 100644 --- a/src/controllers/api/channel/deleteController.js +++ b/src/controllers/api/channel/deleteController.js @@ -19,7 +19,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {exceptionHandler} = require('../../../utils/loggerUtils.js'); -const channelModel = require('../../../schemas/channelSchema'); +const channelModel = require('../../../schemas/channel/channelSchema'); //api account functions module.exports.post = async function(req, res){ diff --git a/src/controllers/api/channel/listController.js b/src/controllers/api/channel/listController.js index fe28a3c..b751b85 100644 --- a/src/controllers/api/channel/listController.js +++ b/src/controllers/api/channel/listController.js @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //local imports -const channelModel = require('../../../schemas/channelSchema'); +const channelModel = require('../../../schemas/channel/channelSchema'); const {exceptionHandler} = require('../../../utils/loggerUtils.js'); //api account functions diff --git a/src/controllers/api/channel/permissionsController.js b/src/controllers/api/channel/permissionsController.js new file mode 100644 index 0000000..f115408 --- /dev/null +++ b/src/controllers/api/channel/permissionsController.js @@ -0,0 +1,73 @@ +/*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 channelModel = require('../../../schemas/channel/channelSchema.js'); + +//api account functions +module.exports.get = async function(req, res){ + try{ + const validResult = validationResult(req); + + if(validResult.isEmpty()){ + const data = matchedData(req); + const channel = await channelModel.findOne({name: data.chanName}); + + + if(channel == null){ + throw new Error("Channel not found."); + } + + res.status(200); + return res.send(channel.permissions); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} + +module.exports.post = async function(req, res){ + try{ + const validResult = validationResult(req); + + if(validResult.isEmpty()){ + const data = matchedData(req); + const channel = await channelModel.findOne({name: data.chanName}); + const permissionsMap = new Map(Object.entries(data.channelPermissionsMap)); + + if(channel == null){ + throw new Error("Channel not found."); + } + + res.status(200); + return res.send(await channel.updateChannelPerms(permissionsMap)); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } + +} \ No newline at end of file diff --git a/src/controllers/api/channel/registerController.js b/src/controllers/api/channel/registerController.js index 6c12dfd..79cff81 100644 --- a/src/controllers/api/channel/registerController.js +++ b/src/controllers/api/channel/registerController.js @@ -19,7 +19,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {exceptionHandler} = require('../../../utils/loggerUtils.js'); -const channelModel = require('../../../schemas/channelSchema'); +const channelModel = require('../../../schemas/channel/channelSchema'); //api account functions module.exports.post = async function(req, res){ diff --git a/src/controllers/api/channel/settingsController.js b/src/controllers/api/channel/settingsController.js index 471f861..7c73411 100644 --- a/src/controllers/api/channel/settingsController.js +++ b/src/controllers/api/channel/settingsController.js @@ -19,7 +19,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {exceptionHandler} = require('../../../utils/loggerUtils.js'); -const channelModel = require('../../../schemas/channelSchema'); +const channelModel = require('../../../schemas/channel/channelSchema'); //api account functions module.exports.get = async function(req, res){ diff --git a/src/controllers/channelSettingsController.js b/src/controllers/channelSettingsController.js index 3520373..27226f2 100644 --- a/src/controllers/channelSettingsController.js +++ b/src/controllers/channelSettingsController.js @@ -19,7 +19,7 @@ const config = require('../../config.json'); //local imports const {exceptionHandler} = require('../utils/loggerUtils.js'); -const channelModel = require('../schemas/channelSchema'); +const channelModel = require('../schemas/channel/channelSchema'); //root index functions module.exports.get = async function(req, res){ diff --git a/src/controllers/indexController.js b/src/controllers/indexController.js index 1edc763..4132071 100644 --- a/src/controllers/indexController.js +++ b/src/controllers/indexController.js @@ -19,7 +19,7 @@ const config = require('../../config.json'); //local imports const {exceptionHandler} = require('../utils/loggerUtils.js'); -const channelModel = require('../schemas/channelSchema'); +const channelModel = require('../schemas/channel/channelSchema'); //root index functions module.exports.get = async function(req, res){ diff --git a/src/routers/api/accountRouter.js b/src/routers/api/accountRouter.js index 8377d36..d02d42f 100644 --- a/src/routers/api/accountRouter.js +++ b/src/routers/api/accountRouter.js @@ -18,7 +18,7 @@ along with this program. If not, see .*/ const { Router } = require('express'); //local imports -const accountValidator = require("../../validators/accountValidator"); +const {accountValidator} = require("../../validators/accountValidator"); const loginController = require("../../controllers/api/account/loginController"); const logoutController = require("../../controllers/api/account/logoutController"); const registerController = require("../../controllers/api/account/registerController"); diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js index 564e131..fa848d3 100644 --- a/src/routers/api/adminRouter.js +++ b/src/routers/api/adminRouter.js @@ -19,7 +19,7 @@ const { Router } = require('express'); //local imports -const accountValidator = require("../../validators/accountValidator"); +const {accountValidator} = require("../../validators/accountValidator"); const {permissionsValidator} = require("../../validators/permissionsValidator"); const permissionSchema = require("../../schemas/permissionSchema"); const listUsersController = require("../../controllers/api/admin/listUsersController"); diff --git a/src/routers/api/channelRouter.js b/src/routers/api/channelRouter.js index 68ea401..e8c8d87 100644 --- a/src/routers/api/channelRouter.js +++ b/src/routers/api/channelRouter.js @@ -19,10 +19,12 @@ const { Router } = require('express'); //local imports const permissionSchema = require("../../schemas/permissionSchema"); -const channelValidator = require("../../validators/channelValidator"); +const {channelValidator} = require("../../validators/channelValidator"); +const {channelPermissionValidator} = require("../../validators/permissionsValidator"); const registerController = require("../../controllers/api/channel/registerController"); const listController = require("../../controllers/api/channel/listController"); const settingsController = require("../../controllers/api/channel/settingsController"); +const permissionsController = require("../../controllers/api/channel/permissionsController") const deleteController = require("../../controllers/api/channel/deleteController"); //globals @@ -38,6 +40,8 @@ router.post('/register', channelValidator.name(), channelValidator.description() 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.post('/delete', channelValidator.name('chanName'), channelValidator.name('confirm'),deleteController.post); module.exports = router; \ No newline at end of file diff --git a/src/schemas/channel/channelPermissionSchema.js b/src/schemas/channel/channelPermissionSchema.js new file mode 100644 index 0000000..4b15a6f --- /dev/null +++ b/src/schemas/channel/channelPermissionSchema.js @@ -0,0 +1,43 @@ +/*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 {mongoose} = require('mongoose'); + +//This originally belonged to the permissionSchema, but this avoids circular dependencies. +const rankEnum = ["anon", "user", "gold", "bot", "mod", "admin"]; + +//Since this is intended to be used as a child schema for multiple parent schemas, we won't export it as a model +const channelPermissionSchema = new mongoose.Schema({ + manageChannel: { + type: mongoose.SchemaTypes.String, + enum: rankEnum, + default: "admin", + required: true + }, + deleteChannel: { + type: mongoose.SchemaTypes.String, + enum: rankEnum, + default: "admin", + required: true + } +}); + +//Only putting the rank enum out, all other logic should be handled by channelSchema methods to avoid circular dependencies +//Alternatively if things get to big we can make it it's own util. +channelPermissionSchema.statics.rankEnum = rankEnum; + +module.exports = channelPermissionSchema; \ No newline at end of file diff --git a/src/schemas/channelSchema.js b/src/schemas/channel/channelSchema.js similarity index 65% rename from src/schemas/channelSchema.js rename to src/schemas/channel/channelSchema.js index 31a3a01..0032a61 100644 --- a/src/schemas/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -18,7 +18,9 @@ along with this program. If not, see .*/ const {mongoose} = require('mongoose'); //Local Imports -const statSchema = require('./statSchema.js'); +const statModel = require('../statSchema.js'); +const permissionModel = require('../permissionSchema.js'); +const channelPermissionSchema = require('./channelPermissionSchema.js'); const channelSchema = new mongoose.Schema({ id: { @@ -45,10 +47,27 @@ const channelSchema = new mongoose.Schema({ type: mongoose.SchemaTypes.Boolean, required: true, default: true + }, + }, + permissions: { + type: channelPermissionSchema, + default: () => ({}) + }, + rankList: [{ + user: { + type: mongoose.SchemaTypes.ObjectID, + required: true, + ref: "user" + }, + rank: { + type: mongoose.SchemaTypes.Boolean, + required: true, + enum: permissionModel.rankEnum } - } + }] }); + channelSchema.pre('save', async function (next){ if(this.isModified("name")){ if(this.name.match(/^[a-z0-9_\-.]+$/i) == null){ @@ -68,7 +87,7 @@ channelSchema.statics.register = async function(channelObj){ if(chanDB){ throw new Error("Channel name already taken!"); }else{ - const id = await statSchema.incrementChannelCount(); + const id = await statModel.incrementChannelCount(); const newChannel = await this.create((thumbnail ? {id, name, description, thumbnail} : {id, name, description})); } } @@ -109,6 +128,46 @@ 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."); + } + + this.permissions[key] = value; + }) + + await this.save(); + + return this.permissions; +} + +channelSchema.methods.channelPermCheck = async function(user, perm){ + const perms = await permissionSchema.getPerms(); + + //Set user to anon rank if no rank was found for the given user + if(user == null || user.rank == null){ + user ={ + rank: "anon" + }; + } + + //Check if this permission exists + if(this.permissions[perm] != null){ + //if so get required rank as a number + requiredRank = permissionModel.rankToNum(this[perm]); + //get the required site-wide rank to override channel perms + requiredOverrideRank = permissionModel.rankToNum(perms.channeOverrides[perm]); + + //get user site rank as a number + userRank = user ? permissionModel.rankToNum(user.rank) : 0; + + }else{ + //if not scream and shout + throw new Error(`Permission check '${perm}' not found!`); + } +} + channelSchema.methods.nuke = async function(confirm){ if(confirm == "" || confirm == null){ throw new Error("Empty Confirmation String!"); diff --git a/src/schemas/permissionSchema.js b/src/schemas/permissionSchema.js index ede3bef..aa12779 100644 --- a/src/schemas/permissionSchema.js +++ b/src/schemas/permissionSchema.js @@ -17,7 +17,12 @@ along with this program. If not, see .*/ //NPM Imports const {mongoose} = require('mongoose'); -const rankEnum = ["anon","user", "gold", "bot", "mod", "admin"]; +//Local Imports +const channelPermissionSchema = require('./channel/channelPermissionSchema'); + +//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: { @@ -50,6 +55,10 @@ const permissionSchema = new mongoose.Schema({ default: "admin", required: true }, + channelOverrides: { + type: channelPermissionSchema, + default: () => ({}) + }, }); //Statics @@ -108,6 +117,7 @@ permissionSchema.statics.permCheck = async function(user, perm){ } } +//Middleware for rank checks permissionSchema.statics.reqPermCheck = function(perm){ return async (req, res, next)=>{ diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index d875abd..16f562d 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -20,13 +20,13 @@ const { check, body, checkSchema, checkExact} = require('express-validator'); //local imports const {isRank} = require('./permissionsValidator'); -module.exports = { +module.exports.accountValidator = { user: (field = 'user') => body(field).escape().trim().isLength({min: 1, max: 22}), //Password security requirements may change over time, therefore we should only validate against strongPassword() when creating new accounts //that way we don't break old ones upon change pass: (field = 'pass') => body(field).notEmpty().escape().trim(), - securePass: (field) => module.exports.pass(field).isStrongPassword({minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1}), + securePass: (field) => module.exports.accountValidator.pass(field).isStrongPassword({minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1}), email: (field = 'email') => body(field).optional().isEmail().normalizeEmail(), diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index e74a9b9..b965766 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -18,9 +18,9 @@ along with this program. If not, see .*/ const { check, body, checkSchema, checkExact} = require('express-validator'); //local imports -const accountValidator = require('./accountValidator'); +const {accountValidator} = require('./accountValidator'); -module.exports = { +module.exports.channelValidator = { name: (field = 'name') => check(field).escape().trim().isLength({min: 1, max: 50}), description: (field = 'description') => body(field).escape().trim().isLength({min: 1, max: 1000}), diff --git a/src/validators/permissionsValidator.js b/src/validators/permissionsValidator.js index 738fa3b..3ffa3b6 100644 --- a/src/validators/permissionsValidator.js +++ b/src/validators/permissionsValidator.js @@ -59,4 +59,23 @@ module.exports.permissionsValidator = { }, } })) +} + +module.exports.channelPermissionValidatorSchema = { + 'channelPermissionsMap.manageChannel': { + optional: true, + custom: { + options: module.exports.isRank + }, + }, + 'channelPermissionsMap.deleteChannel': { + optional: true, + custom: { + options: module.exports.isRank + }, + } +} + +module.exports.channelPermissionValidator = { + channelPermissionsMap: () => checkExact(checkSchema(module.exports.channelPermissionValidatorSchema)) } \ No newline at end of file diff --git a/www/css/channel.css b/www/css/channel.css index 35c0eab..f81ce66 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -157,6 +157,8 @@ p.panel-head-element{ #chat-panel-flair-select{ margin-left: 0.5em; + text-align: center; + appearance: none; } input#chat-panel-prompt{ @@ -176,6 +178,7 @@ input#chat-panel-prompt{ .chat-entry-username{ margin: 0.2em; + margin-left: 0; } .chat-entry-body{ @@ -184,6 +187,7 @@ input#chat-panel-prompt{ .chat-entry-high-level{ margin: 0.2em; + margin-right: 0; z-index: 2; background-image: url("/img/sweet_leaf_simple.png"); background-size: 1.3em; @@ -192,6 +196,7 @@ input#chat-panel-prompt{ background-position-y: top; width: 1.5em; text-align: center; + flex-shrink: 0; } .chat-entry-high-level-img{ diff --git a/www/js/utils.js b/www/js/utils.js index dedb55a..f564c4a 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -236,6 +236,23 @@ class canopyAjaxUtils{ } } + async setChannelPermissions(chanName, permissionsMap){ + var response = await fetch(`/api/channel/permissions`,{ + 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, channelPermissionsMap: Object.fromEntries(permissionsMap)}) + }); + + 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",