Merge branch 'disco-improve-modlogs' into 'develop'

Draft: Add modlog on unban, and split concept of private/public modlog entries

See merge request fatchan/jschan!345
Thomas Lynch 2 weeks ago
commit 8c1e56f016
  1. 6
      CHANGELOG.md
  2. 4
      controllers/forms.js
  3. 33
      controllers/forms/editbans.js
  4. 11
      db/bans.js
  5. 8
      db/modlogs.js
  6. 5
      lib/input/modlogactions.js
  7. 3
      lib/permission/permission.js
  8. 5
      lib/permission/permissions.js
  9. 3
      lib/post/filteractions.js
  10. 3
      locales/en-GB.json
  11. 3
      locales/it-IT.json
  12. 3
      locales/pt-BR.json
  13. 3
      locales/pt-PT.json
  14. 3
      locales/ru-RU.json
  15. 12
      migrations/1.4.2.js
  16. 24
      migrations/1.5.0.js
  17. 5
      models/forms/actionhandler.js
  18. 7
      models/forms/banposter.js
  19. 19
      models/forms/changeboardsettings.js
  20. 18
      models/forms/create.js
  21. 4
      models/forms/denybanappeals.js
  22. 4
      models/forms/editbanduration.js
  23. 4
      models/forms/editbannote.js
  24. 1
      models/forms/editpost.js
  25. 6
      models/forms/removebans.js
  26. 4
      models/forms/upgradebans.js
  27. 4
      models/pages/manage/bans.js
  28. 4
      package-lock.json
  29. 4
      package.json
  30. 8
      views/mixins/ban.pug
  31. 6
      views/mixins/postlink.pug
  32. 11
      views/pages/globalmanagelogs.pug
  33. 2
      views/pages/managelogs.pug
  34. 2
      views/pages/modlog.pug

@ -1,5 +1,11 @@
### 1.6.0
- Filters now have a "replace" mode, by @disco.
- Global bans now show the board it originated from as "Global (<board>)".
- Modlogs now have private entries for staff auditing
- So far covers creating boards, changing board settings, and ban editing/unban/upgrade.
- Modlog entries for e.g. board creation are global and not linked to the board and will not be deleted along with the board (duh).
- Private modlogs for edited global bans will also be available from the originating board.
- Added a permission for whether board ban lists include global bans that originated from that board.
- Global account management now has an option delete all boards owned by an account when deleting it.
- Bugfix moving posts to non existing board not correctly returning an error sometimes.

@ -57,7 +57,7 @@ router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.e
hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), transferController.paramConverter, transferController.controller);
router.post('/board/:board/settings', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), boardSettingsController.paramConverter, boardSettingsController.controller);
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
router.post('/board/:board/editbans', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_BANS), editBansController.paramConverter, editBansController.controller); //edit bans
router.post('/board/:board/addfilter', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), addFilterController.paramConverter, addFilterController.controller); //add new filter
@ -95,7 +95,7 @@ router.post('/board/:board/deletestaff', useSession, sessionRefresh, csrf, Board
hasPerms.one(Permissions.MANAGE_BOARD_STAFF), deleteStaffController.paramConverter, deleteStaffController.controller); //delete board staff
//global management forms
router.post('/global/editbans', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
router.post('/global/editbans', geoIp, processIp, useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_GLOBAL_BANS), editBansController.paramConverter, editBansController.controller); //remove bans
router.post('/global/deleteboard', useSession, sessionRefresh, csrf, deleteBoardController.paramConverter, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_GLOBAL_BOARDS), deleteBoardController.controller); //delete board from global management panel

@ -8,6 +8,8 @@ const config = require(__dirname+'/../../lib/misc/config.js')
, editBanNote = require(__dirname+'/../../models/forms/editbannote.js')
, upgradeBans = require(__dirname+'/../../models/forms/upgradebans.js')
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
, ModlogActions = require(__dirname+'/../../lib/input/modlogactions.js')
, { Bans, Modlogs } = require(__dirname+'/../../db/')
, { checkSchema, lengthBody, numberBody, inArrayBody } = require(__dirname+'/../../lib/input/schema.js');
module.exports = {
@ -44,6 +46,16 @@ module.exports = {
});
}
const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS);
res.locals.bansBoard = req.params.board ? showGlobal ? req.parms.board : { '$eq': req.params.board } : null;
let bans = [];
try {
bans = await Bans.get(req.body.checkedbans, res.locals.bansBoard);
} catch (e) {
return next(e);
}
let amount = 0;
let message;
try {
@ -71,6 +83,27 @@ module.exports = {
default:
throw __('Invalid ban action'); //should never happen anyway
}
if (amount > 0) {
// inserting these into non-public modlogs
const modlogs = bans.map(b => ({
board: Array.isArray(b.board) ? b.board.find(bx => bx != null) : b.board, //TODO: if in future multiple are allowed, update this to use an array
showLinks: true,
postLinks: [],
actions: [ModlogActions.EDIT_BAN],
public: false,
date: new Date(),
showUser: true,
message: message,
user: req.session.user,
ip: {
cloak: res.locals.ip.cloak,
raw: res.locals.ip.raw,
}
}));
await Modlogs.insertMany(modlogs);
}
} catch (err) {
return next(err);
}

@ -29,6 +29,15 @@ module.exports = {
}).toArray();
},
get: (ids, board) => {
return db.find({
'board': board,
'_id': {
'$in': ids
},
}).toArray();
},
upgrade: async (board, ids, upgradeType) => {
const substrProjection = upgradeType === 1
? ['$ip.cloak', 0, 16]
@ -173,7 +182,7 @@ module.exports = {
'board': board,
'_id': {
'$in': ids
}
},
});
},

@ -7,10 +7,11 @@ module.exports = {
db,
getDates: (board) => {
getDates: (board, publicOnly=true) => {
return db.aggregate([
{
'$match': {
...(publicOnly ? { 'public': true } : {}),
'board': board
}
},
@ -70,7 +71,7 @@ module.exports = {
return db.countDocuments(filter);
},
findBetweenDate: (board, start, end) => {
findBetweenDate: (board, start, end, publicOnly=true) => {
const startDate = Mongo.ObjectId.createFromTime(Math.floor(start.getTime()/1000));
const endDate = Mongo.ObjectId.createFromTime(Math.floor(end.getTime()/1000));
return db.find({
@ -78,7 +79,8 @@ module.exports = {
'$gte': startDate,
'$lte': endDate
},
'board': board._id
'board': board._id,
...(publicOnly ? { 'public': true } : {}),
}, {
projection: {
'ip': 0,

@ -27,4 +27,9 @@ module.exports = Object.seal(Object.freeze(Object.preventExtensions({
STICKY: 'Sticky',
CYCLE: 'Cycle',
EDIT_BAN: 'Edit Ban',
SETTINGS: 'Settings',
CREATE_BOARD: 'Create Board',
DELETE_BOARD: 'Delete Board',
})));

@ -44,6 +44,9 @@ class Permission extends BigBitfield {
} else if (this.get(Permissions.MANAGE_BOARD_OWNER)) { //BOs and "global staff"
this.setAll(Permissions._MANAGE_BOARD_BITS);
}
if (this.get(Permissions.MANAGE_GLOBAL_BANS)) {
this.set(Permissions.VIEW_BOARD_GLOBAL_BANS);
}
}
}

@ -33,7 +33,9 @@ const Permissions = Object.seal(Object.freeze(Object.preventExtensions({
MANAGE_BOARD_SETTINGS: 24,
MANAGE_BOARD_CUSTOMISATION: 25,
MANAGE_BOARD_STAFF: 26,
_MANAGE_BOARD_BITS: [20,21,22,23,24,25,26],
_MANAGE_BOARD_BITS: [20,21,22,23,24,25,26], //bits that can be set by a BO and partial bitfield will be stored in board staff object
VIEW_BOARD_GLOBAL_BANS: 30,
USE_MARKDOWN_PINKTEXT: 35,
USE_MARKDOWN_GREENTEXT: 36,
@ -85,6 +87,7 @@ const Metadata = Object.seal(Object.freeze(Object.preventExtensions({
[Permissions.MANAGE_BOARD_SETTINGS]: { label: 'Settings', desc: 'Access board settings. Ability to change any settings. Settings page will show transfer/delete forms for those with "Board Owner" permission.' },
[Permissions.MANAGE_BOARD_CUSTOMISATION]: { label: 'Customisation', desc: 'Access to board assets and custompages. Ability to upload, create, edit, delete.' },
[Permissions.MANAGE_BOARD_STAFF]: { label: 'Staff', desc: 'Access to staff management, and ability to add or remove permissions from others. Can only be given by somebody else with "Board Owner" permission. Use with caution!', parent: Permissions.MANAGE_BOARD_OWNER },
[Permissions.VIEW_BOARD_GLOBAL_BANS]: { label: 'View Board Global Bans', desc: 'Ability to view global bans on board modlog pages if the banned post originated from that board.', parent: Permissions.ROOT },
[Permissions.USE_MARKDOWN_PINKTEXT]: { title: 'Post styling', label: 'Pinktext', desc: 'Use pinktext' },
[Permissions.USE_MARKDOWN_GREENTEXT]: { label: 'Greentext', desc: 'Use greentext' },

@ -18,10 +18,11 @@ module.exports = async (req, res, globalFilter, hitFilter, filterMode,
'redirect': redirect
});
} else {
const banBoard = globalFilter ? null : res.locals.board._id;
const banBoard = globalFilter ? [null, res.locals.board._id] : res.locals.board._id;
const banDate = new Date();
const banExpiry = new Date(filterBanDuration + banDate.getTime());
const ban = {
'global': globalFilter ? true : false,
'ip': {
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw,

@ -1427,5 +1427,6 @@
"Deleted %n records.": {
"one": "Deleted %s record.",
"other": "Deleted %n records."
}
},
"Created board /%s/": "Created board /%s/"
}

@ -1426,5 +1426,6 @@
"Deleted %n records.": {
"one": "Deleted %s record.",
"other": "Deleted %n records."
}
},
"Created board /%s/": "Created board /%s/"
}

@ -1430,5 +1430,6 @@
"Deleted %n records.": {
"one": "Deleted %s record.",
"other": "Deleted %n records."
}
},
"Created board /%s/": "Created board /%s/"
}

@ -1430,5 +1430,6 @@
"Deleted %n records.": {
"one": "Deleted %s record.",
"other": "Deleted %n records."
}
},
"Created board /%s/": "Created board /%s/"
}

@ -1501,5 +1501,6 @@
"Deleted %n records.": {
"one": "Deleted %s record.",
"other": "Deleted %n records."
}
},
"Created board /%s/": "Created board /%s/"
}

@ -0,0 +1,12 @@
'use strict';
module.exports = async(db) => {
console.log('Updating modlogs to add public flag');
await db.collection('modlog').updateMany({}, {
'$set': {
'public': true,
},
});
};

@ -0,0 +1,24 @@
'use strict';
module.exports = async(db) => {
console.log('Updating all bans to have new global true/false flag');
await db.collection('bans').updateMany({
board: null,
}, {
'$set': {
'global': true,
},
});
await db.collection('bans').updateMany({
board: {
'$ne': null,
},
}, {
'$set': {
'global': false,
},
});
};

@ -110,7 +110,7 @@ module.exports = async (req, res, next) => {
if (deleting) {
//OP delete protection. for old OPs or with a lot of replies
if (!isStaffOrGlobal) {
if (!isStaffOrGlobal) { //TODO: make this use a permission bit
const { deleteProtectionAge, deleteProtectionCount } = res.locals.board.settings;
if (deleteProtectionAge > 0 || deleteProtectionCount > 0) {
const protectedThread = res.locals.posts.some(p => {
@ -330,8 +330,9 @@ module.exports = async (req, res, next) => {
//per board actions, all actions combined to one event
modlog[post.board] = {
showLinks: !deleting,
postLinks: [],
postLinks: [], //TODO: rename this to just "links"
actions: modlogActions,
public: true,
date: logDate,
showUser: !req.body.hide_name || logUser === null ? true : false,
message: message,

@ -16,7 +16,7 @@ module.exports = async (req, res) => {
const bans = [];
if (req.body.ban || req.body.global_ban) {
const banBoard = req.body.global_ban ? null : req.params.board;
const banBoard = req.body.global_ban ? [null, req.params.board] : req.params.board;
const ipPosts = res.locals.posts.reduce((acc, post) => {
if (!acc[post.ip.cloak]) {
acc[post.ip.cloak] = [];
@ -55,6 +55,7 @@ module.exports = async (req, res) => {
.join('.');
}
bans.push({
'global': req.body.global_ban != null,
'range': banRange,
'ip': banIp,
'reason': banReason,
@ -73,7 +74,7 @@ module.exports = async (req, res) => {
}
if (req.body.report_ban || req.body.global_report_ban) {
const banBoard = req.body.global_report_ban ? null : req.params.board;
const banBoard = req.body.global_report_ban ? [null, req.params.board] : req.params.board;
res.locals.posts.forEach(post => {
let ips = [];
if (req.body.report_ban) {
@ -95,6 +96,7 @@ module.exports = async (req, res) => {
ips = ips.filter(n => n);
[...new Set(ips)].forEach(ip => {
bans.push({
'global': req.body.global_report_ban != null,
'range': 0,
'ip': ip,
'reason': banReason,
@ -106,6 +108,7 @@ module.exports = async (req, res) => {
allowAppeal,
'appeal': null,
'showUser': !req.body.hide_name,
'note': null,
'seen': false
});
});

@ -1,6 +1,7 @@
'use strict';
const { Boards, Posts } = require(__dirname+'/../../db/')
const { Boards, Posts, Modlogs } = require(__dirname+'/../../db/')
, ModlogActions = require(__dirname+'/../../lib/input/modlogactions.js')
, { debugLogs } = require(__dirname+'/../../configs/secrets.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, config = require(__dirname+'/../../lib/misc/config.js')
@ -223,6 +224,22 @@ module.exports = async (req, res) => {
}
});
promises.push(Modlogs.insertOne({
board: req.params.board,
showLinks: true,
postLinks: [],
actions: [ModlogActions.SETTINGS],
public: false,
date: new Date(),
showUser: true,
message: __('Updated settings.'),
user: req.session.user,
ip: {
cloak: res.locals.ip.cloak,
raw: res.locals.ip.raw,
}
}));
//finish the promises in parallel e.g. removing files
if (promises.length > 0) {
await Promise.all(promises);

@ -1,6 +1,7 @@
'use strict';
const { Boards, Accounts } = require(__dirname+'/../../db/')
const { Boards, Accounts, Modlogs } = require(__dirname+'/../../db/')
, ModlogActions = require(__dirname+'/../../lib/input/modlogactions.js')
, { Binary } = require(__dirname+'/../../db/db.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, roleManager = require(__dirname+'/../../lib/permission/rolemanager.js')
@ -65,6 +66,21 @@ module.exports = async (req, res) => {
};
await Promise.all([
Modlogs.insertOne({
board: null,
showLinks: true,
postLinks: [{ board: uri }],
actions: [ModlogActions.CREATE_BOARD],
public: false,
date: new Date(),
showUser: true,
message: __('Created board /%s/', uri),
user: req.session.user,
ip: {
cloak: res.locals.ip.cloak,
raw: res.locals.ip.raw,
}
}),
Boards.insertOne(newBoard),
Accounts.addOwnedBoard(owner, uri),
ensureDir(`${uploadDirectory}/html/${uri}`),

@ -2,8 +2,8 @@
const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req) => {
module.exports = async (req, res) => {
return Bans.denyAppeal(req.params.board, req.body.checkedbans).then(result => result.modifiedCount);
return Bans.denyAppeal(res.locals.bansBoard, req.body.checkedbans).then(result => result.modifiedCount);
};

@ -2,10 +2,10 @@
const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req) => {
module.exports = async (req, res) => {
//New ban expiry date is current date + ban_duration. Not based on the original ban issue date.
const newExpireAt = new Date(Date.now() + req.body.ban_duration);
return Bans.editDuration(req.params.board, req.body.checkedbans, newExpireAt).then(result => result.modifiedCount);
return Bans.editDuration(res.locals.bansBoard, req.body.checkedbans, newExpireAt).then(result => result.modifiedCount);
};

@ -2,9 +2,9 @@
const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req) => {
module.exports = async (req, res) => {
//New ban note.
return Bans.editNote(req.params.board, req.body.checkedbans, req.body.ban_note).then(result => result.modifiedCount);
return Bans.editNote(res.locals.bansBoard, req.body.checkedbans, req.body.ban_note).then(result => result.modifiedCount);
};

@ -168,6 +168,7 @@ todo: handle some more situations
thread: post.thread,
}],
actions: [ModlogActions.EDIT],
public: true, //TODO: take an optional checkbox also controlled by a BO/global delegated perm
date: new Date(),
showUser: req.body.hide_name ? false : true,
message: req.body.log_message || null,

@ -2,8 +2,10 @@
const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req) => {
module.exports = async (req, res) => {
return Bans.removeMany(req.params.board, req.body.checkedbans).then(result => result.deletedCount);
const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS);
const bansBoard = req.params.board ? showGlobal ? req.parms.board : { '$eq': req.params.board } : null;
return Bans.removeMany(bansBoard, req.body.checkedbans).then(result => result.deletedCount);
};

@ -2,9 +2,9 @@
const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req) => {
module.exports = async (req, res) => {
const nReturned = await Bans.upgrade(req.params.board, req.body.checkedbans, req.body.upgrade)
const nReturned = await Bans.upgrade(res.locals.bansBoard, req.body.checkedbans, req.body.upgrade)
.then(explain => {
if (explain && explain.stages) {
return explain.stages[0].nReturned;

@ -7,7 +7,9 @@ module.exports = async (req, res, next) => {
let bans;
try {
bans = await Bans.getBoardBans(req.params.board);
const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS);
const bansBoard = showGlobal ? req.params.board : { '$eq': req.params.board };
bans = await Bans.getBoardBans(bansBoard);
} catch (err) {
return next(err);
}

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "jschan",
"version": "1.5.0",
"version": "1.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jschan",
"version": "1.5.0",
"version": "1.6.0",
"license": "AGPL-3.0-only",
"dependencies": {
"@fatchan/express-fileupload": "^1.4.5",

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "1.5.0",
"migrateVersion": "1.4.1",
"version": "1.6.0",
"migrateVersion": "1.5.0",
"description": "",
"main": "server.js",
"dependencies": {

@ -3,13 +3,13 @@ include ./post.pug
mixin ban(ban, banpage)
tr
td
if !banpage || (ban.appeal == null && ban.allowAppeal === true)
if (!board || !ban.global) && (!banpage || (ban.appeal == null && ban.allowAppeal === true))
input.post-check(type='checkbox', name='checkedbans' value=ban._id)
td
if ban.board
a(href=`/${ban.board}/`) /#{ban.board}/
if ban.global === true
| #{__('Global')} (#{ban.board.filter(n => n).join(', ')})
else
| #{__('Global')}
a(href=`/${ban.board}/`) /#{ban.board}/
td= ban.reason
- const ip = viewRawIp === true ? ban.ip.raw : ban.ip.cloak;
if viewRawIp === true

@ -1,2 +1,6 @@
mixin postlink(log, postLink, manageLink=false)
a.quote(href=`/${postLink.board || log.board}/${manageLink ? 'manage/' : ''}thread/${postLink.thread || postLink.postId}.html#${postLink.postId}`) &gt;&gt;#{postLink.postId}
if postLink.thread || postLink.postId
a.quote(href=`/${postLink.board || log.board}/${manageLink ? 'manage/' : ''}thread/${postLink.thread || postLink.postId}.html#${postLink.postId}`) &gt;&gt;#{postLink.postId}
else
a.quote(href=`/${postLink.board || log.board}/${manageLink ? 'manage/' : ''}index.html`) &gt;&gt;&gt;/#{postLink.board}/

@ -35,16 +35,19 @@ block content
th #{__('User')}
th #{__('IP')}
th #{__('Actions')}
th #{__('Posts')}
th #{__('Links')}
th #{__('Log Message')}
for log in logs
tr
- const logDate = new Date(log.date);
td: time.reltime(datetime=logDate.toISOString()) #{logDate.toLocaleString(pageLanguage, {hourCycle:'h23'})}
td
a(href=`/${log.board}/index.html`) /#{log.board}/
|
a(href=`?uri=${log.board}`) [+]
if log.board
a(href=`/${log.board}/index.html`) /#{log.board}/
|
a(href=`?uri=${log.board}`) [+]
else
| -
td
if log.user !== __('Unregistered User')
a(href=`accounts.html?username=${log.user}`) #{log.user}

@ -30,7 +30,7 @@ block content
th #{__('User')}
th #{__('IP')}
th #{__('Actions')}
th #{__('Posts')}
th #{__('Links')}
th #{__('Log Message')}
for log in logs
tr

@ -19,7 +19,7 @@ block content
th #{__('Date')}
th #{__('User')}
th #{__('Actions')}
th #{__('Posts')}
th #{__('Links')}
th #{__('Log Message')}
for log in logs
tr

Loading…
Cancel
Save