diff --git a/controllers/forms.js b/controllers/forms.js index 677d87bf..5ebbff9f 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -2,95 +2,102 @@ const express = require('express') , router = express.Router() - , Boards = require(__dirname+'/../db-models/boards.js') - , Posts = require(__dirname+'/../db-models/posts.js') - , Trips = require(__dirname+'/../db-models/trips.js') + , Boards = require(__dirname+'/../db/boards.js') + , Posts = require(__dirname+'/../db/posts.js') + , Trips = require(__dirname+'/../db/trips.js') + , Bans = require(__dirname+'/../db/bans.js') + , banPoster = require(__dirname+'/../models/forms/ban-poster.js') + , removeBans = require(__dirname+'/../models/forms/removebans.js') , makePost = require(__dirname+'/../models/forms/make-post.js') , 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') - , numberConverter = require(__dirname+'/../helpers/number-converter.js'); + , registerAccount = require(__dirname+'/../models/forms/register.js') + , hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js') + , numberConverter = require(__dirname+'/../helpers/number-converter.js') + , banCheck = require(__dirname+'/../helpers/bancheck.js'); // login to account router.post('/login', (req, res, next) => { - const errors = []; - - //check exist - if (!req.body.username || req.body.username.length <= 0) { - errors.push('Missing username'); - } - if (!req.body.password || req.body.password.length <= 0) { - errors.push('Missing password'); - } - - //check too long - if (req.body.username && req.body.username.length > 50) { - errors.push('Username must be 50 characters or less'); - } - if (req.body.password && req.body.password.length > 100) { - errors.push('Password must be 100 characters or less'); - } - - if (errors.length > 0) { - return res.status(400).render('message', { - 'title': 'Bad request', - 'errors': errors, - 'redirect': '/login' - }) - } - - loginAccount(req, res); + const errors = []; + + //check exist + if (!req.body.username || req.body.username.length <= 0) { + errors.push('Missing username'); + } + if (!req.body.password || req.body.password.length <= 0) { + errors.push('Missing password'); + } + + //check too long + if (req.body.username && req.body.username.length > 50) { + errors.push('Username must be 50 characters or less'); + } + if (req.body.password && req.body.password.length > 100) { + errors.push('Password must be 100 characters or less'); + } + + if (errors.length > 0) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': '/login' + }) + } + + loginAccount(req, res, next); }); //register account router.post('/register', (req, res, next) => { - const errors = []; - - //check exist - if (!req.body.username || req.body.username.length <= 0) { - errors.push('Missing username'); - } - if (!req.body.password || req.body.password.length <= 0) { - errors.push('Missing password'); - } - if (!req.body.passwordconfirm || req.body.passwordconfirm.length <= 0) { - errors.push('Missing password confirmation'); - } - - //check too long - if (req.body.username && req.body.username.length > 50) { - errors.push('Username must be 50 characters or less'); - } - if (req.body.password && req.body.password.length > 100) { - errors.push('Password must be 100 characters or less'); - } - if (req.body.passwordconfirm && req.body.passwordconfirm.length > 100) { - errors.push('Password confirmation must be 100 characters or less'); - } - if (req.body.password != req.body.passwordconfirm) { - errors.push('Password and password confirmation must match'); - } - - if (errors.length > 0) { - return res.status(400).render('message', { - 'title': 'Bad request', - 'errors': errors, - 'redirect': '/register' - }) - } - - registerAccount(req, res); + const errors = []; + + //check exist + if (!req.body.username || req.body.username.length <= 0) { + errors.push('Missing username'); + } + if (!req.body.password || req.body.password.length <= 0) { + errors.push('Missing password'); + } + if (!req.body.passwordconfirm || req.body.passwordconfirm.length <= 0) { + errors.push('Missing password confirmation'); + } + + //check too long + if (req.body.username && req.body.username.length > 50) { + errors.push('Username must be 50 characters or less'); + } + if (req.body.password && req.body.password.length > 100) { + errors.push('Password must be 100 characters or less'); + } + if (req.body.passwordconfirm && req.body.passwordconfirm.length > 100) { + errors.push('Password confirmation must be 100 characters or less'); + } + if (req.body.password != req.body.passwordconfirm) { + errors.push('Password and password confirmation must match'); + } + + if (errors.length > 0) { + return res.status(400).render('message', { + 'title': 'Bad request', + 'errors': errors, + 'redirect': '/register' + }) + } + + registerAccount(req, res, next); }); // make new post -router.post('/board/:board', Boards.exists, numberConverter, (req, res, next) => { +router.post('/board/:board/post', Boards.exists, banCheck, numberConverter, async (req, res, next) => { let numFiles = 0; if (req.files && req.files.file) { @@ -134,28 +141,38 @@ router.post('/board/:board', Boards.exists, numberConverter, (req, res, next) => }) } - makePost(req, res, numFiles); + makePost(req, res, next, numFiles); }); -//report, delete, sticky, etc -router.post('/board/:board/posts', Boards.exists, numberConverter, (req, res, next) => { +//report/delete/spoiler/ban +router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, async (req, res, next) => { const errors = []; - if (!req.body.checked || req.body.checked.length === 0 || req.body.checked.length > 10) { + if (!req.body.checkedposts || req.body.checkedposts.length === 0 || req.body.checkedposts.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.reason && req.body.reason.length > 50) { + if (req.body.report_reason && req.body.report_reason.length > 50) { errors.push('Report must be 50 characters or less'); } - if (!(req.body.report || req.body.delete || req.body.dismiss || req.body.spoiler)) { - errors.push('Must select an action') + if (req.body.ban_reason && req.body.ban_reason.length > 50) { + 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.global_dismiss + || req.body.ban + || req.body.global_ban)) { + errors.push('Invalid actions selected') } - if (req.body.report && (!req.body.reason || req.body.reason.length === 0)) { + if (req.body.report && (!req.body.report_reason || req.body.report_reason.length === 0)) { errors.push('Reports must have a reason') } @@ -167,18 +184,214 @@ router.post('/board/:board/posts', Boards.exists, numberConverter, (req, res, ne }) } - if (req.body.report) { - reportPosts(req, res); - } else if (req.body.delete) { - deletePosts(req, res); - } else if (req.body.spoiler) { - spoilerPosts(req, res); - } else if (req.body.dismiss) { - dismissReports(req, res); + const posts = await Posts.getPosts(req.params.board, req.body.checkedposts, 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 { + + // 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) { + // 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}` + }); + +}); + +//unban +router.post('/board/:board/unban', Boards.exists, banCheck, hasPerms, numberConverter, async (req, res, next) => { + + //keep this for later in case i add other options to unbans + 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': `/${req.params.board}/manage` + }); + } + + 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': `/${req.params.board}/manage` + }); + +}); + +router.post('/global/actions', hasPerms, numberConverter, async(req, res, next) => { + + 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` + }); + +}); module.exports = router; + diff --git a/controllers/pages.js b/controllers/pages.js index c3e3bae0..b863aa89 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -2,14 +2,16 @@ const express = require('express') , router = express.Router() - , Boards = require(__dirname+'/../db-models/boards.js') - , checkAuth = require(__dirname+'/../helpers/check-auth.js') + , Boards = require(__dirname+'/../db/boards.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,30 +26,26 @@ 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); +router.get('/:board', Boards.exists, numberConverter, board); // thread view page router.get('/:board/thread/:id(\\d+)', Boards.exists, numberConverter, thread); diff --git a/db-models/accounts.js b/db/accounts.js similarity index 95% rename from db-models/accounts.js rename to db/accounts.js index 0d142d8b..9138c218 100644 --- a/db-models/accounts.js +++ b/db/accounts.js @@ -1,7 +1,7 @@ 'use strict'; -const Mongo = require(__dirname+'/../helpers/db.js') +const Mongo = require(__dirname+'/db.js') , db = Mongo.client.db('jschan').collection('accounts') , bcrypt = require('bcrypt'); diff --git a/db/bans.js b/db/bans.js new file mode 100644 index 00000000..22fd57b1 --- /dev/null +++ b/db/bans.js @@ -0,0 +1,66 @@ + +'use strict'; + +const Mongo = require(__dirname+'/db.js') + , db = Mongo.client.db('jschan').collection('bans'); + +module.exports = { + + db, + + find: (ip, board) => { + return db.find({ + 'ip': ip, + 'board': { + '$in': [board, null] + } + }).toArray(); + }, + + findMany: (board, ids) => { + return db.find({ + '_id': { + '$in': ids + }, + 'board': board + }).toArray(); + }, + + getAllBans: () => { + return db.find({}).toArray(); + }, + + getGlobalBans: () => { + return db.find({ + 'board': null + }).toArray(); + }, + + getBoardBans: (board) => { + return db.find({ + 'board': board, + }).toArray(); + }, + + removeMany: (board, ids) => { + return db.deleteMany({ + 'board': board, + '_id': { + '$in': ids + } + }) + }, + + insertOne: (ban) => { + return db.insertOne(ban); + }, + + insertMany: (bans) => { + return db.insertMany(bans); + }, + + deleteAll: () => { + return db.deleteMany({}); + }, + +} diff --git a/db-models/boards.js b/db/boards.js similarity index 76% rename from db-models/boards.js rename to db/boards.js index eb5a0f37..cbe09a30 100644 --- a/db-models/boards.js +++ b/db/boards.js @@ -1,7 +1,8 @@ 'use strict'; -const Mongo = require(__dirname+'/../helpers/db.js') - , db = Mongo.client.db('jschan'); +const Mongo = require(__dirname+'/db.js') + , db = Mongo.client.db('jschan') + , boardCache = new Map(); module.exports = { @@ -11,7 +12,7 @@ module.exports = { return db.collection('boards').findOne({ '_id': name }); }, - find: (name) => { + find: () => { return db.collection('boards').find({}).toArray(); }, @@ -31,11 +32,20 @@ module.exports = { return db.collection('boards').deleteMany({}); }, + cache: async () => { + const boards = await module.exports.find(); + for (let i = 0; i < boards.length; i++) { + const board = boards[i]; + boardCache.set(board._id, board); + } + }, + exists: async (req, res, next) => { - const board = await module.exports.findOne(req.params.board) + //const board = await module.exports.findOne(req.params.board); + const board = boardCache.get(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(); diff --git a/helpers/db.js b/db/db.js similarity index 100% rename from helpers/db.js rename to db/db.js diff --git a/db-models/posts.js b/db/posts.js similarity index 81% rename from db-models/posts.js rename to db/posts.js index 0688a6ce..a8137378 100644 --- a/db-models/posts.js +++ b/db/posts.js @@ -1,6 +1,6 @@ 'use strict'; -const Mongo = require(__dirname+'/../helpers/db.js') +const Mongo = require(__dirname+'/db.js') , Boards = require(__dirname+'/boards.js') , db = Mongo.client.db('jschan').collection('posts'); @@ -40,7 +40,7 @@ module.exports = { } }).sort({ '_id': -1 - }).limit(3).toArray(); + }).limit(5).toArray(); thread.replies = replies.reverse(); })); @@ -61,7 +61,8 @@ module.exports = { const data = await Promise.all([ db.findOne({ 'postId': id, - 'board': board + 'board': board, + 'thread': null, }, { 'projection': { 'salt': 0, @@ -171,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 @@ -212,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': { @@ -225,50 +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(); }, + dismissGlobalReports: (ids) => { + return db.updateMany({ + '_id': { + '$in': ids + }, + }, { + '$set': { + 'globalreports': [] + } + }); + }, + 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({ diff --git a/db-models/trips.js b/db/trips.js similarity index 85% rename from db-models/trips.js rename to db/trips.js index 22726dbe..c284388e 100644 --- a/db-models/trips.js +++ b/db/trips.js @@ -1,6 +1,6 @@ 'use strict'; -const Mongo = require(__dirname+'/../helpers/db.js') +const Mongo = require(__dirname+'/db.js') , db = Mongo.client.db('jschan').collection('tripcodes'); module.exports = { diff --git a/helpers/bancheck.js b/helpers/bancheck.js new file mode 100644 index 00000000..684f4cbf --- /dev/null +++ b/helpers/bancheck.js @@ -0,0 +1,20 @@ +'use strict'; + +const Bans = require(__dirname+'/../db/bans.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 ? res.locals.board._id : null); + if (bans && bans.length > 0) { + //TODO: show posts banned for, expiry, etc + return res.status(403).render('ban', { + bans: bans + }); + } + } + next(); + +} diff --git a/helpers/check-auth.js b/helpers/check-auth.js deleted file mode 100644 index 3809e84d..00000000 --- a/helpers/check-auth.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = (req, res, next) => { - if (req.session.authenticated === true) return next() - res.redirect('/login') -} diff --git a/helpers/files/video-thumbnail.js b/helpers/files/video-thumbnail.js index 1633b945..91ad1572 100644 --- a/helpers/files/video-thumbnail.js +++ b/helpers/files/video-thumbnail.js @@ -14,7 +14,7 @@ module.exports = (filename) => { count: 1, filename: `thumb-${filename.split('.')[0]}.png`, folder: uploadDirectory, - size: '128x128' + size: '128x?' }); }); diff --git a/helpers/has-perms.js b/helpers/has-perms.js deleted file mode 100644 index 3c64c804..00000000 --- a/helpers/has-perms.js +++ /dev/null @@ -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 -} diff --git a/helpers/hasperms.js b/helpers/hasperms.js new file mode 100644 index 00000000..7131aaac --- /dev/null +++ b/helpers/hasperms.js @@ -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 + ) + ) + ) +} diff --git a/helpers/haspermsmiddleware.js b/helpers/haspermsmiddleware.js new file mode 100644 index 00000000..e928c663 --- /dev/null +++ b/helpers/haspermsmiddleware.js @@ -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(); + +} diff --git a/helpers/isloggedin.js b/helpers/isloggedin.js new file mode 100644 index 00000000..69d540c1 --- /dev/null +++ b/helpers/isloggedin.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = (req, res, next) => { + if (req.session.authenticated === true) { + return next(); + } + res.redirect('/login'); +} diff --git a/helpers/markdown.js b/helpers/markdown.js index a0e100ce..e056568b 100644 --- a/helpers/markdown.js +++ b/helpers/markdown.js @@ -1,26 +1,24 @@ 'use strict'; -const Posts = require(__dirname+'/../db-models/posts.js') - , quoteRegex = /^>>\d+$/gm - , greentextRegex = /^>[^>].+$/gm - , redtextRegex = /^<[^<].+$/gm - , boldRegex = /==.+==/gm - , italicRegex = /__.+__/gm +const Posts = require(__dirname+'/../db/posts.js') + , greentextRegex = /^>([^>].+)/gm + , redtextRegex = /^<([^<].+)/gm + , boldRegex = /==(.+)==/gm + , italicRegex = /__(.+)__/gm , linkRegex = /https?\:\/\/[^\s]+/g - , spoilerRegex = /\|.+\|/gm; + , spoilerRegex = /\|\|(.+)\|\|/gm + , codeRegex = /^```\s([\s\S]+)\s```/gm; module.exports = (board, thread, text) => { //redtext - text = text.replace(redtextRegex, (match) => { - const red = match.substring(1); - return `<${red}`; + text = text.replace(redtextRegex, (match, redtext) => { + return `<${redtext}`; }); //greentext - text = text.replace(greentextRegex, (match) => { - const green = match.substring(1); - return `>${green}`; + text = text.replace(greentextRegex, (match, greentext) => { + return `>${greentext}`; }); //links @@ -28,30 +26,25 @@ module.exports = (board, thread, text) => { return `${match}`; }); - //quotes - text = text.replace(quoteRegex, (match) => { - const quotenum = match.substring(2); - return `>>${quotenum}`; - }); - //bold - text = text.replace(boldRegex, (match) => { - const bold = match.substring(2, match.length-2); + text = text.replace(boldRegex, (match, bold) => { return `${bold}`; }); //italic - text = text.replace(italicRegex, (match) => { - const italic = match.substring(2, match.length-2); - return `${italic}`; + text = text.replace(italicRegex, (match, italic) => { + return `${italic}`; }); //spoilers - text = text.replace(spoilerRegex, (match) => { - const spoiler = match.substring(1, match.length-1); + text = text.replace(spoilerRegex, (match, spoiler) => { return `${spoiler}`; }); + text = text.replace(codeRegex, (match, code) => { + return `${code.trim()}`; + }); + return text; } diff --git a/helpers/number-converter.js b/helpers/number-converter.js index 149ce2f4..795cf377 100644 --- a/helpers/number-converter.js +++ b/helpers/number-converter.js @@ -1,14 +1,19 @@ 'use strict'; +const Mongo = require(__dirname+'/../db/db.js'); + module.exports = (req, res, next) => { //for body if (req.body.thread) { req.body.thread = +req.body.thread; } - if (req.body.checked) { - //syntax casts all string to number - req.body.checked = req.body.checked.map(Number); + if (req.body.checkedposts) { + //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 @@ -19,6 +24,16 @@ module.exports = (req, res, next) => { req.params.page = +req.params.page; } + //and query + if (req.query.p) { + const pnum = +req.query.p; + if (Number.isSafeInteger(pnum)) { + req.query.p = +req.query.p; + } else { + req.query.p = null; + } + } + next(); } diff --git a/helpers/quotes.js b/helpers/quotes.js new file mode 100644 index 00000000..428b6cf9 --- /dev/null +++ b/helpers/quotes.js @@ -0,0 +1,45 @@ +'use strict'; + +const Posts = require(__dirname+'/../db/posts.js') + , quoteRegex = />>\d+/gm; + +module.exports = async (board, text) => { + + //get the matches + const matches = text.match(quoteRegex); + if (!matches) { + return text; + } + + //get all the Ids + const quoteIds = matches.map(x => +x.substring(2)); + + //get all posts with those Ids + const posts = await Posts.getPosts(board, quoteIds, false); + + //turn the result into a map of postId => threadId/postId + const postThreadObject = {}; + let validQuotes = 0; + for (let i = 0; i < posts.length; i++) { + const post = posts[i]; + postThreadObject[post.postId] = post.thread || post.postId; + validQuotes++; + } + + //if none of the quotes were real, dont do a replace + if (validQuotes === 0) { + return text; + } + + //then replace the quotes with only ones that exist + text = text.replace(quoteRegex, (match) => { + const quotenum = +match.substring(2); + if (postThreadObject[quotenum]) { + return `>>${quotenum}`; + } + return match; + }); + + return text; + +} diff --git a/helpers/tripcode.js b/helpers/tripcode.js index f7736f5c..e1ac247b 100644 --- a/helpers/tripcode.js +++ b/helpers/tripcode.js @@ -1,6 +1,6 @@ 'use strict'; -const Tripcodes = require(__dirname+'/../db-models/trips.js') +const Tripcodes = require(__dirname+'/../db/trips.js') , crypto = require('crypto'); module.exports = async (password) => { diff --git a/models/api/get-boards.js b/models/api/get-boards.js deleted file mode 100644 index d8bc0a5d..00000000 --- a/models/api/get-boards.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const Boards = require(__dirname+'/../../db-models/boards.js'); - -module.exports = async (req, res) => { - //get a list of boards - let boards; - try { - boards = await Boards.find(); - } catch (err) { - console.error(err); - return res.status(500).json({ 'message': 'Error fetching from DB' }) - } - - //render the page - res.json(boards) -} diff --git a/models/api/get-catalog.js b/models/api/get-catalog.js deleted file mode 100644 index 9a4e78e4..00000000 --- a/models/api/get-catalog.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const Posts = require(__dirname+'/../../db-models/posts.js'); - -module.exports = async (req, res) => { - //get the recently bumped thread & preview posts - let data; - try { - data = await Posts.getCatalog(req.params.board); - } catch (err) { - console.error(err); - return res.status(500).json({ 'message': 'Error fetching from DB' }); - } - - if (!data) { - return res.status(404).json({ 'message': 'Not found' }); - } - - return res.json(data) -} diff --git a/models/api/get-recent.js b/models/api/get-recent.js deleted file mode 100644 index 2f6cf32b..00000000 --- a/models/api/get-recent.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const Posts = require(__dirname+'/../../db-models/posts.js'); - -module.exports = async (req, res) => { - //get the recently bumped thread & preview posts - let threads; - try { - threads = await Posts.getRecent(req.params.board, req.params.page || 1); - } catch (err) { - console.error(err); - return res.status(500).json({ 'message': 'Error fetching from DB' }); - } - - if (!threads || threads.lenth === 0) { - return res.status(404).json({ 'message': 'Not found' }); - } - - return res.json(threads); -} diff --git a/models/api/get-thread.js b/models/api/get-thread.js deleted file mode 100644 index 58fe7e95..00000000 --- a/models/api/get-thread.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const Posts = require(__dirname+'/../../db-models/posts.js'); - -module.exports = async (req, res) => { - //get the recently bumped thread & preview posts - let thread; - try { - thread = await Posts.getThread(req.params.board, req.params.id); - } catch (err) { - console.error(err); - return res.status(500).json({ 'message': 'Error fetching from DB' }); - } - - if (!thread) { - return res.status(404).json({ 'message': 'Not found' }); - } - - return res.json(thread) -} diff --git a/models/forms/ban-poster.js b/models/forms/ban-poster.js new file mode 100644 index 00000000..25f4218a --- /dev/null +++ b/models/forms/ban-poster.js @@ -0,0 +1,40 @@ +'use strict'; + +const uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') + , hasPerms = require(__dirname+'/../../helpers/hasperms.js') + , Bans = require(__dirname+'/../../db/bans.js') + , Posts = require(__dirname+'/../../db/posts.js'); + +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)) { + throw { + 'status': 403, + 'message': { + 'title': 'Forbidden', + 'message': 'You do not have permission to issue bans', + 'redirect': `/${req.params.board}` + } + }; + } + + const bans = posts.map(post => { + return { + 'ip': post.ip, + 'reason': req.body.ban_reason || 'No reason specified', + 'board': board, + 'post': req.body.preserve_post ? post : null, + 'issuer': req.session.user.username, + 'date': new Date(), + 'expireAt': new Date((new Date).getTime() + (72*1000*60*60)) // 72h ban + } + }); + + const bannedIps = await Bans.insertMany(bans).then(result => result.insertedCount); + + return `Banned ${bannedIps} ips`; + +} diff --git a/models/forms/delete-post.js b/models/forms/delete-post.js index 20d63536..1087d19e 100644 --- a/models/forms/delete-post.js +++ b/models/forms/delete-post.js @@ -5,27 +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') - , Posts = require(__dirname+'/../../db-models/posts.js'); + , hasPerms = require(__dirname+'/../../helpers/hasperms.js') + , Mongo = require(__dirname+'/../../db/db.js') + , Posts = require(__dirname+'/../../db/posts.js'); -module.exports = async (req, res) => { +module.exports = async (req, res, next, checkedPosts) => { - //get all posts that were checked - let posts; - try { - posts = await Posts.getPosts(req.params.board, req.body.checked, true); //admin arument true, fetches passwords and salts - } catch (err) { - console.error(err); - return res.status(500).render('error'); - } - - if (!posts || posts.length === 0) { - return res.status(400).render('message', { - 'title': 'Bad requests', - '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)) { @@ -37,20 +23,29 @@ module.exports = async (req, res) => { && post.password == req.body.password }); if (posts.length === 0) { - return res.status(403).render('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}` + } + }; } } - 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; })) @@ -59,14 +54,8 @@ module.exports = async (req, res) => { const allPosts = posts.concat(threadPosts) //delete posts from DB - let deletedPosts = 0; - try { - const result = await Posts.deleteMany(req.params.board, allPosts.map(x => x.postId)); - deletedPosts = result.deletedCount; - } catch (err) { - console.error(err); - return res.status(500).render('error'); - } + 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 = []; @@ -79,15 +68,11 @@ module.exports = async (req, res) => { //dont question it. return Promise.all([ unlink(uploadDirectory + filename), - unlink(uploadDirectory + 'thumb-' + filename) + unlink(`${uploadDirectory}thumb-${filename.split('.')[0]}.png`) ]) })); //hooray! - return res.render('message', { - 'title': 'Success', - 'message': `Deleted ${threadIds.length} threads and ${deletedPosts} posts`, - 'redirect': `/${req.params.board}` - }); + return `Deleted ${boardThreads.length} threads and ${deletedPosts-boardThreads.length} posts` } diff --git a/models/forms/dismiss-report.js b/models/forms/dismiss-report.js index c22a663d..4fac5fb0 100644 --- a/models/forms/dismiss-report.js +++ b/models/forms/dismiss-report.js @@ -1,31 +1,23 @@ 'use strict'; -const Posts = require(__dirname+'/../../db-models/posts.js') - , hasPerms = require(__dirname+'/../../helpers/has-perms.js'); +const Posts = require(__dirname+'/../../db/posts.js') + , hasPerms = require(__dirname+'/../../helpers/hasperms.js'); -module.exports = async (req, res) => { +module.exports = async (req, res, next) => { if (!hasPerms(req, res)) { - return res.status(403).render('message', { - 'title': 'Forbidden', - 'message': `You are not authorised to dismiss reports.`, - 'redirect': `/${req.params.board}` - }); + throw { + 'status': 403, + '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'); - } + const dismissedReports = await Posts.dismissReports(req.params.board, req.body.checkedposts).then(result => result.modifiedCount); - //hooray! - return res.render('message', { - 'title': 'Success', - 'message': `Dismissed report(s) successfully`, - 'redirect': `/${req.params.board}/manage` - }); + return `Dismissed ${dismissedReports} reports successfully`; } diff --git a/models/forms/dismissglobalreport.js b/models/forms/dismissglobalreport.js new file mode 100644 index 00000000..3fbb1b2a --- /dev/null +++ b/models/forms/dismissglobalreport.js @@ -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`; + +} diff --git a/models/forms/edit-post.js b/models/forms/edit-post.js index 6064c249..37efbecd 100644 --- a/models/forms/edit-post.js +++ b/models/forms/edit-post.js @@ -1,34 +1,9 @@ 'use strict'; -const uuidv4 = require('uuid/v4') - , path = require('path') - , Posts = require(__dirname+'/../../db-models/posts.js') +const Posts = require(__dirname+'/../../db/posts.js'); -module.exports = async (req, res, numFiles) => { +module.exports = async (req, res, next) => { - // get the post that we are trying to edit - let post; - try { - post = await Posts.getPost(req.params.board, req.body.id, true); - } catch (err) { - console.error(err); - return res.status(500).render('error'); - } - if (!thread || thread.thread != null) { - return res.status(400).render('message', { - 'title': 'Bad request', - 'message': 'Post does not exist.', - 'redirect': redirect - }); - } + throw new Error('Not implemented'); - // sticky, lock, sage, spoiler, etc - for (let i = 0; i < req.body.actions.length; i++) { - //TODO - } - - const post = await Posts.updateOne(req.params.board, data) - const successRedirect = `/${req.params.board}/thread/${req.body.thread || post.insertedId}`; - - return res.redirect(successRedirect); } diff --git a/models/forms/globalreportpost.js b/models/forms/globalreportpost.js new file mode 100644 index 00000000..278f312c --- /dev/null +++ b/models/forms/globalreportpost.js @@ -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` + +} diff --git a/models/forms/login.js b/models/forms/login.js index 07720bef..fa8e8b1a 100644 --- a/models/forms/login.js +++ b/models/forms/login.js @@ -1,20 +1,20 @@ 'use strict'; const bcrypt = require('bcrypt') - , Accounts = require(__dirname+'/../../db-models/accounts.js'); + , Accounts = require(__dirname+'/../../db/accounts.js'); -module.exports = async (req, res) => { +module.exports = async (req, res, next) => { const username = req.body.username.toLowerCase(); const password = req.body.password; + const redirect = req.body.redirect; //fetch an account let account; 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 @@ -22,7 +22,7 @@ module.exports = async (req, res) => { return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'Incorrect username or password', - 'redirect': '/login' + 'redirect': redirect ? `/login?redirect=${redirect}` : '/login' }); } @@ -31,8 +31,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 @@ -46,18 +45,14 @@ module.exports = async (req, res) => { req.session.authenticated = true; //successful login - return res.render('message', { - 'title': 'Success', - 'message': `Welcome, ${username}`, - 'redirect': '/' - }); + return res.redirect(redirect || '/'); } return res.status(403).render('message', { 'title': 'Forbidden', 'message': 'Incorrect username or password', - 'redirect': '/login' + 'redirect': redirect ? `/login?redirect=${redirect}` : '/login' }); } diff --git a/models/forms/make-post.js b/models/forms/make-post.js index 65c0e436..30e22da9 100644 --- a/models/forms/make-post.js +++ b/models/forms/make-post.js @@ -6,12 +6,13 @@ const uuidv4 = require('uuid/v4') , crypto = require('crypto') , randomBytes = util.promisify(crypto.randomBytes) , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') - , Posts = require(__dirname+'/../../db-models/posts.js') + , Posts = require(__dirname+'/../../db/posts.js') , getTripCode = require(__dirname+'/../../helpers/tripcode.js') + , linkQuotes = require(__dirname+'/../../helpers/quotes.js') , simpleMarkdown = require(__dirname+'/../../helpers/markdown.js') , sanitize = require('sanitize-html') , sanitizeOptions = { - allowedTags: [ 'span', 'a', 'code', 'em', 'strong' ], + allowedTags: [ 'span', 'a', 'em', 'strong' ], allowedAttributes: { 'a': [ 'href', 'class' ], 'span': [ 'class' ] @@ -25,7 +26,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 +36,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 +100,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 @@ -115,13 +115,11 @@ module.exports = async (req, res, numFiles) => { if (Array.isArray(processedFile.geometryString)) { processedFile.geometryString = processedFile.geometryString[0]; } - files.push(processedFile); } catch (err) { - console.error(err); //TODO: DELETE FAILED FILES - return res.status(500).render('error'); + return next(err); } } } @@ -149,7 +147,9 @@ module.exports = async (req, res, numFiles) => { //simple markdown and sanitize let message = req.body.message; if (message && message.length > 0) { - message = sanitize(simpleMarkdown(req.params.board, req.body.thread, message), sanitizeOptions); + message = simpleMarkdown(req.params.board, req.body.thread, message); + message = await linkQuotes(req.params.board, message); + message = sanitize(message, sanitizeOptions); } //add post to DB @@ -167,6 +167,7 @@ module.exports = async (req, res, numFiles) => { 'files': files, 'salt': !req.body.thread ? salt : '', 'reports': [], + 'globalreports': [], 'spoiler': req.body.spoiler ? true : false, }; @@ -174,11 +175,10 @@ 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}`; + const successRedirect = `/${req.params.board}/thread/${req.body.thread || postId}#${postId}`; return res.redirect(successRedirect); } diff --git a/models/forms/register.js b/models/forms/register.js index 023357e3..51f96bf5 100644 --- a/models/forms/register.js +++ b/models/forms/register.js @@ -1,9 +1,9 @@ 'use strict'; const bcrypt = require('bcrypt') - , Accounts = require(__dirname+'/../../db-models/accounts.js'); + , Accounts = require(__dirname+'/../../db/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,14 +28,9 @@ 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', { - 'title': 'Success', - 'message': `Welcome, ${username}`, - 'redirect': '/' - }); + return res.redirect('/login') } diff --git a/models/forms/removebans.js b/models/forms/removebans.js new file mode 100644 index 00000000..db0af77a --- /dev/null +++ b/models/forms/removebans.js @@ -0,0 +1,13 @@ +'use strict'; + +const Bans = require(__dirname+'/../../db/bans.js') + , { ObjectId } = require('mongodb'); + +module.exports = async (req, res, next) => { + + const banIds = req.body.checkedbans.map(ObjectId); + const removedBans = await Bans.removeMany(req.params.board, banIds).then(result => result.deletedCount); + + return `Removed ${removedBans} bans`; + +} diff --git a/models/forms/report-post.js b/models/forms/report-post.js index 4f454a93..49bb7efb 100644 --- a/models/forms/report-post.js +++ b/models/forms/report-post.js @@ -1,29 +1,20 @@ 'use strict'; -const Posts = require(__dirname+'/../../db-models/posts.js'); +const Posts = require(__dirname+'/../../db/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 = { - 'reason': req.body.reason, + 'reason': req.body.report_reason, 'date': new Date(), 'ip': ip } - try { - //push the report to all checked posts - await Posts.reportMany(req.params.board, req.body.checked, report); - } catch (err) { - console.error(err); - return res.status(500).render('error'); - } + //push the report to all checked posts + const reportedCount = await Posts.reportMany(req.params.board, req.body.checkedposts, report).then(result => result.modifiedCount); //hooray! - return res.render('message', { - 'title': 'Success', - 'message': `Reported post(s) successfully`, - 'redirect': `/${req.params.board}` - }); + return `Reported ${reportedCount} posts successfully` } diff --git a/models/forms/spoiler-post.js b/models/forms/spoiler-post.js index 15847f77..6dcabb93 100644 --- a/models/forms/spoiler-post.js +++ b/models/forms/spoiler-post.js @@ -5,27 +5,14 @@ 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') - , Posts = require(__dirname+'/../../db-models/posts.js'); + , hasPerms = require(__dirname+'/../../helpers/hasperms.js') + , Mongo = require(__dirname+'/../../db/db.js') + , Posts = require(__dirname+'/../../db/posts.js'); -module.exports = async (req, res) => { +module.exports = async (req, res, next, checkedPosts) => { //get all posts that were checked - let posts; - try { - posts = await Posts.getPosts(req.params.board, req.body.checked, true); //admin arument true, fetches passwords and salts - } catch (err) { - console.error(err); - return res.status(500).render('error'); - } - - if (!posts || posts.length === 0) { - return res.status(400).render('message', { - 'title': 'Bad requests', - '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)) { @@ -37,42 +24,38 @@ module.exports = async (req, res) => { && post.password == req.body.password }); if (posts.length === 0) { - return res.status(403).render('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': '/' + } + }; } } - //filter by not spoilered + //filter by not spoilered. maybe i add filters with optiins in the controller where it gets the posts? posts = posts.filter(post => { return !post.spoiler }); if (posts.length === 0) { - return res.status(409).render('message', { - 'title': 'Conflict', - 'message': 'Selected posts are already spoilered', - 'redirect': `/${req.params.board}` - }); + throw { + 'status': 409, + 'message': { + 'title': 'Conflict', + 'message': 'Posts already spoilered', + 'redirect': '/' + } + }; } // spoiler posts - let spoileredPosts = 0; - try { - const result = await Posts.spoilerMany(req.params.board, posts.map(x => x.postId)); - spoileredPosts = result.modifiedCount; - } catch (err) { - console.error(err); - return res.status(500).render('error'); - } + const postMongoIds = posts.map(post => Mongo.ObjectId(post._id)); + const spoileredPosts = await Posts.spoilerMany(postMongoIds).then(result => result.modifiedCount); //hooray! - return res.render('message', { - 'title': 'Success', - 'message': `Spoilered ${spoileredPosts} posts`, - 'redirect': `/${req.params.board}` - }); + return `Spoilered ${spoileredPosts} posts` } diff --git a/models/pages/board.js b/models/pages/board.js index a4f4a64b..649ebae0 100644 --- a/models/pages/board.js +++ b/models/pages/board.js @@ -1,23 +1,28 @@ 'use strict'; -const Posts = require(__dirname+'/../../db-models/posts.js'); +const Posts = require(__dirname+'/../../db/posts.js'); module.exports = async (req, res, next) => { //get the recently bumped thread & preview posts + const page = req.query.p || 1; let threads; let pages; try { - threads = await Posts.getRecent(req.params.board, req.params.page || 1); - 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(); + } + threads = await Posts.getRecent(req.params.board, page); } catch (err) { - console.error(err) - return next(); + return next(err); } + //render the page res.render('board', { csrf: req.csrfToken(), threads: threads || [], - pages: pages + pages, + page, }); } diff --git a/models/pages/catalog.js b/models/pages/catalog.js index aefd36ce..2dd42f0a 100644 --- a/models/pages/catalog.js +++ b/models/pages/catalog.js @@ -1,6 +1,6 @@ 'use strict'; -const Posts = require(__dirname+'/../../db-models/posts.js'); +const Posts = require(__dirname+'/../../db/posts.js'); module.exports = async (req, res, next) => { @@ -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 diff --git a/models/pages/globalmanage.js b/models/pages/globalmanage.js new file mode 100644 index 00000000..f0dfa034 --- /dev/null +++ b/models/pages/globalmanage.js @@ -0,0 +1,24 @@ +'use strict'; + +const Posts = require(__dirname+'/../../db/posts.js') + , Bans = require(__dirname+'/../../db/bans.js'); + +module.exports = async (req, res, next) => { + + let reports; + let bans; + try { + reports = await Posts.getGlobalReports(); + bans = await Bans.getGlobalBans(); + } catch (err) { + return next(err) + } + + //render the page + res.render('globalmanage', { + csrf: req.csrfToken(), + reports, + bans, + }); + +} diff --git a/models/pages/home.js b/models/pages/home.js index f8640e0f..cdddc7ba 100644 --- a/models/pages/home.js +++ b/models/pages/home.js @@ -1,6 +1,6 @@ 'use strict'; -const Boards = require(__dirname+'/../../db-models/boards.js'); +const Boards = require(__dirname+'/../../db/boards.js'); module.exports = async (req, res, next) => { @@ -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 diff --git a/models/pages/login.js b/models/pages/login.js index d4241372..373b330d 100644 --- a/models/pages/login.js +++ b/models/pages/login.js @@ -1,10 +1,11 @@ 'use strict'; -module.exports = (req, res) => { +module.exports = (req, res, next) => { //render the page res.render('login', { - csrf: req.csrfToken() + csrf: req.csrfToken(), + redirect: req.query.redirect, }); } diff --git a/models/pages/manage.js b/models/pages/manage.js index 4a1de412..962f5039 100644 --- a/models/pages/manage.js +++ b/models/pages/manage.js @@ -1,21 +1,24 @@ 'use strict'; -const Posts = require(__dirname+'/../../db-models/posts.js'); +const Posts = require(__dirname+'/../../db/posts.js') + , Bans = require(__dirname+'/../../db/bans.js'); -module.exports = async (req, res) => { +module.exports = async (req, res, next) => { - let posts; + let reports; + let bans; try { - posts = await Posts.getReports(req.params.board); + reports = await Posts.getReports(req.params.board); + bans = await Bans.getBoardBans(req.params.board); } catch (err) { - console.error(err); - return res.status(500).render('error'); + return next(err) } //render the page res.render('manage', { csrf: req.csrfToken(), - posts: posts + reports, + bans, }); } diff --git a/models/pages/register.js b/models/pages/register.js index ce522342..f8ce78fb 100644 --- a/models/pages/register.js +++ b/models/pages/register.js @@ -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', { diff --git a/models/pages/thread.js b/models/pages/thread.js index 67b2b75d..1a887e60 100644 --- a/models/pages/thread.js +++ b/models/pages/thread.js @@ -1,6 +1,6 @@ 'use strict'; -const Posts = require(__dirname+'/../../db-models/posts.js'); +const Posts = require(__dirname+'/../../db/posts.js'); module.exports = async (req, res, next) => { @@ -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) { diff --git a/server.js b/server.js index 5ee75cd9..a91533fc 100644 --- a/server.js +++ b/server.js @@ -13,13 +13,15 @@ const express = require('express') , bodyParser = require('body-parser') , cookieParser = require('cookie-parser') , configs = require(__dirname+'/configs/main.json') - , Mongo = require(__dirname+'/helpers/db.js') + , Mongo = require(__dirname+'/db/db.js') , upload = require('express-fileupload'); (async () => { // let db connect await Mongo.connect(); + const Boards = require(__dirname+'/db/boards.js'); + await Boards.cache(); // parse forms and allow file uploads app.use(bodyParser.urlencoded({extended: true})); @@ -53,7 +55,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')); @@ -74,7 +76,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 diff --git a/static/css/style.css b/static/css/style.css index 5a6d4c3d..04af046d 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -9,10 +9,28 @@ body { margin: 0; } +.code { + border-left: 10px solid #B7C5D9; + display: block; + padding-left: 5px; + font-family: monospace; + margin: 0.5em 0; +} + +.pages a { + text-decoration: none; +} + object { object-fit: scale-down; } +.board-header { + display: flex; + flex-direction: column; + align-items: center; +} + .catalog-tile-button { width: 100%; line-height: 30px; @@ -46,13 +64,14 @@ object { box-shadow: 0 0 3px black; min-width: 64px; min-height: 64px; + object-fit: cover; } .catalog { display:flex; - flex-direction: row; align-items:flex-start; - flex-wrap: wrap; + justify-content: space-evenly; + flex-flow: row wrap; } .spoiler { @@ -85,6 +104,10 @@ object { color: green; } +blockquote a { + color: #d00; +} + blockquote { word-break: break-all; white-space: pre-wrap; @@ -115,15 +138,62 @@ input, textarea { margin: 10px 0; } +.actions { + background: #D6DAF0; + border-color: #B7C5D9; + border-width: 0 1px 1px 0; + border-style: none solid solid none; + max-width: 100%; + display: flex; + flex-direction: column; + margin: 2px 0; + padding: 2px; +} + .action-wrapper { - align-items: center; - /*flex-direction: row;*/ + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.actions label { + 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 { @@ -135,9 +205,7 @@ input, textarea { .post-check { position: relative; top: 3px; - margin: 2px; - margin-right: 4px; - padding: 0; + margin: -3px 1px !important; } .post-files { @@ -177,7 +245,7 @@ input textarea { .board-title { color: #af0a0f; - font: bolder; + font: bolder 28px Tahoma; letter-spacing: -2px; text-align: center; margin: 0; @@ -193,8 +261,13 @@ input textarea { margin: 0; } -.post-container { - margin: 1px; +.post-message { + overflow-y: auto; +} + +.post-container, .ban { + box-sizing: border-box; + margin: 2px 0; padding: 2px; background: #D6DAF0; border-color: #B7C5D9; @@ -204,7 +277,7 @@ input textarea { } .post-container:target, .op:target { - background-color: #d6bad0; + background-color: #d6bad0!important; border-color: #ba9dbf; } @@ -213,7 +286,6 @@ input textarea { margin-left: 0; display: block; border: none; - width: 100%; } .post-subject { @@ -226,16 +298,22 @@ input textarea { font-weight: bold; } -.post-info { - margin-top: -2px; +.post-container.op .post-info, .catalog-tile-content .post-info { + background: none; } -.post-image { +.post-info { + margin: -2px; + /*margin-left: -3px;*/ + padding: 2px; + padding-left: 1px; + /*background-color: #B7C5D9;*/ } -.post-content { - +.post-info * { + margin-bottom: 0; + margin-top: -2px; } .navbar { @@ -244,6 +322,7 @@ input textarea { position: fixed; width: 100%; background: #eef2ff; + z-index: 1; } .nav-item { @@ -256,8 +335,12 @@ input textarea { border-right: 1px solid lightgray; } +.right { + float: right; +} + .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 { @@ -274,7 +357,6 @@ table, th, td { } .boards-table { - font-size: 16pt; margin: 0 auto; } @@ -306,8 +388,13 @@ hr { width: 100%; } + blockquote { + margin: 1em; + } + .post-check { top: 2px; + margin-left: 2px!important; height: 8px; } @@ -317,8 +404,14 @@ hr { .catalog-tile { overflow-y: hidden; - width: 49%; - justify-content: space-evenly; + } + + .boards-table { + width: 100%; + } + + .post-info { + background-color: #B7C5D9; } } diff --git a/views/includes/actionfooter.pug b/views/includes/actionfooter.pug new file mode 100644 index 00000000..ec26a9b0 --- /dev/null +++ b/views/includes/actionfooter.pug @@ -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') diff --git a/views/includes/actionfooter_globalmanage.pug b/views/includes/actionfooter_globalmanage.pug new file mode 100644 index 00000000..1d754996 --- /dev/null +++ b/views/includes/actionfooter_globalmanage.pug @@ -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') + diff --git a/views/includes/actionfooter_manage.pug b/views/includes/actionfooter_manage.pug new file mode 100644 index 00000000..7cbe4f2d --- /dev/null +++ b/views/includes/actionfooter_manage.pug @@ -0,0 +1,32 @@ +.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='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='dismiss' value=1) + | Dismiss Reports + 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') + diff --git a/views/includes/boardheader.pug b/views/includes/boardheader.pug index b95ca162..265ef00e 100644 --- a/views/includes/boardheader.pug +++ b/views/includes/boardheader.pug @@ -1,3 +1,4 @@ -a.no-decoration(href=`/${board._id}`) - h1.board-title /#{board._id}/ - #{board.name} -h4.board-description #{board.description} +section.board-header + 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 deleted file mode 100644 index 7f74c19f..00000000 --- a/views/includes/deletefooter.pug +++ /dev/null @@ -1,15 +0,0 @@ -section.action-wrapper - span - label - input.post-check(type='checkbox', name='delete' value=1) - | Delete - label - input.post-check(type='checkbox', name='spoiler' value=1) - | Spoiler - input#password(type='password', name='password', placeholder='post password' autocomplete='off') - span - label - input.post-check(type='checkbox', name='report' value=1) - | Report - input#report(type='text', name='reason', placeholder='reason' autocomplete='off') - input(type='submit', value='submit') diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index 9d77b7a0..ad2623ca 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -1,5 +1,9 @@ nav.navbar a.nav-item(href='/') Home - a.nav-item(href='/login') Login if board - a.nav-item(href=`/${board._id}/manage`) Manage Board + a.nav-item.right(href=`/${board._id}/manage`) Manage Board + a.nav-item.right(href=`/login?redirect=/${board._id}/`) Login + else + a.nav-item.right(href='/login') Login + a.nav-item.right(href='/register') Register + a.nav-item.right(href='/logout') Logout diff --git a/views/includes/pages.pug b/views/includes/pages.pug new file mode 100644 index 00000000..e9834882 --- /dev/null +++ b/views/includes/pages.pug @@ -0,0 +1,6 @@ +span.pages Page: + - for(let i = 1; i <= pages; i++) + if i === page + span: a(href=`/${board._id}?p=${i}`) [#{i}] + else + span: a(href=`/${board._id}?p=${i}`) #{i} diff --git a/views/includes/postform.pug b/views/includes/postform.pug index d304b6a1..7277af5e 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -1,25 +1,29 @@ section.form-wrapper - form.form-post(action='/forms/board/'+board._id, 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(type='hidden' name='_csrf' value=csrf) - input#title(type='text', name='subject', placeholder='subject' autocomplete='off' maxlength='50') + input(type='hidden' name='thread' value=thread != null ? thread.postId : null) - 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#title(type='text', name='subject', placeholder='subject' autocomplete='off' maxlength='50') - input#password(type='password', name='password', placeholder='post password' autocomplete='off' maxlength='50') + input#name(type='text', name='name', placeholder='name' autocomplete='off' maxlength='50') - textarea#message(name='message', rows='8', cols='50', placeholder='message' autocomplete='off' maxlength='2000') + input#name(type='text', name='email', placeholder='email' autocomplete='off' maxlength='50') - 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#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') + + + diff --git a/views/mixins/ban.pug b/views/mixins/ban.pug new file mode 100644 index 00000000..c6043836 --- /dev/null +++ b/views/mixins/ban.pug @@ -0,0 +1,18 @@ +include ./post.pug + +mixin ban(ban) + .ban + input.post-check(type='checkbox', name='checkedbans[]' value=ban._id) + 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, false) + diff --git a/views/mixins/catalogtile.pug b/views/mixins/catalogtile.pug index 462db87b..1fc4588f 100644 --- a/views/mixins/catalogtile.pug +++ b/views/mixins/catalogtile.pug @@ -4,7 +4,10 @@ mixin catalogtile(board, post, truncate) if post.files.length > 0 .post-file-src a(href=`/${board._id}/thread/${post.postId}#${post.postId}`) - object.catalog-thumb(data=`/img/thumb-${post.files[0].filename.split('.')[0]}.png` width='64' height='64') + if post.spoiler + object(data='/img/spoiler.png' width='64' height='64') + else + object.catalog-thumb(data=`/img/thumb-${post.files[0].filename.split('.')[0]}.png` width='64' height='64') header.post-info if post.subject span: a.no-decoration.post-subject(href=`/${board._id}/thread/${post.postId}#${post.postId}`) #{post.subject} diff --git a/views/mixins/post.pug b/views/mixins/post.pug index 9af10305..a4d31ba8 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -1,7 +1,11 @@ -mixin post(board, post, truncate) +mixin post(post, truncate, manage, globalmanage) article(id=post.postId class='post-container '+(post.thread ? '' : 'op')) header.post-info - input.post-check(type='checkbox', name='checked[]' value=post.postId) + span + 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 @@ -11,12 +15,12 @@ mixin post(board, post, truncate) 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} + span: a(href=`/${post.board}/thread/${post.thread || post.postId}#${post.postId}`) ##{post.postId} if post.files.length > 0 .post-files each file in post.files .post-file - .post-file-info + small.post-file-info span: a(href='/img/'+file.filename download=file.originalFilename) #{file.originalFilename} br span (#{file.sizeString} #{file.geometryString}) @@ -39,13 +43,18 @@ mixin post(board, post, truncate) } if truncated blockquote.post-message !{truncatedMessage} - p Message too long. #[a(href=`/${board._id}/thread/${post.thread == null ? post.postId : post.thread}#${post.postId}`) Click here] to view the full text. + p Message too long. #[a(href=`/${post.board}/thread/${post.thread || post.postId}#${post.postId}`) Click here] to view the full text. else blockquote.post-message !{post.message} else blockquote.post-message !{post.message} - if 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} diff --git a/views/pages/ban.pug b/views/pages/ban.pug new file mode 100644 index 00000000..1a733185 --- /dev/null +++ b/views/pages/ban.pug @@ -0,0 +1,14 @@ +extends ../layout.pug +include ../mixins/ban.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 + +ban(ban) + hr(size=1) diff --git a/views/pages/board.pug b/views/pages/board.pug index 75137981..daac336b 100644 --- a/views/pages/board.pug +++ b/views/pages/board.pug @@ -6,24 +6,22 @@ block head block content include ../includes/boardheader.pug - hr(size=1) include ../includes/postform.pug .mode Posting mode: Thread [#[a.no-decoration(href=`/${board._id}/catalog`) Catalog]] hr(size=1) - form(action='/forms/board/'+board._id+'/posts' method='POST' enctype='application/x-www-form-urlencoded') + include ../includes/pages.pug + hr(size=1) + form(action='/forms/board/'+board._id+'/actions' method='POST' enctype='application/x-www-form-urlencoded') input(type='hidden' name='_csrf' value=csrf) if threads.length === 0 p No posts. hr(size=1) for thread in threads section.thread - +post(board, thread, true) + +post(thread, true) for post in thread.replies - +post(board, post, true) - hr(size=1) - if pages > 0 - span.pages Page - - for(let i = 0; i < pages; i++) - span: a(href=`/${board._id}/${i+1}`) #{i+1} + +post(post, true) hr(size=1) - include ../includes/deletefooter.pug + include ../includes/pages.pug + hr(size=1) + include ../includes/actionfooter.pug diff --git a/views/pages/globalmanage.pug b/views/pages/globalmanage.pug new file mode 100644 index 00000000..faedc003 --- /dev/null +++ b/views/pages/globalmanage.pug @@ -0,0 +1,35 @@ +extends ../layout.pug +include ../mixins/post.pug +include ../mixins/ban.pug + +block head + title Manage + +block content + h1.board-title Global Management + h4 All Reports: + form(action=`/forms/global/actions` method='POST' enctype='application/x-www-form-urlencoded') + input(type='hidden' name='_csrf' value=csrf) + if reports.length === 0 + p No reports. + hr(size=1) + else + for report in reports + section.thread + +post(report, false, false, true) + hr(size=1) + include ../includes/actionfooter_globalmanage.pug + hr(size=1) + h4 All Bans: + form(action=`/forms/global/unban` method='POST' enctype='application/x-www-form-urlencoded') + input(type='hidden' name='_csrf' value=csrf) + if bans.length === 0 + p No bans. + hr(size=1) + else + for ban in bans + section.thread + +ban(ban) + hr(size=1) + section.action-wrapper + input(type='submit', value='unban') diff --git a/views/pages/login.pug b/views/pages/login.pug index 85d15f09..fcd7d50f 100644 --- a/views/pages/login.pug +++ b/views/pages/login.pug @@ -7,6 +7,7 @@ block content section.form-wrapper form.form-post(action='/forms/login' method='POST') input(type='hidden' name='_csrf' value=csrf) + input(type='hidden' name='redirect' value=redirect) input#username(type='text', name='username', placeholder='username' maxlength='50') input#password(type='password', name='password', placeholder='password' maxlength='100') input(type='submit', value='submit') diff --git a/views/pages/manage.pug b/views/pages/manage.pug index a99461b1..f8236717 100644 --- a/views/pages/manage.pug +++ b/views/pages/manage.pug @@ -1,35 +1,36 @@ extends ../layout.pug include ../mixins/post.pug +include ../mixins/ban.pug block head - title Login + title Manage block content 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') + h4 Reports: + form(action=`/forms/board/${board._id}/actions` method='POST' enctype='application/x-www-form-urlencoded') input(type='hidden' name='_csrf' value=csrf) - if posts.length === 0 + if reports.length === 0 p No reports. hr(size=1) - for post in posts - section.thread - +post(board, post) + else + for report in reports + section.thread + +post(report, false, true) + hr(size=1) + include ../includes/actionfooter_manage.pug hr(size=1) - section.action-wrapper - span - label - input.post-check(type='checkbox', name='delete' value=1) - | Delete - span - label - input.post-check(type='checkbox', name='spoiler' value=1) - | Spoiler - span - label - input.post-check(type='checkbox', name='dismiss' value=1) - | Dismiss - input(type='submit', value='submit') - - + h4 Bans: + form(action=`/forms/board/${board._id}/unban` method='POST' enctype='application/x-www-form-urlencoded') + input(type='hidden' name='_csrf' value=csrf) + if bans.length === 0 + p No bans. + hr(size=1) + else + for ban in bans + section.thread + +ban(ban) + hr(size=1) + section.action-wrapper + input(type='submit', value='unban') + diff --git a/views/pages/message.pug b/views/pages/message.pug index dab4ab0c..70fb77c1 100644 --- a/views/pages/message.pug +++ b/views/pages/message.pug @@ -7,6 +7,10 @@ block content h1 #{title} if message blockquote #{message} + if messages + ul + each msg in messages + li #{msg} if errors ul each error in errors diff --git a/views/pages/thread.pug b/views/pages/thread.pug index cb23c2ba..465040f2 100644 --- a/views/pages/thread.pug +++ b/views/pages/thread.pug @@ -11,15 +11,14 @@ block head block content include ../includes/boardheader.pug - hr(size=1) include ../includes/postform.pug .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') + form(action=`/forms/board/${board._id}/actions` method='POST' enctype='application/x-www-form-urlencoded') input(type='hidden' name='_csrf' value=csrf) section.thread - +post(board, thread) + +post(thread) for post in thread.replies - +post(board, post) + +post(post) hr(size=1) - include ../includes/deletefooter.pug + include ../includes/actionfooter.pug diff --git a/wipe.js b/wipe.js index 59c62519..b9e39de3 100644 --- a/wipe.js +++ b/wipe.js @@ -1,6 +1,6 @@ 'use strict'; -const Mongo = require(__dirname+'/helpers/db.js') +const Mongo = require(__dirname+'/db/db.js') , util = require('util') , path = require('path') , fs = require('fs') @@ -10,10 +10,11 @@ const Mongo = require(__dirname+'/helpers/db.js') (async () => { console.log('connecting to db...') await Mongo.connect(); - const Boards = require(__dirname+'/db-models/boards.js') - , Posts = require(__dirname+'/db-models/posts.js') - , Trips = require(__dirname+'/db-models/trips.js') - , Accounts = require(__dirname+'/db-models/accounts.js'); + const Boards = require(__dirname+'/db/boards.js') + , Posts = require(__dirname+'/db/posts.js') + , Bans = require(__dirname+'/db/bans.js') + , Trips = require(__dirname+'/db/trips.js') + , Accounts = require(__dirname+'/db/accounts.js'); console.log('deleting accounts') await Accounts.deleteAll(); console.log('deleting posts') @@ -24,6 +25,8 @@ const Mongo = require(__dirname+'/helpers/db.js') await Boards.deleteIncrement('b'); await Boards.deleteAll(); await Trips.deleteAll(); + console.log('deleting bans'); + await Bans.deleteAll(); console.log('adding b and pol') await Boards.insertOne({ _id: 'pol', @@ -40,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({ @@ -61,6 +66,15 @@ const Mongo = require(__dirname+'/helpers/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')