Files
habitica/website/server/models/blocker.js
Phillip Thelen 12773d539e Add interface to block ip-addresses or clients due to abuse (#15484)
* Read IP blocks from database

* begin building general blocking solution

* add new frontend files

* Add UI for managing blockers

* correctly reset local data after creating blocker

* Tweak wording

* Add UI for managing blockers

* restructure admin pages

* improve test coverage

* Improve blocker UI

* add blocker to block emails from registration

* lint fix

* fix

* lint fixes

* fix import

* add new permission for managing blockers

* improve permission check

* fix managing permissions from admin

* improve navbar display for non fullAccess admin

* update block error strings

* lint fix

* add option to errorHandler to skip logging

* validate blocker value during input

* improve blocker form display

* chore(subproj): reconcile habitica-images

* fix(scripts): use same Mongo version for dev/test

* fix(whitespace): eof

* documentation improvements

* remove nconf import

* remove old test

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2025-08-06 15:08:07 -05:00

104 lines
2.5 KiB
JavaScript

/* eslint-disable camelcase */
import mongoose from 'mongoose';
import EventEmitter from 'events';
import baseModel from '../libs/baseModel';
export const blockTypes = [
'ipaddress',
'email',
'client',
];
export const blockArea = [
'full',
'payments',
];
export const schema = new mongoose.Schema({
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: ['administrator', 'system', 'worker'], default: 'administrator', // who created the block
},
reason: {
$type: String, required: false, // e.g. 'abusive behavior'
},
}, {
strict: true,
minimize: false, // So empty objects are returned
typeKey: '$type', // So that we can use fields named `type`
});
schema.plugin(baseModel, {
timestamps: true,
});
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);