Merge pull request #13 from fatchan/bans

add bans + numerous other improvements
merge-requests/208/head
Tom 5 years ago committed by GitHub
commit 530dd7d1c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 383
      controllers/forms.js
  2. 46
      controllers/pages.js
  3. 2
      db/accounts.js
  4. 66
      db/bans.js
  5. 20
      db/boards.js
  6. 0
      db/db.js
  7. 83
      db/posts.js
  8. 2
      db/trips.js
  9. 20
      helpers/bancheck.js
  10. 6
      helpers/check-auth.js
  11. 2
      helpers/files/video-thumbnail.js
  12. 9
      helpers/has-perms.js
  13. 16
      helpers/hasperms.js
  14. 16
      helpers/haspermsmiddleware.js
  15. 8
      helpers/isloggedin.js
  16. 45
      helpers/markdown.js
  17. 21
      helpers/number-converter.js
  18. 45
      helpers/quotes.js
  19. 2
      helpers/tripcode.js
  20. 17
      models/api/get-boards.js
  21. 20
      models/api/get-catalog.js
  22. 20
      models/api/get-recent.js
  23. 20
      models/api/get-thread.js
  24. 40
      models/forms/ban-poster.js
  25. 67
      models/forms/delete-post.js
  26. 34
      models/forms/dismiss-report.js
  27. 25
      models/forms/dismissglobalreport.js
  28. 31
      models/forms/edit-post.js
  29. 23
      models/forms/globalreportpost.js
  30. 21
      models/forms/login.js
  31. 26
      models/forms/make-post.js
  32. 16
      models/forms/register.js
  33. 13
      models/forms/removebans.js
  34. 21
      models/forms/report-post.js
  35. 67
      models/forms/spoiler-post.js
  36. 17
      models/pages/board.js
  37. 5
      models/pages/catalog.js
  38. 24
      models/pages/globalmanage.js
  39. 5
      models/pages/home.js
  40. 5
      models/pages/login.js
  41. 17
      models/pages/manage.js
  42. 11
      models/pages/register.js
  43. 5
      models/pages/thread.js
  44. 11
      server.js
  45. 135
      static/css/style.css
  46. 31
      views/includes/actionfooter.pug
  47. 24
      views/includes/actionfooter_globalmanage.pug
  48. 32
      views/includes/actionfooter_manage.pug
  49. 7
      views/includes/boardheader.pug
  50. 15
      views/includes/deletefooter.pug
  51. 8
      views/includes/navbar.pug
  52. 6
      views/includes/pages.pug
  53. 38
      views/includes/postform.pug
  54. 18
      views/mixins/ban.pug
  55. 5
      views/mixins/catalogtile.pug
  56. 21
      views/mixins/post.pug
  57. 14
      views/pages/ban.pug
  58. 18
      views/pages/board.pug
  59. 35
      views/pages/globalmanage.pug
  60. 1
      views/pages/login.pug
  61. 49
      views/pages/manage.pug
  62. 4
      views/pages/message.pug
  63. 9
      views/pages/thread.pug
  64. 24
      wipe.js

@ -2,95 +2,102 @@
const express = require('express') const express = require('express')
, router = express.Router() , router = express.Router()
, Boards = require(__dirname+'/../db-models/boards.js') , Boards = require(__dirname+'/../db/boards.js')
, Posts = require(__dirname+'/../db-models/posts.js') , Posts = require(__dirname+'/../db/posts.js')
, Trips = require(__dirname+'/../db-models/trips.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') , makePost = require(__dirname+'/../models/forms/make-post.js')
, deletePosts = require(__dirname+'/../models/forms/delete-post.js') , deletePosts = require(__dirname+'/../models/forms/delete-post.js')
, spoilerPosts = require(__dirname+'/../models/forms/spoiler-post.js') , spoilerPosts = require(__dirname+'/../models/forms/spoiler-post.js')
, reportPosts = require(__dirname+'/../models/forms/report-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') , dismissReports = require(__dirname+'/../models/forms/dismiss-report.js')
, dismissGlobalReports = require(__dirname+'/../models/forms/dismissglobalreport.js')
, loginAccount = require(__dirname+'/../models/forms/login.js') , loginAccount = require(__dirname+'/../models/forms/login.js')
, registerAccount = require(__dirname+'/../models/forms/register.js') , registerAccount = require(__dirname+'/../models/forms/register.js')
, numberConverter = require(__dirname+'/../helpers/number-converter.js'); , hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js')
, numberConverter = require(__dirname+'/../helpers/number-converter.js')
, banCheck = require(__dirname+'/../helpers/bancheck.js');
// login to account // login to account
router.post('/login', (req, res, next) => { router.post('/login', (req, res, next) => {
const errors = []; const errors = [];
//check exist //check exist
if (!req.body.username || req.body.username.length <= 0) { if (!req.body.username || req.body.username.length <= 0) {
errors.push('Missing username'); errors.push('Missing username');
} }
if (!req.body.password || req.body.password.length <= 0) { if (!req.body.password || req.body.password.length <= 0) {
errors.push('Missing password'); errors.push('Missing password');
} }
//check too long //check too long
if (req.body.username && req.body.username.length > 50) { if (req.body.username && req.body.username.length > 50) {
errors.push('Username must be 50 characters or less'); errors.push('Username must be 50 characters or less');
} }
if (req.body.password && req.body.password.length > 100) { if (req.body.password && req.body.password.length > 100) {
errors.push('Password must be 100 characters or less'); errors.push('Password must be 100 characters or less');
} }
if (errors.length > 0) { if (errors.length > 0) {
return res.status(400).render('message', { return res.status(400).render('message', {
'title': 'Bad request', 'title': 'Bad request',
'errors': errors, 'errors': errors,
'redirect': '/login' 'redirect': '/login'
}) })
} }
loginAccount(req, res); loginAccount(req, res, next);
}); });
//register account //register account
router.post('/register', (req, res, next) => { router.post('/register', (req, res, next) => {
const errors = []; const errors = [];
//check exist //check exist
if (!req.body.username || req.body.username.length <= 0) { if (!req.body.username || req.body.username.length <= 0) {
errors.push('Missing username'); errors.push('Missing username');
} }
if (!req.body.password || req.body.password.length <= 0) { if (!req.body.password || req.body.password.length <= 0) {
errors.push('Missing password'); errors.push('Missing password');
} }
if (!req.body.passwordconfirm || req.body.passwordconfirm.length <= 0) { if (!req.body.passwordconfirm || req.body.passwordconfirm.length <= 0) {
errors.push('Missing password confirmation'); errors.push('Missing password confirmation');
} }
//check too long //check too long
if (req.body.username && req.body.username.length > 50) { if (req.body.username && req.body.username.length > 50) {
errors.push('Username must be 50 characters or less'); errors.push('Username must be 50 characters or less');
} }
if (req.body.password && req.body.password.length > 100) { if (req.body.password && req.body.password.length > 100) {
errors.push('Password must be 100 characters or less'); errors.push('Password must be 100 characters or less');
} }
if (req.body.passwordconfirm && req.body.passwordconfirm.length > 100) { if (req.body.passwordconfirm && req.body.passwordconfirm.length > 100) {
errors.push('Password confirmation must be 100 characters or less'); errors.push('Password confirmation must be 100 characters or less');
} }
if (req.body.password != req.body.passwordconfirm) { if (req.body.password != req.body.passwordconfirm) {
errors.push('Password and password confirmation must match'); errors.push('Password and password confirmation must match');
} }
if (errors.length > 0) { if (errors.length > 0) {
return res.status(400).render('message', { return res.status(400).render('message', {
'title': 'Bad request', 'title': 'Bad request',
'errors': errors, 'errors': errors,
'redirect': '/register' 'redirect': '/register'
}) })
} }
registerAccount(req, res); registerAccount(req, res, next);
}); });
// make new post // 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; let numFiles = 0;
if (req.files && req.files.file) { 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 //report/delete/spoiler/ban
router.post('/board/:board/posts', Boards.exists, numberConverter, (req, res, next) => { router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, async (req, res, next) => {
const errors = []; 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') errors.push('Must select 1-10 posts')
} }
if (req.body.password && req.body.password.length > 50) { if (req.body.password && req.body.password.length > 50) {
errors.push('Password must be 50 characters or less'); 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'); errors.push('Report must be 50 characters or less');
} }
if (!(req.body.report || req.body.delete || req.body.dismiss || req.body.spoiler)) { if (req.body.ban_reason && req.body.ban_reason.length > 50) {
errors.push('Must select an action') 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') 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) { const posts = await Posts.getPosts(req.params.board, req.body.checkedposts, true);
reportPosts(req, res); if (!posts || posts.length === 0) {
} else if (req.body.delete) { return res.status(404).render('message', {
deletePosts(req, res); 'title': 'Not found',
} else if (req.body.spoiler) { 'errors': 'Selected posts not found',
spoilerPosts(req, res); 'redirect': `/${req.params.board}`
} else if (req.body.dismiss) { })
dismissReports(req, res); }
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; module.exports = router;

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

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const Mongo = require(__dirname+'/../helpers/db.js') const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('accounts') , db = Mongo.client.db('jschan').collection('accounts')
, bcrypt = require('bcrypt'); , bcrypt = require('bcrypt');

@ -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({});
},
}

@ -1,7 +1,8 @@
'use strict'; 'use strict';
const Mongo = require(__dirname+'/../helpers/db.js') const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan'); , db = Mongo.client.db('jschan')
, boardCache = new Map();
module.exports = { module.exports = {
@ -11,7 +12,7 @@ module.exports = {
return db.collection('boards').findOne({ '_id': name }); return db.collection('boards').findOne({ '_id': name });
}, },
find: (name) => { find: () => {
return db.collection('boards').find({}).toArray(); return db.collection('boards').find({}).toArray();
}, },
@ -31,11 +32,20 @@ module.exports = {
return db.collection('boards').deleteMany({}); 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) => { 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) { 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 res.locals.board = board; // can acces this in views or next route handlers
next(); next();

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Mongo = require(__dirname+'/../helpers/db.js') const Mongo = require(__dirname+'/db.js')
, Boards = require(__dirname+'/boards.js') , Boards = require(__dirname+'/boards.js')
, db = Mongo.client.db('jschan').collection('posts'); , db = Mongo.client.db('jschan').collection('posts');
@ -40,7 +40,7 @@ module.exports = {
} }
}).sort({ }).sort({
'_id': -1 '_id': -1
}).limit(3).toArray(); }).limit(5).toArray();
thread.replies = replies.reverse(); thread.replies = replies.reverse();
})); }));
@ -61,7 +61,8 @@ module.exports = {
const data = await Promise.all([ const data = await Promise.all([
db.findOne({ db.findOne({
'postId': id, 'postId': id,
'board': board 'board': board,
'thread': null,
}, { }, {
'projection': { 'projection': {
'salt': 0, '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) => { insertOne: async (board, data) => {
// bump thread if name not sage // 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) => { dismissReports: (board, ids) => {
return db.updateMany({ return db.updateMany({
'postId': { 'postId': {
@ -225,50 +256,60 @@ module.exports = {
}); });
}, },
getReports: (board) => { getGlobalReports: () => {
return db.find({ return db.find({
'reports.0': { 'globalreports.0': {
'$exists': true '$exists': true
}, }
'board': board
}, { }, {
'projection': { 'projection': {
'salt': 0, 'salt': 0,
'password': 0, 'password': 0,
'ip': 0, 'ip': 0,
'reports': 0,
} }
}).toArray(); }).toArray();
}, },
dismissGlobalReports: (ids) => {
return db.updateMany({
'_id': {
'$in': ids
},
}, {
'$set': {
'globalreports': []
}
});
},
deleteOne: (board, options) => { deleteOne: (board, options) => {
return db.deleteOne(options); return db.deleteOne(options);
}, },
deleteMany: (board, ids) => { deleteMany: (ids) => {
return db.deleteMany({ return db.deleteMany({
'postId': { '_id': {
'$in': ids '$in': ids
}, }
'board': board
}); });
}, },
spoilerMany: (board, ids) => { spoilerMany: (ids) => {
return db.updateMany({ return db.updateMany({
'postId': { '_id': {
'$in': ids '$in': ids
}, }
'board': board }, {
}, { '$set': {
'$set': {
'spoiler': true 'spoiler': true
} }
}); });
}, },
deleteAll: (board) => { deleteAll: (board) => {
return db.deleteMany({ return db.deleteMany({

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Mongo = require(__dirname+'/../helpers/db.js') const Mongo = require(__dirname+'/db.js')
, db = Mongo.client.db('jschan').collection('tripcodes'); , db = Mongo.client.db('jschan').collection('tripcodes');
module.exports = { module.exports = {

@ -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();
}

@ -1,6 +0,0 @@
'use strict';
module.exports = (req, res, next) => {
if (req.session.authenticated === true) return next()
res.redirect('/login')
}

@ -14,7 +14,7 @@ module.exports = (filename) => {
count: 1, count: 1,
filename: `thumb-${filename.split('.')[0]}.png`, filename: `thumb-${filename.split('.')[0]}.png`,
folder: uploadDirectory, folder: uploadDirectory,
size: '128x128' size: '128x?'
}); });
}); });

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

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

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

@ -0,0 +1,8 @@
'use strict';
module.exports = (req, res, next) => {
if (req.session.authenticated === true) {
return next();
}
res.redirect('/login');
}

@ -1,26 +1,24 @@
'use strict'; 'use strict';
const Posts = require(__dirname+'/../db-models/posts.js') const Posts = require(__dirname+'/../db/posts.js')
, quoteRegex = /^>>\d+$/gm , greentextRegex = /^>([^>].+)/gm
, greentextRegex = /^>[^>].+$/gm , redtextRegex = /^<([^<].+)/gm
, redtextRegex = /^<[^<].+$/gm , boldRegex = /==(.+)==/gm
, boldRegex = /==.+==/gm , italicRegex = /__(.+)__/gm
, italicRegex = /__.+__/gm
, linkRegex = /https?\:\/\/[^\s]+/g , linkRegex = /https?\:\/\/[^\s]+/g
, spoilerRegex = /\|.+\|/gm; , spoilerRegex = /\|\|(.+)\|\|/gm
, codeRegex = /^```\s([\s\S]+)\s```/gm;
module.exports = (board, thread, text) => { module.exports = (board, thread, text) => {
//redtext //redtext
text = text.replace(redtextRegex, (match) => { text = text.replace(redtextRegex, (match, redtext) => {
const red = match.substring(1); return `<span class='redtext'>&lt;${redtext}</span>`;
return `<span class='redtext'>&lt;${red}</span>`;
}); });
//greentext //greentext
text = text.replace(greentextRegex, (match) => { text = text.replace(greentextRegex, (match, greentext) => {
const green = match.substring(1); return `<span class='greentext'>&gt;${greentext}</span>`;
return `<span class='greentext'>&gt;${green}</span>`;
}); });
//links //links
@ -28,30 +26,25 @@ module.exports = (board, thread, text) => {
return `<a href="${match}">${match}</a>`; return `<a href="${match}">${match}</a>`;
}); });
//quotes
text = text.replace(quoteRegex, (match) => {
const quotenum = match.substring(2);
return `<a class='quote' href='/${board}/thread/${thread}#${quotenum}'>&gt;&gt;${quotenum}</a>`;
});
//bold //bold
text = text.replace(boldRegex, (match) => { text = text.replace(boldRegex, (match, bold) => {
const bold = match.substring(2, match.length-2);
return `<strong>${bold}</strong>`; return `<strong>${bold}</strong>`;
}); });
//italic //italic
text = text.replace(italicRegex, (match) => { text = text.replace(italicRegex, (match, italic) => {
const italic = match.substring(2, match.length-2); return `<em>${italic}</em>`;
return `<italic>${italic}</em>`;
}); });
//spoilers //spoilers
text = text.replace(spoilerRegex, (match) => { text = text.replace(spoilerRegex, (match, spoiler) => {
const spoiler = match.substring(1, match.length-1);
return `<span class='spoiler'>${spoiler}</span>`; return `<span class='spoiler'>${spoiler}</span>`;
}); });
text = text.replace(codeRegex, (match, code) => {
return `<span class='code'>${code.trim()}</span>`;
});
return text; return text;
} }

@ -1,14 +1,19 @@
'use strict'; 'use strict';
const Mongo = require(__dirname+'/../db/db.js');
module.exports = (req, res, next) => { module.exports = (req, res, next) => {
//for body //for body
if (req.body.thread) { if (req.body.thread) {
req.body.thread = +req.body.thread; req.body.thread = +req.body.thread;
} }
if (req.body.checked) { if (req.body.checkedposts) {
//syntax casts all string to number //syntax tries to convert all string to number
req.body.checked = req.body.checked.map(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 //and for params
@ -19,6 +24,16 @@ module.exports = (req, res, next) => {
req.params.page = +req.params.page; 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(); next();
} }

@ -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 `<a class='quote' href='/${board}/thread/${postThreadObject[quotenum]}#${quotenum}'>&gt;&gt;${quotenum}</a>`;
}
return match;
});
return text;
}

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Tripcodes = require(__dirname+'/../db-models/trips.js') const Tripcodes = require(__dirname+'/../db/trips.js')
, crypto = require('crypto'); , crypto = require('crypto');
module.exports = async (password) => { module.exports = async (password) => {

@ -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)
}

@ -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)
}

@ -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);
}

@ -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)
}

@ -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`;
}

@ -5,27 +5,13 @@ const path = require('path')
, fs = require('fs') , fs = require('fs')
, unlink = util.promisify(fs.unlink) , unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js') , hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Posts = require(__dirname+'/../../db-models/posts.js'); , 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 = checkedPosts;
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}`
});
}
//if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null //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)) { if (!hasPerms(req, res)) {
@ -37,20 +23,29 @@ module.exports = async (req, res) => {
&& post.password == req.body.password && post.password == req.body.password
}); });
if (posts.length === 0) { if (posts.length === 0) {
return res.status(403).render('message', { throw {
'title': 'Forbidden', 'status': 403,
'message': 'Password did not match any selected posts', 'message': {
'redirect': `/${req.params.board}` '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 //get posts from all threads
let threadPosts = [] let threadPosts = []
await Promise.all(threadIds.map(async id => { await Promise.all(boardThreads.map(async data => {
const currentThreadPosts = await Posts.getThreadPosts(req.params.board, id); const currentThreadPosts = await Posts.getThreadPosts(data.board, data.thread);
threadPosts = threadPosts.concat(currentThreadPosts); threadPosts = threadPosts.concat(currentThreadPosts);
return; return;
})) }))
@ -59,14 +54,8 @@ module.exports = async (req, res) => {
const allPosts = posts.concat(threadPosts) const allPosts = posts.concat(threadPosts)
//delete posts from DB //delete posts from DB
let deletedPosts = 0; const postMongoIds = allPosts.map(post => Mongo.ObjectId(post._id))
try { const deletedPosts = await Posts.deleteMany(postMongoIds).then(result => result.deletedCount);
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');
}
//get filenames from all the posts //get filenames from all the posts
let fileNames = []; let fileNames = [];
@ -79,15 +68,11 @@ module.exports = async (req, res) => {
//dont question it. //dont question it.
return Promise.all([ return Promise.all([
unlink(uploadDirectory + filename), unlink(uploadDirectory + filename),
unlink(uploadDirectory + 'thumb-' + filename) unlink(`${uploadDirectory}thumb-${filename.split('.')[0]}.png`)
]) ])
})); }));
//hooray! //hooray!
return res.render('message', { return `Deleted ${boardThreads.length} threads and ${deletedPosts-boardThreads.length} posts`
'title': 'Success',
'message': `Deleted ${threadIds.length} threads and ${deletedPosts} posts`,
'redirect': `/${req.params.board}`
});
} }

@ -1,31 +1,23 @@
'use strict'; 'use strict';
const Posts = require(__dirname+'/../../db-models/posts.js') const Posts = require(__dirname+'/../../db/posts.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js'); , hasPerms = require(__dirname+'/../../helpers/hasperms.js');
module.exports = async (req, res) => { module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) { if (!hasPerms(req, res)) {
return res.status(403).render('message', { throw {
'title': 'Forbidden', 'status': 403,
'message': `You are not authorised to dismiss reports.`, 'message': {
'redirect': `/${req.params.board}` 'title': 'Forbidden',
}); 'message': `You are not authorised to dismiss reports.`,
'redirect': `/${req.params.board}`
}
};
} }
try { const dismissedReports = await Posts.dismissReports(req.params.board, req.body.checkedposts).then(result => result.modifiedCount);
//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 `Dismissed ${dismissedReports} reports successfully`;
return res.render('message', {
'title': 'Success',
'message': `Dismissed report(s) successfully`,
'redirect': `/${req.params.board}/manage`
});
} }

@ -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`;
}

@ -1,34 +1,9 @@
'use strict'; 'use strict';
const uuidv4 = require('uuid/v4') const Posts = require(__dirname+'/../../db/posts.js');
, path = require('path')
, Posts = require(__dirname+'/../../db-models/posts.js')
module.exports = async (req, res, numFiles) => { module.exports = async (req, res, next) => {
// get the post that we are trying to edit throw new Error('Not implemented');
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
});
}
// 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);
} }

@ -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`
}

@ -1,20 +1,20 @@
'use strict'; 'use strict';
const bcrypt = require('bcrypt') 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 username = req.body.username.toLowerCase();
const password = req.body.password; const password = req.body.password;
const redirect = req.body.redirect;
//fetch an account //fetch an account
let account; let account;
try { try {
account = await Accounts.findOne(username); account = await Accounts.findOne(username);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return res.status(500).render('error');
} }
//if the account doesnt exist, reject //if the account doesnt exist, reject
@ -22,7 +22,7 @@ module.exports = async (req, res) => {
return res.status(403).render('message', { return res.status(403).render('message', {
'title': 'Forbidden', 'title': 'Forbidden',
'message': 'Incorrect username or password', 'message': 'Incorrect username or password',
'redirect': '/login' 'redirect': redirect ? `/login?redirect=${redirect}` : '/login'
}); });
} }
@ -31,8 +31,7 @@ module.exports = async (req, res) => {
try { try {
passwordMatch = await bcrypt.compare(password, account.passwordHash); passwordMatch = await bcrypt.compare(password, account.passwordHash);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return res.status(500).render('error');
} }
//if hashes matched //if hashes matched
@ -46,18 +45,14 @@ module.exports = async (req, res) => {
req.session.authenticated = true; req.session.authenticated = true;
//successful login //successful login
return res.render('message', { return res.redirect(redirect || '/');
'title': 'Success',
'message': `Welcome, ${username}`,
'redirect': '/'
});
} }
return res.status(403).render('message', { return res.status(403).render('message', {
'title': 'Forbidden', 'title': 'Forbidden',
'message': 'Incorrect username or password', 'message': 'Incorrect username or password',
'redirect': '/login' 'redirect': redirect ? `/login?redirect=${redirect}` : '/login'
}); });
} }

@ -6,12 +6,13 @@ const uuidv4 = require('uuid/v4')
, crypto = require('crypto') , crypto = require('crypto')
, randomBytes = util.promisify(crypto.randomBytes) , randomBytes = util.promisify(crypto.randomBytes)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') , 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') , getTripCode = require(__dirname+'/../../helpers/tripcode.js')
, linkQuotes = require(__dirname+'/../../helpers/quotes.js')
, simpleMarkdown = require(__dirname+'/../../helpers/markdown.js') , simpleMarkdown = require(__dirname+'/../../helpers/markdown.js')
, sanitize = require('sanitize-html') , sanitize = require('sanitize-html')
, sanitizeOptions = { , sanitizeOptions = {
allowedTags: [ 'span', 'a', 'code', 'em', 'strong' ], allowedTags: [ 'span', 'a', 'em', 'strong' ],
allowedAttributes: { allowedAttributes: {
'a': [ 'href', 'class' ], 'a': [ 'href', 'class' ],
'span': [ 'class' ] 'span': [ 'class' ]
@ -25,7 +26,7 @@ const uuidv4 = require('uuid/v4')
, videoIdentify = require(__dirname+'/../../helpers/files/video-identify.js') , videoIdentify = require(__dirname+'/../../helpers/files/video-identify.js')
, formatSize = require(__dirname+'/../../helpers/files/format-size.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 // check if this is responding to an existing thread
let redirect = `/${req.params.board}` let redirect = `/${req.params.board}`
@ -35,8 +36,7 @@ module.exports = async (req, res, numFiles) => {
try { try {
thread = await Posts.getPost(req.params.board, req.body.thread, true); thread = await Posts.getPost(req.params.board, req.body.thread, true);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return res.status(500).render('error');
} }
if (!thread || thread.thread != null) { if (!thread || thread.thread != null) {
return res.status(400).render('message', { return res.status(400).render('message', {
@ -100,7 +100,7 @@ module.exports = async (req, res, numFiles) => {
await videoThumbnail(filename); await videoThumbnail(filename);
break; break;
default: default:
return res.status(500).render('error'); //how did we get here? return next(err);
} }
//make thumbnail //make thumbnail
@ -115,13 +115,11 @@ module.exports = async (req, res, numFiles) => {
if (Array.isArray(processedFile.geometryString)) { if (Array.isArray(processedFile.geometryString)) {
processedFile.geometryString = processedFile.geometryString[0]; processedFile.geometryString = processedFile.geometryString[0];
} }
files.push(processedFile); files.push(processedFile);
} catch (err) { } catch (err) {
console.error(err);
//TODO: DELETE FAILED FILES //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 //simple markdown and sanitize
let message = req.body.message; let message = req.body.message;
if (message && message.length > 0) { 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 //add post to DB
@ -167,6 +167,7 @@ module.exports = async (req, res, numFiles) => {
'files': files, 'files': files,
'salt': !req.body.thread ? salt : '', 'salt': !req.body.thread ? salt : '',
'reports': [], 'reports': [],
'globalreports': [],
'spoiler': req.body.spoiler ? true : false, 'spoiler': req.body.spoiler ? true : false,
}; };
@ -174,11 +175,10 @@ module.exports = async (req, res, numFiles) => {
try { try {
postId = await Posts.insertOne(req.params.board, data); postId = await Posts.insertOne(req.params.board, data);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return res.status(500).render('error');
} }
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); return res.redirect(successRedirect);
} }

@ -1,9 +1,9 @@
'use strict'; 'use strict';
const bcrypt = require('bcrypt') 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 username = req.body.username.toLowerCase();
const password = req.body.password; const password = req.body.password;
@ -12,8 +12,7 @@ module.exports = async (req, res) => {
try { try {
account = await Accounts.findOne(username); account = await Accounts.findOne(username);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return res.status(500).render('error');
} }
// if the account exists reject // if the account exists reject
@ -29,14 +28,9 @@ module.exports = async (req, res) => {
try { try {
await Accounts.insertOne(username, password, 1); await Accounts.insertOne(username, password, 1);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return res.status(500).render('error');
} }
return res.render('message', { return res.redirect('/login')
'title': 'Success',
'message': `Welcome, ${username}`,
'redirect': '/'
});
} }

@ -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`;
}

@ -1,29 +1,20 @@
'use strict'; '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 ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
const report = { const report = {
'reason': req.body.reason, 'reason': req.body.report_reason,
'date': new Date(), 'date': new Date(),
'ip': ip 'ip': ip
} }
try { //push the report to all checked posts
//push the report to all checked posts const reportedCount = await Posts.reportMany(req.params.board, req.body.checkedposts, report).then(result => result.modifiedCount);
await Posts.reportMany(req.params.board, req.body.checked, report);
} catch (err) {
console.error(err);
return res.status(500).render('error');
}
//hooray! //hooray!
return res.render('message', { return `Reported ${reportedCount} posts successfully`
'title': 'Success',
'message': `Reported post(s) successfully`,
'redirect': `/${req.params.board}`
});
} }

@ -5,27 +5,14 @@ const path = require('path')
, fs = require('fs') , fs = require('fs')
, unlink = util.promisify(fs.unlink) , unlink = util.promisify(fs.unlink)
, uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js') , uploadDirectory = require(__dirname+'/../../helpers/uploadDirectory.js')
, hasPerms = require(__dirname+'/../../helpers/has-perms.js') , hasPerms = require(__dirname+'/../../helpers/hasperms.js')
, Posts = require(__dirname+'/../../db-models/posts.js'); , 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 //get all posts that were checked
let posts; let posts = checkedPosts;
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}`
});
}
//if user is not logged in OR if lgoged in but not authed, filter the posts by passwords that are not null //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)) { if (!hasPerms(req, res)) {
@ -37,42 +24,38 @@ module.exports = async (req, res) => {
&& post.password == req.body.password && post.password == req.body.password
}); });
if (posts.length === 0) { if (posts.length === 0) {
return res.status(403).render('message', { throw {
'title': 'Forbidden', 'status': 403,
'message': 'Password did not match any selected posts', 'message': {
'redirect': `/${req.params.board}` '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 => { posts = posts.filter(post => {
return !post.spoiler return !post.spoiler
}); });
if (posts.length === 0) { if (posts.length === 0) {
return res.status(409).render('message', { throw {
'title': 'Conflict', 'status': 409,
'message': 'Selected posts are already spoilered', 'message': {
'redirect': `/${req.params.board}` 'title': 'Conflict',
}); 'message': 'Posts already spoilered',
'redirect': '/'
}
};
} }
// spoiler posts // spoiler posts
let spoileredPosts = 0; const postMongoIds = posts.map(post => Mongo.ObjectId(post._id));
try { const spoileredPosts = await Posts.spoilerMany(postMongoIds).then(result => result.modifiedCount);
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');
}
//hooray! //hooray!
return res.render('message', { return `Spoilered ${spoileredPosts} posts`
'title': 'Success',
'message': `Spoilered ${spoileredPosts} posts`,
'redirect': `/${req.params.board}`
});
} }

@ -1,23 +1,28 @@
'use strict'; 'use strict';
const Posts = require(__dirname+'/../../db-models/posts.js'); const Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
//get the recently bumped thread & preview posts //get the recently bumped thread & preview posts
const page = req.query.p || 1;
let threads; let threads;
let pages; let pages;
try { try {
threads = await Posts.getRecent(req.params.board, req.params.page || 1); pages = Math.ceil((await Posts.getPages(req.params.board)) / 10) || 1;
pages = Math.ceil((await Posts.getPages(req.params.board)) / 10); if (page > pages) {
return next();
}
threads = await Posts.getRecent(req.params.board, page);
} catch (err) { } catch (err) {
console.error(err) return next(err);
return next();
} }
//render the page //render the page
res.render('board', { res.render('board', {
csrf: req.csrfToken(), csrf: req.csrfToken(),
threads: threads || [], threads: threads || [],
pages: pages pages,
page,
}); });
} }

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Posts = require(__dirname+'/../../db-models/posts.js'); const Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
@ -9,8 +9,7 @@ module.exports = async (req, res, next) => {
try { try {
threads = await Posts.getCatalog(req.params.board); threads = await Posts.getCatalog(req.params.board);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return next();
} }
//render the page //render the page

@ -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,
});
}

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Boards = require(__dirname+'/../../db-models/boards.js'); const Boards = require(__dirname+'/../../db/boards.js');
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
@ -9,8 +9,7 @@ module.exports = async (req, res, next) => {
try { try {
boards = await Boards.find(); boards = await Boards.find();
} catch (err) { } catch (err) {
console.error(err) return next(err);
return next();
} }
//render the page //render the page

@ -1,10 +1,11 @@
'use strict'; 'use strict';
module.exports = (req, res) => { module.exports = (req, res, next) => {
//render the page //render the page
res.render('login', { res.render('login', {
csrf: req.csrfToken() csrf: req.csrfToken(),
redirect: req.query.redirect,
}); });
} }

@ -1,21 +1,24 @@
'use strict'; '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 { try {
posts = await Posts.getReports(req.params.board); reports = await Posts.getReports(req.params.board);
bans = await Bans.getBoardBans(req.params.board);
} catch (err) { } catch (err) {
console.error(err); return next(err)
return res.status(500).render('error');
} }
//render the page //render the page
res.render('manage', { res.render('manage', {
csrf: req.csrfToken(), csrf: req.csrfToken(),
posts: posts reports,
bans,
}); });
} }

@ -1,15 +1,6 @@
'use strict'; 'use strict';
module.exports = (req, res) => { module.exports = (req, res, next) => {
//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': '/'
});
}
//render the page //render the page
res.render('register', { res.render('register', {

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Posts = require(__dirname+'/../../db-models/posts.js'); const Posts = require(__dirname+'/../../db/posts.js');
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
@ -9,8 +9,7 @@ module.exports = async (req, res, next) => {
try { try {
thread = await Posts.getThread(req.params.board, req.params.id); thread = await Posts.getThread(req.params.board, req.params.id);
} catch (err) { } catch (err) {
console.error(err); return next(err);
return next();
} }
if (!thread) { if (!thread) {

@ -13,13 +13,15 @@ const express = require('express')
, bodyParser = require('body-parser') , bodyParser = require('body-parser')
, cookieParser = require('cookie-parser') , cookieParser = require('cookie-parser')
, configs = require(__dirname+'/configs/main.json') , configs = require(__dirname+'/configs/main.json')
, Mongo = require(__dirname+'/helpers/db.js') , Mongo = require(__dirname+'/db/db.js')
, upload = require('express-fileupload'); , upload = require('express-fileupload');
(async () => { (async () => {
// let db connect // let db connect
await Mongo.connect(); await Mongo.connect();
const Boards = require(__dirname+'/db/boards.js');
await Boards.cache();
// parse forms and allow file uploads // parse forms and allow file uploads
app.use(bodyParser.urlencoded({extended: true})); app.use(bodyParser.urlencoded({extended: true}));
@ -53,7 +55,7 @@ const express = require('express')
// use pug view engine // use pug view engine
app.set('view engine', 'pug'); app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views/pages')); app.set('views', path.join(__dirname, 'views/pages'));
// app.enable('view cache'); app.enable('view cache');
// static files // static files
app.use('/css', express.static(__dirname + '/static/css')); app.use('/css', express.static(__dirname + '/static/css'));
@ -74,7 +76,10 @@ const express = require('express')
return res.status(403).send('Invalid CSRF token') return res.status(403).send('Invalid CSRF token')
} }
console.error(err.stack) 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 // listen

@ -9,10 +9,28 @@ body {
margin: 0; 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 {
object-fit: scale-down; object-fit: scale-down;
} }
.board-header {
display: flex;
flex-direction: column;
align-items: center;
}
.catalog-tile-button { .catalog-tile-button {
width: 100%; width: 100%;
line-height: 30px; line-height: 30px;
@ -46,13 +64,14 @@ object {
box-shadow: 0 0 3px black; box-shadow: 0 0 3px black;
min-width: 64px; min-width: 64px;
min-height: 64px; min-height: 64px;
object-fit: cover;
} }
.catalog { .catalog {
display:flex; display:flex;
flex-direction: row;
align-items:flex-start; align-items:flex-start;
flex-wrap: wrap; justify-content: space-evenly;
flex-flow: row wrap;
} }
.spoiler { .spoiler {
@ -85,6 +104,10 @@ object {
color: green; color: green;
} }
blockquote a {
color: #d00;
}
blockquote { blockquote {
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
@ -115,15 +138,62 @@ input, textarea {
margin: 10px 0; 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 { .action-wrapper {
align-items: center; display: flex;
/*flex-direction: row;*/ 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 { .form-post {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 100%; max-width: 100%;
margin-top: 10px;
}
.togglable {
display: none;
} }
.user-id { .user-id {
@ -135,9 +205,7 @@ input, textarea {
.post-check { .post-check {
position: relative; position: relative;
top: 3px; top: 3px;
margin: 2px; margin: -3px 1px !important;
margin-right: 4px;
padding: 0;
} }
.post-files { .post-files {
@ -177,7 +245,7 @@ input textarea {
.board-title { .board-title {
color: #af0a0f; color: #af0a0f;
font: bolder; font: bolder 28px Tahoma;
letter-spacing: -2px; letter-spacing: -2px;
text-align: center; text-align: center;
margin: 0; margin: 0;
@ -193,8 +261,13 @@ input textarea {
margin: 0; margin: 0;
} }
.post-container { .post-message {
margin: 1px; overflow-y: auto;
}
.post-container, .ban {
box-sizing: border-box;
margin: 2px 0;
padding: 2px; padding: 2px;
background: #D6DAF0; background: #D6DAF0;
border-color: #B7C5D9; border-color: #B7C5D9;
@ -204,7 +277,7 @@ input textarea {
} }
.post-container:target, .op:target { .post-container:target, .op:target {
background-color: #d6bad0; background-color: #d6bad0!important;
border-color: #ba9dbf; border-color: #ba9dbf;
} }
@ -213,7 +286,6 @@ input textarea {
margin-left: 0; margin-left: 0;
display: block; display: block;
border: none; border: none;
width: 100%;
} }
.post-subject { .post-subject {
@ -226,16 +298,22 @@ input textarea {
font-weight: bold; font-weight: bold;
} }
.post-info { .post-container.op .post-info, .catalog-tile-content .post-info {
margin-top: -2px; 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 { .navbar {
@ -244,6 +322,7 @@ input textarea {
position: fixed; position: fixed;
width: 100%; width: 100%;
background: #eef2ff; background: #eef2ff;
z-index: 1;
} }
.nav-item { .nav-item {
@ -256,8 +335,12 @@ input textarea {
border-right: 1px solid lightgray; border-right: 1px solid lightgray;
} }
.right {
float: right;
}
.nav-item:hover { .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 { .footer {
@ -274,7 +357,6 @@ table, th, td {
} }
.boards-table { .boards-table {
font-size: 16pt;
margin: 0 auto; margin: 0 auto;
} }
@ -306,8 +388,13 @@ hr {
width: 100%; width: 100%;
} }
blockquote {
margin: 1em;
}
.post-check { .post-check {
top: 2px; top: 2px;
margin-left: 2px!important;
height: 8px; height: 8px;
} }
@ -317,8 +404,14 @@ hr {
.catalog-tile { .catalog-tile {
overflow-y: hidden; overflow-y: hidden;
width: 49%; }
justify-content: space-evenly;
.boards-table {
width: 100%;
}
.post-info {
background-color: #B7C5D9;
} }
} }

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

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

@ -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')

@ -1,3 +1,4 @@
a.no-decoration(href=`/${board._id}`) section.board-header
h1.board-title /#{board._id}/ - #{board.name} a.no-decoration(href=`/${board._id}`)
h4.board-description #{board.description} h1.board-title /#{board._id}/ - #{board.name}
h4.board-description #{board.description}

@ -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')

@ -1,5 +1,9 @@
nav.navbar nav.navbar
a.nav-item(href='/') Home a.nav-item(href='/') Home
a.nav-item(href='/login') Login
if board 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

@ -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}

@ -1,25 +1,29 @@
section.form-wrapper 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='_csrf' value=csrf)
input(type='hidden' name='thread' value=thread != null ? thread.postId : null)
input#title(type='text', name='subject', placeholder='subject' autocomplete='off' maxlength='50') input(type='hidden' name='thread' value=thread != null ? thread.postId : null)
input#name(type='text', name='name', placeholder='name' autocomplete='off' maxlength='50') input#title(type='text', name='subject', placeholder='subject' autocomplete='off' maxlength='50')
input#name(type='text', name='email', placeholder='email' autocomplete='off' maxlength='50')
input#password(type='password', name='password', placeholder='post password' autocomplete='off' maxlength='50') 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#password(type='password', name='password', placeholder='post password' autocomplete='off' maxlength='50')
input#file(type='file', name='file' multiple)
label textarea#message(name='message', rows='8', cols='50', placeholder='message' autocomplete='off' maxlength='2000')
input.post-check#spoiler(type='checkbox', name='spoiler', value='true')
| Spoiler
input(type='submit', value='submit')
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')

@ -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)

@ -4,7 +4,10 @@ mixin catalogtile(board, post, truncate)
if post.files.length > 0 if post.files.length > 0
.post-file-src .post-file-src
a(href=`/${board._id}/thread/${post.postId}#${post.postId}`) 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 header.post-info
if post.subject if post.subject
span: a.no-decoration.post-subject(href=`/${board._id}/thread/${post.postId}#${post.postId}`) #{post.subject} span: a.no-decoration.post-subject(href=`/${board._id}/thread/${post.postId}#${post.postId}`) #{post.subject}

@ -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')) article(id=post.postId class='post-container '+(post.thread ? '' : 'op'))
header.post-info 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 if post.subject
span.post-subject #{post.subject} span.post-subject #{post.subject}
if post.email if post.email
@ -11,12 +15,12 @@ mixin post(board, post, truncate)
span.post-name #{post.name} span.post-name #{post.name}
span #{post.date.toLocaleString()} span #{post.date.toLocaleString()}
span.user-id(style=`background: #${post.userId}`) #{post.userId} 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 if post.files.length > 0
.post-files .post-files
each file in post.files each file in post.files
.post-file .post-file
.post-file-info small.post-file-info
span: a(href='/img/'+file.filename download=file.originalFilename) #{file.originalFilename} span: a(href='/img/'+file.filename download=file.originalFilename) #{file.originalFilename}
br br
span (#{file.sizeString} #{file.geometryString}) span (#{file.sizeString} #{file.geometryString})
@ -39,13 +43,18 @@ mixin post(board, post, truncate)
} }
if truncated if truncated
blockquote.post-message !{truncatedMessage} 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 else
blockquote.post-message !{post.message} blockquote.post-message !{post.message}
else else
blockquote.post-message !{post.message} blockquote.post-message !{post.message}
if post.reports if manage === true
each report in post.reports each report in post.reports
.reports.post-container .reports.post-container
span Date: #{report.date.toLocaleString()} span Date: #{report.date.toLocaleString()}
span Reason: #{report.reason} span Reason: #{report.reason}
if globalmanage === true
each report in post.globalreports
.reports.post-container
span Date: #{report.date.toLocaleString()}
span Reason: #{report.reason}

@ -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)

@ -6,24 +6,22 @@ block head
block content block content
include ../includes/boardheader.pug include ../includes/boardheader.pug
hr(size=1)
include ../includes/postform.pug include ../includes/postform.pug
.mode Posting mode: Thread [#[a.no-decoration(href=`/${board._id}/catalog`) Catalog]] .mode Posting mode: Thread [#[a.no-decoration(href=`/${board._id}/catalog`) Catalog]]
hr(size=1) 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) input(type='hidden' name='_csrf' value=csrf)
if threads.length === 0 if threads.length === 0
p No posts. p No posts.
hr(size=1) hr(size=1)
for thread in threads for thread in threads
section.thread section.thread
+post(board, thread, true) +post(thread, true)
for post in thread.replies for post in thread.replies
+post(board, post, true) +post(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}
hr(size=1) hr(size=1)
include ../includes/deletefooter.pug include ../includes/pages.pug
hr(size=1)
include ../includes/actionfooter.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')

@ -7,6 +7,7 @@ block content
section.form-wrapper section.form-wrapper
form.form-post(action='/forms/login' method='POST') form.form-post(action='/forms/login' method='POST')
input(type='hidden' name='_csrf' value=csrf) 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#username(type='text', name='username', placeholder='username' maxlength='50')
input#password(type='password', name='password', placeholder='password' maxlength='100') input#password(type='password', name='password', placeholder='password' maxlength='100')
input(type='submit', value='submit') input(type='submit', value='submit')

@ -1,35 +1,36 @@
extends ../layout.pug extends ../layout.pug
include ../mixins/post.pug include ../mixins/post.pug
include ../mixins/ban.pug
block head block head
title Login title Manage
block content block content
include ../includes/boardheader.pug include ../includes/boardheader.pug
hr(size=1) h4 Reports:
h4.board-description Management Panel form(action=`/forms/board/${board._id}/actions` method='POST' enctype='application/x-www-form-urlencoded')
form(action='/forms/board/'+board._id+'/posts' method='POST' enctype='application/x-www-form-urlencoded')
input(type='hidden' name='_csrf' value=csrf) input(type='hidden' name='_csrf' value=csrf)
if posts.length === 0 if reports.length === 0
p No reports. p No reports.
hr(size=1) hr(size=1)
for post in posts else
section.thread for report in reports
+post(board, post) section.thread
+post(report, false, true)
hr(size=1)
include ../includes/actionfooter_manage.pug
hr(size=1) hr(size=1)
section.action-wrapper h4 Bans:
span form(action=`/forms/board/${board._id}/unban` method='POST' enctype='application/x-www-form-urlencoded')
label input(type='hidden' name='_csrf' value=csrf)
input.post-check(type='checkbox', name='delete' value=1) if bans.length === 0
| Delete p No bans.
span hr(size=1)
label else
input.post-check(type='checkbox', name='spoiler' value=1) for ban in bans
| Spoiler section.thread
span +ban(ban)
label hr(size=1)
input.post-check(type='checkbox', name='dismiss' value=1) section.action-wrapper
| Dismiss input(type='submit', value='unban')
input(type='submit', value='submit')

@ -7,6 +7,10 @@ block content
h1 #{title} h1 #{title}
if message if message
blockquote #{message} blockquote #{message}
if messages
ul
each msg in messages
li #{msg}
if errors if errors
ul ul
each error in errors each error in errors

@ -11,15 +11,14 @@ block head
block content block content
include ../includes/boardheader.pug include ../includes/boardheader.pug
hr(size=1)
include ../includes/postform.pug include ../includes/postform.pug
.mode Posting mode: Reply [#[a.no-decoration(href=`/${board._id}`) Go Back]] .mode Posting mode: Reply [#[a.no-decoration(href=`/${board._id}`) Go Back]]
hr(size=1) 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) input(type='hidden' name='_csrf' value=csrf)
section.thread section.thread
+post(board, thread) +post(thread)
for post in thread.replies for post in thread.replies
+post(board, post) +post(post)
hr(size=1) hr(size=1)
include ../includes/deletefooter.pug include ../includes/actionfooter.pug

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Mongo = require(__dirname+'/helpers/db.js') const Mongo = require(__dirname+'/db/db.js')
, util = require('util') , util = require('util')
, path = require('path') , path = require('path')
, fs = require('fs') , fs = require('fs')
@ -10,10 +10,11 @@ const Mongo = require(__dirname+'/helpers/db.js')
(async () => { (async () => {
console.log('connecting to db...') console.log('connecting to db...')
await Mongo.connect(); await Mongo.connect();
const Boards = require(__dirname+'/db-models/boards.js') const Boards = require(__dirname+'/db/boards.js')
, Posts = require(__dirname+'/db-models/posts.js') , Posts = require(__dirname+'/db/posts.js')
, Trips = require(__dirname+'/db-models/trips.js') , Bans = require(__dirname+'/db/bans.js')
, Accounts = require(__dirname+'/db-models/accounts.js'); , Trips = require(__dirname+'/db/trips.js')
, Accounts = require(__dirname+'/db/accounts.js');
console.log('deleting accounts') console.log('deleting accounts')
await Accounts.deleteAll(); await Accounts.deleteAll();
console.log('deleting posts') console.log('deleting posts')
@ -24,6 +25,8 @@ const Mongo = require(__dirname+'/helpers/db.js')
await Boards.deleteIncrement('b'); await Boards.deleteIncrement('b');
await Boards.deleteAll(); await Boards.deleteAll();
await Trips.deleteAll(); await Trips.deleteAll();
console.log('deleting bans');
await Bans.deleteAll();
console.log('adding b and pol') console.log('adding b and pol')
await Boards.insertOne({ await Boards.insertOne({
_id: 'pol', _id: 'pol',
@ -40,6 +43,8 @@ const Mongo = require(__dirname+'/helpers/db.js')
moderators: [], moderators: [],
}) })
console.log('creating indexes') console.log('creating indexes')
await Bans.db.dropIndexes();
await Bans.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 });
await Posts.db.dropIndexes(); await Posts.db.dropIndexes();
//these are fucked //these are fucked
await Posts.db.createIndex({ 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 readdir('static/img/').then(async files => {
await Promise.all(files.map(async file => { await Promise.all(files.map(async file => {
if (file != 'spoiler.png') if (file != 'spoiler.png')

Loading…
Cancel
Save