global and board bans/reports separated, improved action selection form

merge-requests/208/head
fatchan 5 years ago
parent e80e9e4452
commit d5870187bc
  1. 119
      controllers/forms.js
  2. 6
      db/bans.js
  3. 4
      db/boards.js
  4. 80
      db/posts.js
  5. 5
      helpers/number-converter.js
  6. 18
      models/forms/delete-post.js
  7. 4
      models/forms/dismiss-report.js
  8. 25
      models/forms/dismissglobalreport.js
  9. 23
      models/forms/globalreportpost.js
  10. 1
      models/forms/make-post.js
  11. 4
      models/forms/report-post.js
  12. 19
      models/forms/spoiler-post.js
  13. 2
      models/pages/board.js
  14. 4
      models/pages/globalmanage.js
  15. 2
      server.js
  16. 32
      static/css/style.css
  17. 31
      views/includes/actionfooter.pug
  18. 24
      views/includes/actionfooter_globalmanage.pug
  19. 6
      views/includes/actionfooter_manage.pug
  20. 49
      views/includes/postform.pug
  21. 14
      views/mixins/post.pug
  22. 2
      views/pages/board.pug
  23. 4
      views/pages/globalmanage.pug
  24. 2
      views/pages/manage.pug
  25. 2
      views/pages/thread.pug
  26. 9
      wipe.js

@ -12,7 +12,9 @@ const express = require('express')
, deletePosts = require(__dirname+'/../models/forms/delete-post.js')
, spoilerPosts = require(__dirname+'/../models/forms/spoiler-post.js')
, reportPosts = require(__dirname+'/../models/forms/report-post.js')
, globalReportPosts = require(__dirname+'/../models/forms/globalreportpost.js')
, dismissReports = require(__dirname+'/../models/forms/dismiss-report.js')
, dismissGlobalReports = require(__dirname+'/../models/forms/dismissglobalreport.js')
, loginAccount = require(__dirname+'/../models/forms/login.js')
, registerAccount = require(__dirname+'/../models/forms/register.js')
, hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js')
@ -161,9 +163,11 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a
errors.push('Ban reason must be 50 characters or less');
}
if (!(req.body.report
|| req.body.global_report
|| req.body.spoiler
|| req.body.delete
|| req.body.dismiss
|| req.body.spoiler
|| req.body.global_dismiss
|| req.body.ban
|| req.body.global_ban)) {
errors.push('Invalid actions selected')
@ -191,24 +195,37 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a
const messages = [];
try {
// if getting global banned, board ban doesnt matter
if (req.body.global_ban) {
messages.push((await banPoster(req, res, next, null, posts)));
} else if (req.body.ban) {
messages.push((await banPoster(req, res, next, req.params.board, posts)));
}
//ban before deleting
if (req.body.delete) {
messages.push((await deletePosts(req, res, next, posts)));
} else {
// if it was getting deleted, we cant do any of these
if (req.body.spoiler) {
messages.push((await spoilerPosts(req, res, next, posts)));
}
// cannot report and dismiss at same time
if (req.body.report) {
messages.push((await reportPosts(req, res, next)));
} else if (req.body.dismiss) {
messages.push((await dismissReports(req, res, next)));
}
// cannot report and dismiss at same time
if (req.body.global_report) {
messages.push((await globalReportPosts(req, res, next, posts)));
} else if (req.body.global_dismiss) {
messages.push((await dismissGlobalReports(req, res, next, posts)));
}
}
} catch (err) {
//something not right
if (err.status) {
@ -268,13 +285,111 @@ router.post('/board/:board/unban', Boards.exists, banCheck, hasPerms, numberConv
router.post('/global/actions', hasPerms, numberConverter, async(req, res, next) => {
//TODO
const errors = [];
if (!req.body.globalcheckedposts || req.body.globalcheckedposts.length === 0 || req.body.globalcheckedposts.length > 10) {
errors.push('Must select 1-10 posts')
}
if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Ban reason must be 50 characters or less');
}
if (!(req.body.spoiler
|| req.body.delete
|| req.body.global_dismiss
|| req.body.global_ban)) {
errors.push('Invalid actions selected')
}
if (errors.length > 0) {
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
'redirect': '/globalmanage'
})
}
const posts = await Posts.globalGetPosts(req.body.globalcheckedposts, true);
if (!posts || posts.length === 0) {
return res.status(404).render('message', {
'title': 'Not found',
'errors': 'Selected posts not found',
'redirect': '/globalmanage'
})
}
const messages = [];
try {
if (req.body.global_ban) {
messages.push((await banPoster(req, res, next, null, posts)));
}
//ban before deleting
if (req.body.delete) {
messages.push((await deletePosts(req, res, next, posts)));
} else {
// if it was getting deleted, we cant do any of these
if (req.body.spoiler) {
messages.push((await spoilerPosts(req, res, next, posts)));
}
if (req.body.global_dismiss) {
messages.push((await dismissGlobalReports(req, res, next, posts)));
}
}
} catch (err) {
//something not right
if (err.status) {
// return out special error
return res.status(err.status).render('message', err.message);
}
//some other error, use regular error handler
return next(err);
}
return res.render('message', {
'title': 'Success',
'messages': messages,
'redirect': `/${req.params.board}`
});
});
router.post('/global/unban', hasPerms, numberConverter, async(req, res, next) => {
//TODO
const errors = [];
if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) {
errors.push('Must select 1-10 bans')
}
if (errors.length > 0) {
return res.status(400).render('message', {
'title': 'Bad request',
'errors': errors,
'redirect': `/globalmanage`
});
}
const messages = [];
try {
messages.push((await removeBans(req, res, next)));
} catch (err) {
//something not right
if (err.status) {
// return out special error
return res.status(err.status).render('message', err.message);
}
//some other error, use regular error handler
return next(err);
}
return res.render('message', {
'title': 'Success',
'messages': messages,
'redirect': `/globalmanage`
});
});

@ -30,6 +30,12 @@ module.exports = {
return db.find({}).toArray();
},
getGlobalBans: () => {
return db.find({
'board': null
}).toArray();
},
getBoardBans: (board) => {
return db.find({
'board': board,

@ -33,9 +33,9 @@ module.exports = {
exists: async (req, res, next) => {
const board = await module.exports.findOne(req.params.board)
const board = await module.exports.findOne(req.params.board);
if (!board) {
return res.status(404).render('404')
return res.status(404).render('404');
}
res.locals.board = board; // can acces this in views or next route handlers
next();

@ -40,7 +40,7 @@ module.exports = {
}
}).sort({
'_id': -1
}).limit(3).toArray();
}).limit(5).toArray();
thread.replies = replies.reverse();
}));
@ -172,6 +172,15 @@ module.exports = {
},
//takes array "ids" of mongo ids to get posts from any board
globalGetPosts: (ids) => {
return db.find({
'_id': {
'$in': ids
},
}).toArray();
},
insertOne: async (board, data) => {
// bump thread if name not sage
@ -213,6 +222,27 @@ module.exports = {
});
},
globalReportMany: (ids, report) => {
return db.updateMany({
'_id': {
'$in': ids
},
}, {
'$push': {
'globalreports': report
}
});
},
getReports: (board) => {
return db.find({
'reports.0': {
'$exists': true
},
'board': board
}).toArray();
},
dismissReports: (board, ids) => {
return db.updateMany({
'postId': {
@ -226,58 +256,60 @@ module.exports = {
});
},
getReports: (board) => {
getGlobalReports: () => {
return db.find({
'reports.0': {
'globalreports.0': {
'$exists': true
},
'board': board
}
}, {
'projection': {
'salt': 0,
'password': 0,
'ip': 0,
'reports': 0,
}
}).toArray();
},
getAllReports: () => {
return db.find({
'reports.0': {
'$exists': true
dismissGlobalReports: (ids) => {
return db.updateMany({
'_id': {
'$in': ids
},
}, {
'$set': {
'globalreports': []
}
}).toArray();
});
},
deleteOne: (board, options) => {
return db.deleteOne(options);
},
deleteMany: (board, ids) => {
deleteMany: (ids) => {
return db.deleteMany({
'postId': {
'_id': {
'$in': ids
},
'board': board
}
});
},
spoilerMany: (board, ids) => {
spoilerMany: (ids) => {
return db.updateMany({
'postId': {
'$in': ids
},
'board': board
}, {
'$set': {
return db.updateMany({
'_id': {
'$in': ids
}
}, {
'$set': {
'spoiler': true
}
});
});
},
},
deleteAll: (board) => {
return db.deleteMany({

@ -1,5 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/../db/db.js');
module.exports = (req, res, next) => {
//for body
@ -10,6 +12,9 @@ module.exports = (req, res, next) => {
//syntax tries to convert all string to number
req.body.checkedposts = req.body.checkedposts.map(Number);
}
if (req.body.globalcheckedposts) {
req.body.globalcheckedposts = req.body.globalcheckedposts.map(Mongo.ObjectId)
}
//and for params
if (req.params.id) {

@ -6,6 +6,7 @@ const path = require('path')
, unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, checkedPosts) => {
@ -33,12 +34,18 @@ module.exports = async (req, res, next, checkedPosts) => {
}
}
const threadIds = posts.filter(x => x.thread == null).map(x => x.postId);
//filter to threads, then get the board and thread for each
const boardThreads = posts.filter(x => x.thread == null).map(x => {
return {
board: x.board,
thread: x.postId
};
});
//get posts from all threads
let threadPosts = []
await Promise.all(threadIds.map(async id => {
const currentThreadPosts = await Posts.getThreadPosts(req.params.board, id);
await Promise.all(boardThreads.map(async data => {
const currentThreadPosts = await Posts.getThreadPosts(data.board, data.thread);
threadPosts = threadPosts.concat(currentThreadPosts);
return;
}))
@ -47,7 +54,8 @@ module.exports = async (req, res, next, checkedPosts) => {
const allPosts = posts.concat(threadPosts)
//delete posts from DB
const deletedPosts = await Posts.deleteMany(req.params.board, allPosts.map(x => x.postId)).then(result => result.deletedCount);
const postMongoIds = allPosts.map(post => Mongo.ObjectId(post._id))
const deletedPosts = await Posts.deleteMany(postMongoIds).then(result => result.deletedCount);
//get filenames from all the posts
let fileNames = [];
@ -65,6 +73,6 @@ module.exports = async (req, res, next, checkedPosts) => {
}));
//hooray!
return `Deleted ${threadIds.length} threads and ${deletedPosts-threadIds.length} posts`
return `Deleted ${boardThreads.length} threads and ${deletedPosts-boardThreads.length} posts`
}

@ -16,8 +16,8 @@ module.exports = async (req, res, next) => {
};
}
await Posts.dismissReports(req.params.board, req.body.checkedposts);
const dismissedReports = await Posts.dismissReports(req.params.board, req.body.checkedposts).then(result => result.modifiedCount);
return `Dismissed report(s) successfully`;
return `Dismissed ${dismissedReports} reports successfully`;
}

@ -0,0 +1,25 @@
'use strict';
const Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js');
module.exports = async (req, res, next, posts) => {
if (!hasPerms(req, res)) {
throw {
'status': 403,
'message': {
'title': 'Forbidden',
'message': 'You are not authorised to dismiss global reports.',
'redirect': '/'
}
};
}
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id))
const dismissedCount = await Posts.dismissGlobalReports(postMongoIds).then(result => result.modifiedCount);
return `Dismissed ${dismissedCount} reports successfully`;
}

@ -0,0 +1,23 @@
'use strict';
const Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, posts) => {
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
const report = {
'reason': req.body.report_reason,
'date': new Date(),
'ip': ip
}
const ids = posts.map(p => Mongo.ObjectId(p._id))
//push the report to all checked posts
const reportedPosts = await Posts.globalReportMany(ids, report).then(result => result.modifiedCount);
//hooray!
return `Global reported ${reportedPosts} posts successfully`
}

@ -168,6 +168,7 @@ module.exports = async (req, res, next, numFiles) => {
'files': files,
'salt': !req.body.thread ? salt : '',
'reports': [],
'globalreports': [],
'spoiler': req.body.spoiler ? true : false,
};

@ -12,9 +12,9 @@ module.exports = async (req, res, next) => {
}
//push the report to all checked posts
await Posts.reportMany(req.params.board, req.body.checkedposts, report);
const reportedCount = await Posts.reportMany(req.params.board, req.body.checkedposts, report).then(result => result.modifiedCount);
//hooray!
return `Reported post(s) successfully`
return `Reported ${reportedCount} posts successfully`
}

@ -6,6 +6,7 @@ const path = require('path')
, unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Mongo = require(__dirname+'/../../db/db.js')
, Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next, checkedPosts) => {
@ -13,17 +14,6 @@ module.exports = async (req, res, next, checkedPosts) => {
//get all posts that were checked
let posts = checkedPosts;
if (!posts || posts.length === 0) {
throw {
'status': 400,
'message': {
'title': 'Bad requests',
'message': 'No posts found',
'redirect': `/${req.params.board}`
}
};
}
//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)) {
@ -39,7 +29,7 @@ module.exports = async (req, res, next, checkedPosts) => {
'message': {
'title': 'Forbidden',
'message': 'Password did not match any selected posts',
'redirect': `/${req.params.board}`
'redirect': '/'
}
};
}
@ -56,13 +46,14 @@ module.exports = async (req, res, next, checkedPosts) => {
'message': {
'title': 'Conflict',
'message': 'Posts already spoilered',
'redirect': `/${req.params.board}`
'redirect': '/'
}
};
}
// spoiler posts
const spoileredPosts = await Posts.spoilerMany(req.params.board, posts.map(x => x.postId)).then(result => result.modifiedCount);
const postMongoIds = posts.map(post => Mongo.ObjectId(post._id));
const spoileredPosts = await Posts.spoilerMany(postMongoIds).then(result => result.modifiedCount);
//hooray!
return `Spoilered ${spoileredPosts} posts`

@ -8,7 +8,7 @@ module.exports = async (req, res, next) => {
let threads;
let pages;
try {
pages = Math.ceil((await Posts.getPages(req.params.board)) / 10);
pages = Math.ceil((await Posts.getPages(req.params.board)) / 10) || 1;
if (page > pages) {
return next();
}

@ -8,8 +8,8 @@ module.exports = async (req, res, next) => {
let reports;
let bans;
try {
reports = await Posts.getAllReports();
bans = await Bans.getAllBans();
reports = await Posts.getGlobalReports();
bans = await Bans.getGlobalBans();
} catch (err) {
return next(err)
}

@ -53,7 +53,7 @@ const express = require('express')
// use pug view engine
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views/pages'));
// app.enable('view cache');
app.enable('view cache');
// static files
app.use('/css', express.static(__dirname + '/static/css'));

@ -152,10 +152,40 @@ input, textarea {
margin: 2px 0;
}
.toggle-label:hover {
box-shadow: inset 0 0 100px 100px rgba(255,255,255,.25);
}
.toggle-label {
background: #D6DAF0;
padding: 10px;
border-radius: 3px;
text-align: center;
border: 1px solid lightgray;
max-width: 100%;
box-sizing: border-box;
/*-webkit-appearance: button;
-moz-appearance: button;
appearance: button;*/
}
.toggle {
display: none;
}
.toggle:checked + .form-post {
display: flex;
}
.form-post {
display: flex;
flex-direction: column;
max-width: 100%;
margin-top: 10px;
}
.togglable {
display: none;
}
.user-id {
@ -302,7 +332,7 @@ input textarea {
}
.nav-item:hover {
box-shadow: inset 0 0 100px 100px rgba(255,255,255,.1);
box-shadow: inset 0 0 100px 100px rgba(255,255,255,.25);
}
.footer {

@ -0,0 +1,31 @@
.action-wrapper
.actions Actions:
label
input.post-check(type='checkbox', name='delete' value=1)
| Delete
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler Images
label
input#password(type='text', name='password', placeholder='post password' autocomplete='off')
label
input.post-check(type='checkbox', name='report' value=1)
| Report
label
input.post-check(type='checkbox', name='global_report' value=1)
| Global Report
label
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off')
.actions Mod Actions:
label
input.post-check(type='checkbox', name='ban' value=1)
| Ban Poster
label
input.post-check(type='checkbox', name='global_ban' value=1)
| Global Ban Poster
label
input.post-check(type='checkbox', name='preserve_post' value=1)
| Show Post In Ban
label
input#report(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off')
input(type='submit', value='submit')

@ -0,0 +1,24 @@
.action-wrapper
.actions Actions:
label
input.post-check(type='checkbox', name='delete' value=1)
| Delete
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler Images
label
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off')
.actions Mod Actions:
label
input.post-check(type='checkbox', name='global_dismiss' value=1)
| Dismiss Global Reports
label
input.post-check(type='checkbox', name='global_ban' value=1)
| Global Ban Poster
label
input.post-check(type='checkbox', name='preserve_post' value=1)
| Show Post In Ban
label
input#report(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off')
input(type='submit', value='submit')

@ -2,15 +2,15 @@
.actions Actions:
label
input.post-check(type='checkbox', name='delete' value=1)
| Delete Post
| Delete
label
input.post-check(type='checkbox', name='spoiler' value=1)
| Spoiler Images
label
input#password(type='text', name='password', placeholder='post password' autocomplete='off')
label
input.post-check(type='checkbox', name='report' value=1)
| Report Post
input.post-check(type='checkbox', name='global_report' value=1)
| Global Report
label
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off')
.actions Mod Actions:

@ -1,24 +1,29 @@
section.form-wrapper
form.form-post(action=`/forms/board/${board._id}/post`, enctype='multipart/form-data', method='POST')
label.toggle-label Show Post Form
input.toggle(type='checkbox')
form.form-post.togglable(action=`/forms/board/${board._id}/post`, enctype='multipart/form-data', method='POST')
input(type='hidden' name='_csrf' value=csrf)
input(type='hidden' name='thread' value=thread != null ? thread.postId : null)
input#title(type='text', name='subject', placeholder='subject' autocomplete='off' maxlength='50')
input#name(type='text', name='name', placeholder='name' autocomplete='off' maxlength='50')
input#name(type='text', name='email', placeholder='email' autocomplete='off' maxlength='50')
input#password(type='password', name='password', placeholder='post password' autocomplete='off' maxlength='50')
textarea#message(name='message', rows='8', cols='50', placeholder='message' autocomplete='off' maxlength='2000')
span
input#file(type='file', name='file' multiple)
label
input.post-check#spoiler(type='checkbox', name='spoiler', value='true')
| Spoiler
input(type='submit', value='submit')
input(type='hidden' name='_csrf' value=csrf)
input(type='hidden' name='thread' value=thread != null ? thread.postId : null)
input#title(type='text', name='subject', placeholder='subject' autocomplete='off' maxlength='50')
input#name(type='text', name='name', placeholder='name' autocomplete='off' maxlength='50')
input#name(type='text', name='email', placeholder='email' autocomplete='off' maxlength='50')
input#password(type='password', name='password', placeholder='post password' autocomplete='off' maxlength='50')
textarea#message(name='message', rows='8', cols='50', placeholder='message' autocomplete='off' maxlength='2000')
span
input#file(type='file', name='file' multiple)
label
input.post-check#spoiler(type='checkbox', name='spoiler', value='true')
| Spoiler
input(type='submit', value='submit')

@ -1,8 +1,11 @@
mixin post(post, truncate, showreports)
mixin post(post, truncate, manage, globalmanage)
article(id=post.postId class='post-container '+(post.thread ? '' : 'op'))
header.post-info
span
input.post-check(type='checkbox', name='checkedposts[]' value=post.postId)
if globalmanage
input.post-check(type='checkbox', name='globalcheckedposts[]' value=post._id)
else
input.post-check(type='checkbox', name='checkedposts[]' value=post.postId)
if post.subject
span.post-subject #{post.subject}
if post.email
@ -45,8 +48,13 @@ mixin post(post, truncate, showreports)
blockquote.post-message !{post.message}
else
blockquote.post-message !{post.message}
if showreports && post.reports
if manage === true
each report in post.reports
.reports.post-container
span Date: #{report.date.toLocaleString()}
span Reason: #{report.reason}
if globalmanage === true
each report in post.globalreports
.reports.post-container
span Date: #{report.date.toLocaleString()}
span Reason: #{report.reason}

@ -24,4 +24,4 @@ block content
hr(size=1)
include ../includes/pages.pug
hr(size=1)
include ../includes/deletefooter.pug
include ../includes/actionfooter.pug

@ -16,9 +16,9 @@ block content
else
for report in reports
section.thread
+post(report, false, true)
+post(report, false, false, true)
hr(size=1)
include ../includes/deletefooter.pug
include ../includes/actionfooter_globalmanage.pug
hr(size=1)
h4 All Bans:
form(action=`/forms/global/unban` method='POST' enctype='application/x-www-form-urlencoded')

@ -18,7 +18,7 @@ block content
section.thread
+post(report, false, true)
hr(size=1)
include ../includes/deletefooter.pug
include ../includes/actionfooter_manage.pug
hr(size=1)
h4 Bans:
form(action=`/forms/board/${board._id}/unban` method='POST' enctype='application/x-www-form-urlencoded')

@ -21,4 +21,4 @@ block content
for post in thread.replies
+post(post)
hr(size=1)
include ../includes/deletefooter.pug
include ../includes/actionfooter.pug

@ -66,6 +66,15 @@ const Mongo = require(__dirname+'/db/db.js')
}
}
});
await Posts.db.createIndex({
'globalreports.0': 1
}, {
partialFilterExpression: {
'globalreports.0': {
'$exists': true
}
}
});
await readdir('static/img/').then(async files => {
await Promise.all(files.map(async file => {
if (file != 'spoiler.png')

Loading…
Cancel
Save