diff --git a/controllers/forms.js b/controllers/forms.js index d6a0152d..d53e8883 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -6,8 +6,9 @@ const express = require('express') , Posts = require(__dirname+'/../db-models/posts.js') , Trips = require(__dirname+'/../db-models/trips.js') , makePost = require(__dirname+'/../models/forms/make-post.js') - , deletePost = require(__dirname+'/../models/forms/delete-post.js') - , reportPost = require(__dirname+'/../models/forms/report-post.js') + , deletePosts = require(__dirname+'/../models/forms/delete-post.js') + , reportPosts = require(__dirname+'/../models/forms/report-post.js') + , dismissReports = require(__dirname+'/../models/forms/dismiss-report.js') , loginAccount = require(__dirname+'/../models/forms/login.js') , registerAccount = require(__dirname+'/../models/forms/register.js') , numberConverter = require(__dirname+'/../helpers/number-converter.js'); @@ -117,6 +118,9 @@ router.post('/board/:board', Boards.exists, numberConverter, (req, res, next) => if (req.body.subject && req.body.subject.length > 50) { errors.push('Subject must be 50 characters or less'); } + if (req.body.email && req.body.email.length > 50) { + errors.push('Email must be 50 characters or less'); + } if (req.body.password && req.body.password.length > 50) { errors.push('Password must be 50 characters or less'); } @@ -138,17 +142,20 @@ router.post('/board/:board/posts', Boards.exists, numberConverter, (req, res, ne const errors = []; + if (!req.body.checked || req.body.checked.length === 0 || req.body.checked.length > 10) { + errors.push('Must select 1-10 posts') + } if (req.body.password && req.body.password.length > 50) { errors.push('Password must be 50 characters or less'); } - if (req.body.report && req.body.report.length > 50) { + if (req.body.reason && req.body.reason.length > 50) { errors.push('Report must be 50 characters or less'); } - if (req.body.password && req.body.report) { - errors.push('Can only report or delete, not both'); + if (!(req.body.report || req.body.delete || req.body.dismiss)) { + errors.push('Must select an action') } - if (!req.body.checked || req.body.checked.length === 0 || req.body.checked.length > 10) { //10 for now just for _some_ limit - errors.push('Must select 1-10 posts') + if (req.body.report && (!req.body.reason || req.body.reason.length === 0)) { + errors.push('Reports must have a reason') } if (errors.length > 0) { @@ -159,16 +166,16 @@ router.post('/board/:board/posts', Boards.exists, numberConverter, (req, res, ne }) } - //we checked to make sure there are not both, so... if (req.body.report) { - //if theres a report reason, handle reports - reportPost(req, res); + reportPosts(req, res); + } else if (req.body.delete) { + deletePosts(req, res); } else { - //otherwise, must be delete request which - //for authed users DOES NOT requoie passwrd - deletePost(req, res); + dismissReports(req, res); } }); + + module.exports = router; diff --git a/db-models/boards.js b/db-models/boards.js index 5761c0d1..eb5a0f37 100644 --- a/db-models/boards.js +++ b/db-models/boards.js @@ -49,7 +49,11 @@ module.exports = { || res.locals.board.moderators.includes(req.session.user.username)) { return next(); } - return res.redirect('/login'); + return res.status(403).render('message', { + 'title': 'Forbidden', + 'message': 'You do not have permission to manage this board', + 'redirect': '/login' + }); }, diff --git a/db-models/posts.js b/db-models/posts.js index 7e3842b8..b3577354 100644 --- a/db-models/posts.js +++ b/db-models/posts.js @@ -19,6 +19,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, 'reports': 0 } }).sort({ @@ -34,6 +35,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, 'reports': 0 } }).sort({ @@ -48,7 +50,8 @@ module.exports = { getPages: (board) => { return db.countDocuments({ - 'board': board + 'board': board, + 'thread': null }); }, @@ -63,6 +66,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, 'reports': 0 } }), @@ -89,6 +93,7 @@ module.exports = { 'projection': { 'salt': 0 , 'password': 0, + 'ip': 0, 'reports': 0 } }).sort({ @@ -107,6 +112,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, 'reports': 0 } }).toArray(); @@ -130,6 +136,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, 'reports': 0 } }); @@ -157,6 +164,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, 'reports': 0 } }).toArray(); @@ -166,7 +174,7 @@ module.exports = { insertOne: async (board, data) => { // bump thread if name not sage - if (data.thread !== null && data.name !== 'sage') { + if (data.thread !== null && data.email !== 'sage') { await db.updateOne({ 'postId': data.thread, 'board': board @@ -204,6 +212,19 @@ module.exports = { }); }, + dismissReports: (board, ids) => { + return db.updateMany({ + 'postId': { + '$in': ids + }, + 'board': board + }, { + '$set': { + 'reports': [] + } + }); + }, + getReports: (board) => { return db.find({ 'reports.0': { @@ -214,6 +235,7 @@ module.exports = { 'projection': { 'salt': 0, 'password': 0, + 'ip': 0, } }).toArray(); }, diff --git a/helpers/delete-perms.js b/helpers/has-perms.js similarity index 55% rename from helpers/delete-perms.js rename to helpers/has-perms.js index 1ea9334a..3c64c804 100644 --- a/helpers/delete-perms.js +++ b/helpers/has-perms.js @@ -3,7 +3,7 @@ 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 - || res.locals.board.owner == req.session.user.username - || res.locals.board.moderators.includes(req.session.user.username)); + && (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 } diff --git a/models/forms/delete-post.js b/models/forms/delete-post.js index b2eb83b7..c35ebdde 100644 --- a/models/forms/delete-post.js +++ b/models/forms/delete-post.js @@ -5,10 +5,11 @@ const path = require('path') , fs = require('fs') , unlink = util.promisify(fs.unlink) , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') - , deletePerms = require(__dirname+'/../../helpers/delete-perms.js') + , hasPerms = require(__dirname+'/../../helpers/has-perms.js') , Posts = require(__dirname+'/../../db-models/posts.js'); module.exports = async (req, res) => { + //get all posts that were checked let posts; try { @@ -19,7 +20,7 @@ module.exports = async (req, res) => { } //if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null - if (!deletePerms(req, res)) { + if (!hasPerms(req, res)) { // filter posts by password only if NOT board moderator or owner posts = posts.filter(post => { // only include posts that have a password and that matches diff --git a/models/forms/dismiss-report.js b/models/forms/dismiss-report.js new file mode 100644 index 00000000..c22a663d --- /dev/null +++ b/models/forms/dismiss-report.js @@ -0,0 +1,31 @@ +'use strict'; + +const Posts = require(__dirname+'/../../db-models/posts.js') + , hasPerms = require(__dirname+'/../../helpers/has-perms.js'); + +module.exports = async (req, res) => { + + if (!hasPerms(req, res)) { + return res.status(403).render('message', { + 'title': 'Forbidden', + 'message': `You are not authorised to dismiss reports.`, + 'redirect': `/${req.params.board}` + }); + } + + try { + //dismiss reports from all checked posts + await Posts.dismissReports(req.params.board, req.body.checked); + } catch (err) { + console.error(err); + return res.status(500).render('error'); + } + + //hooray! + return res.render('message', { + 'title': 'Success', + 'message': `Dismissed report(s) successfully`, + 'redirect': `/${req.params.board}/manage` + }); + +} diff --git a/models/forms/report-post.js b/models/forms/report-post.js index e8425a23..4f454a93 100644 --- a/models/forms/report-post.js +++ b/models/forms/report-post.js @@ -4,9 +4,16 @@ const Posts = require(__dirname+'/../../db-models/posts.js'); module.exports = async (req, res) => { + const ip = req.headers['x-real-ip'] || req.connection.remoteAddress; + const report = { + 'reason': req.body.reason, + 'date': new Date(), + 'ip': ip + } + try { //push the report to all checked posts - await Posts.reportMany(req.params.board, req.body.checked, req.body.report); + await Posts.reportMany(req.params.board, req.body.checked, report); } catch (err) { console.error(err); return res.status(500).render('error'); diff --git a/models/pages/home.js b/models/pages/home.js index c3124da8..f8640e0f 100644 --- a/models/pages/home.js +++ b/models/pages/home.js @@ -3,6 +3,7 @@ const Boards = require(__dirname+'/../../db-models/boards.js'); module.exports = async (req, res, next) => { + //get a list of boards let boards; try { diff --git a/static/css/style.css b/static/css/style.css index 2b32a69f..70388718 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -9,6 +9,21 @@ body { margin: 0; } +.spoiler { + background: black; +} + +.spoiler:hover { + color: white; +} + +.post-mode { + background-color: red; + color: white; + font-weight: bold; + text-align: center; +} + .reports { background: #fca!important; border-color: #c97!important; @@ -54,7 +69,7 @@ input, textarea { margin: 10px 0; } -.delete-wrapper { +.action-wrapper { align-items: center; /*flex-direction: row;*/ } @@ -121,6 +136,11 @@ input textarea { margin: 0; } +.no-decoration { + text-decoration: none; + color: white; +} + .board-description { text-align:center; margin: 0; diff --git a/views/includes/boardheader.pug b/views/includes/boardheader.pug new file mode 100644 index 00000000..b95ca162 --- /dev/null +++ b/views/includes/boardheader.pug @@ -0,0 +1,3 @@ +a.no-decoration(href=`/${board._id}`) + h1.board-title /#{board._id}/ - #{board.name} +h4.board-description #{board.description} diff --git a/views/includes/deletefooter.pug b/views/includes/deletefooter.pug index 7b965d78..dee52503 100644 --- a/views/includes/deletefooter.pug +++ b/views/includes/deletefooter.pug @@ -1,5 +1,12 @@ -section.delete-wrapper - p Report OR delete selected posts - input#report(type='report', name='report', placeholder='report reason' autocomplete='off') - input#password(type='password', name='password', placeholder='password (for deletion)' autocomplete='off') +section.action-wrapper + span + label + input.post-check(type='checkbox', name='delete' value=1) + | Delete + input#password(type='password', name='password', placeholder='deletion password' autocomplete='off') + span + label + input.post-check(type='checkbox', name='report' value=1) + | Report + input#report(type='text', name='reason', placeholder='report reason' autocomplete='off') input(type='submit', value='submit') diff --git a/views/includes/postform.pug b/views/includes/postform.pug index a611d716..a519a1fc 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -8,6 +8,8 @@ section.form-wrapper 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='password (for deletion)' autocomplete='off' maxlength='50') diff --git a/views/mixins/post.pug b/views/mixins/post.pug index 5d5a4981..d8a02ad3 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -4,7 +4,11 @@ mixin post(board, post, truncate) input.post-check(type='checkbox', name='checked[]' value=post.postId) if post.subject span.post-subject #{post.subject} - span.post-name #{post.name} + if post.email + a(href=`mailto:${post.email}`) + span.post-name #{post.name} + else + span.post-name #{post.name} span #{post.date.toLocaleString()} span.user-id(style=`background: #${post.userId}`) #{post.userId} span: a(href=`/${board._id}/thread/${post.thread ? post.thread : post.postId}#${post.postId}`) ##{post.postId} @@ -39,4 +43,6 @@ mixin post(board, post, truncate) blockquote.post-message !{post.message} if post.reports each report in post.reports - span.reports.post-container #{report} + .reports.post-container + span Date: #{report.date.toLocaleString()} + span Reason: #{report.reason} diff --git a/views/pages/board.pug b/views/pages/board.pug index b2459939..4b664005 100644 --- a/views/pages/board.pug +++ b/views/pages/board.pug @@ -5,9 +5,10 @@ block head title /#{board._id}/ - Recent Posts block content - h1.board-title /#{board._id}/ - #{board.name} - h4.board-description #{board.description} + include ../includes/boardheader.pug + hr(size=1) include ../includes/postform.pug + .post-mode Posting mode: Thread hr(size=1) form(action='/forms/board/'+board._id+'/posts' method='POST' enctype='application/x-www-form-urlencoded') input(type='hidden' name='_csrf' value=csrf) diff --git a/views/pages/manage.pug b/views/pages/manage.pug index 1a9c028d..190c8d70 100644 --- a/views/pages/manage.pug +++ b/views/pages/manage.pug @@ -5,9 +5,9 @@ block head title Login block content - h1.board-title /#{board._id}/ - #{board.name} - h4.board-description Management Panel + include ../includes/boardheader.pug hr(size=1) + h4.board-description Management Panel form(action='/forms/board/'+board._id+'/posts' method='POST' enctype='application/x-www-form-urlencoded') input(type='hidden' name='_csrf' value=csrf) if posts.length === 0 @@ -17,6 +17,15 @@ block content section.thread +post(board, post) hr(size=1) - section.delete-wrapper - input(type='submit', value='delete') - + section.action-wrapper + span + label + input.post-check(type='checkbox', name='delete' value=1) + | Delete + span + label + input.post-check(type='checkbox', name='dismiss' value=1) + | Dismiss + input(type='submit', value='submit') + + diff --git a/views/pages/thread.pug b/views/pages/thread.pug index a1d62c60..56b2aebd 100644 --- a/views/pages/thread.pug +++ b/views/pages/thread.pug @@ -10,9 +10,10 @@ block head meta(property='og:image', content=thread.files.length > 0 ? '/img/'+thread.files[0].filename : '') block content - a(href='/'+board._id) Back to /#{board._id}/ + include ../includes/boardheader.pug hr(size=1) include ../includes/postform.pug + .post-mode Posting mode: Reply [#[a.no-decoration(href=`/${board._id}`) Go Back]] hr(size=1) form(action='/forms/board/'+board._id+'/posts' method='POST' enctype='application/x-www-form-urlencoded') input(type='hidden' name='_csrf' value=csrf) @@ -22,7 +23,3 @@ block content +post(board, post) hr(size=1) include ../includes/deletefooter.pug - - - -