implementing global limits for board settings and configurable defaults for board creation

merge-requests/208/head
fatchan 5 years ago
parent f82b76ae08
commit d5bd6a600e
  1. 27
      README.md
  2. 64
      configs/main.json.example
  3. 9
      controllers/forms.js
  4. 23
      controllers/forms/boardsettings.js
  5. 7
      controllers/forms/makepost.js
  6. 8
      controllers/forms/uploadbanners.js
  7. BIN
      gulp/res/img/flags.png
  8. 36
      gulpfile.js
  9. 4
      helpers/render.js
  10. 1
      models/forms/changeboardsettings.js
  11. 38
      models/forms/create.js
  12. 11
      models/forms/makepost.js
  13. 1
      package.json
  14. 7
      server.js
  15. 2
      views/includes/postform.pug
  16. 2
      views/mixins/ban.pug
  17. 2
      views/mixins/newspost.pug
  18. 5
      views/mixins/post.pug
  19. 14
      views/pages/manage.pug
  20. 2
      views/pages/modlog.pug

@ -7,28 +7,25 @@ Demo site running at https://fatpeople.lol
## Goals
- Oldschool imageboard look
- Works on TOR with javascript disabled (maybe the name is a bit ironic)
- Take advantage of modern html5/css features for some usability perks
- Leverage nginx to serve static files and handle GeoIP lookups
## Features
- [x] Classic post styling e.g. greentext, spoilers, quotes
- [x] Post styling (markdown-esque)
- [x] Quote linking and replies
- [x] Multiple files per post
- [x] Optional user created boards ala infinity
- [x] User created boards ala infinity
- [x] Captcha and basic antispam
- [x] read-only JSON api
- [x] Multi-select posts for reports, bans, post deletions, etc
- [x] Public board modlogs
- [x] Homepage boards sorted by active users, pph, total posts descending
- [x] Management page with reports, bans, banners, board settings and news
- [x] Customise homepage, faq, rules or add custom pages
- [x] Read-only JSON api
- [x] Public modlogs
- [x] Multi-select posts for moderation actions/reports
- [x] Fully functional for users with javascript disabled
## Todo
- Post moving/thread merging
- Geographic and custom uploaded flags
- IP range bans
- IP notes/records/ban history of some sort
- Staff post editing/moving/thread merging
- IP range bans + IP ban history
- Configuration editor
- Overboard/multiboard/meta boards
- Boards search page
- Boards list and search page
- User created board custom pages
- File URL uploads
@ -42,7 +39,7 @@ Please note:
- Node.js (to run the app)
- MongoDB (database, duh)
- Redis (sessions, queue, locks and caching)
- Nginx (handle https, serve static content and html)
- Nginx (handle https, serve static content, GeoIP lookup)
- Certbot/letsencrypt (for https cert)
- Imagemagick (thumbnailing images)
- Ffmpeg (thumbnailing videos)

@ -1,5 +1,5 @@
{
"dbURL": "mongodb://username:password@host:port",
"dbURL": "mongodb://username:password@localhost:27017",
"redis": {
"host": "127.0.0.1",
"port": "6379",
@ -11,11 +11,67 @@
"ipHashSecret": "long random string",
"postPasswordSecret": "long random string",
"cacheTemplates": true,
"refererCheck": false,
"enableUserBoards": true,
"refererCheck": true,
"refererRegex": "^https?:\\/\\/(?:www\\.)?domain\\.com\\/",
"defaultTheme": "lain",
"meta": {
"siteName": "site name",
"siteName": "imageboard",
"url": "https://domain.com"
},
"globalLimits": {
"threadLimit": {
"min": 10,
"max": 200
},
"replyLimit": {
"min": 10,
"max": 500
},
"postFiles": {
"max": 3
},
"bannerFiles": {
"max": 10
},
"messageLength": {
"max": 4000
}
},
"boardDefaults": {
"theme": "lain",
"locked": false,
"captchaMode": 0,
"tphTrigger": 0,
"tphTriggerAction": 0,
"forceAnon": false,
"early404": true,
"ids": false,
"flags": false,
"userPostDelete": true,
"userPostSpoiler": true,
"userPostUnlink": true,
"threadLimit": 200,
"replyLimit": 500,
"maxFiles": 1,
"forceReplyMessage": false,
"forceReplyFile": false,
"forceThreadMessage": false,
"forceThreadFile": false,
"forceThreadSubject": false,
"minThreadMessageLength": 0,
"minReplyMessageLength": 0,
"defaultName": "Anon",
"filters": [],
"filterMode": 0,
"filterBanDuration": 0,
"announcement": {
"raw": null,
"markdown": null
},
"allowedFileTypes": {
"animatedImage": true,
"image": true,
"video": true
}
}
}

@ -3,6 +3,7 @@
const express = require('express')
, router = express.Router()
, Boards = require(__dirname+'/../db/boards.js')
, { globalLimits } = require(__dirname+'/../configs/main.json')
//middlewares
, calcPerms = require(__dirname+'/../helpers/checks/calcpermsmiddleware.js')
, hasPerms = require(__dirname+'/../helpers/checks/haspermsmiddleware.js')
@ -19,7 +20,7 @@ const express = require('express')
preserveExtension: 4,
limits: {
fileSize: 10 * 1024 * 1024,
files: 3 //todo: add configs for these values
files: globalLimits.postFiles.max
},
abortOnLimit: true,
useTempFiles: true,
@ -31,7 +32,11 @@ const express = require('express')
preserveExtension: 3,
limits: {
fileSize: 10 * 1024 * 1024,
files: 10
/*
currently not possible to limit each file to files/fileSize with express-filesupload/busboy.
will need to do separately in banner upload model if desired.
*/
files: globalLimits.bannerFiles.max
},
abortOnLimit: true,
useTempFiles: true,

@ -1,7 +1,8 @@
'use strict';
const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardsettings.js')
, themes = require(__dirname+'/../../helpers/themes.js');
, themes = require(__dirname+'/../../helpers/themes.js')
, { globalLimits } = require(__dirname+'/../../configs/main.json');
module.exports = async (req, res, next) => {
@ -28,20 +29,20 @@ module.exports = async (req, res, next) => {
if (req.body.default_name && (req.body.default_name.length < 1 || req.body.default_name.length > 50)) {
errors.push('Anon name must be 1-50 characters');
}
if (typeof req.body.reply_limit === 'number' && (req.body.reply_limit < 1 || req.body.reply_limit > 1000)) {
errors.push('Reply Limit must be from 1-1000');
if (typeof req.body.reply_limit === 'number' && (req.body.reply_limit < globalLimits.replyLimit.min || req.body.reply_limit > globalLimits.replyLimit.max)) {
errors.push(`Reply Limit must be ${globalLimits.replyLimit.min}-${globalLimits.replyLimit.max}`);
}
if (typeof req.body.thread_limit === 'number' && (req.body.thread_limit < 10 || req.body.thread_limit > 250)) {
errors.push('Threads Limit must be 10-250');
if (typeof req.body.thread_limit === 'number' && (req.body.thread_limit < globalLimits.threadLimit.min || req.body.thread_limit > globalLimits.threadLimit.max)) {
errors.push(`Threads Limit must be ${globalLimits.threadLimit.min}-${globalLimits.threadLimit.max}`);
}
if (typeof req.body.max_files === 'number' && (req.body.max_files < 0 || req.body.max_files > 3)) {
errors.push('Max files must be 0-3');
if (typeof req.body.max_files === 'number' && (req.body.max_files < 0 || req.body.max_files > globalLimits.postFiles.max)) {
errors.push(`Max files must be 0-${globalLimits.postFiles.max}`);
}
if (typeof req.body.min_thread_message_length === 'number' && (req.body.min_thread_message_length < 0 || req.body.min_thread_message_length > 4000)) {
errors.push('Min thread message length must be 0-4000. 0 is disabled.');
if (typeof req.body.min_thread_message_length === 'number' && (req.body.min_thread_message_length < 0 || req.body.min_thread_message_length > globalLimits.messageLength.max)) {
errors.push(`Min thread message length must be 0-${globalLimits.messageLength.max}. 0 is disabled.`);
}
if (typeof req.body.min_reply_message_length === 'number' && (req.body.min_reply_message_length < 0 || req.body.min_reply_message_length > 4000)) {
errors.push('Min reply message length must be 0-4000. 0 is disabled.');
if (typeof req.body.min_reply_message_length === 'number' && (req.body.min_reply_message_length < 0 || req.body.min_reply_message_length > globalLimits.messageLength.max)) {
errors.push(`Min reply message length must be 0-${globalLimits.messageLength.max}. 0 is disabled.`);
}
if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) {
errors.push('Invalid captcha mode.');

@ -2,6 +2,7 @@
const makePost = require(__dirname+'/../../models/forms/makepost.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, { globalLimits } = require(__dirname+'/../../configs/main.json')
, { Files } = require(__dirname+'/../../db/');
module.exports = async (req, res, next) => {
@ -29,7 +30,7 @@ module.exports = async (req, res, next) => {
errors.push('Threads must include a subject');
} //no option to force op subject, seems useless
}
if (res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0) {
if (globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0) {
if (!req.body.thread && res.locals.board.settings.forceThreadFile) {
errors.push('Threads must include a file');
} else if (res.locals.board.settings.forceReplyFile) {
@ -44,8 +45,8 @@ module.exports = async (req, res, next) => {
}
}
if (req.body.message) {
if (req.body.message.length > 4000) {
errors.push('Message must be 4000 characters or less');
if (req.body.message.length > globalLimits.messageLength.max) {
errors.push(`Message must be ${globalLimits.messageLength.max} characters or less`);
} else if (!req.body.thread && req.body.message.length < res.locals.board.settings.minThreadMessageLength) {
errors.push(`Thread messages must be at least ${res.locals.board.settings.minMessageLength} characters long`);
} else if (req.body.thread && req.body.message.length < res.locals.board.settings.minReplyMessageLength) {

@ -1,7 +1,8 @@
'use strict';
const uploadBanners = require(__dirname+'/../../models/forms/uploadbanners.js')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js');
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, { globalLimits } = require(__dirname+'/../../configs/main.json');
module.exports = async (req, res, next) => {
@ -18,8 +19,9 @@ module.exports = async (req, res, next) => {
if (res.locals.numFiles === 0) {
errors.push('Must provide a file');
}
if (res.locals.board.banners.length+res.locals.numFiles > 100) {
} else if (res.locals.numFiles > globalLimits.bannerFiles.max) {
errors.push(`Exceeded max banner uploads in one request of ${globalLimits.bannerFiles.max}`)
} else if (res.locals.board.banners.length+res.locals.numFiles > 100) {
errors.push('Number of uploads would exceed 100 banner limit');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

@ -63,39 +63,7 @@ async function wipe() {
'description': 'testing board',
'tags': [],
'moderators': [],
'captchaMode': 0,
'locked': false,
'tphTrigger': 10,
'tphTriggerAction': 2,
'forceAnon': true,
'early404': true,
'ids': false,
'userPostDelete': true,
'userPostSpoiler': true,
'userPostUnlink': true,
'threadLimit': 200,
'replyLimit': 500,
'maxFiles': 0,
'forceReplyMessage':false,
'forceReplyFile':false,
'forceThreadMessage':false,
'forceThreadFile':false,
'forceThreadSubject':false,
'minThreadMessageLength':0,
'minReplyMessageLength':0,
'defaultName': 'Anonymous',
'filters':[],
'filterMode': 0,
'filterBanDuration': 0,
'announcement': {
'raw':null,
'markdown':null
},
'allowedFileTypes': {
'animatedImage': true,
'image': true,
'video': true,
}
...configs.boardDefaults
}
})
//add indexes - should profiled and changed at some point if necessary
@ -152,7 +120,7 @@ function custompages() {
return gulp.src(paths.pug.src)
.pipe(pug({
locals: {
defaultTheme: configs.defaultTheme
defaultTheme: configs.boardDefaults.theme
}
}))
.pipe(gulp.dest(paths.pug.dest));

@ -1,6 +1,6 @@
'use strict';
const { defaultTheme, cacheTemplates, meta }= require(__dirname+'/../configs/main.json')
const { globalLimits, boardDefaults, cacheTemplates, meta }= require(__dirname+'/../configs/main.json')
, { outputFile } = require('fs-extra')
, pug = require('pug')
, path = require('path')
@ -9,7 +9,7 @@ const { defaultTheme, cacheTemplates, meta }= require(__dirname+'/../configs/mai
, templateDirectory = path.join(__dirname+'/../views/pages/')
module.exports = async (htmlName, templateName, options, json=null) => {
const html = pug.renderFile(`${templateDirectory}${templateName}`, { ...options, cache: cacheTemplates, meta, defaultTheme });
const html = pug.renderFile(`${templateDirectory}${templateName}`, { ...options, cache: cacheTemplates, meta, defaultTheme: boardDefaults.theme, globalLimits });
const lock = await redlock.lock(`locks:${htmlName}`, 3000); //what is a reasonable ttl?
const htmlPromise = outputFile(`${uploadDirectory}html/${htmlName}`, html);
let jsonPromise;

@ -43,6 +43,7 @@ module.exports = async (req, res, next) => {
'locked': req.body.locked ? true : false,
'early404': req.body.early404 ? true : false,
'ids': req.body.ids ? true : false,
'flags': req.body.flags ? true : false,
'forceAnon': req.body.force_anon ? true : false,
'userPostDelete': req.body.user_post_delete ? true : false,
'userPostSpoiler': req.body.user_post_spoiler ? true : false,

@ -1,6 +1,7 @@
'use strict';
const { Boards } = require(__dirname+'/../../db/');
const { Boards } = require(__dirname+'/../../db/')
, { boardDefaults } = require(__dirname+'/../../configs/main.json');
module.exports = async (req, res, next) => {
@ -30,40 +31,7 @@ module.exports = async (req, res, next) => {
description,
tags,
'moderators': [],
'locked': false,
'captchaMode': 0,
'tphTrigger': 10,
'tphTriggerAction': 1,
'forceAnon': false,
'early404': true,
'ids': false,
'userPostDelete': true,
'userPostSpoiler': true,
'userPostUnlink': true,
'threadLimit': 200,
'replyLimit': 500,
'maxFiles': 3,
'forceReplyMessage': false,
'forceReplyFile': false,
'forceThreadMessage': false,
'forceThreadFile': false,
'forceThreadSubject': false,
'minThreadMessageLength': 0,
'minReplyMessageLength': 0,
'defaultName': 'Anonymous',
'filters': [],
'filterMode': 0,
'filterBanDuration': 0,
'theme': 'lain',
'announcement': {
'raw':null,
'markdown':null
},
'allowedFileTypes': {
'animatedImage': true,
'image': true,
'video': true,
}
...boardDefaults
}
}

@ -51,7 +51,7 @@ module.exports = async (req, res, next) => {
maxFiles, forceAnon, replyLimit,
threadLimit, ids, userPostSpoiler,
defaultName, tphTrigger, tphTriggerAction,
captchaMode, locked, allowedFileTypes } = res.locals.board.settings;
captchaMode, locked, allowedFileTypes, flags } = res.locals.board.settings;
if (locked === true) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
@ -240,7 +240,13 @@ module.exports = async (req, res, next) => {
const fullUserIdHash = createHash('sha256').update(salt + res.locals.ip).digest('hex');
userId = fullUserIdHash.substring(fullUserIdHash.length-6);
}
let country = null;
if (flags === true) {
country = {
'code': req.headers['x-country-code'],
'name': req.headers['x-country-name']
}
}
let password = null;
if (req.body.password) {
password = createHash('sha256').update(postPasswordSecret + req.body.password).digest('base64');
@ -312,6 +318,7 @@ module.exports = async (req, res, next) => {
const data = {
'date': new Date(),
name,
country,
'board': req.params.board,
tripcode,
capcode,

@ -8,7 +8,6 @@
"body-parser": "^1.19.0",
"bull": "^3.10.0",
"connect-redis": "^4.0.0",
"convert-svg-to-png": "^0.5.0",
"cookie-parser": "^1.4.4",
"csurf": "^1.10.0",
"del": "^4.1.1",

@ -61,12 +61,15 @@ const express = require('express')
// use pug view engine
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views/pages'));
//default theme in views with settings.defaultTheme
app.locals.defaultTheme = configs.defaultTheme;
//cache loaded templates
if (configs.cacheTemplates === true) {
app.enable('view cache');
}
//default settings
app.locals.defaultTheme = configs.boardDefaults.theme;
app.locals.globalLimits = configs.globalLimits;
// routes
app.use('/forms', require(__dirname+'/controllers/forms.js'));
app.use('/', require(__dirname+'/controllers/pages.js'));

@ -34,7 +34,7 @@ section.form-wrapper.flex-center
.label Message
if messageRequired
.required *
textarea#message(name='message', rows='5', autocomplete='off' maxlength='4000' required=messageRequired)
textarea#message(name='message', rows='5', autocomplete='off' maxlength=globalLimits.messageLength.max required=messageRequired)
if board.settings.maxFiles > 0
section.row
.label Files

@ -13,7 +13,7 @@ mixin ban(ban, banpage)
| for: #{ban.reason}
div Issued by: #{ban.issuer}
div Issued against: ...#{ban.ip.slice(-10)}
div Banned: #{ban.date.toLocaleString()}
div Banned: #{ban.date.toLocaleString(undefined, {hour12:false})}
div Expires: #{ban.expireAt.toLocaleString()}
if ban.posts && ban.posts.length > 0
span Banned for the following post#{ban.posts.length > 1 ? 's' : ''}:

@ -7,7 +7,7 @@ mixin newspost(post, globalmanage=false)
if globalmanage === true
input.left.post-check(type='checkbox', name='checkednews[]' value=post._id)
a.left(href=`#${post._id}`) #{post.title}
p.right.no-m-p #{post.date.toLocaleString()}
p.right.no-m-p #{post.date.toLocaleString(undefined, {hour12:false})}
tr.table-row
td
if globalmanage === true

@ -21,13 +21,16 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
else
span.post-name #{post.name}
|
if post.country && post.country.code
span(class=`flag flag-${post.country.code.toLowerCase()}` title=post.country.name alt=post.country.name)
|
if post.tripcode
span.post-tripcode #{post.tripcode}
|
if post.capcode
span.post-capcode #{post.capcode}
|
time.post-date(datetime=post.date.toISOString()) #{post.date.toLocaleString()}
time.post-date(datetime=post.date.toISOString()) #{post.date.toLocaleString(undefined, {hour12:false})}
|
if post.userId
span.user-id(style=`background: #${post.userId}`) #{post.userId}

@ -52,7 +52,7 @@ block content
input(type='text' name='default_name' value=board.settings.defaultName)
section.row
.label Max Files
input(type='number' name='max_files' value=board.settings.maxFiles)
input(type='number' name='max_files' value=board.settings.maxFiles max=globalLimits.postFiles.max)
section.row
.label Allow Video Files
label.postform-style.ph-5
@ -73,6 +73,10 @@ block content
.label IDs
label.postform-style.ph-5
input(type='checkbox', name='ids', value='true' checked=board.settings.ids)
section.row
.label Geo Flags
label.postform-style.ph-5
input(type='checkbox', name='flags', value='true' checked=board.settings.flags)
section.row
.label User Post Deletion
label.postform-style.ph-5
@ -111,16 +115,16 @@ block content
input(type='checkbox', name='force_reply_file', value='true' checked=board.settings.forceReplyFile)
section.row
.label Min Thread Message Length
input(type='number' name='min_thread_message_length' value=board.settings.minThreadMessageLength placeholder='0-4000')
input(type='number' name='min_thread_message_length' value=board.settings.minThreadMessageLength max=globalLimits.messageLength.max)
section.row
.label Min Reply Message Length
input(type='number' name='min_reply_message_length' value=board.settings.minReplyMessageLength placeholder='0-4000')
input(type='number' name='min_reply_message_length' value=board.settings.minReplyMessageLength max=globalLimits.messageLength.max)
section.row
.label Thread Limit
input(type='number' name='thread_limit' value=board.settings.threadLimit)
input(type='number' name='thread_limit' value=board.settings.threadLimit min=globalLimits.threadLimit.min max=globalLimits.threadLimit.max)
section.row
.label Reply Limit
input(type='number' name='reply_limit' value=board.settings.replyLimit)
input(type='number' name='reply_limit' value=board.settings.replyLimit min=globalLimits.replyLimit.min max=globalLimits.replyLimit.max)
section.row
.label Moderators
textarea(name='moderators' placeholder='newline separated, max 10') #{board.settings.moderators.join('\n')}

@ -26,7 +26,7 @@ block content
th Log Message
for log in logs
tr
td #{log.date.toLocaleString()}
td #{log.date.toLocaleString(undefined, {hour12:false})}
td #{log.user}
td #{log.actions}
td #{log.postIds}

Loading…
Cancel
Save