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",