punishments.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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: data };
  194. } else if (filterType === "numberLesser") {
  195. newQuery[filter.property] = { $lt: data };
  196. } else if (filterType === "numberGreater") {
  197. newQuery[filter.property] = { $gt: data };
  198. } else if (filterType === "numberGreaterEqual") {
  199. newQuery[filter.property] = { $gte: data };
  200. } else if (filterType === "numberEquals" || filterType === "boolean") {
  201. newQuery[filter.property] = { $eq: data };
  202. }
  203. if (filter.property === "value") return { $or: [newQuery, { valueUsername: newQuery.value }] };
  204. if (filter.property === "punishedBy")
  205. return { $or: [newQuery, { punishedByUsername: newQuery.punishedBy }] };
  206. return newQuery;
  207. });
  208. if (queryError) next(queryError);
  209. const queryObject = {};
  210. if (newQueries.length > 0) {
  211. if (operator === "and") queryObject.$and = newQueries;
  212. else if (operator === "or") queryObject.$or = newQueries;
  213. else if (operator === "nor") queryObject.$nor = newQueries;
  214. }
  215. pipeline.push({ $match: queryObject });
  216. next(null, pipeline);
  217. },
  218. // Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
  219. (pipeline, next) => {
  220. const newSort = Object.fromEntries(
  221. Object.entries(sort).map(([property, direction]) => [
  222. property,
  223. direction === "ascending" ? 1 : -1
  224. ])
  225. );
  226. if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
  227. next(null, pipeline);
  228. },
  229. // Adds first project stage to aggregation pipeline, responsible for including only the requested properties
  230. (pipeline, next) => {
  231. pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
  232. next(null, pipeline);
  233. },
  234. // Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
  235. (pipeline, next) => {
  236. pipeline.push({
  237. $facet: {
  238. count: [{ $count: "count" }],
  239. documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
  240. }
  241. });
  242. // console.dir(pipeline, { depth: 6 });
  243. next(null, pipeline);
  244. },
  245. // Executes the aggregation pipeline
  246. (pipeline, next) => {
  247. punishmentModel.aggregate(pipeline).exec((err, result) => {
  248. // console.dir(err);
  249. // console.dir(result, { depth: 6 });
  250. if (err) return next(err);
  251. if (result[0].count.length === 0) return next(null, 0, []);
  252. const { count } = result[0].count[0];
  253. const { documents } = result[0];
  254. // console.log(111, err, result, count, documents[0]);
  255. return next(null, count, documents);
  256. });
  257. }
  258. ],
  259. async (err, count, punishments) => {
  260. if (err && err !== true) {
  261. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  262. this.log("ERROR", "PUNISHMENTS_GET_DATA", `Failed to get data from punishments. "${err}"`);
  263. return cb({ status: "error", message: err });
  264. }
  265. this.log("SUCCESS", "PUNISHMENTS_GET_DATA", `Got data from punishments successfully.`);
  266. return cb({
  267. status: "success",
  268. message: "Successfully got data from punishments.",
  269. data: { data: punishments, count }
  270. });
  271. }
  272. );
  273. }),
  274. /**
  275. * Gets all punishments for a user
  276. *
  277. * @param {object} session - the session object automatically added by the websocket
  278. * @param {string} userId - the id of the user
  279. * @param {Function} cb - gets called with the result
  280. */
  281. getPunishmentsForUser: isAdminRequired(async function getPunishmentsForUser(session, userId, cb) {
  282. const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
  283. punishmentModel.find({ type: "banUserId", value: userId }, async (err, punishments) => {
  284. if (err) {
  285. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  286. this.log(
  287. "ERROR",
  288. "GET_PUNISHMENTS_FOR_USER",
  289. `Getting punishments for user ${userId} failed. "${err}"`
  290. );
  291. return cb({ status: "error", message: err });
  292. }
  293. this.log("SUCCESS", "GET_PUNISHMENTS_FOR_USER", `Got punishments for user ${userId} successful.`);
  294. return cb({ status: "success", data: { punishments } });
  295. });
  296. }),
  297. /**
  298. * Returns a punishment by id
  299. *
  300. * @param {object} session - the session object automatically added by the websocket
  301. * @param {string} punishmentId - the punishment id
  302. * @param {Function} cb - gets called with the result
  303. */
  304. findOne: isAdminRequired(async function findOne(session, punishmentId, cb) {
  305. const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
  306. async.waterfall([next => punishmentModel.findOne({ _id: punishmentId }, next)], async (err, punishment) => {
  307. if (err) {
  308. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  309. this.log(
  310. "ERROR",
  311. "GET_PUNISHMENT_BY_ID",
  312. `Getting punishment with id ${punishmentId} failed. "${err}"`
  313. );
  314. return cb({ status: "error", message: err });
  315. }
  316. this.log("SUCCESS", "GET_PUNISHMENT_BY_ID", `Got punishment with id ${punishmentId} successful.`);
  317. return cb({ status: "success", data: { punishment } });
  318. });
  319. }),
  320. /**
  321. * Bans an IP address
  322. *
  323. * @param {object} session - the session object automatically added by the websocket
  324. * @param {string} value - the ip address that is going to be banned
  325. * @param {string} reason - the reason for the ban
  326. * @param {string} expiresAt - the time the ban expires
  327. * @param {Function} cb - gets called with the result
  328. */
  329. banIP: isAdminRequired(function banIP(session, value, reason, expiresAt, cb) {
  330. async.waterfall(
  331. [
  332. next => {
  333. if (!value) return next("You must provide an IP address to ban.");
  334. if (!reason) return next("You must provide a reason for the ban.");
  335. return next();
  336. },
  337. next => {
  338. if (!expiresAt || typeof expiresAt !== "string") return next("Invalid expire date.");
  339. const date = new Date();
  340. switch (expiresAt) {
  341. case "1h":
  342. expiresAt = date.setHours(date.getHours() + 1);
  343. break;
  344. case "12h":
  345. expiresAt = date.setHours(date.getHours() + 12);
  346. break;
  347. case "1d":
  348. expiresAt = date.setDate(date.getDate() + 1);
  349. break;
  350. case "1w":
  351. expiresAt = date.setDate(date.getDate() + 7);
  352. break;
  353. case "1m":
  354. expiresAt = date.setMonth(date.getMonth() + 1);
  355. break;
  356. case "3m":
  357. expiresAt = date.setMonth(date.getMonth() + 3);
  358. break;
  359. case "6m":
  360. expiresAt = date.setMonth(date.getMonth() + 6);
  361. break;
  362. case "1y":
  363. expiresAt = date.setFullYear(date.getFullYear() + 1);
  364. break;
  365. case "never":
  366. expiresAt = new Date(3093527980800000);
  367. break;
  368. default:
  369. return next("Invalid expire date.");
  370. }
  371. return next();
  372. },
  373. next => {
  374. PunishmentsModule.runJob(
  375. "ADD_PUNISHMENT",
  376. {
  377. type: "banUserIp",
  378. value,
  379. reason,
  380. expiresAt,
  381. punishedBy: session.userId
  382. },
  383. this
  384. )
  385. .then(punishment => {
  386. next(null, punishment);
  387. })
  388. .catch(next);
  389. }
  390. ],
  391. async (err, punishment) => {
  392. if (err && err !== true) {
  393. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  394. this.log(
  395. "ERROR",
  396. "BAN_IP",
  397. `User ${session.userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`
  398. );
  399. cb({ status: "error", message: err });
  400. }
  401. this.log(
  402. "SUCCESS",
  403. "BAN_IP",
  404. `User ${session.userId} has successfully banned IP address ${value} with the reason ${reason}.`
  405. );
  406. CacheModule.runJob("PUB", {
  407. channel: "ip.ban",
  408. value: { ip: value, punishment }
  409. });
  410. return cb({
  411. status: "success",
  412. message: "Successfully banned IP address."
  413. });
  414. }
  415. );
  416. })
  417. };