diff --git a/controllers/forms/editaccounts.js b/controllers/forms/editaccounts.js index 6074f60b..75eb5dd9 100644 --- a/controllers/forms/editaccounts.js +++ b/controllers/forms/editaccounts.js @@ -9,7 +9,7 @@ module.exports = async (req, res, next) => { if (!req.body.checkedaccounts || req.body.checkedaccounts.length === 0 || req.body.checkedaccounts.length > 10) { errors.push('Must select 1-10 accounts'); } - if (!req.body.auth_level && !req.body.delete_account) { + if (typeof req.body.auth_level !== 'number' && !req.body.delete_account) { errors.push('Missing auth level or delete action'); } if (typeof req.body.auth_level === 'number' && req.body.auth_level < 0 || req.body.auth_level > 4) { diff --git a/controllers/forms/transfer.js b/controllers/forms/transfer.js index 6ba988d4..af37b14f 100644 --- a/controllers/forms/transfer.js +++ b/controllers/forms/transfer.js @@ -14,7 +14,7 @@ module.exports = async (req, res, next) => { errors.push('Transfer username must be at less than 50 characters'); } if (req.body.username === res.locals.board.owner) { - errors.push('You are already board owner...'); + errors.push('New owner username must not be same as old owner'); } if (alphaNumericRegex.test(req.body.username) !== true) { errors.push('URI must contain a-z 0-9 only'); diff --git a/controllers/pages.js b/controllers/pages.js index 9db16053..7f702980 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -16,7 +16,7 @@ const express = require('express') , { globalManageReports, globalManageBans, globalManageRecent, globalManageAccounts, globalManageNews } = require(__dirname+'/../models/pages/globalmanage/') , { changePassword, home, register, login, logout, create, board, catalog, banners, randombanner, news, captchaPage, - captcha, thread, modlog, modloglist, boardlist } = require(__dirname+'/../models/pages/'); + captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/'); //homepage router.get('/index.html', home); @@ -60,6 +60,7 @@ router.get('/captcha', captcha); //get captcha image and cookie router.get('/captcha.html', captchaPage); //iframed for noscript users //accounts +router.get('/account.html', sessionRefresh, isLoggedIn, account); //page showing boards you are mod/owner of, links to password rese, logout, etc router.get('/login.html', login); router.get('/register.html', register); router.get('/changepassword.html', changePassword); diff --git a/db/accounts.js b/db/accounts.js index a551b4b1..a8eaad1f 100644 --- a/db/accounts.js +++ b/db/accounts.js @@ -32,18 +32,20 @@ module.exports = { '_id': username, 'passwordHash': passwordHash, 'authLevel': authLevel, + 'ownedBoards': [], + 'modBoards': [] }); }, changePassword: async (username, newPassword) => { const passwordHash = await bcrypt.hash(newPassword, 12); return db.updateOne({ - '_id': username - }, { - '$set': { - 'passwordHash': passwordHash - } - }); + '_id': username + }, { + '$set': { + 'passwordHash': passwordHash + } + }); }, find: (skip=0, limit=0) => { @@ -64,6 +66,75 @@ module.exports = { }); }, + addOwnedBoard: (username, board) => { + return db.updateOne({ + '_id': username + }, { + '$addToSet': { + 'ownedBoards': board + } + }); + }, + + removeOwnedBoard: (username, board) => { + return db.updateOne({ + '_id': username + }, { + '$pull': { + 'ownedBoards': board + } + }); + }, + + addModBoard: (usernames, board) => { + return db.updateMany({ + '_id': { + '$in': usernames + } + }, { + '$addToSet': { + 'modBoards': board + } + }); + }, + + removeModBoard: (usernames, board) => { + return db.updateMany({ + '_id': { + '$in': usernames + } + }, { + '$pull': { + 'modBoards': board + } + }); + }, + + getOwnedOrModBoards: (usernames) => { + return db.find({ + '_id': { + '$in': usernames + }, + '$or': [ + { + 'ownedBoards.0': { + '$exists': true + }, + }, + { + 'modBoards.0': { + '$exists': true + } + } + ] + }, { + 'projection': { + 'ownedBoards': 1, + 'modBoards': 1, + } + }).toArray(); + }, + setLevel: (usernames, level) => { //increase users auth level return db.updateMany({ diff --git a/db/boards.js b/db/boards.js index f9bfd806..5ce32519 100644 --- a/db/boards.js +++ b/db/boards.js @@ -131,7 +131,11 @@ module.exports = { }, count: (showUnlisted=false) => { - return db.countDocuments(showUnlisted ? {} : { 'settings.unlisted': false }); + if (showUnlisted) { + return db.countDocuments({ 'settings.unlisted': false }); + } else { + return db.estimatedDocumentCount(); + } }, totalStats: () => { diff --git a/helpers/checks/isloggedin.js b/helpers/checks/isloggedin.js index 84a2eba2..f338b969 100644 --- a/helpers/checks/isloggedin.js +++ b/helpers/checks/isloggedin.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = async (req, res, next) => { - if (req.session.authenticated === true) { + if (req.session && req.session.authenticated === true) { return next(); } let goto; @@ -9,5 +9,5 @@ module.exports = async (req, res, next) => { //coming from a GET page isLoggedIn middleware check goto = req.path; } - res.redirect(`/login.html${goto ? '?goto='+goto : ''}`); + return res.redirect(`/login.html${goto ? '?goto='+goto : ''}`); } diff --git a/helpers/sessionrefresh.js b/helpers/sessionrefresh.js index e914a28b..68bf67c4 100644 --- a/helpers/sessionrefresh.js +++ b/helpers/sessionrefresh.js @@ -11,7 +11,9 @@ module.exports = async (req, res, next) => { } else { req.session.user = { 'username': account._id, - 'authLevel': account.authLevel + 'authLevel': account.authLevel, + 'modBoards': account.modBoards, + 'ownedBoards': account.ownedBoards, }; } } diff --git a/models/forms/changeboardsettings.js b/models/forms/changeboardsettings.js index 7ceb2ef6..6e31fa2f 100644 --- a/models/forms/changeboardsettings.js +++ b/models/forms/changeboardsettings.js @@ -13,8 +13,12 @@ const { Boards, Posts, Accounts } = require(__dirname+'/../../db/') module.exports = async (req, res, next) => { + //oldsettings before changes const oldSettings = res.locals.board.settings; + //array of promises we might need + const promises = []; + let markdownAnnouncement; if (req.body.announcement !== oldSettings.announcement.raw) { //remarkup the announcement if it changes @@ -24,17 +28,34 @@ module.exports = async (req, res, next) => { markdownAnnouncement = sanitized; } - let moderators = req.body.moderators != null ? req.body.moderators.split('\r\n').filter(n => n).slice(0,10) : oldSettings.moderators - if (moderators !== oldSettings.moderators) { - //make sure moderators actually have existing accounts + let moderators = req.body.moderators != null ? req.body.moderators.split('\r\n').filter(n => n).slice(0,10) : []; + if (moderators.length === 0 && oldSettings.moderators.length > 0) { + //remove all mods if mod list being emptied + promises.push(Accounts.removeModBoard(oldSettings.moderators, req.params.board)); + } else if (moderators !== oldSettings.moderators) { if (moderators.length > 0) { + //make sure moderators actually have existing accounts const validCount = await Accounts.countUsers(moderators); if (validCount !== moderators.length) { + //some usernames were not valid, reset to old setting moderators = oldSettings.moderators; + } else { + //all accounts exist, check added/removed + const modsRemoved = oldSettings.moderators.filter(m => !moderators.includes(m)); + const modsAdded = moderators.filter(m => !oldSettings.moderators.includes(m)); + if (modsRemoved.length > 0) { + //remove mod from accounts + promises.push(Accounts.removeModBoard(modsRemoved, req.params.board)); + } + if (modsAdded.length > 0) { + //add mod to accounts + promises.push(Accounts.addModBoard(modsAdded, req.params.board)); + } } } } +//todo: make separate functions for handling array, boolean, number, text settings. const newSettings = { moderators, 'name': req.body.name && req.body.name.trim().length > 0 ? req.body.name : oldSettings.name, @@ -92,9 +113,6 @@ module.exports = async (req, res, next) => { //update this in locals incase is used in later parts res.locals.board.settings = newSettings; - //array of promises we might need - const promises = []; - //pages in new vs old settings const oldMaxPage = Math.ceil(oldSettings.threadLimit/10); const newMaxPage = Math.ceil(newSettings.threadLimit/10); diff --git a/models/forms/create.js b/models/forms/create.js index 71543fd3..8616cb0e 100644 --- a/models/forms/create.js +++ b/models/forms/create.js @@ -1,6 +1,6 @@ 'use strict'; -const { Boards } = require(__dirname+'/../../db/') +const { Boards, Accounts } = require(__dirname+'/../../db/') , { boardDefaults } = require(__dirname+'/../../configs/main.js'); module.exports = async (req, res, next) => { @@ -8,6 +8,7 @@ module.exports = async (req, res, next) => { const { name, description } = req.body , uri = req.body.uri.toLowerCase() , tags = req.body.tags.split('\n').filter(n => n) + , owner = req.session.user.username , board = await Boards.findOne(uri); // if board exists reject @@ -22,7 +23,7 @@ module.exports = async (req, res, next) => { //todo: add a settings for defaults const newBoard = { '_id': uri, - 'owner': req.session.user.username, + owner, 'banners': [], 'sequence_value': 1, 'pph': 0, @@ -37,7 +38,10 @@ module.exports = async (req, res, next) => { } } - await Boards.insertOne(newBoard); + await Promise.all([ + Boards.insertOne(newBoard), + Accounts.addOwnedBoard(owner, uri) + ]); return res.redirect(`/${uri}/index.html`); diff --git a/models/forms/deleteboard.js b/models/forms/deleteboard.js index 3120b790..cb07bdf3 100644 --- a/models/forms/deleteboard.js +++ b/models/forms/deleteboard.js @@ -1,6 +1,6 @@ 'use strict'; -const { Boards, Stats, Posts, Bans, Modlogs } = require(__dirname+'/../../db/') +const { Accounts, Boards, Stats, Posts, Bans, Modlogs } = require(__dirname+'/../../db/') , cache = require(__dirname+'/../../redis.js') , deletePosts = require(__dirname+'/deletepost.js') , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') @@ -17,6 +17,8 @@ module.exports = async (uri, board) => { await deletePosts(allPosts, uri, true); } await Promise.all([ + Accounts.removeOwnedBoard(board.owner, uri), //remove board from owner account + board.settings.moderators.length > 0 ? Accounts.removeModBoard(board.settings.moderators) : void 0, //remove board from mods accounts Modlogs.deleteBoard(uri), //modlogs for the board Bans.deleteBoard(uri), //bans for the board Stats.deleteBoard(uri), //stats for the board diff --git a/models/forms/editaccounts.js b/models/forms/editaccounts.js index d0f1a95b..91f7bcf6 100644 --- a/models/forms/editaccounts.js +++ b/models/forms/editaccounts.js @@ -1,21 +1,67 @@ 'use strict'; -const { Accounts } = require(__dirname+'/../../db/'); +const { Accounts, Boards } = require(__dirname+'/../../db/') + , cache = require(__dirname+'/../../redis.js') module.exports = async (req, res, next) => { //edit the accounts let amount = 0; if (req.body.delete_account) { + const accountsWithBoards = await Accounts.getOwnedOrModBoards(req.body.checkedaccounts); + if (accountsWithBoards.length > 0) { + const bulkWrites = []; + for (let i = 0; i < accountsWithBoards.length; i++) { + const acc = accountsWithBoards[i]; + if (acc.modBoards.length > 0) { + //remove from moderators of any boards they are mod on + bulkWrites.push({ + 'updateMany': { + 'filter': { + '_id': { + '$in': acc.modBoards + } + }, + 'update': { + '$pull': { + 'settings.moderators': acc._id + } + } + } + }); + cache.del(acc.modBoards.map(b => `board_${b}`)); + } + if (acc.ownedBoards.length > 0) { + //remove from moderators of any boards they are mod on + bulkWrites.push({ + 'updateMany': { + 'filter': { + '_id': { + '$in': acc.ownedBoards + } + }, + 'update': { + '$set': { + 'owner': null //board has no owner + } + } + } + }); + cache.del(acc.ownedBoards.map(b => `board_${b}`)); +//todo: use list of board with no owners for claims + } + } + await Boards.db.bulkWrite(bulkWrites); + } amount = await Accounts.deleteMany(req.body.checkedaccounts).then(res => res.deletedCount); } else { amount = await Accounts.setLevel(req.body.checkedaccounts, req.body.auth_level).then(res => res.modifiedCount); } return res.render('message', { - 'title': 'Success', - 'message': `${req.body.delete_account ? 'Deleted' : 'Edited'} ${amount} accounts`, - 'redirect': '/globalmanage/accounts.html' - }); + 'title': 'Success', + 'message': `${req.body.delete_account ? 'Deleted' : 'Edited'} ${amount} accounts`, + 'redirect': '/globalmanage/accounts.html' + }); } diff --git a/models/forms/login.js b/models/forms/login.js index 07343aaa..143c5a25 100644 --- a/models/forms/login.js +++ b/models/forms/login.js @@ -7,7 +7,7 @@ module.exports = async (req, res, next) => { const username = req.body.username.toLowerCase(); const password = req.body.password; - const goto = req.body.goto; + const goto = req.body.goto || '/account.html'; const failRedirect = `/login.html${goto ? '?goto='+goto : ''}` //fetch an account @@ -31,12 +31,14 @@ module.exports = async (req, res, next) => { // add the account to the session and authenticate if password was correct req.session.user = { 'username': account._id, - 'authLevel': account.authLevel + 'authLevel': account.authLevel, + 'ownedBoards': account.ownedBoards, + 'modBoards': account.modBoards, }; req.session.authenticated = true; //successful login - return res.redirect(goto || '/'); + return res.redirect(goto); } diff --git a/models/forms/transferboard.js b/models/forms/transferboard.js index 64c4a08e..ce58af36 100644 --- a/models/forms/transferboard.js +++ b/models/forms/transferboard.js @@ -14,6 +14,10 @@ module.exports = async (req, res, next) => { }); } + //modify accounts with new board ownership + await Accounts.removeOwnedBoard(res.locals.board.owner, req.params.board) + await Accounts.addOwnedBoard(newOwner._id, req.params.board); + //set owner in memory and in db res.locals.board.owner = newOwner._id; await Boards.setOwner(req.params.board, res.locals.board.owner); diff --git a/models/pages/account.js b/models/pages/account.js new file mode 100644 index 00000000..e7e53603 --- /dev/null +++ b/models/pages/account.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = async (req, res, next) => { + + return res.render('account', { + user: req.session.user + }); + +} diff --git a/models/pages/index.js b/models/pages/index.js index 66d79b5b..01894e14 100644 --- a/models/pages/index.js +++ b/models/pages/index.js @@ -3,6 +3,7 @@ module.exports = { changePassword: require(__dirname+'/changepassword.js'), register: require(__dirname+'/register.js'), + account: require(__dirname+'/account.js'), home: require(__dirname+'/home.js'), login: require(__dirname+'/login.js'), logout: require(__dirname+'/logout.js'), diff --git a/queue.js b/queue.js index 5d40869e..da5b6451 100644 --- a/queue.js +++ b/queue.js @@ -8,8 +8,8 @@ module.exports = { queue: taskQueue, - push: (data, options) => { - taskQueue.add(data, options); + push: (data) => { + taskQueue.add(data, { removeOnComplete: true }); } } diff --git a/redis.js b/redis.js index b7a8d84a..350d3666 100644 --- a/redis.js +++ b/redis.js @@ -33,8 +33,12 @@ module.exports = { }, //delete value with key - del: (key) => { - return client.del(key); + del: (keyOrKeys) => { + if (Array.isArray(keyOrKeys)) { + return client.del(...keyOrKeys); + } else { + return client.del(keyOrKeys); + } }, deletePattern: (pattern) => { diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index df09a434..495ef521 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -2,6 +2,6 @@ nav.navbar#top a.nav-item(href='/index.html') Home a.nav-item(href='/news.html') News a.nav-item(href='/boards.html') Boards + a.nav-item(href='/account.html') Account a.nav-item(href=`/${board ? board._id+'/manage/reports' : 'globalmanage/recent'}.html`) Manage - a.nav-item(href='/create.html') Create a.jsonly.nav-item.right#settings ⚙ diff --git a/views/pages/account.pug b/views/pages/account.pug new file mode 100644 index 00000000..0768844a --- /dev/null +++ b/views/pages/account.pug @@ -0,0 +1,49 @@ +extends ../layout.pug + +block head + script(src='/js/all.js') + title Manage + +block content + .board-header + h1.board-title Welcome, #{user.username} + h4.board-description Auth level: #{user.authLevel} + br + hr(size=1) + h4.no-m-p General: + ul + if user.authLevel <= 1 + li: a(href='/globalmanage/recent.html') Global management + li: a(href='/create.html') Create a board + li: a(href='/changepassword.html') Change password + li: a(href='/logout') Log out + hr(size=1) + h4.no-m-p Boards you own: + if user.ownedBoards && user.ownedBoards.length > 0 + ul + for b in user.ownedBoards + li + a(href=`/${b}/index.html`) /#{b}/ + | - + a(href=`/${b}/manage/reports.html`) Reports + | , + a(href=`/${b}/manage/bans.html`) Bans + | , + a(href=`/${b}/manage/settings.html`) Settings + | , + a(href=`/${b}/manage/banners.html`) Banners + else + p None + hr(size=1) + h4.no-m-p Boards you moderate: + if user.modBoards && user.modBoards.length > 0 + ul + for b in user.modBoards + li + a(href=`/${b}/index.html`) /#{b}/ + | - + a(href=`/${b}/manage/reports.html`) Reports + | , + a(href=`/${b}/manage/bans.html`) Bans + else + p None diff --git a/views/pages/globalmanageaccounts.pug b/views/pages/globalmanageaccounts.pug index 8235b193..52a45523 100644 --- a/views/pages/globalmanageaccounts.pug +++ b/views/pages/globalmanageaccounts.pug @@ -20,11 +20,21 @@ block content th th Username th Auth Level + th Own Boards + th Mod Boards for account in accounts tr td: input(type='checkbox', name='checkedaccounts' value=account._id) td #{account._id} td #{account.authLevel} + td + for b in account.ownedBoards + a(href=`/${b}/index.html`) /#{b}/ + | + td + for b in account.modBoards + a(href=`/${b}/index.html`) /#{b}/ + | .pages.mt-5 include ../includes/pages.pug .row