diff --git a/config.example.json b/config.example.json
index 5cdac66..9ca23e9 100644
--- a/config.example.json
+++ b/config.example.json
@@ -1,5 +1,6 @@
{
"instanceName": "Canopy",
+ "verbose": false,
"port": 8080,
"protocol": "http",
"domain": "localhost",
diff --git a/src/app/channel/channelManager.js b/src/app/channel/channelManager.js
index edd7e2d..9c3e6ab 100644
--- a/src/app/channel/channelManager.js
+++ b/src/app/channel/channelManager.js
@@ -18,6 +18,7 @@ along with this program. If not, see .*/
const channelModel = require('../../schemas/channel/channelSchema');
const emoteModel = require('../../schemas/emoteSchema');
const {userModel} = require('../../schemas/user/userSchema');
+const userBanModel = require('../../schemas/user/userBanSchema');
const loggerUtils = require('../../utils/loggerUtils');
const csrfUtils = require('../../utils/csrfUtils');
const activeChannel = require('./activeChannel');
@@ -39,23 +40,29 @@ module.exports = class{
}
async handleConnection(socket){
- //Prevent logged out connections and authenticate socket
- if(socket.request.session.user != null){
- try{
+ try{
+ //ensure unbanned ip and valid CSRF token
+ if(!(await this.validateSocket(socket))){
+ socket.disconnect();
+ return;
+ }
+
+ //Prevent logged out connections and authenticate socket
+ if(socket.request.session.user != null){
//Authenticate socket
const userDB = await this.authSocket(socket);
//Get the active channel based on the socket
var {activeChan, chanDB} = await this.getActiveChan(socket);
- //Check for ban
+ //Check for chan ban
const ban = await chanDB.checkBanByUserDoc(userDB);
if(ban != null){
//Toss out banned user's
if(ban.expirationDays < 0){
- socket.emit("kick", {type: "Banned", reason: "You have been permanently banned from this channel!"});
+ socket.emit("kick", {type: "kicked", reason: "You have been permanently banned from this channel!"});
}else{
- socket.emit("kick", {type: "Banned", reason: `You have been temporarily banned from this channel, and will be unbanned in ${ban.getDaysUntilExpiration()} day(s)!`});
+ socket.emit("kick", {type: "kicked", reason: `You have been temporarily banned from this channel, and will be unbanned in ${ban.getDaysUntilExpiration()} day(s)!`});
}
socket.disconnect();
return;
@@ -68,24 +75,41 @@ module.exports = class{
//Connect the socket to it's given channel
//Lil' hacky to pass chanDB like that, but why double up on DB calls?
activeChan.handleConnection(userDB, chanDB, socket);
- }catch(err){
- //Flip a table if something fucks up
- return loggerUtils.socketCriticalExceptionHandler(socket, err);
+ }else{
+ //Toss out anon's
+ socket.emit("kick", {type: "disconnected", reason: "You must log-in to join this channel!"});
+ socket.disconnect();
+ return;
}
- }else{
- //Toss out anon's
- socket.emit("kick", {type: "Disconnected", reason: "You must log-in to join this channel!"});
- socket.disconnect();
- return;
+ }catch(err){
+ //Flip a table if something fucks up
+ return loggerUtils.socketCriticalExceptionHandler(socket, err);
+ }
+ }
+
+ async validateSocket(socket){
+ //Look for ban by IP
+ const ipBanDB = await userBanModel.checkBanByIP(socket.handshake.address);
+
+ //If this ip is randy bobandy
+ if(ipBanDB != null){
+ //tell it to fuck off
+ socket.emit("kick", {type: "kicked", reason: "The IP address you are trying to connect from has been banned!"});
+ return false;
}
+
+
+ //Check for Cross-Site Request Forgery
+ if(!csrfUtils.isRequestValid(socket.request)){
+ socket.emit("kick", {type: "disconnected", reason: "Invalid CSRF Token!"});
+ return false;
+ }
+
+
+ return true;
}
async authSocket(socket){
- //Check for Cross-Site Request Forgery
- if(!csrfUtils.isRequestValid(socket.request)){
- throw new Error("Invalid CSRF Token!");
- }
-
//Find the user in the Database since the session won't store enough data to fulfill our needs :P
const userDB = await userModel.findOne({user: socket.request.session.user.user});
diff --git a/src/controllers/api/account/emailChangeRequestController.js b/src/controllers/api/account/emailChangeRequestController.js
index f1f7181..83fc654 100644
--- a/src/controllers/api/account/emailChangeRequestController.js
+++ b/src/controllers/api/account/emailChangeRequestController.js
@@ -71,7 +71,6 @@ module.exports.post = async function(req, res){
return res.send({errors: validResult.array()});
}
}catch(err){
- console.log(err)
return exceptionHandler(res, err);
}
}
\ No newline at end of file
diff --git a/src/controllers/api/account/registerController.js b/src/controllers/api/account/registerController.js
index f2b294e..3fe962b 100644
--- a/src/controllers/api/account/registerController.js
+++ b/src/controllers/api/account/registerController.js
@@ -52,6 +52,22 @@ module.exports.post = async function(req, res){
return errorHandler(res, 'Cannot re-create banned account!', 'unauthorized');
}
+ //Look for ban by IP
+ const ipBanDB = await userBanModel.checkBanByIP(req.ip);
+
+ //If this ip is randy bobandy
+ if(ipBanDB != null){
+ //Make the code and message look pretty (kinda) at the same time
+ const banMsg = [
+ 'The IP address you are trying to register an account from has been banned.',
+ 'If you beleive this to be an error feel free to reach out to your server administrator.',
+ 'Otherwise, fuck off :)'
+ ];
+
+ //tell it to fuck off
+ return errorHandler(res, banMsg.join('
'), 'unauthorized');
+ }
+
await userModel.register(user, req.ip);
return res.sendStatus(200);
}else{
@@ -59,7 +75,6 @@ module.exports.post = async function(req, res){
return res.send({errors: validResult.array()});
}
}catch(err){
- console.log(err);
return exceptionHandler(res, err);
}
}
\ No newline at end of file
diff --git a/src/controllers/api/admin/banController.js b/src/controllers/api/admin/banController.js
index fc7a549..15a1ff0 100644
--- a/src/controllers/api/admin/banController.js
+++ b/src/controllers/api/admin/banController.js
@@ -40,7 +40,7 @@ module.exports.post = async function(req, res){
try{
const validResult = validationResult(req);
if(validResult.isEmpty()){
- const {user, permanent, expirationDays} = matchedData(req);
+ const {user, permanent, ipBan, expirationDays} = matchedData(req);
const userDB = await userModel.findOne({user});
if(userDB == null){
@@ -54,7 +54,7 @@ module.exports.post = async function(req, res){
return errorHandler(res, 'You cannot ban peer/outranking users', 'Unauthorized', 401);
}
- await banModel.banByUserDoc(userDB, permanent, expirationDays);
+ await banModel.banByUserDoc(userDB, permanent, expirationDays, ipBan);
res.status(200);
return res.send(await banModel.getBans());
diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js
index 242e74f..71a7aa4 100644
--- a/src/routers/api/adminRouter.js
+++ b/src/routers/api/adminRouter.js
@@ -50,7 +50,7 @@ router.post('/changeRank', permissionSchema.reqPermCheck("changeRank"), account
//Ban
router.get('/ban', permissionSchema.reqPermCheck("adminPanel"), banController.get);
//Sometimes they're so simple you don't need to put your validators in their own special place :P
-router.post('/ban', permissionSchema.reqPermCheck("banUser"), accountValidator.user(), body("permanent").isBoolean(), body("expirationDays").isInt(), banController.post);
+router.post('/ban', permissionSchema.reqPermCheck("banUser"), accountValidator.user(), body("permanent").isBoolean(), body("ipBan").isBoolean(), body("expirationDays").isInt(), banController.post);
router.delete('/ban', permissionSchema.reqPermCheck("banUser"), accountValidator.user(), banController.delete);
//TokeCommands
router.get('/tokeCommands', permissionSchema.reqPermCheck("adminPanel"), tokeCommandController.get);
diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js
index 32eb67a..b147fe6 100644
--- a/src/schemas/channel/channelSchema.js
+++ b/src/schemas/channel/channelSchema.js
@@ -328,17 +328,9 @@ channelSchema.methods.rankCrawl = async function(userDB,cb){
//TODO: replace this with rank check function shared with setRank
this.rankList.forEach(async (rankObj, rankIndex) => {
//check against user ID to speed things up
- if(rankObj.user.user == null){
- if(rankObj.user.toString() == userDB._id.toString()){
- //If we found a match, call back
- cb(rankObj, rankIndex);
- }
- }else{
- //in case someone populated the users
- if(rankObj.user.user == userDB.user){
- //If we found a match, call back
- cb(rankObj, rankIndex);
- }
+ if(rankObj.user != null && rankObj.user._id.toString() == userDB._id.toString()){
+ //If we found a match, call back
+ cb(rankObj, rankIndex);
}
});
}
@@ -379,14 +371,22 @@ channelSchema.methods.setRank = async function(userDB,rank){
channelSchema.methods.getRankList = async function(){
//Create an empty array to hold the user list
const rankList = new Map()
+ //Create temp rank list to replace the current one in the advant we have busted users
+ let tempRankList = [];
+ //Flag that lets us know we gotta save
+ let reqSave = false;
//Populate the user objects in our ranklist based off of their DB ID's
await this.populate('rankList.user');
//For each rank object in the rank list
- this.rankList.forEach(async (rankObj, rankObjIndex) => {
+ for(rankObjIndex in this.rankList){
+ const rankObj = this.rankList[rankObjIndex];
//If the use still exists
if(rankObj.user != null){
+ //Push current rank object to the temp rank list in the advant that it doesn't get saved
+ tempRankList.push(rankObj);
+
//Create a new user object from rank object data
const userObj = {
id: rankObj.user.id,
@@ -397,12 +397,20 @@ channelSchema.methods.getRankList = async function(){
//Add our user object to the list
rankList.set(rankObj.user.user, userObj);
+ //Otherwise if it's an invalid rank for a deleted user
}else{
- //Otherwise clean deleted users out of list
- this.rankList.splice(rankObjIndex,1);
- await this.save();
+ //Ignore the rank object and throw the save flag to save the temporary rank list
+ reqSave = true;
}
- });
+ }
+
+ //if we need to save the temp rank list
+ if(reqSave){
+ //set rank list
+ this.rankList = tempRankList;
+ //save
+ await this.save();
+ }
//return userList
return rankList;
diff --git a/src/schemas/emoteSchema.js b/src/schemas/emoteSchema.js
index f556045..dafda2a 100644
--- a/src/schemas/emoteSchema.js
+++ b/src/schemas/emoteSchema.js
@@ -75,7 +75,6 @@ emoteSchema.statics.loadDefaults = async function(){
}catch(err){
if(emote != null){
- console.log(err);
console.log(`Error loading emote [${emote.name}]:`);
}else{
console.log("Error, null emote:");
diff --git a/src/schemas/flairSchema.js b/src/schemas/flairSchema.js
index c458447..a10589e 100644
--- a/src/schemas/flairSchema.js
+++ b/src/schemas/flairSchema.js
@@ -60,7 +60,6 @@ flairSchema.statics.loadDefaults = async function(){
}catch(err){
if(flair != null){
- console.log(err);
console.log(`Error loading flair '${flair.name}':`);
}else{
console.log("Error, null flair:");
diff --git a/src/schemas/tokebot/tokeCommandSchema.js b/src/schemas/tokebot/tokeCommandSchema.js
index d79111e..fffbe9a 100644
--- a/src/schemas/tokebot/tokeCommandSchema.js
+++ b/src/schemas/tokebot/tokeCommandSchema.js
@@ -93,7 +93,6 @@ tokeCommandSchema.statics.loadDefaults = async function(){
}catch(err){
if(toke != null){
- console.log(err);
console.log(`Error loading toke command: '!${toke}'`);
}else{
console.log("Error, null toke!");
diff --git a/src/schemas/user/userBanSchema.js b/src/schemas/user/userBanSchema.js
index 03bad3c..947730d 100644
--- a/src/schemas/user/userBanSchema.js
+++ b/src/schemas/user/userBanSchema.js
@@ -18,6 +18,7 @@ along with this program. If not, see .*/
const {mongoose} = require('mongoose');
//Local Imports
+const hashUtil = require('../../utils/hashUtils');
const {userModel} = require('./userSchema');
const userBanSchema = new mongoose.Schema({
@@ -25,16 +26,20 @@ const userBanSchema = new mongoose.Schema({
type: mongoose.SchemaTypes.ObjectID,
ref: "user"
},
- //To be used in future when ip-hashing/better session tracking is implemented
ips: {
- type: [mongoose.SchemaTypes.String],
- required: false
+ plaintext: {
+ type: [mongoose.SchemaTypes.String],
+ required: false
+ },
+ hashed: {
+ type: [mongoose.SchemaTypes.String],
+ required: false
+ }
},
- //To be used in future when alt-detection has been implemented
- alts: {
+ alts: [{
type: mongoose.SchemaTypes.ObjectID,
ref: "user"
- },
+ }],
deletedNames: {
type: [mongoose.SchemaTypes.String],
required: false
@@ -58,15 +63,92 @@ const userBanSchema = new mongoose.Schema({
}
});
+userBanSchema.statics.checkBanByIP = async function(ip){
+ //Get hash of ip
+ const ipHash = hashUtil.hashIP(ip);
+ //Get all bans
+ const banDB = await this.find({});
+ //Create null variable to hold any found ban
+ let foundBan = null;
+
+ //For every ban
+ for(ban of banDB){
+ //Create empty list to hold unmatched hashes in the advent that we match one
+ let tempHashes = [];
+ //Create flag to throw to save tempHashes in the advent that we have matches we dont want to save as hashes
+ let saveBan = false;
+
+
+ //For every plaintext IP in the ban
+ for(ipIndex in ban.ips.plaintext){
+ //Get the current ip
+ const curIP = ban.ips.plaintext[ipIndex];
+
+ //Check the current IP against the given ip
+ if(ip == curIP){
+ //If it matches we found the ban
+ foundBan = ban;
+ }
+ }
+
+ //For every hashed IP in the ban
+ for(ipIndex in ban.ips.hashed){
+ //Get the current ip hash
+ const curHash = ban.ips.hashed[ipIndex];
+
+ //Check the current hash against the given hash
+ if(ipHash == curHash){
+ //If it matches we found the ban
+ foundBan = ban;
+
+ //Push the match to plaintext IPs so we know who the fucker is
+ ban.ips.plaintext.push(ip);
+
+ //Throw the save ban flag to save the ban
+ saveBan = true;
+ //Otherwise
+ }else{
+ //Keep the hash since it hasn't been matched yet
+ tempHashes.push(curHash);
+ }
+ }
+
+ //If we matched a hashed ip and we need to save it as plaintext
+ if(saveBan){
+ //Keep unmatched hashes
+ ban.ips.hashed = tempHashes;
+
+ //Save the current ban
+ await ban.save();
+ }
+ }
+
+ return foundBan;
+}
+
userBanSchema.statics.checkBanByUserDoc = async function(userDB){
const banDB = await this.find({});
var foundBan = null;
banDB.forEach((ban) => {
if(ban.user != null){
+ //if we found a match
if(ban.user.toString() == userDB._id.toString()){
+ //Set found ban
foundBan = ban;
}
+
+ //For each banned alt
+ for(altIndex in ban.alts){
+ //get current alt
+ const alt = ban.alts[altIndex];
+
+ //if the alt matches our user
+ if(alt._id.toString() == userDB._id.toString()){
+ //Set found ban
+ foundBan = ban;
+ }
+ }
}
});
@@ -99,7 +181,7 @@ userBanSchema.statics.checkProcessedBans = async function(user){
return foundBan;
}
-userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays){
+userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays, ipBan = false){
//Prevent missing users
if(userDB == null){
throw new Error("User not found")
@@ -110,6 +192,7 @@ userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expiratio
throw new Error("User already banned");
}
+ //Verify time to expire/delete depending on action
if(expirationDays < 0){
throw new Error("Expiration Days must be a positive integer!");
}else if(expirationDays < 30 && permanent){
@@ -118,20 +201,65 @@ userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expiratio
throw new Error("Expiration/Deletion date cannot be longer than half a year out from the original ban date.");
}
- //Log the user out
- if(permanent){
- await userDB.killAllSessions(`Your account has been permanently banned, and will be nuked from the database in ${expirationDays} day(s).`);
- }else{
- await userDB.killAllSessions(`Your account has been temporarily banned, and will be reinstated in: ${expirationDays} day(s).`);
- }
+ await banSessions(userDB);
//Add the ban to the database
- return await this.create({user: userDB._id, permanent, expirationDays});
+ const banDB = await this.create({user: userDB._id, permanent, expirationDays});
+
+ //If we're banning the users IP
+ if(ipBan){
+ //Scrape IP's from current user into the ban record
+ await scrapeUserIPs(userDB);
+
+ //Populate the users alts
+ await userDB.populate('alts');
+
+ //For each of the users alts
+ for(altIndex in userDB.alts){
+ //Add the current alt to the ban record
+ banDB.alts.push(userDB.alts[altIndex]._id);
+
+ //Scrape out the IPs from the current alt into the ban record
+ await scrapeUserIPs(userDB.alts[altIndex]);
+
+ //Kill all of alts sessions
+ await banSessions(userDB.alts[altIndex]);
+ }
+
+ //Save commited IP information to the ban record
+ await banDB.save();
+
+ async function scrapeUserIPs(curRecord){
+ //For each hashed ip on record for this user
+ for(hashIndex in curRecord.recentIPs){
+ //Look for any occurance of the current hash
+ const foundHash = banDB.ips.hashed.indexOf(curRecord.recentIPs[hashIndex].ipHash);
+
+ //If its not listed in the ban record
+ if(foundHash == -1){
+ //Add it to the list of hashed IPs for this ban
+ banDB.ips.hashed.push(curRecord.recentIPs[hashIndex].ipHash);
+ }
+ }
+ }
+ }
+
+ //return the ban record
+ return banDB;
+
+ async function banSessions(user){
+ //Log the user out
+ if(permanent){
+ await user.killAllSessions(`Your account has been permanently banned, and will be nuked from the database in ${expirationDays} day(s).`);
+ }else{
+ await user.killAllSessions(`Your account has been temporarily banned, and will be reinstated in: ${expirationDays} day(s).`);
+ }
+ }
}
-userBanSchema.statics.ban = async function(user, permanent, expirationDays){
+userBanSchema.statics.ban = async function(user, permanent, expirationDays, ipBan){
const userDB = await userModel.findOne({user: user.user});
- return this.banByUserDoc(userDB, permanent, expirationDays);
+ return this.banByUserDoc(userDB, permanent, expirationDays, ipBan);
}
userBanSchema.statics.unbanByUserDoc = async function(userDB){
@@ -217,6 +345,7 @@ userBanSchema.statics.getBans = async function(){
userBanSchema.statics.processExpiredBans = async function(){
const banDB = await this.find({});
+ //Firem all off all at once seperately without waiting for one another
banDB.forEach(async (ban) => {
//This ban was already processed, and it's user has been deleted. There is no more to be done...
if(ban.user == null){
@@ -227,14 +356,27 @@ userBanSchema.statics.processExpiredBans = async function(){
if(ban.getDaysUntilExpiration() <= 0){
//If the ban is permanent
if(ban.permanent){
- //Populate the user field
+ //Populate the user and alt fields
await ban.populate('user');
+ await ban.populate('alts');
//Add the name to our deleted names list
ban.deletedNames.push(ban.user.user);
//Hey hey hey, goodbye!
await userModel.deleteOne({_id: ban.user._id});
//Empty out the reference
ban.user = null;
+
+ //For every alt
+ for(alt of ban.alts){
+ //Add the alts name to the deleted names list
+ ban.deletedNames.push(alt.user);
+ //Motherfuckin' Kablewie!
+ await userModel.deleteOne({_id: alt._id});
+ }
+
+ //Clear out the alts array
+ ban.alts = [];
+
//Save the ban
await ban.save();
}else{
@@ -242,7 +384,7 @@ userBanSchema.statics.processExpiredBans = async function(){
await this.deleteOne({_id: ban._id});
}
}
- })
+ });
}
//methods
diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js
index 4ed4fa6..efa3239 100644
--- a/src/utils/loggerUtils.js
+++ b/src/utils/loggerUtils.js
@@ -14,6 +14,9 @@ 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 .*/
+//Config
+const config = require('../../config.json');
+
//At some point this will be a bit more advanced, right now it's just a placeholder :P
module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){
//Some controllers do things after sending headers, for those, we should remain silent
@@ -24,11 +27,23 @@ module.exports.errorHandler = function(res, msg, type = "Generic", status = 400)
}
module.exports.exceptionHandler = function(res, err){
+ //If we're being verbose
+ if(config.verbose){
+ //Log the error
+ console.log(err)
+ }
+
//if not yell at the browser for fucking up, and tell it what it did wrong.
module.exports.errorHandler(res, err.message, "Caught Exception");
}
module.exports.socketExceptionHandler = function(socket, err){
+ //If we're being verbose
+ if(config.verbose){
+ //Log the error
+ console.log(err)
+ }
+
//if not yell at the browser for fucking up, and tell it what it did wrong.
return socket.emit("error", {errors: [{type: "Caught Exception", msg: err.message, date: new Date()}]});
}
diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js
index 39e35a2..7395f5a 100644
--- a/src/utils/sessionUtils.js
+++ b/src/utils/sessionUtils.js
@@ -32,6 +32,15 @@ module.exports.authenticateSession = async function(user, pass, req){
//Grab previous attempts
const attempt = failedAttempts.get(user);
+ //Look for ban by IP
+ const ipBanDB = await userBanModel.checkBanByIP(req.ip);
+
+ //If this ip is randy bobandy
+ if(ipBanDB != null){
+ //tell it to fuck off
+ throw new Error(`The IP address you are trying to login from has been banned.`);
+ }
+
//If we have failed attempts
if(attempt != null){
//If we have more failed attempts than allowed
@@ -53,13 +62,15 @@ module.exports.authenticateSession = async function(user, pass, req){
//Authenticate the session
const userDB = await userModel.authenticate(user, pass);
- const banDB = await userBanModel.checkBanByUserDoc(userDB);
+
+ //Check for user ban
+ const userBanDB = await userBanModel.checkBanByUserDoc(userDB);
//If the user is banned
- if(banDB){
+ if(userBanDB){
//Make the number a little prettier despite the lack of precision since we're not doing calculations here :P
- const expiration = banDB.getDaysUntilExpiration() < 1 ? 0 : banDB.getDaysUntilExpiration();
- if(banDB.permanent){
+ const expiration = userBanDB.getDaysUntilExpiration() < 1 ? 0 : userBanDB.getDaysUntilExpiration();
+ if(userBanDB.permanent){
throw new Error(`Your account has been permanently banned, and will be nuked from the database in: ${expiration} day(s)`);
}else{
throw new Error(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`);
diff --git a/www/css/global.css b/www/css/global.css
index 41771d6..b1d666c 100644
--- a/www/css/global.css
+++ b/www/css/global.css
@@ -150,6 +150,10 @@ p.navbar-item, input.navbar-item{
margin-top: 0;
}
+.popup-plaintext-content{
+ text-align: center;
+}
+
/* tooltip */
div.tooltip{
position: fixed;
diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js
index e035538..819f563 100644
--- a/www/js/adminPanel.js
+++ b/www/js/adminPanel.js
@@ -139,7 +139,7 @@ class canopyAdminUtils{
}
}
- async banUser(user, permanent, expirationDays){
+ async banUser(user, permanent, ipBan, expirationDays){
var response = await fetch(`/api/admin/ban`,{
method: "POST",
headers: {
@@ -147,7 +147,7 @@ class canopyAdminUtils{
"x-csrf-token": utils.ajax.getCSRFToken()
},
//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, permanent, expirationDays})
+ body: JSON.stringify({user, permanent, ipBan, expirationDays})
});
if(response.status == 200){
diff --git a/www/js/utils.js b/www/js/utils.js
index f8bd2cc..b723215 100644
--- a/www/js/utils.js
+++ b/www/js/utils.js
@@ -234,6 +234,7 @@ class canopyUXUtils{
this.contentDiv.innerHTML = await utils.ajax.getPopup(this.content);
}else{
this.contentDiv.innerHTML = this.content;
+ this.contentDiv.classList.add('popup-plaintext-content');
}
//display popup nodes