mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
Read IP blocks from database
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
Forbidden,
|
Forbidden,
|
||||||
} from '../libs/errors';
|
} from '../libs/errors';
|
||||||
import { apiError } from '../libs/apiError';
|
import { apiError } from '../libs/apiError';
|
||||||
|
import { model as Blocker } from '../models/blocker';
|
||||||
|
|
||||||
// Middleware to block unwanted IP addresses
|
// Middleware to block unwanted IP addresses
|
||||||
|
|
||||||
@@ -22,10 +23,28 @@ const blockedIps = BLOCKED_IPS_RAW
|
|||||||
.filter(blockedIp => Boolean(blockedIp))
|
.filter(blockedIp => Boolean(blockedIp))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
Blocker.watchBlockers({
|
||||||
|
type: 'ipaddress',
|
||||||
|
area: 'full',
|
||||||
|
}, {
|
||||||
|
initial: true,
|
||||||
|
}).on('change', async change => {
|
||||||
|
const { operation, blocker } = change;
|
||||||
|
if (operation === 'add') {
|
||||||
|
if (blocker.value && !blockedIps.includes(blocker.value)) {
|
||||||
|
blockedIps.push(blocker.value);
|
||||||
|
}
|
||||||
|
} else if (operation === 'delete') {
|
||||||
|
const index = blockedIps.indexOf(blocker.value);
|
||||||
|
if (index !== -1) {
|
||||||
|
blockedIps.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default function ipBlocker (req, res, next) {
|
export default function ipBlocker (req, res, next) {
|
||||||
// If there are no IPs to block, skip the middleware
|
// If there are no IPs to block, skip the middleware
|
||||||
if (blockedIps.length === 0) return next();
|
if (blockedIps.length === 0) return next();
|
||||||
|
|
||||||
// Is the client IP, req.ip, blocked?
|
// Is the client IP, req.ip, blocked?
|
||||||
const match = blockedIps.find(blockedIp => blockedIp === req.ip) !== undefined;
|
const match = blockedIps.find(blockedIp => blockedIp === req.ip) !== undefined;
|
||||||
|
|
||||||
|
|||||||
113
website/server/models/blocker.js
Normal file
113
website/server/models/blocker.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import validator from 'validator';
|
||||||
|
import baseModel from '../libs/baseModel';
|
||||||
|
|
||||||
|
export const blockTypes = [
|
||||||
|
'ipaddress',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const blockArea = [
|
||||||
|
'full',
|
||||||
|
'payment',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const schema = new mongoose.Schema({
|
||||||
|
id: {
|
||||||
|
$type: String,
|
||||||
|
default: uuid,
|
||||||
|
validate: [v => validator.isUUID(v), 'Invalid uuid for tag.'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
$type: Boolean, default: false, // If true, the block is disabled
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
$type: String, enum: blockTypes, required: true,
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
$type: String, enum: blockArea, default: 'full', // full or payment
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
$type: String, required: true, // e.g. IP address
|
||||||
|
},
|
||||||
|
blockSource: {
|
||||||
|
$type: String, enum: ['user', 'system', 'worker'], default: 'user', // who created the block
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
$type: String, required: false, // e.g. 'abusive behavior'
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
strict: true,
|
||||||
|
minimize: false, // So empty objects are returned
|
||||||
|
_id: false, // use id instead of _id
|
||||||
|
typeKey: '$type', // So that we can use fields named `type`
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.plugin(baseModel, {
|
||||||
|
timestamps: true,
|
||||||
|
noSet: ['_id'],
|
||||||
|
_id: false, // use id instead of _id
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.statics.watchBlockers = function watchBlockers (query, options) {
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
const matchQuery = {
|
||||||
|
$match: {},
|
||||||
|
};
|
||||||
|
if (query) {
|
||||||
|
if (query.type) {
|
||||||
|
matchQuery.$match['fullDocument.type'] = query.type;
|
||||||
|
}
|
||||||
|
if (query.area) {
|
||||||
|
matchQuery.$match['fullDocument.area'] = query.area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.watch([matchQuery], {
|
||||||
|
fullDocument: 'updateLookup',
|
||||||
|
})
|
||||||
|
.on('change', change => {
|
||||||
|
if (!change.fullDocument) {
|
||||||
|
return; // Ignore changes that don't have a fullDocument
|
||||||
|
}
|
||||||
|
if (change.operationType === 'insert' || !change.fullDocument.disabled) {
|
||||||
|
emitter.emit('change', {
|
||||||
|
operation: 'add',
|
||||||
|
blocker: change.fullDocument,
|
||||||
|
});
|
||||||
|
} else if (change.operationType === 'update' && change.fullDocument.disabled) {
|
||||||
|
emitter.emit('change', {
|
||||||
|
operation: 'delete',
|
||||||
|
blocker: change.fullDocument,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', error => {
|
||||||
|
emitter.emit('error', error);
|
||||||
|
});
|
||||||
|
if (options.initial) {
|
||||||
|
const initialQuery = {
|
||||||
|
disabled: false,
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
this.find(initialQuery).then(docs => {
|
||||||
|
for (const doc of docs) {
|
||||||
|
emitter.emit('change', {
|
||||||
|
operation: 'add',
|
||||||
|
blocker: doc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
emitter.emit('error', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return emitter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const model = mongoose.model('Blocker', schema);
|
||||||
Reference in New Issue
Block a user