diff --git a/src/controllers/adminPanelController.js b/src/controllers/adminPanelController.js index 501a02b..a422f9a 100644 --- a/src/controllers/adminPanelController.js +++ b/src/controllers/adminPanelController.js @@ -17,6 +17,7 @@ along with this program. If not, see .*/ //Config const config = require('../../config.json'); const userModel = require('../schemas/userSchema'); +const permissionModel = require('../schemas/permissionSchema'); const channelModel = require('../schemas/channelSchema'); const {exceptionHandler} = require("../utils/loggerUtils"); @@ -25,7 +26,7 @@ module.exports.get = async function(req, res){ try{ const chanGuide = await channelModel.getChannelList(true); const userList = await userModel.getUserList(true); - return res.render('adminPanel', {instance: config.instanceName, user: req.session.user, chanGuide: chanGuide, userList: userList}); + return res.render('adminPanel', {instance: config.instanceName, user: req.session.user, chanGuide: chanGuide, userList: userList, rankEnum: permissionModel.rankEnum}); }catch(err){ return exceptionHandler(res,err); } diff --git a/src/controllers/api/admin/changeRankController.js b/src/controllers/api/admin/changeRankController.js new file mode 100644 index 0000000..ce2130c --- /dev/null +++ b/src/controllers/api/admin/changeRankController.js @@ -0,0 +1,54 @@ +/*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'); +const permissionModel = require('../../../schemas/permissionSchema'); +const userModel = require('../../../schemas/userSchema'); + +//api account functions +module.exports.post = async function(req, res){ + try{ + const validResult = validationResult(req); + + if(validResult.isEmpty()){ + const data = matchedData(req); + const user = await userModel.findOne({user: data.user}); + + if(user == null){ + res.status(400); + res.send({errors:[{type: "Bad Query", msg: "User not found.", date: new Date()}]}); + }else if(permissionModel.rankToNum(data.rank) >= permissionModel.rankToNum(req.session.user.rank)){ + res.status(401); + return res.send({errors:[{type: "Unauthorized", msg: "New rank must be below that of the user changing it.", date: new Date()}]}); + } + + user.rank = data.rank; + await user.save(); + + res.status(200); + return res.send({user: user.user, id: user.id, rank: user.rank}); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + return exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/controllers/api/admin/listChannelsController.js b/src/controllers/api/admin/listChannelsController.js new file mode 100644 index 0000000..0e64cd6 --- /dev/null +++ b/src/controllers/api/admin/listChannelsController.js @@ -0,0 +1,31 @@ +/*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 .*/ + +//local imports +const channelModel = require('../../../schemas/channelSchema'); + +//api account functions +module.exports.get = async function(req, res){ + try{ + const chanGuide = await channelModel.getChannelList(true); + + res.status(200); + return res.send(chanGuide); + }catch(err){ + res.status(400); + return res.send(err.message); + } +} \ No newline at end of file diff --git a/src/controllers/api/admin/listUsersController.js b/src/controllers/api/admin/listUsersController.js new file mode 100644 index 0000000..6de55d4 --- /dev/null +++ b/src/controllers/api/admin/listUsersController.js @@ -0,0 +1,31 @@ +/*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 .*/ + +//local imports +const userModel = require('../../../schemas/userSchema'); + +//api account functions +module.exports.get = async function(req, res){ + try{ + const userList = await userModel.getUserList(true); + + res.status(200); + return res.send(userList); + }catch(err){ + res.status(400); + return res.send(err.message); + } +} \ No newline at end of file diff --git a/src/controllers/api/channel/listController.js b/src/controllers/api/channel/listController.js index 1a6cc4a..8474561 100644 --- a/src/controllers/api/channel/listController.js +++ b/src/controllers/api/channel/listController.js @@ -20,8 +20,7 @@ const channelModel = require('../../../schemas/channelSchema'); //api account functions module.exports.get = async function(req, res){ try{ - //I'm not sanatizing this, it never gets processed... - const chanGuide = await channelModel.getChannelList(req.query.showHidden || req.query.showHidden === ''); + const chanGuide = await channelModel.getChannelList(); res.status(200); return res.send(chanGuide); diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js new file mode 100644 index 0000000..d94c11d --- /dev/null +++ b/src/routers/api/adminRouter.js @@ -0,0 +1,39 @@ +/*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 { Router } = require('express'); + + +//local imports +const {accountValidator} = require("../../utils/validators"); +const permissionSchema = require("../../schemas/permissionSchema"); +const listUsersController = require("../../controllers/api/admin/listUsersController"); +const listChannelsController = require("../../controllers/api/admin/listChannelsController"); +const changeRankController = require("../../controllers/api/admin/changeRankController"); + +//globals +const router = Router(); + +//Use authentication middleware +router.use(permissionSchema.reqPermCheck("adminPanel")) + +//routing functions +router.get('/listUsers', listUsersController.get); +router.get('/listChannels', listChannelsController.get); +router.post('/changeRank', accountValidator.user(), accountValidator.rank(), changeRankController.post); + +module.exports = router; diff --git a/src/routers/apiRouter.js b/src/routers/apiRouter.js index 50ced5e..9e9048d 100644 --- a/src/routers/apiRouter.js +++ b/src/routers/apiRouter.js @@ -20,6 +20,7 @@ const { Router } = require('express'); //local imports const accountRouter = require("./api/accountRouter"); const channelRouter = require("./api/channelRouter"); +const adminRouter = require("./api/adminRouter"); //globals const router = Router(); @@ -27,5 +28,6 @@ const router = Router(); //routing functions router.use('/account', accountRouter); router.use('/channel', channelRouter); +router.use('/admin', adminRouter); module.exports = router; diff --git a/src/utils/validators.js b/src/utils/validators.js index b3c2da2..3faf476 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -17,6 +17,15 @@ along with this program. If not, see .*/ //NPM Imports const { check, body, checkSchema, checkExact} = require('express-validator'); +//local imports +const permissionSchema = require("../schemas/permissionSchema"); + +function isRank(value){ + rankVal = permissionSchema.rankToNum(value); + + return rankVal != -1; +} + module.exports.accountValidator = { user: (field = 'user') => body(field).escape().trim().isLength({min: 1, max: 22}), @@ -32,6 +41,8 @@ module.exports.accountValidator = { signature: (field = 'signature') => body(field).optional().escape().trim().isLength({min: 1, max: 150}), bio: (field = 'bio') => body(field).optional().escape().trim().isLength({min: 1, max: 1000}), + + rank: (field = 'rank') => body(field).escape().trim().custom(isRank) } module.exports.channelValidator = { diff --git a/src/views/adminPanel.ejs b/src/views/adminPanel.ejs index f43cc43..20534ab 100644 --- a/src/views/adminPanel.ejs +++ b/src/views/adminPanel.ejs @@ -23,10 +23,10 @@ along with this program. If not, see .--> <%- include('partial/navbar', {user}); %> <%- include('partial/adminPanel/channelList', {chanGuide}) %> - <%- include('partial/adminPanel/userList', {userList}) %> + <%- include('partial/adminPanel/userList', {user, userList, rankEnum}) %>
<%- include('partial/scripts', {user}); %> - +
diff --git a/src/views/partial/adminPanel/userList.ejs b/src/views/partial/adminPanel/userList.ejs index 84ad952..59a32a8 100644 --- a/src/views/partial/adminPanel/userList.ejs +++ b/src/views/partial/adminPanel/userList.ejs @@ -21,31 +21,39 @@

Sign-Up Date

- <% userList.forEach((user) => { %> - - - - + <% userList.forEach((curUser) => { %> + + + + - - - <%- user.id %> + + + <%- curUser.id %> - - - <%- user.name %> + + + <%- curUser.name %> - - <%- user.rank %> + + <% if(rankEnum.indexOf(curUser.rank) < rankEnum.indexOf(user.rank)){%> + + <% }else{ %> + <%- curUser.rank %> + <% } %> - - <%- user.email ? user.email : "N/A" %> + + <%- curUser.email ? curUser.email : "N/A" %> - - <%- user.date %> + + <%- curUser.date %> <% }); %> diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js new file mode 100644 index 0000000..f7fe6e4 --- /dev/null +++ b/www/js/adminPanel.js @@ -0,0 +1,69 @@ +/*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 .*/ + +class canopyAdminUtils{ + constructor(){ + + } + + async setUserRank(user, rank){ + var response = await fetch(`/api/admin/changeRank`,{ + 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({user, rank}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } +} + +class adminUserList{ + constructor(){ + this.rankSelectors = document.querySelectorAll(".admin-user-list-rank-select"); + + this.setupInput(); + } + + setupInput(){ + this.rankSelectors.forEach((rankSelector)=>{ + rankSelector.addEventListener("change", this.setRank.bind(this)) + }); + } + + async setRank(event){ + const user = event.target.id.replace("admin-user-list-rank-select-",""); + const rank = event.target.value; + + this.updateSelect(await adminUtil.setUserRank(user, rank), event.target); + } + + updateSelect(update, select){ + if(update != null){ + select.value = update.rank; + } + } + +} + +const adminUtil = new canopyAdminUtils(); +const userList = new adminUserList(); \ No newline at end of file diff --git a/www/js/profile.js b/www/js/profile.js index c127efc..7620549 100644 --- a/www/js/profile.js +++ b/www/js/profile.js @@ -172,7 +172,7 @@ class deleteAccountPrompt{ if(response.status == 200){ window.location.pathname = '/'; }else{ - displayResponseError(await response.json()); + utils.ux.displayResponseError(await response.json()); } } } diff --git a/www/js/utils.js b/www/js/utils.js index 674e36a..c980c6c 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -25,6 +25,13 @@ class canopyUXUtils{ constructor(){ } + async displayResponseError(body){ + const errors = body.errors; + errors.forEach((err)=>{ + window.alert(`ERROR: ${err.msg} \nTYPE: ${err.type} \nDATE: ${err.date}`); + }); + } + static clickDragger = class{ constructor(handle, element, leftHandle = true){ @@ -117,13 +124,7 @@ class canopyAjaxUtils{ constructor(){ - } - - //Profile - async displayResponseError(body){ - const err = body.msg; - window.alert(`ERROR:\n${err}`); - } + } async register(user, pass, passConfirm, email){ var response = await fetch(`/api/account/register`,{