diff --git a/package.json b/package.json index eda7b2d..3a67d61 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "express-session": "^1.18.0", "express-validator": "^7.2.0", "mongoose": "^8.4.3", + "node-cron": "^3.0.3", "socket.io": "^4.8.1" }, "scripts": { diff --git a/src/controllers/api/account/registerController.js b/src/controllers/api/account/registerController.js index ee07654..4ab51c4 100644 --- a/src/controllers/api/account/registerController.js +++ b/src/controllers/api/account/registerController.js @@ -19,6 +19,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {userModel} = require('../../../schemas/userSchema'); +const userBanModel = require('../../../schemas/userBanSchema.js'); const {exceptionHandler} = require('../../../utils/loggerUtils.js'); module.exports.post = async function(req, res){ @@ -27,6 +28,17 @@ module.exports.post = async function(req, res){ if(validResult.isEmpty()){ const user = matchedData(req); + + //Would prefer to stick this in userModel.statics.register() but we end up with circular dependencies >:( + const nukedBans = await userBanModel.checkProcessedBans(user.user); + + //if we found any related nuked bans + if(nukedBans != null){ + //Shit our pants! + res.status(401); + return res.send({errors:[{msg:"Cannot re-create banned account!",type:"unauthorized"}]}); + } + await userModel.register(user) return res.sendStatus(200); }else{ diff --git a/src/controllers/api/admin/banController.js b/src/controllers/api/admin/banController.js index a54a79d..3960fa0 100644 --- a/src/controllers/api/admin/banController.js +++ b/src/controllers/api/admin/banController.js @@ -65,14 +65,8 @@ module.exports.delete = async function(req, res){ const validResult = validationResult(req); if(validResult.isEmpty()){ const {user} = matchedData(req); - const userDB = await userModel.findOne({user}); - if(userDB == null){ - res.status(400); - return res.send({errors:[{type: "Bad Query", msg: "User not found.", date: new Date()}]}); - } - - await banModel.unbanByUserDoc(userDB); + await banModel.unban({user}); res.status(200); return res.send(await banModel.getBans()); diff --git a/src/schemas/userBanSchema.js b/src/schemas/userBanSchema.js index 9c98c38..e7ad279 100644 --- a/src/schemas/userBanSchema.js +++ b/src/schemas/userBanSchema.js @@ -18,12 +18,11 @@ along with this program. If not, see .*/ const {mongoose} = require('mongoose'); //Local Imports -const {userSchema} = require('./userSchema'); +const {userModel, userSchema} = require('./userSchema'); const userBanSchema = new mongoose.Schema({ user: { type: mongoose.SchemaTypes.ObjectID, - required: true, ref: "user" }, //To be used in future when ip-hashing/better session tracking is implemented @@ -36,6 +35,10 @@ const userBanSchema = new mongoose.Schema({ type: [userSchema], required: false }, + deletedNames: { + type: [mongoose.SchemaTypes.String], + required: false + }, banDate: { type: mongoose.SchemaTypes.Date, @@ -60,8 +63,10 @@ userBanSchema.statics.checkBanByUserDoc = async function(userDB){ var foundBan = null; banDB.forEach((ban) => { - if(ban.user.toString() == userDB._id.toString()){ - foundBan = ban; + if(ban.user != null){ + if(ban.user.toString() == userDB._id.toString()){ + foundBan = ban; + } } }); @@ -73,6 +78,27 @@ userBanSchema.statics.checkBan = async function(user){ return this.checkBanByUserDoc(userDB); } +userBanSchema.statics.checkProcessedBans = async function(user){ + //Pull banlist and create empty variable to hold any found ban + const banDB = await this.find({}); + var foundBan = null; + + //For each ban in list + banDB.forEach((ban)=>{ + //For each deleted account associated with the ban + ban.deletedNames.forEach((name)=>{ + //If the banned name equals the name we're checking against + if(name == user){ + //We've found our ban + foundBan = ban; + } + }) + }); + + //Return any found associated ban + return foundBan; +} + userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays){ //Prevent missing users if(userDB == null){ @@ -111,20 +137,40 @@ userBanSchema.statics.unbanByUserDoc = async function(userDB){ throw new Error("User not found") } - const ban = await this.checkBanByUserDoc(userDB); + const banDB = await this.checkBanByUserDoc(userDB); - if(!ban){ + if(!banDB){ throw new Error("User already un-banned"); } //Use _id in-case mongoose wants to be a cunt - var oldBan = await this.deleteOne({_id: ban._id}); + var oldBan = await this.deleteOne({_id: banDB._id}); + return oldBan; +} + +userBanSchema.statics.unbanDeleted = async function(user){ + const banDB = await this.checkProcessedBans(user); + + if(!banDB){ + throw new Error("User already un-banned"); + } + + const oldBan = await this.deleteOne({_id: banDB._id}); return oldBan; } userBanSchema.statics.unban = async function(user){ + //Find user in DB const userDB = await userModel.findOne({user: user.user}); - return this.unbanByUserDoc(userDB); + + //If user was deleted + if(userDB == null){ + //unban deleted user + return await this.unbanDeleted(user.user); + }else{ + //unban by user doc + return await this.unbanByUserDoc(userDB); + } } userBanSchema.statics.getBans = async function(){ @@ -136,11 +182,14 @@ userBanSchema.statics.getBans = async function(){ var expirationDate = new Date(ban.banDate); expirationDate.setDate(expirationDate.getDate() + ban.expirationDays); - const userObj = { - id: ban.user.id, - user: ban.user.user, - img: ban.user.img, - date: ban.user.date + //Make sure we're not about to read the properties of a null object + if(ban.user != null){ + var userObj = { + id: ban.user.id, + user: ban.user.user, + img: ban.user.img, + date: ban.user.date + } } const banObj = { @@ -150,6 +199,7 @@ userBanSchema.statics.getBans = async function(){ user: userObj, ips: ban.ips, alts: ban.alts, + deletedNames: ban.deletedNames, permanent: ban.permanent } @@ -159,6 +209,38 @@ userBanSchema.statics.getBans = async function(){ return bans; } +userBanSchema.statics.processExpiredBans = async function(){ + const banDB = await this.find({}); + + 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){ + console.log(ban); + return; + } + + //If the ban hasn't been processed and it's got 0 or less days to go + if(ban.getDaysUntilExpiration() <= 0){ + //If the ban is permanent + if(ban.permanent){ + //Populate the user field + await ban.populate('user'); + //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; + //Save the ban + await ban.save(); + }else{ + //Otherwise, delete the ban and let our user back in :P + await this.deleteOne({_id: ban._id}); + } + } + }) +} + //methods userBanSchema.methods.getDaysUntilExpiration = function(){ //Get ban date @@ -166,7 +248,7 @@ userBanSchema.methods.getDaysUntilExpiration = function(){ //Get expiration days and calculate expiration date expirationDate.setDate(expirationDate.getDate() + this.expirationDays); //Calculate and return days until ban expiration - return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); + return daysUntilExpiraiton = ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); } module.exports = mongoose.model("userBan", userBanSchema); \ No newline at end of file diff --git a/src/schemas/userSchema.js b/src/schemas/userSchema.js index b820a9f..1ab231e 100644 --- a/src/schemas/userSchema.js +++ b/src/schemas/userSchema.js @@ -18,10 +18,10 @@ along with this program. If not, see .*/ const {mongoose} = require('mongoose'); //local imports -const server = require('../server.js'); -const statModel = require('./statSchema.js'); -const flairModel = require('./flairSchema.js'); -const permissionModel = require('./permissionSchema.js'); +const server = require('../server'); +const statModel = require('./statSchema'); +const flairModel = require('./flairSchema'); +const permissionModel = require('./permissionSchema'); const hashUtil = require('../utils/hashUtils'); diff --git a/src/server.js b/src/server.js index c7a10f2..9cbaac9 100644 --- a/src/server.js +++ b/src/server.js @@ -24,9 +24,10 @@ const mongoStore = require('connect-mongo'); const mongoose = require('mongoose'); //Define Local Imports +const channelManager = require('./app/channel/channelManager'); +const scheduler = require('./utils/scheduler'); const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); -const channelManager = require('./app/channel/channelManager'); const indexRouter = require('./routers/indexRouter'); const registerRouter = require('./routers/registerRouter'); const profileRouter = require('./routers/profileRouter'); @@ -112,6 +113,9 @@ statModel.incrementLaunchCount(); //Load flairs flairModel.loadDefaults(); +//Kick off scheduled-jobs +scheduler.kickoff(); + //Hand over general-namespace socket.io connections to the channel manager module.exports.channelManager = new channelManager(io) diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js new file mode 100644 index 0000000..fc1c11f --- /dev/null +++ b/src/utils/scheduler.js @@ -0,0 +1,26 @@ +/*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 cron = require('node-cron'); + +//Local Imports +const userBanSchema = require('../schemas/userBanSchema'); + +module.exports.kickoff = function(){ + //Process expired bans every night at midnight + cron.schedule('* * * * *', ()=>{userBanSchema.processExpiredBans()},{scheduled: true, timezone: "UTC"}); +} \ No newline at end of file diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 25b3454..3325fad 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -27,10 +27,12 @@ module.exports.authenticateSession = async function(user, pass, req){ const banDB = await userBanModel.checkBanByUserDoc(userDB); if(banDB){ + //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){ - throw new Error(`Your account has been banned, and will be permanently deleted in: ${banDB.getDaysUntilExpiration()} day(s)`); + throw new Error(`Your account has been banned, and will be permanently deleted in: ${expiration} day(s)`); }else{ - throw new Error(`Your account has been temporarily banned, and will be reinstated in: ${banDB.getDaysUntilExpiration()} day(s)`); + throw new Error(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`); } } diff --git a/www/img/flair/gold.gif b/www/img/flair/gold.gif index 28eaa51..04cf8d0 100644 Binary files a/www/img/flair/gold.gif and b/www/img/flair/gold.gif differ diff --git a/www/img/flair/gold_big.gif b/www/img/flair/gold_big.gif new file mode 100644 index 0000000..28eaa51 Binary files /dev/null and b/www/img/flair/gold_big.gif differ diff --git a/www/img/nuked.png b/www/img/nuked.png new file mode 100644 index 0000000..9997106 Binary files /dev/null and b/www/img/nuked.png differ diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js index 33b78cf..e1584eb 100644 --- a/www/js/adminPanel.js +++ b/www/js/adminPanel.js @@ -283,7 +283,31 @@ class adminUserBanList{ renderBanList(banList){ this.clearBanList(); + console.log(banList); banList.forEach((ban) => { + //Calculate expiration date and expiration days + const expirationDate = new Date(ban.expirationDate); + const expirationDays = ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); + var expirationDateString = `${expirationDate.toDateString()} (${expirationDays} day(s) left)`; + var banActionString = ban.permanent ? "Account Deletion" : "Un-Ban"; + console.log(ban); + if(ban.user == null){ + //Fudge the user object if it's already been deleted + ban.user = { + img: "/img/nuked.png", + id: "-", + user: ban.deletedNames[0] ? ban.deletedNames[0] : "UNKNOWN", + deleted: true + }; + + //Fake the display string + var signUpDateString = "-" + expirationDateString = "Accounts Nuked" + banActionString = "Accounts Nuked" + + }else{ + var signUpDateString = new Date(ban.user.date).toDateString() + } //Create entry row const entryRow = document.createElement('tr'); entryRow.classList.add("admin-list-entry"); @@ -293,10 +317,6 @@ class adminUserBanList{ imgNode.classList.add("admin-list-entry","admin-list-entry-item"); imgNode.src = ban.user.img; - //Calculate expiration date and expiration days - const expirationDate = new Date(ban.expirationDate); - const expirationDays = ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); - //Create unban icon const unbanIcon = document.createElement('i'); unbanIcon.classList.add("bi-emoji-smile-fill","admin-user-list-icon","admin-user-list-unban-icon"); @@ -315,11 +335,11 @@ class adminUserBanList{ entryRow.appendChild(newCell(imgNode, true)); entryRow.appendChild(newCell(ban.user.id)); entryRow.appendChild(newCell(ban.user.user)); - entryRow.appendChild(newCell(new Date(ban.user.date).toDateString())); + entryRow.appendChild(newCell(signUpDateString)); entryRow.appendChild(newCell(new Date(ban.banDate).toDateString())); - entryRow.appendChild(newCell(`${expirationDate.toDateString()} (${expirationDays} day(s) left)`)); - entryRow.appendChild(newCell(ban.permanent ? "Account Deletion" : "Un-Ban")); - entryRow.appendChild(newCell([unbanIcon, nukeAccount])); + entryRow.appendChild(newCell(expirationDateString)); + entryRow.appendChild(newCell(banActionString)); + entryRow.appendChild(newCell(ban.user.deleted ? unbanIcon : [unbanIcon, nukeAccount])); //Append row to table this.table.appendChild(entryRow); diff --git a/www/js/utils.js b/www/js/utils.js index 339a3f0..9b44e2d 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -25,10 +25,12 @@ class canopyUXUtils{ constructor(){ } + //Update this and popup class to use nodes + //and display multiple errors in one popup displayResponseError(body){ const errors = body.errors; errors.forEach((err)=>{ - new canopyUXUtils.popup(`

Server Error:


Message: ${err.msg}`); + new canopyUXUtils.popup(`

Server Error:


${err.msg}

`); }); }