Merge branch 'ip-cloaking' into develop

indiachan-spamvector
Thomas Lynch 2 years ago
commit 6d01d90753
  1. 3
      CHANGELOG.md
  2. 2
      controllers/forms/boardsettings.js
  3. 2
      controllers/forms/editpost.js
  4. 12
      db/bans.js
  5. 66
      db/posts.js
  6. 2
      gulpfile.js
  7. 2
      helpers/captcha/verify.js
  8. 4
      helpers/checks/spamcheck.js
  9. 5
      helpers/decodequeryip.js
  10. 17
      helpers/processip.js
  11. 77
      migrations/0.4.0.js
  12. 6
      models/forms/actionhandler.js
  13. 2
      models/forms/appeal.js
  14. 20
      models/forms/banposter.js
  15. 4
      models/forms/editpost.js
  16. 12
      models/forms/makepost.js
  17. 2
      models/forms/reportpost.js
  18. 2
      models/pages/captcha.js
  19. 11
      models/pages/globalmanage/logs.js
  20. 2
      models/pages/manage/recent.js
  21. 594
      package-lock.json
  22. 4
      package.json
  23. 8
      schedules/tasks/ips.js
  24. 7
      views/mixins/ban.pug
  25. 4
      views/mixins/post.pug
  26. 2
      views/mixins/report.pug
  27. 2
      views/pages/globalmanagelogs.pug
  28. 2
      views/pages/managelogs.pug
  29. 2
      views/pages/managerecent.pug

@ -1,3 +1,6 @@
### 0.4.0
- Hashed IPs now use an irc-style "cloaking".
### 0.3.3
- Minor bugfix to filenames of expanded images being incorrectly truncated in some circumstances, when "image loading bars" is enabled.

@ -85,7 +85,7 @@ module.exports = {
if (res.locals.permLevel > 1) { //if not global staff or above
const ratelimitBoard = await Ratelimits.incrmentQuota(req.params.board, 'settings', rateLimitCost.boardSettings); //2 changes a minute
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.single, 'settings', rateLimitCost.boardSettings));
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'settings', rateLimitCost.boardSettings));
if (ratelimitBoard > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited',

@ -43,7 +43,7 @@ module.exports = {
if (res.locals.permLevel > 1) { //if not global staff or above
const ratelimitUser = await Ratelimits.incrmentQuota(req.session.user, 'edit', rateLimitCost.editPost);
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.single, 'edit', rateLimitCost.editPost));
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'edit', rateLimitCost.editPost));
if (ratelimitUser > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited',

@ -10,15 +10,19 @@ module.exports = {
find: (ip, board) => {
let ipQuery;
if (typeof ip === 'object') { //object with hash and ranges in bancheck
if (typeof ip === 'object') {
ipQuery = {
'$in': [ip.single, ip.qrange, ip.hrange] //gets single and range ban in 1 query
'$in': [
ip.cloak, //full ip
ip.cloak.split('.').slice(0,2).join('.'), //qrange
ip.cloak.split('.').slice(0,1).join('.'), //hrange
],
}
} else {
ipQuery = ip;
}
return db.find({
'ip.single': ipQuery,
'ip.cloak': ipQuery,
'board': {
'$in': [board, null]
}
@ -42,7 +46,7 @@ module.exports = {
'_id': {
'$in': ids
},
'ip.single': ip,
'ip.cloak': ip,
'allowAppeal': true,
'appeal': null
}, {

@ -1,6 +1,7 @@
'use strict';
const Mongo = require(__dirname+'/db.js')
, { isIP } = require('net')
, Boards = require(__dirname+'/boards.js')
, Stats = require(__dirname+'/stats.js')
, db = Mongo.db.collection('posts')
@ -35,10 +36,12 @@ module.exports = {
} else {
projection['globalreports'] = 0;
}
if (ip instanceof RegExp) {
query['ip.single'] = ip;
} else if (typeof ip === 'string') {
query['ip.raw'] = ip;
if (ip != null) {
if (isIP(ip)) {
query['ip.raw'] = ip;
} else {
query['ip.cloak'] = ip;
}
}
if (permLevel > config.get.ipHashPermLevel) {
projection['ip.raw'] = 0;
@ -54,21 +57,6 @@ module.exports = {
}).sort({
'_id': -1
}).skip(offset).limit(limit).toArray();
posts.forEach(p => {
//kill me
p.ip.single = p.ip.single.slice(-10);
p.ip.qrange = p.ip.qrange.slice(-10);
p.ip.hrange = p.ip.hrange.slice(-10);
if (board) {
p.reports.forEach(r => {
r.ip.single = r.ip.single.slice(-10);
});
} else {
p.globalreports.forEach(r => {
r.ip.single = r.ip.single.slice(-10);
});
}
});
return posts;
},
@ -479,7 +467,7 @@ module.exports = {
//insert the post itself
const postMongoId = await db.insertOne(data).then(result => result.insertedId); //_id of post
const statsIp = (config.get.statsCountAnonymizers === false && res.locals.anonymizer === true) ? null : data.ip.single;
const statsIp = (config.get.statsCountAnonymizers === false && res.locals.anonymizer === true) ? null : data.ip.cloak;
await Stats.updateOne(board._id, statsIp, data.thread == null);
//add backlinks to the posts this post quotes
@ -545,14 +533,6 @@ module.exports = {
},
'board': board
}, { projection }).toArray();
posts.forEach(p => {
p.ip.single = p.ip.single.slice(-10);
p.ip.qrange = p.ip.qrange.slice(-10);
p.ip.hrange = p.ip.hrange.slice(-10);
p.reports.forEach(r => {
r.ip.single = r.ip.single.slice(-10);
});
});
return posts;
},
@ -571,26 +551,20 @@ module.exports = {
'$exists': true
}
}
if (ip instanceof RegExp) {
query['$or'] = [
{ 'ip.single': ip },
{ 'globalreports.ip.single': ip }
];
} else if (typeof ip === 'string') {
query['$or'] = [
{ 'ip.raw': ip },
{ 'globalreports.ip.raw': ip }
];
if (ip != null) {
if (isIP(ip)) {
query['$or'] = [
{ 'ip.raw': ip },
{ 'globalreports.ip.raw': ip }
];
} else {
query['$or'] = [
{ 'ip.cloak': ip },
{ 'globalreports.ip.cloak': ip }
];
}
}
const posts = await db.find(query, { projection }).skip(offset).limit(limit).toArray();
posts.forEach(p => {
p.ip.single = p.ip.single.slice(-10);
p.ip.qrange = p.ip.qrange.slice(-10);
p.ip.hrange = p.ip.hrange.slice(-10);
p.globalreports.forEach(r => {
r.ip.single = r.ip.single.slice(-10);
});
});
return posts;
},

@ -192,7 +192,7 @@ async function wipe() {
await CustomPages.db.createIndex({ 'board': 1, 'page': 1 }, { unique: true })
await Modlogs.db.createIndex({ 'board': 1 })
await Files.db.createIndex({ 'count': 1 })
await Bans.db.createIndex({ 'ip.single': 1 , 'board': 1 })
await Bans.db.createIndex({ 'ip.cloak': 1 , 'board': 1 })
await Bans.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }) //custom expiry, i.e. it will expire when current date > than this date
await Bypass.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 })
await Captchas.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 300 }) //captchas valid for 5 minutes

@ -50,7 +50,7 @@ module.exports = async (req, res, next) => {
//for builtin captchas, clear captchaid cookie, delete file and reset quota
res.clearCookie('captchaid');
await Promise.all([
!res.locals.anonymizer && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
!res.locals.anonymizer && Ratelimits.resetQuota(res.locals.ip.cloak, 'captcha'),
remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
]);
}

@ -57,7 +57,7 @@ module.exports = async (req, res) => {
'_id': {
'$gt': sameContentSameIpMongoId
},
'ip.single': res.locals.ip.single,
'ip.cloak': res.locals.ip.cloak,
'$or': contentOr
});
}
@ -69,7 +69,7 @@ module.exports = async (req, res) => {
'_id': {
'$gt': anyContentSameIpMongoId
},
'ip.single': res.locals.ip.single
'ip.cloak': res.locals.ip.cloak
})
}

@ -8,10 +8,9 @@ module.exports = (query, permLevel) => {
const { ipHashPermLevel } = config.get;
if (query.ip && typeof query.ip === 'string') {
const decoded = decodeURIComponent(query.ip);
if (permLevel <= ipHashPermLevel && (isIP(decoded) || decoded.match(/[a-z0-9]{24}/i))) { //if perms to view raw ip or bypass, allow querying
if (permLevel <= ipHashPermLevel || !isIP(decoded)) {
//if they have perm to view raw IP, or its NOT a raw ip, return
return decoded;
} else if (decoded.length === 10) { //otherwise, only allow last 10 char substring
return new RegExp(`${escapeRegExp(decoded)}$`);
}
}
return null; //else, no ip filter

@ -12,10 +12,8 @@ module.exports = (req, res, next) => {
if (res.locals.anonymizer) {
const pseudoIp = res.locals.preFetchedBypassId || req.signedCookies.bypassid;
res.locals.ip = {
raw: pseudoIp,
single: pseudoIp,
qrange: pseudoIp,
hrange: pseudoIp,
raw: `${pseudoIp}.BP`,
cloak: `${pseudoIp}.BP`,
};
return next();
}
@ -31,8 +29,8 @@ module.exports = (req, res, next) => {
zeroElide: false,
zeroPad: false,
});
let qrange = ''
, hrange = '';
let qrange
, hrange;
if (ipKind === 'ipv4') {
qrange = createCIDR(ipStr, 24).toString();
hrange = createCIDR(ipStr, 16).toString();
@ -40,11 +38,10 @@ module.exports = (req, res, next) => {
qrange = createCIDR(ipStr, 64).toString();
hrange = createCIDR(ipStr, 48).toString();
}
const cloak = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`;
res.locals.ip = {
raw: ipHashPermLevel === -1 ? hashIp(ipStr) : ipStr,
single: hashIp(ipStr),
qrange: hashIp(qrange),
hrange: hashIp(hrange),
raw: ipHashPermLevel === -1 ? cloak : ipStr,
cloak,
}
next();
} catch(e) {

@ -0,0 +1,77 @@
'use strict';
const hashIp = require(__dirname+'/../helpers/haship.js')
, { createCIDR, parse } = require('ip6addr')
, config = require(__dirname+'/../config.js');
module.exports = async(db, redis) => {
const postIps = await db.collection('posts').distinct('ip.raw');
const logIps = await db.collection('modlog').distinct('ip.raw');
const banIps = await db.collection('bans').distinct('ip.raw');
const allDistinctIps = postIps.concat(logIps).concat(banIps);
const bulkWrites = allDistinctIps.map(ip => {
const ipSet = {};
try {
const ipParsed = parse(ip);
const ipKind = ipParsed.kind();
const ipStr = ipParsed.toString({
format: ipKind === 'ipv4' ? 'v4' : 'v6',
zeroElide: false,
zeroPad: false,
});
let qrange
, hrange;
if (ipKind === 'ipv4') {
qrange = createCIDR(ipStr, 24).toString();
hrange = createCIDR(ipStr, 16).toString();
} else {
qrange = createCIDR(ipStr, 64).toString();
hrange = createCIDR(ipStr, 48).toString();
}
ipSet['ip.cloak'] = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`;
} catch (e) {
//-1 old "iphashpermlevel" or bypass ids, just shorten them
const shortenedOldHash = `${hashIp(ip).slice(-10)}.IP`;
ipSet['ip.raw'] = shortenedOldHash;
ipSet['ip.cloak'] = shortenedOldHash;
}
return {
'updateMany': {
'filter': {
'ip.raw': ip
},
'update': {
'$unset': {
'ip.single': '',
'ip.qrange': '',
'ip.hrange': '',
},
'$set': ipSet,
}
}
};
});
console.log('adjusting ip in modlogs, bans and posts');
//the bulkwrites should work for ip, bans, and logs
await db.collection('posts').bulkWrite(bulkWrites);
await db.collection('modlog').bulkWrite(bulkWrites);
await db.collection('bans').bulkWrite(bulkWrites);
console.log('removing saved posts inside bans');
await db.collection('bans').updateMany({}, {
'$set':{
'posts': null,
}
});
console.log('clearing reports')
await db.collection('posts').updateMany({}, {
'$set':{
'reports': [],
'globalreports': [],
}
});
//drop old ban indexes that indexed ip.single, then recreate
console.log('recreating bans indexes');
await db.collection('bans').dropIndexes();
await db.collection('bans').createIndex({ 'ip.cloak': 1 , 'board': 1 });
await db.collection('bans').createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 });
}

@ -118,13 +118,13 @@ module.exports = async (req, res, next) => {
}
const postsBefore = res.locals.posts.length;
if (req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread) {
const deletePostIps = res.locals.posts.map(x => x.ip.single);
const deletePostIps = res.locals.posts.map(x => x.ip.cloak);
const deletePostMongoIds = res.locals.posts.map(x => x._id)
let query = {
'_id': {
'$nin': deletePostMongoIds
},
'ip.single': {
'ip.cloak': {
'$in': deletePostIps
}
};
@ -314,7 +314,7 @@ module.exports = async (req, res, next) => {
message: message,
user: logUser,
ip: {
single: res.locals.ip.single,
cloak: res.locals.ip.cloak,
raw: res.locals.ip.raw
}
};

@ -4,6 +4,6 @@ const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req, res, next) => {
return Bans.appeal(res.locals.ip.single, req.body.checkedbans, req.body.message).then(r => r.modifiedCount);
return Bans.appeal(res.locals.ip.cloak, req.body.checkedbans, req.body.message).then(r => r.modifiedCount);
}

@ -16,25 +16,31 @@ module.exports = async (req, res, next) => {
if (req.body.ban || req.body.global_ban) {
const banBoard = req.body.global_ban ? null : req.params.board;
const ipPosts = res.locals.posts.reduce((acc, post) => {
if (!acc[post.ip.single]) {
acc[post.ip.single] = [];
if (!acc[post.ip.cloak]) {
acc[post.ip.cloak] = [];
}
acc[post.ip.single].push(post);
acc[post.ip.cloak].push(post);
return acc;
}, {});
for (let ip in ipPosts) {
const thisIpPosts = ipPosts[ip];
let type = 'single';
let banIp = {
single: ip,
raw: thisIpPosts[0].ip.raw
cloak: thisIpPosts[0].ip.cloak,
raw: thisIpPosts[0].ip.raw,
};
if (req.body.ban_h) {
type = 'half';
banIp.single = thisIpPosts[0].ip.hrange;
banIp.cloak = thisIpPosts[0].ip.cloak
.split('.')
.slice(0,1)
.join('.');
} else if (req.body.ban_q) {
type = 'quarter';
banIp.single = thisIpPosts[0].ip.qrange;
banIp.cloak = thisIpPosts[0].ip.cloak
.split('.')
.slice(0,2)
.join('.');
}
bans.push({
type,

@ -51,7 +51,7 @@ todo: handle some more situations
const banExpiry = new Date(globalSettings.filterBanDuration + banDate.getTime());
const ban = {
'ip': {
'single': res.locals.ip.single,
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw,
},
'type': 'single',
@ -163,7 +163,7 @@ todo: handle some more situations
message: req.body.log_message || null,
user: req.session.user,
ip: {
single: res.locals.ip.single,
cloak: res.locals.ip.cloak,
raw: res.locals.ip.raw,
}
});

@ -144,7 +144,7 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')).
const banExpiry = new Date(useFilterBanDuration + banDate.getTime());
const ban = {
'ip': {
'single': res.locals.ip.single,
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw,
},
'type': 'single',
@ -612,15 +612,15 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')).
//dont emit thread to this socket, because the room onyl exists when the thread is open
Socketio.emitRoom(`${res.locals.board._id}-${data.thread}`, 'newPost', projectedPost);
}
const { raw, single } = data.ip;
const { raw, cloak } = data.ip;
//but emit it to manage pages because they need to get all posts through socket including thread
Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw: null } });
Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw: null } });
Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { cloak, raw: null } });
Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { cloak, raw: null } });
if (ipHashPermLevel > -1) {
//small optimisation for boards where this is manually set to -1 for privacy, no need to emit to rooms that cant be accessed
//even if they are empty it will create extra communication noise in redis, socket adapter, etc.
Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw } });
Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw } });
Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { cloak, raw } });
Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { cloak, raw } });
}
//now add other pages to be built in background

@ -9,7 +9,7 @@ module.exports = (req, res) => {
'reason': req.body.report_reason,
'date': new Date(),
'ip': {
'single': res.locals.ip.single,
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw
}
}

@ -21,7 +21,7 @@ module.exports = async (req, res, next) => {
let maxAge = 5*60*1000;
try {
if (!res.locals.anonymizer) {
const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.single, 'captcha', rateLimitCost.captcha);
const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'captcha', rateLimitCost.captcha);
if (ratelimit > 100) {
return res.status(429).redirect('/file/ratelimit.png');
}

@ -3,6 +3,7 @@
const { Modlogs } = require(__dirname+'/../../../db/')
, pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js')
, decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js')
, { isIP } = require('net')
, limit = 50;
module.exports = async (req, res, next) => {
@ -19,10 +20,12 @@ module.exports = async (req, res, next) => {
filter.board = uri;
}
const ipMatch = decodeQueryIP(req.query, res.locals.permLevel);
if (ipMatch instanceof RegExp) {
filter['ip.single'] = ipMatch;
} else if (typeof ipMatch === 'string') {
filter['ip.raw'] = ipMatch;
if (ipMatch != null) {
if (isIP(ipMatch)) {
filter['ip.raw'] = ipMatch;
} else {
filter['ip.cloak'] = ipMatch;
}
}
let logs, maxPage;

@ -13,7 +13,7 @@ module.exports = async (req, res, next) => {
if (postId && +postId === parseInt(postId) && Number.isSafeInteger(+postId)) {
const fetchedPost = await Posts.getPost(req.params.board, +postId, true);
if (fetchedPost) {
ip = decodeQueryIP({ ip: fetchedPost.ip.single.slice(-10) }, res.locals.permlevel);
ip = decodeQueryIP({ ip: fetchedPost.ip.cloak }, res.locals.permlevel);
}
}

594
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "0.3.3",
"migrateVersion": "0.2.0",
"version": "0.4.0",
"migrateVersion": "0.4.0",
"description": "",
"main": "server.js",
"dependencies": {

@ -22,7 +22,7 @@ module.exports = {
$ne: true
}
}).forEach(post => {
const randomIP = createHash('sha256').update(tempIpHashSecret + post.ip.single).digest('base64');
const randomIP = createHash('sha256').update(tempIpHashSecret + post.ip.cloak).digest('base64');
bulkWrites.push({
updateOne: {
filter: {
@ -31,10 +31,8 @@ module.exports = {
update: {
$set: {
'ip.pruned': true,
'ip.raw': randomIP,
'ip.single': randomIP,
'ip.qrange': randomIP,
'ip.hrange': randomIP,
'ip.raw': `${randomIP.slice(-10)}.PRUNED`,
'ip.cloak': `${randomIP.slice(-10)}.PRUNED`,
}
}
}

@ -10,8 +10,11 @@ mixin ban(ban, banpage)
else
| Global
td= ban.reason
- const ip = permLevel > ipHashPermLevel ? ban.ip.single.slice(-10) : ban.ip.raw;
td #{ip}
- const ip = permLevel > ipHashPermLevel ? ban.ip.cloak : ban.ip.raw;
if permLevel > ipHashPermLevel
td #{ip}#{ban.type === 'half' ? '.*.*' : (ban.type === 'quarter' ? '.*' : '')}
else
td #{ip}
td #{ban.type}
td #{(!banpage || ban.showUser === true) ? ban.issuer : 'Hidden User'}
- const banDate = new Date(ban.date);

@ -20,12 +20,12 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false, overboar
input.post-check(type='checkbox', name='checkedposts' value=post.postId)
|
if manage
- const ip = permLevel > ipHashPermLevel ? post.ip.single.slice(-10) : post.ip.raw;
- const ip = permLevel > ipHashPermLevel ? post.ip.cloak : post.ip.raw;
a.bold(href=`${upLevel ? '../' : ''}recent.html?ip=${encodeURIComponent(ip)}`) [#{ip}]
else if modview
a.bold(href=`${upLevel ? '../' : ''}recent.html?postid=${post.postId}`) [+]
else if globalmanage
- const ip = permLevel > ipHashPermLevel ? post.ip.single.slice(-10) : post.ip.raw;
- const ip = permLevel > ipHashPermLevel ? post.ip.cloak : post.ip.raw;
a.bold(href=`?ip=${encodeURIComponent(ip)}`) [#{ip}]
|
if !post.thread

@ -2,7 +2,7 @@ mixin report(r, manage=false)
.reports.post-container
input.post-check(type='checkbox', name='checkedreports' value=r.id)
|
- const ip = permLevel > ipHashPermLevel ? r.ip.single.slice(-10) : r.ip.raw;
- const ip = permLevel > ipHashPermLevel ? r.ip.cloak : r.ip.raw;
a.bold(href=`${manage ? 'recent.html' : ''}?ip=${encodeURIComponent(ip)}`) [#{ip}]
|
- const reportDate = new Date(r.date);

@ -52,7 +52,7 @@ block content
|
a(href=`?username=${log.user}`) [+]
td
- const logIp = permLevel > ipHashPermLevel ? log.ip.single.slice(-10) : log.ip.raw;
- const logIp = permLevel > ipHashPermLevel ? log.ip.cloak : log.ip.raw;
a(href=`recent.html?ip=${encodeURIComponent(logIp)}`) #{logIp}
|
a(href=`?ip=${encodeURIComponent(logIp)}`) [+]

@ -40,7 +40,7 @@ block content
|
a(href=`?username=${log.user}`) [+]
td
- const logIp = permLevel > ipHashPermLevel ? log.ip.single.slice(-10) : log.ip.raw;
- const logIp = permLevel > ipHashPermLevel ? log.ip.cloak : log.ip.raw;
| #{logIp}
td #{log.actions}
td

@ -23,7 +23,7 @@ block content
if posts.length === 0
p No posts.
else
- const ip = permLevel > ipHashPermLevel ? posts[0].ip.single.slice(-10) : posts[0].ip.raw;
- const ip = permLevel > ipHashPermLevel ? posts[0].ip.cloak : posts[0].ip.raw;
if postId || (queryIp && queryIp === ip)
h4.no-m-p Post history for #{ip}
|

Loading…
Cancel
Save