punishments.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import async from "async";
  2. import { isAdminRequired } from "./hooks";
  3. import moduleManager from "../../index";
  4. const DBModule = moduleManager.modules.db;
  5. const UtilsModule = moduleManager.modules.utils;
  6. const WSModule = moduleManager.modules.ws;
  7. const CacheModule = moduleManager.modules.cache;
  8. const PunishmentsModule = moduleManager.modules.punishments;
  9. CacheModule.runJob("SUB", {
  10. channel: "ip.ban",
  11. cb: data => {
  12. WSModule.runJob("EMIT_TO_ROOM", {
  13. room: "admin.punishments",
  14. args: ["event:admin.punishment.created", { data: { punishment: data.punishment } }]
  15. });
  16. WSModule.runJob("SOCKETS_FROM_IP", { ip: data.ip }, this).then(sockets => {
  17. sockets.forEach(socket => {
  18. socket.disconnect(true);
  19. });
  20. });
  21. }
  22. });
  23. export default {
  24. /**
  25. * Gets punishments, used in the admin punishments page by the AdvancedTable component
  26. *
  27. * @param {object} session - the session object automatically added by the websocket
  28. * @param page - the page
  29. * @param pageSize - the size per page
  30. * @param properties - the properties to return for each punishment
  31. * @param sort - the sort object
  32. * @param queries - the queries array
  33. * @param operator - the operator for queries
  34. * @param cb
  35. */
  36. getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
  37. const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
  38. async.waterfall(
  39. [
  40. // Creates pipeline array
  41. next => next(null, []),
  42. // If a filter or property exists for status, add status property to all documents
  43. (pipeline, next) => {
  44. // Check if a filter with the status property exists
  45. const statusFilterExists = queries.map(query => query.filter.property).indexOf("status") !== -1;
  46. // Check if a property with the status property exists
  47. const statusPropertyExists = properties.indexOf("status") !== -1;
  48. // If no such filter or property exists, skip this function
  49. if (!statusFilterExists && !statusPropertyExists) return next(null, pipeline);
  50. // Adds status field, set to Inactive if active is false, otherwise it sets it to Inactive if expiresAt has already passed, Active if not
  51. pipeline.push({
  52. $addFields: {
  53. status: {
  54. $cond: [
  55. { $eq: ["$active", true] },
  56. { $cond: [{ $gt: [new Date(), "$expiresAt"] }, "Inactive", "Active"] },
  57. "Inactive"
  58. ]
  59. }
  60. }
  61. });
  62. return next(null, pipeline);
  63. },
  64. // If a filter exists for value, add valueUsername property to all documents
  65. (pipeline, next) => {
  66. // Check if a filter with the value property exists
  67. const valueFilterExists = queries.map(query => query.filter.property).indexOf("value") !== -1;
  68. // If no such filter exists, skip this function
  69. if (!valueFilterExists) return next(null, pipeline);
  70. // Adds valueOID field, which is an ObjectId version of value
  71. pipeline.push({
  72. $addFields: {
  73. valueOID: {
  74. $convert: {
  75. input: "$value",
  76. to: "objectId",
  77. onError: "unknown",
  78. onNull: "unknown"
  79. }
  80. }
  81. }
  82. });
  83. // Looks up user(s) with the same _id as the valueOID and puts the result in the valueUser field
  84. pipeline.push({
  85. $lookup: {
  86. from: "users",
  87. localField: "valueOID",
  88. foreignField: "_id",
  89. as: "valueUser"
  90. }
  91. });
  92. // Unwinds the valueUser array field into an object
  93. pipeline.push({
  94. $unwind: {
  95. path: "$valueUser",
  96. preserveNullAndEmptyArrays: true
  97. }
  98. });
  99. // Adds valueUsername field from the valueUser username, or unknown if it doesn't exist, or Musare if it's set to Musare
  100. pipeline.push({
  101. $addFields: {
  102. valueUsername: {
  103. $cond: [
  104. { $eq: ["$type", "banUserId"] },
  105. { $ifNull: ["$valueUser.username", "unknown"] },
  106. null
  107. ]
  108. }
  109. }
  110. });
  111. // Removes the valueOID and valueUser property, just in case it doesn't get removed at a later stage
  112. pipeline.push({
  113. $project: {
  114. valueOID: 0,
  115. valueUser: 0
  116. }
  117. });
  118. return next(null, pipeline);
  119. },
  120. // If a filter exists for punishedBy, add punishedByUsername property to all documents
  121. (pipeline, next) => {
  122. // Check if a filter with the punishedBy property exists
  123. const punishedByFilterExists =
  124. queries.map(query => query.filter.property).indexOf("punishedBy") !== -1;
  125. // If no such filter exists, skip this function
  126. if (!punishedByFilterExists) return next(null, pipeline);
  127. // Adds punishedByOID field, which is an ObjectId version of punishedBy
  128. pipeline.push({
  129. $addFields: {
  130. punishedByOID: {
  131. $convert: {
  132. input: "$punishedBy",
  133. to: "objectId",
  134. onError: "unknown",
  135. onNull: "unknown"
  136. }
  137. }
  138. }
  139. });
  140. // Looks up user(s) with the same _id as the punishedByOID and puts the result in the punishedByUser field
  141. pipeline.push({
  142. $lookup: {
  143. from: "users",
  144. localField: "punishedByOID",
  145. foreignField: "_id",
  146. as: "punishedByUser"
  147. }
  148. });
  149. // Unwinds the punishedByUser array field into an object
  150. pipeline.push({
  151. $unwind: {
  152. path: "$punishedByUser",
  153. preserveNullAndEmptyArrays: true
  154. }
  155. });
  156. // Adds punishedByUsername field from the punishedByUser username, or unknown if it doesn't exist
  157. pipeline.push({
  158. $addFields: {
  159. punishedByUsername: {
  160. $ifNull: ["$punishedByUser.username", "unknown"]
  161. }
  162. }
  163. });
  164. // Removes the punishedByOID and punishedByUser property, just in case it doesn't get removed at a later stage
  165. pipeline.push({
  166. $project: {
  167. punishedByOID: 0,
  168. punishedByUser: 0
  169. }
  170. });
  171. return next(null, pipeline);
  172. },
  173. // Adds the match stage to aggregation pipeline, which is responsible for filtering
  174. (pipeline, next) => {
  175. let queryError;
  176. const newQueries = queries.flatMap(query => {
  177. const { data, filter, filterType } = query;
  178. const newQuery = {};
  179. if (filterType === "regex") {
  180. newQuery[filter.property] = new RegExp(`${data.slice(1, data.length - 1)}`, "i");
  181. } else if (filterType === "contains") {
  182. newQuery[filter.property] = new RegExp(
  183. `${data.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
  184. "i"
  185. );
  186. } else if (filterType === "exact") {
  187. newQuery[filter.property] = data.toString();
  188. } else if (filterType === "datetimeBefore") {
  189. newQuery[filter.property] = { $lte: new Date(data) };
  190. } else if (filterType === "datetimeAfter") {
  191. newQuery[filter.property] = { $gte: new Date(data) };
  192. } else if (filterType === "numberLesserEqual") {
  193. newQuery[filter.property] = { $lte: Number(data) };
  194. } else if (filterType === "numberLesser") {
  195. newQuery[filter.property] = { $lt: Number(data) };
  196. } else if (filterType === "numberGreater") {
  197. newQuery[filter.property] = { $gt: Number(data) };
  198. } else if (filterType === "numberGreaterEqual") {
  199. newQuery[filter.property] = { $gte: Number(data) };
  200. } else if (filterType === "numberEquals") {
  201. newQuery[filter.property] = { $eq: Number(data) };
  202. } else if (filterType === "boolean") {
  203. newQuery[filter.property] = { $eq: !!data };
  204. }
  205. if (filter.property === "value") return { $or: [newQuery, { valueUsername: newQuery.value }] };
  206. if (filter.property === "punishedBy")
  207. return { $or: [newQuery, { punishedByUsername: newQuery.punishedBy }] };
  208. return newQuery;
  209. });
  210. if (queryError) next(queryError);
  211. const queryObject = {};
  212. if (newQueries.length > 0) {
  213. if (operator === "and") queryObject.$and = newQueries;
  214. else if (operator === "or") queryObject.$or = newQueries;
  215. else if (operator === "nor") queryObject.$nor = newQueries;
  216. }
  217. pipeline.push({ $match: queryObject });
  218. next(null, pipeline);
  219. },
  220. // Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
  221. (pipeline, next) => {
  222. const newSort = Object.fromEntries(
  223. Object.entries(sort).map(([property, direction]) => [
  224. property,
  225. direction === "ascending" ? 1 : -1
  226. ])
  227. );
  228. if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
  229. next(null, pipeline);
  230. },
  231. // Adds first project stage to aggregation pipeline, responsible for including only the requested properties
  232. (pipeline, next) => {
  233. pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
  234. next(null, pipeline);
  235. },
  236. // Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
  237. (pipeline, next) => {
  238. pipeline.push({
  239. $facet: {
  240. count: [{ $count: "count" }],
  241. documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
  242. }
  243. });
  244. // console.dir(pipeline, { depth: 6 });
  245. next(null, pipeline);
  246. },
  247. // Executes the aggregation pipeline
  248. (pipeline, next) => {
  249. punishmentModel.aggregate(pipeline).exec((err, result) => {
  250. // console.dir(err);
  251. // console.dir(result, { depth: 6 });
  252. if (err) return next(err);
  253. if (result[0].count.length === 0) return next(null, 0, []);
  254. const { count } = result[0].count[0];
  255. const { documents } = result[0];
  256. // console.log(111, err, result, count, documents[0]);
  257. return next(null, count, documents);
  258. });
  259. }
  260. ],
  261. async (err, count, punishments) => {
  262. if (err && err !== true) {
  263. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  264. this.log("ERROR", "PUNISHMENTS_GET_DATA", `Failed to get data from punishments. "${err}"`);
  265. return cb({ status: "error", message: err });
  266. }
  267. this.log("SUCCESS", "PUNISHMENTS_GET_DATA", `Got data from punishments successfully.`);
  268. return cb({
  269. status: "success",
  270. message: "Successfully got data from punishments.",
  271. data: { data: punishments, count }
  272. });
  273. }
  274. );
  275. }),
  276. /**
  277. * Gets all punishments for a user
  278. *
  279. * @param {object} session - the session object automatically added by the websocket
  280. * @param {string} userId - the id of the user
  281. * @param {Function} cb - gets called with the result
  282. */
  283. getPunishmentsForUser: isAdminRequired(async function getPunishmentsForUser(session, userId, cb) {
  284. const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
  285. punishmentModel.find({ type: "banUserId", value: userId }, async (err, punishments) => {
  286. if (err) {
  287. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  288. this.log(
  289. "ERROR",
  290. "GET_PUNISHMENTS_FOR_USER",
  291. `Getting punishments for user ${userId} failed. "${err}"`
  292. );
  293. return cb({ status: "error", message: err });
  294. }
  295. this.log("SUCCESS", "GET_PUNISHMENTS_FOR_USER", `Got punishments for user ${userId} successful.`);
  296. return cb({ status: "success", data: { punishments } });
  297. });
  298. }),
  299. /**
  300. * Returns a punishment by id
  301. *
  302. * @param {object} session - the session object automatically added by the websocket
  303. * @param {string} punishmentId - the punishment id
  304. * @param {Function} cb - gets called with the result
  305. */
  306. findOne: isAdminRequired(async function findOne(session, punishmentId, cb) {
  307. const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
  308. async.waterfall([next => punishmentModel.findOne({ _id: punishmentId }, next)], async (err, punishment) => {
  309. if (err) {
  310. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  311. this.log(
  312. "ERROR",
  313. "GET_PUNISHMENT_BY_ID",
  314. `Getting punishment with id ${punishmentId} failed. "${err}"`
  315. );
  316. return cb({ status: "error", message: err });
  317. }
  318. this.log("SUCCESS", "GET_PUNISHMENT_BY_ID", `Got punishment with id ${punishmentId} successful.`);
  319. return cb({ status: "success", data: { punishment } });
  320. });
  321. }),
  322. /**
  323. * Bans an IP address
  324. *
  325. * @param {object} session - the session object automatically added by the websocket
  326. * @param {string} value - the ip address that is going to be banned
  327. * @param {string} reason - the reason for the ban
  328. * @param {string} expiresAt - the time the ban expires
  329. * @param {Function} cb - gets called with the result
  330. */
  331. banIP: isAdminRequired(function banIP(session, value, reason, expiresAt, cb) {
  332. async.waterfall(
  333. [
  334. next => {
  335. if (!value) return next("You must provide an IP address to ban.");
  336. if (!reason) return next("You must provide a reason for the ban.");
  337. return next();
  338. },
  339. next => {
  340. if (!expiresAt || typeof expiresAt !== "string") return next("Invalid expire date.");
  341. const date = new Date();
  342. switch (expiresAt) {
  343. case "1h":
  344. expiresAt = date.setHours(date.getHours() + 1);
  345. break;
  346. case "12h":
  347. expiresAt = date.setHours(date.getHours() + 12);
  348. break;
  349. case "1d":
  350. expiresAt = date.setDate(date.getDate() + 1);
  351. break;
  352. case "1w":
  353. expiresAt = date.setDate(date.getDate() + 7);
  354. break;
  355. case "1m":
  356. expiresAt = date.setMonth(date.getMonth() + 1);
  357. break;
  358. case "3m":
  359. expiresAt = date.setMonth(date.getMonth() + 3);
  360. break;
  361. case "6m":
  362. expiresAt = date.setMonth(date.getMonth() + 6);
  363. break;
  364. case "1y":
  365. expiresAt = date.setFullYear(date.getFullYear() + 1);
  366. break;
  367. case "never":
  368. expiresAt = new Date(3093527980800000);
  369. break;
  370. default:
  371. return next("Invalid expire date.");
  372. }
  373. return next();
  374. },
  375. next => {
  376. PunishmentsModule.runJob(
  377. "ADD_PUNISHMENT",
  378. {
  379. type: "banUserIp",
  380. value,
  381. reason,
  382. expiresAt,
  383. punishedBy: session.userId
  384. },
  385. this
  386. )
  387. .then(punishment => {
  388. next(null, punishment);
  389. })
  390. .catch(next);
  391. }
  392. ],
  393. async (err, punishment) => {
  394. if (err && err !== true) {
  395. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  396. this.log(
  397. "ERROR",
  398. "BAN_IP",
  399. `User ${session.userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`
  400. );
  401. cb({ status: "error", message: err });
  402. }
  403. this.log(
  404. "SUCCESS",
  405. "BAN_IP",
  406. `User ${session.userId} has successfully banned IP address ${value} with the reason ${reason}.`
  407. );
  408. CacheModule.runJob("PUB", {
  409. channel: "ip.ban",
  410. value: { ip: value, punishment }
  411. });
  412. return cb({
  413. status: "success",
  414. message: "Successfully banned IP address."
  415. });
  416. }
  417. );
  418. })
  419. };