Created channel perms schema and api routes.

This commit is contained in:
rainbownapkin 2024-11-23 13:41:48 -05:00
parent 9dbbc4e924
commit 8384e732f3
21 changed files with 250 additions and 20 deletions

View file

@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Local Imports //Local Imports
const channelModel = require('../../schemas/channelSchema'); const channelModel = require('../../schemas/channel/channelSchema');
const flairModel = require('../../schemas/flairSchema'); const flairModel = require('../../schemas/flairSchema');
const userModel = require('../../schemas/userSchema'); const userModel = require('../../schemas/userSchema');
const loggerUtils = require('../../utils/loggerUtils'); const loggerUtils = require('../../utils/loggerUtils');

View file

@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const config = require('../../config.json'); const config = require('../../config.json');
const userModel = require('../schemas/userSchema'); const userModel = require('../schemas/userSchema');
const permissionModel = require('../schemas/permissionSchema'); const permissionModel = require('../schemas/permissionSchema');
const channelModel = require('../schemas/channelSchema'); const channelModel = require('../schemas/channel/channelSchema');
const {exceptionHandler} = require("../utils/loggerUtils"); const {exceptionHandler} = require("../utils/loggerUtils");
//register page functions //register page functions

View file

@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//local imports //local imports
const {exceptionHandler} = require('../../../utils/loggerUtils.js'); const {exceptionHandler} = require('../../../utils/loggerUtils.js');
const channelModel = require('../../../schemas/channelSchema.js'); const channelModel = require('../../../schemas/channel/channelSchema.js');
//api list channel functions //api list channel functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){

View file

@ -19,7 +19,7 @@ const {validationResult, matchedData} = require('express-validator');
//local imports //local imports
const {exceptionHandler} = require('../../../utils/loggerUtils.js'); const {exceptionHandler} = require('../../../utils/loggerUtils.js');
const channelModel = require('../../../schemas/channelSchema'); const channelModel = require('../../../schemas/channel/channelSchema');
//api account functions //api account functions
module.exports.post = async function(req, res){ module.exports.post = async function(req, res){

View file

@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//local imports //local imports
const channelModel = require('../../../schemas/channelSchema'); const channelModel = require('../../../schemas/channel/channelSchema');
const {exceptionHandler} = require('../../../utils/loggerUtils.js'); const {exceptionHandler} = require('../../../utils/loggerUtils.js');
//api account functions //api account functions

View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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);
}
}

View file

@ -19,7 +19,7 @@ const {validationResult, matchedData} = require('express-validator');
//local imports //local imports
const {exceptionHandler} = require('../../../utils/loggerUtils.js'); const {exceptionHandler} = require('../../../utils/loggerUtils.js');
const channelModel = require('../../../schemas/channelSchema'); const channelModel = require('../../../schemas/channel/channelSchema');
//api account functions //api account functions
module.exports.post = async function(req, res){ module.exports.post = async function(req, res){

View file

@ -19,7 +19,7 @@ const {validationResult, matchedData} = require('express-validator');
//local imports //local imports
const {exceptionHandler} = require('../../../utils/loggerUtils.js'); const {exceptionHandler} = require('../../../utils/loggerUtils.js');
const channelModel = require('../../../schemas/channelSchema'); const channelModel = require('../../../schemas/channel/channelSchema');
//api account functions //api account functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){

View file

@ -19,7 +19,7 @@ const config = require('../../config.json');
//local imports //local imports
const {exceptionHandler} = require('../utils/loggerUtils.js'); const {exceptionHandler} = require('../utils/loggerUtils.js');
const channelModel = require('../schemas/channelSchema'); const channelModel = require('../schemas/channel/channelSchema');
//root index functions //root index functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){

View file

@ -19,7 +19,7 @@ const config = require('../../config.json');
//local imports //local imports
const {exceptionHandler} = require('../utils/loggerUtils.js'); const {exceptionHandler} = require('../utils/loggerUtils.js');
const channelModel = require('../schemas/channelSchema'); const channelModel = require('../schemas/channel/channelSchema');
//root index functions //root index functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){

View file

@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const { Router } = require('express'); const { Router } = require('express');
//local imports //local imports
const accountValidator = require("../../validators/accountValidator"); const {accountValidator} = require("../../validators/accountValidator");
const loginController = require("../../controllers/api/account/loginController"); const loginController = require("../../controllers/api/account/loginController");
const logoutController = require("../../controllers/api/account/logoutController"); const logoutController = require("../../controllers/api/account/logoutController");
const registerController = require("../../controllers/api/account/registerController"); const registerController = require("../../controllers/api/account/registerController");

View file

@ -19,7 +19,7 @@ const { Router } = require('express');
//local imports //local imports
const accountValidator = require("../../validators/accountValidator"); const {accountValidator} = require("../../validators/accountValidator");
const {permissionsValidator} = require("../../validators/permissionsValidator"); const {permissionsValidator} = require("../../validators/permissionsValidator");
const permissionSchema = require("../../schemas/permissionSchema"); const permissionSchema = require("../../schemas/permissionSchema");
const listUsersController = require("../../controllers/api/admin/listUsersController"); const listUsersController = require("../../controllers/api/admin/listUsersController");

View file

@ -19,10 +19,12 @@ const { Router } = require('express');
//local imports //local imports
const permissionSchema = require("../../schemas/permissionSchema"); 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 registerController = require("../../controllers/api/channel/registerController");
const listController = require("../../controllers/api/channel/listController"); const listController = require("../../controllers/api/channel/listController");
const settingsController = require("../../controllers/api/channel/settingsController"); const settingsController = require("../../controllers/api/channel/settingsController");
const permissionsController = require("../../controllers/api/channel/permissionsController")
const deleteController = require("../../controllers/api/channel/deleteController"); const deleteController = require("../../controllers/api/channel/deleteController");
//globals //globals
@ -38,6 +40,8 @@ router.post('/register', channelValidator.name(), channelValidator.description()
router.get('/list', listController.get); router.get('/list', listController.get);
router.get('/settings', channelValidator.name('chanName'), settingsController.get); router.get('/settings', channelValidator.name('chanName'), settingsController.get);
router.post('/settings', channelValidator.name('chanName'), channelValidator.settingsMap(), settingsController.post); 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); router.post('/delete', channelValidator.name('chanName'), channelValidator.name('confirm'),deleteController.post);
module.exports = router; module.exports = router;

View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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;

View file

@ -18,7 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const {mongoose} = require('mongoose'); const {mongoose} = require('mongoose');
//Local Imports //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({ const channelSchema = new mongoose.Schema({
id: { id: {
@ -45,10 +47,27 @@ const channelSchema = new mongoose.Schema({
type: mongoose.SchemaTypes.Boolean, type: mongoose.SchemaTypes.Boolean,
required: true, required: true,
default: 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){ channelSchema.pre('save', async function (next){
if(this.isModified("name")){ if(this.isModified("name")){
if(this.name.match(/^[a-z0-9_\-.]+$/i) == null){ if(this.name.match(/^[a-z0-9_\-.]+$/i) == null){
@ -68,7 +87,7 @@ channelSchema.statics.register = async function(channelObj){
if(chanDB){ if(chanDB){
throw new Error("Channel name already taken!"); throw new Error("Channel name already taken!");
}else{ }else{
const id = await statSchema.incrementChannelCount(); const id = await statModel.incrementChannelCount();
const newChannel = await this.create((thumbnail ? {id, name, description, thumbnail} : {id, name, description})); 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; 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){ channelSchema.methods.nuke = async function(confirm){
if(confirm == "" || confirm == null){ if(confirm == "" || confirm == null){
throw new Error("Empty Confirmation String!"); throw new Error("Empty Confirmation String!");

View file

@ -17,7 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//NPM Imports //NPM Imports
const {mongoose} = require('mongoose'); 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({ const permissionSchema = new mongoose.Schema({
adminPanel: { adminPanel: {
@ -50,6 +55,10 @@ const permissionSchema = new mongoose.Schema({
default: "admin", default: "admin",
required: true required: true
}, },
channelOverrides: {
type: channelPermissionSchema,
default: () => ({})
},
}); });
//Statics //Statics
@ -108,6 +117,7 @@ permissionSchema.statics.permCheck = async function(user, perm){
} }
} }
//Middleware for rank checks
permissionSchema.statics.reqPermCheck = function(perm){ permissionSchema.statics.reqPermCheck = function(perm){
return async (req, res, next)=>{ return async (req, res, next)=>{

View file

@ -20,13 +20,13 @@ const { check, body, checkSchema, checkExact} = require('express-validator');
//local imports //local imports
const {isRank} = require('./permissionsValidator'); const {isRank} = require('./permissionsValidator');
module.exports = { module.exports.accountValidator = {
user: (field = 'user') => body(field).escape().trim().isLength({min: 1, max: 22}), 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 //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 //that way we don't break old ones upon change
pass: (field = 'pass') => body(field).notEmpty().escape().trim(), 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(), email: (field = 'email') => body(field).optional().isEmail().normalizeEmail(),

View file

@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const { check, body, checkSchema, checkExact} = require('express-validator'); const { check, body, checkSchema, checkExact} = require('express-validator');
//local imports //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}), name: (field = 'name') => check(field).escape().trim().isLength({min: 1, max: 50}),
description: (field = 'description') => body(field).escape().trim().isLength({min: 1, max: 1000}), description: (field = 'description') => body(field).escape().trim().isLength({min: 1, max: 1000}),

View file

@ -60,3 +60,22 @@ 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))
}

View file

@ -157,6 +157,8 @@ p.panel-head-element{
#chat-panel-flair-select{ #chat-panel-flair-select{
margin-left: 0.5em; margin-left: 0.5em;
text-align: center;
appearance: none;
} }
input#chat-panel-prompt{ input#chat-panel-prompt{
@ -176,6 +178,7 @@ input#chat-panel-prompt{
.chat-entry-username{ .chat-entry-username{
margin: 0.2em; margin: 0.2em;
margin-left: 0;
} }
.chat-entry-body{ .chat-entry-body{
@ -184,6 +187,7 @@ input#chat-panel-prompt{
.chat-entry-high-level{ .chat-entry-high-level{
margin: 0.2em; margin: 0.2em;
margin-right: 0;
z-index: 2; z-index: 2;
background-image: url("/img/sweet_leaf_simple.png"); background-image: url("/img/sweet_leaf_simple.png");
background-size: 1.3em; background-size: 1.3em;
@ -192,6 +196,7 @@ input#chat-panel-prompt{
background-position-y: top; background-position-y: top;
width: 1.5em; width: 1.5em;
text-align: center; text-align: center;
flex-shrink: 0;
} }
.chat-entry-high-level-img{ .chat-entry-high-level-img{

View file

@ -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){ async deleteChannel(chanName, confirm){
var response = await fetch(`/api/channel/delete`,{ var response = await fetch(`/api/channel/delete`,{
method: "POST", method: "POST",