Added user rank changes to admin page.

This commit is contained in:
rainbownapkin 2024-11-17 17:37:07 -05:00
parent 064109556c
commit 8a4a21cff0
13 changed files with 276 additions and 30 deletions

View file

@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Config //Config
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 channelModel = require('../schemas/channelSchema'); const channelModel = require('../schemas/channelSchema');
const {exceptionHandler} = require("../utils/loggerUtils"); const {exceptionHandler} = require("../utils/loggerUtils");
@ -25,7 +26,7 @@ module.exports.get = async function(req, res){
try{ try{
const chanGuide = await channelModel.getChannelList(true); const chanGuide = await channelModel.getChannelList(true);
const userList = await userModel.getUserList(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){ }catch(err){
return exceptionHandler(res,err); return exceptionHandler(res,err);
} }

View file

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

View file

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

View file

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

View file

@ -20,8 +20,7 @@ const channelModel = require('../../../schemas/channelSchema');
//api account functions //api account functions
module.exports.get = async function(req, res){ module.exports.get = async function(req, res){
try{ try{
//I'm not sanatizing this, it never gets processed... const chanGuide = await channelModel.getChannelList();
const chanGuide = await channelModel.getChannelList(req.query.showHidden || req.query.showHidden === '');
res.status(200); res.status(200);
return res.send(chanGuide); return res.send(chanGuide);

View file

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

View file

@ -20,6 +20,7 @@ const { Router } = require('express');
//local imports //local imports
const accountRouter = require("./api/accountRouter"); const accountRouter = require("./api/accountRouter");
const channelRouter = require("./api/channelRouter"); const channelRouter = require("./api/channelRouter");
const adminRouter = require("./api/adminRouter");
//globals //globals
const router = Router(); const router = Router();
@ -27,5 +28,6 @@ const router = Router();
//routing functions //routing functions
router.use('/account', accountRouter); router.use('/account', accountRouter);
router.use('/channel', channelRouter); router.use('/channel', channelRouter);
router.use('/admin', adminRouter);
module.exports = router; module.exports = router;

View file

@ -17,6 +17,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//NPM Imports //NPM Imports
const { check, body, checkSchema, checkExact} = require('express-validator'); 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 = { 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}),
@ -32,6 +41,8 @@ module.exports.accountValidator = {
signature: (field = 'signature') => body(field).optional().escape().trim().isLength({min: 1, max: 150}), 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}), 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 = { module.exports.channelValidator = {

View file

@ -23,10 +23,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.-->
<body> <body>
<%- include('partial/navbar', {user}); %> <%- include('partial/navbar', {user}); %>
<%- include('partial/adminPanel/channelList', {chanGuide}) %> <%- include('partial/adminPanel/channelList', {chanGuide}) %>
<%- include('partial/adminPanel/userList', {userList}) %> <%- include('partial/adminPanel/userList', {user, userList, rankEnum}) %>
</body> </body>
<footer> <footer>
<%- include('partial/scripts', {user}); %> <%- include('partial/scripts', {user}); %>
<script src="js/adminPanel.js"></script> <script src="/js/adminPanel.js"></script>
</footer> </footer>
</html> </html>

View file

@ -21,31 +21,39 @@
<h3>Sign-Up Date</h3> <h3>Sign-Up Date</h3>
</td> </td>
</tr> </tr>
<% userList.forEach((user) => { %> <% userList.forEach((curUser) => { %>
<tr id="admin-user-list-entry-<%- user.name %>" class="admin-list-entry"> <tr id="admin-user-list-entry-<%- curUser.name %>" class="admin-list-entry">
<td id="admin-user-list-entry-img-<%- user.name %>" class="admin-list-entry admin-list-entry-item"> <td id="admin-user-list-entry-img-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item">
<a href="/profile/<%- user.name %>" class="admin-list-entry admin-list-entry-item"> <a href="/profile/<%- curUser.name %>" class="admin-list-entry admin-list-entry-item">
<img id="admin-user-list-entry-img-<%- user.name %>" class="admin-list-entry admin-list-entry-item" src="<%- user.img %>"> <img id="admin-user-list-entry-img-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item" src="<%- curUser.img %>">
</a> </a>
</td> </td>
<td id="admin-user-list-entry-id-<%- user.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row"> <td id="admin-user-list-entry-id-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row">
<a href="/profile/<%- user.name %>" class="admin-list-entry admin-list-entry-item"> <a href="/profile/<%- curUser.name %>" class="admin-list-entry admin-list-entry-item">
<%- user.id %> <%- curUser.id %>
</a> </a>
</td> </td>
<td id="admin-user-list-entry-name-<%- user.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row"> <td id="admin-user-list-entry-name-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row">
<a href="/profile/<%- user.name %>" class="admin-list-entry admin-list-entry-item"> <a href="/profile/<%- curUser.name %>" class="admin-list-entry admin-list-entry-item">
<%- user.name %> <%- curUser.name %>
</a> </a>
</td> </td>
<td id="admin-user-list-entry-rank-<%- user.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row"> <td id="admin-user-list-entry-rank-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row">
<%- user.rank %> <% if(rankEnum.indexOf(curUser.rank) < rankEnum.indexOf(user.rank)){%>
<select id="admin-user-list-rank-select-<%- curUser.name %>" class="admin-user-list-rank-select">
<%rankEnum.slice().reverse().forEach((rank)=>{ %>
<option <%if(curUser.rank == rank){%> selected <%}%> value="<%- rank %>"><%- rank %></option>
<% }); %>
</select>
<% }else{ %>
<%- curUser.rank %>
<% } %>
</td> </td>
<td id="admin-user-list-entry-email-<%- user.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row"> <td id="admin-user-list-entry-email-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row">
<%- user.email ? user.email : "N/A" %> <%- curUser.email ? curUser.email : "N/A" %>
</td> </td>
<td id="admin-user-list-entry-date-<%- user.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row"> <td id="admin-user-list-entry-date-<%- curUser.name %>" class="admin-list-entry admin-list-entry-item admin-list-entry-not-first-row">
<%- user.date %> <%- curUser.date %>
</td> </td>
</tr> </tr>
<% }); %> <% }); %>

69
www/js/adminPanel.js Normal file
View file

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

View file

@ -172,7 +172,7 @@ class deleteAccountPrompt{
if(response.status == 200){ if(response.status == 200){
window.location.pathname = '/'; window.location.pathname = '/';
}else{ }else{
displayResponseError(await response.json()); utils.ux.displayResponseError(await response.json());
} }
} }
} }

View file

@ -25,6 +25,13 @@ class canopyUXUtils{
constructor(){ 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{ static clickDragger = class{
constructor(handle, element, leftHandle = true){ constructor(handle, element, leftHandle = true){
@ -119,12 +126,6 @@ class canopyAjaxUtils{
} }
//Profile
async displayResponseError(body){
const err = body.msg;
window.alert(`ERROR:\n${err}`);
}
async register(user, pass, passConfirm, email){ async register(user, pass, passConfirm, email){
var response = await fetch(`/api/account/register`,{ var response = await fetch(`/api/account/register`,{
method: "POST", method: "POST",