punishments.js 14 KB

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