global and board IP bans, improved error handling, improved permissions checks

merge-requests/208/head
fatchan 5 years ago
parent e823cad14e
commit db963d4607
  1. 41
      controllers/forms.js
  2. 43
      controllers/pages.js
  3. 2
      db-models/bans.js
  4. 16
      db-models/posts.js
  5. 10
      helpers/bancheck.js
  6. 9
      helpers/has-perms.js
  7. 16
      helpers/hasperms.js
  8. 16
      helpers/haspermsmiddleware.js
  9. 0
      helpers/isloggedin.js
  10. 35
      models/forms/ban-poster.js
  11. 41
      models/forms/delete-post.js
  12. 12
      models/forms/dismiss-report.js
  13. 13
      models/forms/edit-post.js
  14. 8
      models/forms/login.js
  15. 13
      models/forms/make-post.js
  16. 8
      models/forms/register.js
  17. 8
      models/forms/report-post.js
  18. 13
      models/forms/spoiler-post.js
  19. 3
      models/pages/board.js
  20. 3
      models/pages/catalog.js
  21. 20
      models/pages/globalmanage.js
  22. 3
      models/pages/home.js
  23. 2
      models/pages/login.js
  24. 5
      models/pages/manage.js
  25. 11
      models/pages/register.js
  26. 3
      models/pages/thread.js
  27. 5
      server.js
  28. 4
      views/mixins/post.pug
  29. 25
      views/pages/ban.pug
  30. 11
      views/pages/globalmanage.pug
  31. 6
      views/pages/manage.pug
  32. 2
      wipe.js

@ -46,7 +46,7 @@ router.post('/login', (req, res, next) => {
})
}
loginAccount(req, res);
loginAccount(req, res, next);
});
@ -88,7 +88,7 @@ router.post('/register', (req, res, next) => {
})
}
registerAccount(req, res);
registerAccount(req, res, next);
});
@ -137,11 +137,11 @@ router.post('/board/:board', Boards.exists, banCheck, numberConverter, async (re
})
}
makePost(req, res, numFiles);
makePost(req, res, next, numFiles);
});
//report, delete, sticky, etc
//report/delete/spoiler/ban
router.post('/board/:board/posts', Boards.exists, banCheck, numberConverter, async (req, res, next) => {
const errors = [];
@ -178,38 +178,45 @@ router.post('/board/:board/posts', Boards.exists, banCheck, numberConverter, asy
})
}
const posts = await Posts.getPosts(req.params.board, req.body.checked, true);
if (!posts || posts.length === 0) {
return res.status(404).render('message', {
'title': 'Not found',
'errors': 'Selected posts not found',
'redirect': `/${req.params.board}`
})
}
const messages = [];
try {
//TODO: maybe fetch the posts first instead of checking multiple times with multiple actions
//global or board ban
if (req.body.global_ban) {
messages.push((await banPoster(req, res, null)));
messages.push((await banPoster(req, res, next, null, posts)));
} else if (req.body.ban) {
messages.push((await banPoster(req, res, req.params.board)));
messages.push((await banPoster(req, res, next, req.params.board, posts)));
}
// then if not deleting, we can spoiler and report or dismiss reports
if (req.body.delete) {
messages.push((await deletePosts(req, res)));
messages.push((await deletePosts(req, res, next, posts)));
} else {
if (req.body.spoiler) {
messages.push((await spoilerPosts(req, res)));
messages.push((await spoilerPosts(req, res, next, posts)));
}
if (req.body.report) {
messages.push((await reportPosts(req, res)));
messages.push((await reportPosts(req, res, next)));
} else if (req.body.dismiss) {
messages.push((await dismissReports(req, res)));
messages.push((await dismissReports(req, res, next)));
}
}
} catch (err) {
//something not right
if (err.status) {
// return out special error
return res.status(err.status).render('message', err.message);
}
console.error(err);
return res.status(500).render('error');
//some other error, use regular error handler
return next(err);
}
return res.render('message', {
@ -220,6 +227,4 @@ router.post('/board/:board/posts', Boards.exists, banCheck, numberConverter, asy
});
module.exports = router;

@ -3,13 +3,15 @@
const express = require('express')
, router = express.Router()
, Boards = require(__dirname+'/../db-models/boards.js')
, checkAuth = require(__dirname+'/../helpers/check-auth.js')
, hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js')
, isLoggedIn = require(__dirname+'/../helpers/isloggedin.js')
, numberConverter = require(__dirname+'/../helpers/number-converter.js')
//page models
, home = require(__dirname+'/../models/pages/home.js')
, register = require(__dirname+'/../models/pages/register.js')
, manage = require(__dirname+'/../models/pages/manage.js')
, login = require(__dirname+'/../models/pages/login.js')
, home = require(__dirname+'/../models/pages/home.js')
, register = require(__dirname+'/../models/pages/register.js')
, manage = require(__dirname+'/../models/pages/manage.js')
, globalmanage = require(__dirname+'/../models/pages/globalmanage.js')
, login = require(__dirname+'/../models/pages/login.js')
, board = require(__dirname+'/../models/pages/board.js')
, catalog = require(__dirname+'/../models/pages/catalog.js')
, thread = require(__dirname+'/../models/pages/thread.js');
@ -24,27 +26,24 @@ router.get('/login', login);
router.get('/register', register);
//logout
router.get('/logout', (req, res, next) => {
if (req.session.authenticated === true) {
req.session.destroy();
return res.render('message', {
'title': 'Success',
'message': 'You have been logged out successfully',
'redirect': '/'
});
}
return res.status(400).render('message', {
'title': 'Bad request',
'message': 'You are not logged in',
'redirect': '/login'
})
router.get('/logout', isLoggedIn, (req, res, next) => {
//remove session
req.session.destroy();
return res.render('message', {
'title': 'Success',
'message': 'You have been logged out successfully',
'redirect': '/'
});
});
//board manage page
router.get('/:board/manage', Boards.exists, checkAuth, Boards.canManage, manage);
router.get('/:board/manage', Boards.exists, isLoggedIn, hasPerms, manage);
//board manage page
router.get('/globalmanage', isLoggedIn, hasPerms, globalmanage);
// board page/recents
router.get('/:board/:page(\\d+)?', Boards.exists, numberConverter, board);

@ -5,6 +5,8 @@ const Mongo = require(__dirname+'/../helpers/db.js')
module.exports = {
db,
find: (ip, board) => {
return db.find({
'ip': ip,

@ -240,6 +240,22 @@ module.exports = {
}).toArray();
},
getAllReports: () => {
return db.find({
'reports.0': {
'$exists': true
}
}, {
'projection': {
'salt': 0,
'password': 0,
'ip': 0,
}
}).sort({
'board': 1
}).toArray();
},
deleteOne: (board, options) => {
return db.deleteOne(options);
},

@ -1,19 +1,17 @@
'use strict';
const Bans = require(__dirname+'/../db-models/bans.js')
, hasPerms = require(__dirname+'/has-perms.js');
, hasPerms = require(__dirname+'/hasperms.js');
module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) {
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
const bans = await Bans.find(ip, res.locals.board._id);
const bans = await Bans.find(ip, res.locals.board ? res.locals.board._id : null);
if (bans && bans.length > 0) {
//TODO: show posts banned for, expiry, etc
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'You are banned',
'redirect': '/'
return res.status(403).render('ban', {
bans: bans
});
}
}

@ -1,9 +0,0 @@
'use strict';
module.exports = (req, res) => {
return req.session.authenticated //if the user is authed
&& req.session.user //if the user is logged in
&& (req.session.user.authLevel > 1 //and is not a regular user
|| res.locals.board.owner == req.session.user.username //or us board owner
|| res.locals.board.moderators.includes(req.session.user.username)); //or is board moderator
}

@ -0,0 +1,16 @@
'use strict';
module.exports = (req, res) => {
return req.session.authenticated //if the user is authed
&& req.session.user //if the user is logged in
&& (
req.session.user.authLevel > 1 //and is not a regular user
|| (
res.locals.board
&& (
res.locals.board.owner == req.session.user.username //and board owner
|| res.locals.board.moderators.includes(req.session.user.username) //or board mod
)
)
)
}

@ -0,0 +1,16 @@
'use strict';
const hasPerms = require(__dirname+'/hasperms.js');
module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) {
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'You do not have permission to access this page',
'redirect': '/'
});
}
next();
}

@ -1,11 +1,13 @@
'use strict';
const uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Bans = require(__dirname+'/../../db-models/bans.js')
, Posts = require(__dirname+'/../../db-models/posts.js');
module.exports = async (req, res, board) => {
module.exports = async (req, res, next, board, checkedPosts) => {
const posts = checkedPosts;
//if user is not logged in or if logged in but not authed, they cannot ban
if (!hasPerms(req, res)) {
@ -19,33 +21,24 @@ module.exports = async (req, res, board) => {
};
}
//get all posts that were checked
let posts = await Posts.getPosts(req.params.board, req.body.checked, true); //admin arument true, fetches passwords and salts
if (!posts || posts.length === 0) {
throw {
'status': 400,
'message': {
'title': 'Bad requests',
'message': 'No posts found',
'redirect': `/${req.params.board}`
}
};
}
const bans = posts.map(post => {
return {
'ip': post.ip,
'reason': req.body.reason || 'No reason specified',
'board': board,
'post': post,
'issuer': req.session.user.username
'post': req.body.delete ? null : post,
'issuer': req.session.user.username,
'date': new Date(),
'expireAt': new Date((new Date).getTime() + (72*1000*60*60)) // 72h ban
}
});
let bannedIps = 0;
const result = await Bans.insertMany(bans, board);
console.log(result)
bannedIps = result.insertedCount;
try {
bannedIps = await Bans.insertMany(bans).then(result => result.insertedCount);
} catch (err) {
return next(err);
}
return `Banned ${bannedIps} ips`;

@ -5,24 +5,12 @@ const path = require('path')
, fs = require('fs')
, unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Posts = require(__dirname+'/../../db-models/posts.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next, checkedPosts) => {
//get all posts that were checked
let posts = await Posts.getPosts(req.params.board, req.body.checked, true); //admin arument true, fetches passwords and salts
if (!posts || posts.length === 0) {
throw {
'status': 400,
'message': {
'title': 'Bad request',
'message': 'No posts found',
'redirect': `/${req.params.board}`
}
};
}
let posts = checkedPosts;
//if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null
if (!hasPerms(req, res)) {
@ -34,14 +22,14 @@ module.exports = async (req, res) => {
&& post.password == req.body.password
});
if (posts.length === 0) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'Password did not match any selected posts',
'redirect': `/${req.params.board}`
}
};
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'Password did not match any selected posts',
'redirect': `/${req.params.board}`
}
};
}
}
@ -60,8 +48,11 @@ module.exports = async (req, res) => {
//delete posts from DB
let deletedPosts = 0;
const result = await Posts.deleteMany(req.params.board, allPosts.map(x => x.postId));
deletedPosts = result.deletedCount;
try {
deletedPosts = await Posts.deleteMany(req.params.board, allPosts.map(x => x.postId)).then(result => result.deletedCount);
} catch (err) {
return next(err);
}
//get filenames from all the posts
let fileNames = [];

@ -1,9 +1,9 @@
'use strict';
const Posts = require(__dirname+'/../../db-models/posts.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js');
, hasPerms = require(__dirname+'/../../helpers/hasperms.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) {
throw {
@ -16,8 +16,12 @@ module.exports = async (req, res) => {
};
}
await Posts.dismissReports(req.params.board, req.body.checked);
try {
await Posts.dismissReports(req.params.board, req.body.checked);
} catch (err) {
return next(err);
}
return `Dismissed report(s) successfully`
return `Dismissed report(s) successfully`;
}

@ -1,13 +1,18 @@
'use strict';
const uuidv4 = require('uuid/v4')
, path = require('path')
, Posts = require(__dirname+'/../../db-models/posts.js')
, path = require('path')
, Posts = require(__dirname+'/../../db-models/posts.js')
module.exports = async (req, res, numFiles) => {
module.exports = async (req, res, next, numFiles) => {
// get the post that we are trying to edit
let post = await Posts.getPost(req.params.board, req.body.id, true);
let post;
try {
post = await Posts.getPost(req.params.board, req.body.id, true);
} catch (err) {
return next(err);
}
if (!thread || thread.thread != null) {
throw {

@ -3,7 +3,7 @@
const bcrypt = require('bcrypt')
, Accounts = require(__dirname+'/../../db-models/accounts.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next) => {
const username = req.body.username.toLowerCase();
const password = req.body.password;
@ -13,8 +13,7 @@ module.exports = async (req, res) => {
try {
account = await Accounts.findOne(username);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err);
}
//if the account doesnt exist, reject
@ -31,8 +30,7 @@ module.exports = async (req, res) => {
try {
passwordMatch = await bcrypt.compare(password, account.passwordHash);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err);
}
//if hashes matched

@ -25,7 +25,7 @@ const uuidv4 = require('uuid/v4')
, videoIdentify = require(__dirname+'/../../helpers/files/video-identify.js')
, formatSize = require(__dirname+'/../../helpers/files/format-size.js')
module.exports = async (req, res, numFiles) => {
module.exports = async (req, res, next, numFiles) => {
// check if this is responding to an existing thread
let redirect = `/${req.params.board}`
@ -35,8 +35,7 @@ module.exports = async (req, res, numFiles) => {
try {
thread = await Posts.getPost(req.params.board, req.body.thread, true);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err);
}
if (!thread || thread.thread != null) {
return res.status(400).render('message', {
@ -100,7 +99,7 @@ module.exports = async (req, res, numFiles) => {
await videoThumbnail(filename);
break;
default:
return res.status(500).render('error'); //how did we get here?
return next(err);
}
//make thumbnail
@ -119,9 +118,8 @@ module.exports = async (req, res, numFiles) => {
files.push(processedFile);
} catch (err) {
console.error(err);
//TODO: DELETE FAILED FILES
return res.status(500).render('error');
return next(err);
}
}
}
@ -174,8 +172,7 @@ module.exports = async (req, res, numFiles) => {
try {
postId = await Posts.insertOne(req.params.board, data);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err);
}
const successRedirect = `/${req.params.board}/thread/${req.body.thread || postId}#${postId}`;

@ -3,7 +3,7 @@
const bcrypt = require('bcrypt')
, Accounts = require(__dirname+'/../../db-models/accounts.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next) => {
const username = req.body.username.toLowerCase();
const password = req.body.password;
@ -12,8 +12,7 @@ module.exports = async (req, res) => {
try {
account = await Accounts.findOne(username);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err);
}
// if the account exists reject
@ -29,8 +28,7 @@ module.exports = async (req, res) => {
try {
await Accounts.insertOne(username, password, 1);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err);
}
return res.render('message', {

@ -2,7 +2,7 @@
const Posts = require(__dirname+'/../../db-models/posts.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next) => {
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
const report = {
@ -12,7 +12,11 @@ module.exports = async (req, res) => {
}
//push the report to all checked posts
await Posts.reportMany(req.params.board, req.body.checked, report);
try {
await Posts.reportMany(req.params.board, req.body.checked, report);
} catch (err) {
return next(err);
}
//hooray!
return `Reported post(s) successfully`

@ -5,13 +5,13 @@ const path = require('path')
, fs = require('fs')
, unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Posts = require(__dirname+'/../../db-models/posts.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next, checkedPosts) => {
//get all posts that were checked
let posts = await Posts.getPosts(req.params.board, req.body.checked, true); //admin arument true, fetches passwords and salts
let posts = checkedPosts;
if (!posts || posts.length === 0) {
throw {
@ -63,8 +63,11 @@ module.exports = async (req, res) => {
// spoiler posts
let spoileredPosts = 0;
const result = await Posts.spoilerMany(req.params.board, posts.map(x => x.postId));
spoileredPosts = result.modifiedCount;
try {
spoileredPosts = await Posts.spoilerMany(req.params.board, posts.map(x => x.postId)).then(result => result.modifiedCount);
} catch (err) {
return next(err);
}
//hooray!
return `Spoilered ${spoileredPosts} posts`

@ -10,8 +10,7 @@ module.exports = async (req, res, next) => {
threads = await Posts.getRecent(req.params.board, req.params.page || 1);
pages = Math.ceil((await Posts.getPages(req.params.board)) / 10);
} catch (err) {
console.error(err)
return next();
return next(err);
}
//render the page

@ -9,8 +9,7 @@ module.exports = async (req, res, next) => {
try {
threads = await Posts.getCatalog(req.params.board);
} catch (err) {
console.error(err);
return next();
return next(err);
}
//render the page

@ -0,0 +1,20 @@
'use strict';
const Posts = require(__dirname+'/../../db-models/posts.js');
module.exports = async (req, res, next) => {
let posts;
try {
posts = await Posts.getAllReports();
} catch (err) {
return next(err);
}
//render the page
res.render('globalmanage', {
csrf: req.csrfToken(),
posts: posts
});
}

@ -9,8 +9,7 @@ module.exports = async (req, res, next) => {
try {
boards = await Boards.find();
} catch (err) {
console.error(err)
return next();
return next(err);
}
//render the page

@ -1,6 +1,6 @@
'use strict';
module.exports = (req, res) => {
module.exports = (req, res, next) => {
//render the page
res.render('login', {

@ -2,14 +2,13 @@
const Posts = require(__dirname+'/../../db-models/posts.js');
module.exports = async (req, res) => {
module.exports = async (req, res, next) => {
let posts;
try {
posts = await Posts.getReports(req.params.board);
} catch (err) {
console.error(err);
return res.status(500).render('error');
return next(err)
}
//render the page

@ -1,15 +1,6 @@
'use strict';
module.exports = (req, res) => {
//send home if already logged in
if (req.session.authenticated === true) {
return res.status(400).render('message', {
'title': 'Notice',
'message': 'You are already logged in. Redirecting you to back home.',
'redirect': '/'
});
}
module.exports = (req, res, next) => {
//render the page
res.render('register', {

@ -9,8 +9,7 @@ module.exports = async (req, res, next) => {
try {
thread = await Posts.getThread(req.params.board, req.params.id);
} catch (err) {
console.error(err);
return next();
return next(err);
}
if (!thread) {

@ -74,7 +74,10 @@ const express = require('express')
return res.status(403).send('Invalid CSRF token')
}
console.error(err.stack)
return res.status(500).render('error')
return res.status(500).render('message', {
'title': 'Internal Server Error',
'redirect': req.header('Referer') || '/'
})
})
// listen

@ -1,4 +1,4 @@
mixin post(board, post, truncate)
mixin post(board, post, truncate, showreports)
article(id=post.postId class='post-container '+(post.thread ? '' : 'op'))
header.post-info
input.post-check(type='checkbox', name='checked[]' value=post.postId)
@ -44,7 +44,7 @@ mixin post(board, post, truncate)
blockquote.post-message !{post.message}
else
blockquote.post-message !{post.message}
if post.reports
if showreports && post.reports
each report in post.reports
.reports.post-container
span Date: #{report.date.toLocaleString()}

@ -0,0 +1,25 @@
extends ../layout.pug
include ../mixins/post.pug
block head
title Banned!
block content
h1.board-title Banned!
hr(size=1)
Bans currently in place against your IP:
hr(size=1)
for ban in bans
if ban.board
div Board: #[a(href=`/${ban.board}`) /#{ban.board}/]
else
div Global ban.
div Reason: #{ban.reason}
div Issuer: #{ban.issuer}
div Date: #{ban.date}
div Expiry: #{ban.expireAt}
if ban.post
span Post:
section.thread
+post(ban.post.board, ban.post, false)
hr(size=1)

@ -0,0 +1,11 @@
extends ../layout.pug
include ../mixins/post.pug
block head
title Manage
block content
h1.board-title Global Management
hr(size=1)
p under construction

@ -15,7 +15,7 @@ block content
hr(size=1)
for post in posts
section.thread
+post(board, post)
+post(board, post, false, true)
hr(size=1)
section.action-wrapper
span
@ -26,6 +26,10 @@ block content
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler
span
label
input.post-check(type='checkbox', name='dismiss' value=1)
| Dismiss
span
label
input.post-check(type='checkbox', name='ban' value=1)

@ -43,6 +43,8 @@ const Mongo = require(__dirname+'/helpers/db.js')
moderators: [],
})
console.log('creating indexes')
await Bans.db.dropIndexes();
await Bans.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 });
await Posts.db.dropIndexes();
//these are fucked
await Posts.db.createIndex({

Loading…
Cancel
Save