users.js 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223
  1. import config from "config";
  2. import async from "async";
  3. import request from "request";
  4. import bcrypt from "bcrypt";
  5. import sha256 from "sha256";
  6. import { isAdminRequired, isLoginRequired } from "./hooks";
  7. import moduleManager from "../../index";
  8. const DBModule = moduleManager.modules.db;
  9. const UtilsModule = moduleManager.modules.utils;
  10. const IOModule = moduleManager.modules.io;
  11. const CacheModule = moduleManager.modules.cache;
  12. const MailModule = moduleManager.modules.mail;
  13. const PunishmentsModule = moduleManager.modules.punishments;
  14. const ActivitiesModule = moduleManager.modules.activities;
  15. const PlaylistsModule = moduleManager.modules.playlists;
  16. CacheModule.runJob("SUB", {
  17. channel: "user.updatePreferences",
  18. cb: res => {
  19. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
  20. response.sockets.forEach(socket => {
  21. socket.emit("keep.event:user.preferences.changed", res.preferences);
  22. });
  23. });
  24. }
  25. });
  26. CacheModule.runJob("SUB", {
  27. channel: "user.updateOrderOfPlaylists",
  28. cb: res => {
  29. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  30. response.sockets.forEach(socket => {
  31. socket.emit("event:user.orderOfPlaylists.changed", res.orderOfPlaylists);
  32. });
  33. });
  34. IOModule.runJob("EMIT_TO_ROOM", {
  35. room: `profile-${res.userId}`,
  36. args: ["event:user.orderOfPlaylists.changed", res.orderOfPlaylists]
  37. });
  38. }
  39. });
  40. CacheModule.runJob("SUB", {
  41. channel: "user.updateUsername",
  42. cb: user => {
  43. IOModule.runJob("SOCKETS_FROM_USER", { userId: user._id }).then(response => {
  44. response.sockets.forEach(socket => {
  45. socket.emit("event:user.username.changed", user.username);
  46. });
  47. });
  48. }
  49. });
  50. CacheModule.runJob("SUB", {
  51. channel: "user.removeSessions",
  52. cb: userId => {
  53. IOModule.runJob("SOCKETS_FROM_USER_WITHOUT_CACHE", { userId }).then(response => {
  54. response.sockets.forEach(socket => {
  55. socket.emit("keep.event:user.session.removed");
  56. });
  57. });
  58. }
  59. });
  60. CacheModule.runJob("SUB", {
  61. channel: "user.linkPassword",
  62. cb: userId => {
  63. IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
  64. response.sockets.forEach(socket => {
  65. socket.emit("event:user.linkPassword");
  66. });
  67. });
  68. }
  69. });
  70. CacheModule.runJob("SUB", {
  71. channel: "user.unlinkPassword",
  72. cb: userId => {
  73. IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
  74. response.sockets.forEach(socket => {
  75. socket.emit("event:user.unlinkPassword");
  76. });
  77. });
  78. }
  79. });
  80. CacheModule.runJob("SUB", {
  81. channel: "user.linkGithub",
  82. cb: userId => {
  83. IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
  84. response.sockets.forEach(socket => {
  85. socket.emit("event:user.linkGithub");
  86. });
  87. });
  88. }
  89. });
  90. CacheModule.runJob("SUB", {
  91. channel: "user.unlinkGithub",
  92. cb: userId => {
  93. IOModule.runJob("SOCKETS_FROM_USER", { userId }).then(response => {
  94. response.sockets.forEach(socket => {
  95. socket.emit("event:user.unlinkGithub");
  96. });
  97. });
  98. }
  99. });
  100. CacheModule.runJob("SUB", {
  101. channel: "user.ban",
  102. cb: data => {
  103. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  104. response.sockets.forEach(socket => {
  105. socket.emit("keep.event:banned", data.punishment);
  106. socket.disconnect(true);
  107. });
  108. });
  109. }
  110. });
  111. CacheModule.runJob("SUB", {
  112. channel: "user.favoritedStation",
  113. cb: data => {
  114. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  115. response.sockets.forEach(socket => {
  116. socket.emit("event:user.favoritedStation", data.stationId);
  117. });
  118. });
  119. }
  120. });
  121. CacheModule.runJob("SUB", {
  122. channel: "user.unfavoritedStation",
  123. cb: data => {
  124. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  125. response.sockets.forEach(socket => {
  126. socket.emit("event:user.unfavoritedStation", data.stationId);
  127. });
  128. });
  129. }
  130. });
  131. export default {
  132. /**
  133. * Lists all Users
  134. *
  135. * @param {object} session - the session object automatically added by socket.io
  136. * @param {Function} cb - gets called with the result
  137. */
  138. index: isAdminRequired(async function index(session, cb) {
  139. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  140. async.waterfall(
  141. [
  142. next => {
  143. userModel.find({}).exec(next);
  144. }
  145. ],
  146. async (err, users) => {
  147. if (err) {
  148. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  149. this.log("ERROR", "USER_INDEX", `Indexing users failed. "${err}"`);
  150. return cb({ status: "failure", message: err });
  151. }
  152. this.log("SUCCESS", "USER_INDEX", `Indexing users successful.`);
  153. const filteredUsers = [];
  154. users.forEach(user => {
  155. filteredUsers.push({
  156. _id: user._id,
  157. username: user.username,
  158. role: user.role,
  159. liked: user.liked,
  160. disliked: user.disliked,
  161. songsRequested: user.statistics.songsRequested,
  162. email: {
  163. address: user.email.address,
  164. verified: user.email.verified
  165. },
  166. hasPassword: !!user.services.password,
  167. services: { github: user.services.github }
  168. });
  169. });
  170. return cb({ status: "success", data: filteredUsers });
  171. }
  172. );
  173. }),
  174. /**
  175. * Logs user in
  176. *
  177. * @param {object} session - the session object automatically added by socket.io
  178. * @param {string} identifier - the email of the user
  179. * @param {string} password - the plaintext of the user
  180. * @param {Function} cb - gets called with the result
  181. */
  182. async login(session, identifier, password, cb) {
  183. identifier = identifier.toLowerCase();
  184. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  185. const sessionSchema = await CacheModule.runJob(
  186. "GET_SCHEMA",
  187. {
  188. schemaName: "session"
  189. },
  190. this
  191. );
  192. async.waterfall(
  193. [
  194. // check if a user with the requested identifier exists
  195. next => {
  196. userModel.findOne(
  197. {
  198. $or: [{ "email.address": identifier }]
  199. },
  200. next
  201. );
  202. },
  203. // if the user doesn't exist, respond with a failure
  204. // otherwise compare the requested password and the actual users password
  205. (user, next) => {
  206. if (!user) return next("User not found");
  207. if (!user.services.password || !user.services.password.password)
  208. return next("The account you are trying to access uses GitHub to log in.");
  209. return bcrypt.compare(sha256(password), user.services.password.password, (err, match) => {
  210. if (err) return next(err);
  211. if (!match) return next("Incorrect password");
  212. return next(null, user);
  213. });
  214. },
  215. (user, next) => {
  216. UtilsModule.runJob("GUID", {}, this).then(sessionId => {
  217. next(null, user, sessionId);
  218. });
  219. },
  220. (user, sessionId, next) => {
  221. CacheModule.runJob(
  222. "HSET",
  223. {
  224. table: "sessions",
  225. key: sessionId,
  226. value: sessionSchema(sessionId, user._id)
  227. },
  228. this
  229. )
  230. .then(() => {
  231. next(null, sessionId);
  232. })
  233. .catch(next);
  234. }
  235. ],
  236. async (err, sessionId) => {
  237. if (err && err !== true) {
  238. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  239. this.log(
  240. "ERROR",
  241. "USER_PASSWORD_LOGIN",
  242. `Login failed with password for user "${identifier}". "${err}"`
  243. );
  244. return cb({ status: "failure", message: err });
  245. }
  246. this.log("SUCCESS", "USER_PASSWORD_LOGIN", `Login successful with password for user "${identifier}"`);
  247. return cb({
  248. status: "success",
  249. message: "Login successful",
  250. user: {},
  251. SID: sessionId
  252. });
  253. }
  254. );
  255. },
  256. /**
  257. * Registers a new user
  258. *
  259. * @param {object} session - the session object automatically added by socket.io
  260. * @param {string} username - the username for the new user
  261. * @param {string} email - the email for the new user
  262. * @param {string} password - the plaintext password for the new user
  263. * @param {object} recaptcha - the recaptcha data
  264. * @param {Function} cb - gets called with the result
  265. */
  266. async register(session, username, email, password, recaptcha, cb) {
  267. email = email.toLowerCase();
  268. const verificationToken = await UtilsModule.runJob(
  269. "GENERATE_RANDOM_STRING",
  270. {
  271. length: 64
  272. },
  273. this
  274. );
  275. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  276. const verifyEmailSchema = await MailModule.runJob(
  277. "GET_SCHEMA",
  278. {
  279. schemaName: "verifyEmail"
  280. },
  281. this
  282. );
  283. async.waterfall(
  284. [
  285. next => {
  286. if (config.get("registrationDisabled") === true)
  287. return next("Registration is not allowed at this time.");
  288. return next();
  289. },
  290. next => {
  291. if (!DBModule.passwordValid(password))
  292. return next("Invalid password. Check if it meets all the requirements.");
  293. return next();
  294. },
  295. // verify the request with google recaptcha
  296. next => {
  297. if (config.get("apis.recaptcha.enabled") === true)
  298. request(
  299. {
  300. url: "https://www.google.com/recaptcha/api/siteverify",
  301. method: "POST",
  302. form: {
  303. secret: config.get("apis").recaptcha.secret,
  304. response: recaptcha
  305. }
  306. },
  307. next
  308. );
  309. else next(null, null, null);
  310. },
  311. // check if the response from Google recaptcha is successful
  312. // if it is, we check if a user with the requested username already exists
  313. (response, body, next) => {
  314. if (config.get("apis.recaptcha.enabled") === true) {
  315. const json = JSON.parse(body);
  316. if (json.success !== true) return next("Response from recaptcha was not successful.");
  317. }
  318. return userModel.findOne({ username: new RegExp(`^${username}$`, "i") }, next);
  319. },
  320. // if the user already exists, respond with that
  321. // otherwise check if a user with the requested email already exists
  322. (user, next) => {
  323. if (user) return next("A user with that username already exists.");
  324. return userModel.findOne({ "email.address": email }, next);
  325. },
  326. // if the user already exists, respond with that
  327. // otherwise, generate a salt to use with hashing the new users password
  328. (user, next) => {
  329. if (user) return next("A user with that email already exists.");
  330. return bcrypt.genSalt(10, next);
  331. },
  332. // hash the password
  333. (salt, next) => {
  334. bcrypt.hash(sha256(password), salt, next);
  335. },
  336. (hash, next) => {
  337. UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 12 }, this).then(_id => {
  338. next(null, hash, _id);
  339. });
  340. },
  341. // create the user object
  342. (hash, _id, next) => {
  343. next(null, {
  344. _id,
  345. username,
  346. email: {
  347. address: email,
  348. verificationToken
  349. },
  350. services: {
  351. password: {
  352. password: hash
  353. }
  354. }
  355. });
  356. },
  357. // generate the url for gravatar avatar
  358. (user, next) => {
  359. UtilsModule.runJob(
  360. "CREATE_GRAVATAR",
  361. {
  362. email: user.email.address
  363. },
  364. this
  365. ).then(url => {
  366. user.avatar = {
  367. type: "gravatar",
  368. url
  369. };
  370. next(null, user);
  371. });
  372. },
  373. // save the new user to the database
  374. (user, next) => {
  375. userModel.create(user, next);
  376. },
  377. // respond with the new user
  378. (user, next) => {
  379. verifyEmailSchema(email, username, verificationToken, err => {
  380. next(err, user._id);
  381. });
  382. },
  383. // create a liked songs playlist for the new user
  384. (userId, next) => {
  385. PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
  386. userId,
  387. displayName: "Liked Songs"
  388. })
  389. .then(likedSongsPlaylist => {
  390. next(null, likedSongsPlaylist, userId);
  391. })
  392. .catch(err => next(err));
  393. },
  394. // create a disliked songs playlist for the new user
  395. (likedSongsPlaylist, userId, next) => {
  396. PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
  397. userId,
  398. displayName: "Disliked Songs"
  399. })
  400. .then(dislikedSongsPlaylist => {
  401. next(null, { likedSongsPlaylist, dislikedSongsPlaylist }, userId);
  402. })
  403. .catch(err => next(err));
  404. },
  405. // associate liked + disliked songs playlist to the user object
  406. ({ likedSongsPlaylist, dislikedSongsPlaylist }, userId, next) => {
  407. userModel.updateOne(
  408. { _id: userId },
  409. { $set: { likedSongsPlaylist, dislikedSongsPlaylist } },
  410. { runValidators: true },
  411. err => {
  412. if (err) return next(err);
  413. return next(null, userId);
  414. }
  415. );
  416. }
  417. ],
  418. async (err, user) => {
  419. if (err && err !== true) {
  420. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  421. this.log(
  422. "ERROR",
  423. "USER_PASSWORD_REGISTER",
  424. `Register failed with password for user "${username}"."${err}"`
  425. );
  426. return cb({ status: "failure", message: err });
  427. }
  428. ActivitiesModule.runJob("ADD_ACTIVITY", {
  429. userId: user._id,
  430. activityType: "created_account"
  431. });
  432. this.log(
  433. "SUCCESS",
  434. "USER_PASSWORD_REGISTER",
  435. `Register successful with password for user "${username}".`
  436. );
  437. const result = await this.module.runJob(
  438. "RUN_ACTION2",
  439. {
  440. session,
  441. namespace: "users",
  442. action: "login",
  443. args: [email, password]
  444. },
  445. this
  446. );
  447. const obj = {
  448. status: "success",
  449. message: "Successfully registered."
  450. };
  451. if (result.status === "success") {
  452. obj.SID = result.SID;
  453. }
  454. return cb(obj);
  455. }
  456. );
  457. },
  458. /**
  459. * Logs out a user
  460. *
  461. * @param {object} session - the session object automatically added by socket.io
  462. * @param {Function} cb - gets called with the result
  463. */
  464. logout(session, cb) {
  465. async.waterfall(
  466. [
  467. next => {
  468. CacheModule.runJob(
  469. "HGET",
  470. {
  471. table: "sessions",
  472. key: session.sessionId
  473. },
  474. this
  475. )
  476. .then(session => {
  477. next(null, session);
  478. })
  479. .catch(next);
  480. },
  481. (session, next) => {
  482. if (!session) return next("Session not found");
  483. return next(null, session);
  484. },
  485. (session, next) => {
  486. CacheModule.runJob(
  487. "HDEL",
  488. {
  489. table: "sessions",
  490. key: session.sessionId
  491. },
  492. this
  493. )
  494. .then(() => {
  495. next();
  496. })
  497. .catch(next);
  498. }
  499. ],
  500. async err => {
  501. if (err && err !== true) {
  502. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  503. this.log("ERROR", "USER_LOGOUT", `Logout failed. "${err}" `);
  504. cb({ status: "failure", message: err });
  505. } else {
  506. this.log("SUCCESS", "USER_LOGOUT", `Logout successful.`);
  507. cb({
  508. status: "success",
  509. message: "Successfully logged out."
  510. });
  511. }
  512. }
  513. );
  514. },
  515. /**
  516. * Removes all sessions for a user
  517. *
  518. * @param {object} session - the session object automatically added by socket.io
  519. * @param {string} userId - the id of the user we are trying to delete the sessions of
  520. * @param {Function} cb - gets called with the result
  521. */
  522. removeSessions: isLoginRequired(async function removeSessions(session, userId, cb) {
  523. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  524. async.waterfall(
  525. [
  526. next => {
  527. userModel.findOne({ _id: session.userId }, (err, user) => {
  528. if (err) return next(err);
  529. if (user.role !== "admin" && session.userId !== userId)
  530. return next("Only admins and the owner of the account can remove their sessions.");
  531. return next();
  532. });
  533. },
  534. next => {
  535. CacheModule.runJob("HGETALL", { table: "sessions" }, this)
  536. .then(sessions => {
  537. next(null, sessions);
  538. })
  539. .catch(next);
  540. },
  541. (sessions, next) => {
  542. if (!sessions) return next("There are no sessions for this user to remove.");
  543. const keys = Object.keys(sessions);
  544. return next(null, keys, sessions);
  545. },
  546. (keys, sessions, next) => {
  547. CacheModule.runJob("PUB", {
  548. channel: "user.removeSessions",
  549. value: userId
  550. });
  551. async.each(
  552. keys,
  553. (sessionId, callback) => {
  554. const session = sessions[sessionId];
  555. if (session.userId === userId) {
  556. // TODO Also maybe add this to this runJob
  557. CacheModule.runJob("HDEL", {
  558. channel: "sessions",
  559. key: sessionId
  560. })
  561. .then(() => {
  562. callback(null);
  563. })
  564. .catch(next);
  565. }
  566. },
  567. err => {
  568. next(err);
  569. }
  570. );
  571. }
  572. ],
  573. async err => {
  574. if (err) {
  575. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  576. this.log(
  577. "ERROR",
  578. "REMOVE_SESSIONS_FOR_USER",
  579. `Couldn't remove all sessions for user "${userId}". "${err}"`
  580. );
  581. return cb({ status: "failure", message: err });
  582. }
  583. this.log("SUCCESS", "REMOVE_SESSIONS_FOR_USER", `Removed all sessions for user "${userId}".`);
  584. return cb({
  585. status: "success",
  586. message: "Successfully removed all sessions."
  587. });
  588. }
  589. );
  590. }),
  591. /**
  592. * Updates the order of a user's playlists
  593. *
  594. * @param {object} session - the session object automatically added by socket.io
  595. * @param {Array} orderOfPlaylists - array of playlist ids (with a specific order)
  596. * @param {Function} cb - gets called with the result
  597. */
  598. updateOrderOfPlaylists: isLoginRequired(async function updateOrderOfPlaylists(session, orderOfPlaylists, cb) {
  599. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  600. async.waterfall(
  601. [
  602. next => {
  603. userModel.updateOne(
  604. { _id: session.userId },
  605. { $set: { "preferences.orderOfPlaylists": orderOfPlaylists } },
  606. { runValidators: true },
  607. next
  608. );
  609. }
  610. ],
  611. async err => {
  612. if (err) {
  613. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  614. this.log(
  615. "ERROR",
  616. "UPDATE_ORDER_OF_USER_PLAYLISTS",
  617. `Couldn't update order of playlists for user "${session.userId}" to "${orderOfPlaylists}". "${err}"`
  618. );
  619. return cb({ status: "failure", message: err });
  620. }
  621. CacheModule.runJob("PUB", {
  622. channel: "user.updateOrderOfPlaylists",
  623. value: {
  624. orderOfPlaylists,
  625. userId: session.userId
  626. }
  627. });
  628. this.log(
  629. "SUCCESS",
  630. "UPDATE_ORDER_OF_USER_PLAYLISTS",
  631. `Updated order of playlists for user "${session.userId}" to "${orderOfPlaylists}".`
  632. );
  633. return cb({
  634. status: "success",
  635. message: "Order of playlists successfully updated"
  636. });
  637. }
  638. );
  639. }),
  640. /**
  641. * Updates a user's preferences
  642. *
  643. * @param {object} session - the session object automatically added by socket.io
  644. * @param {object} preferences - object containing preferences
  645. * @param {boolean} preferences.nightmode - whether or not the user is using the night mode theme
  646. * @param {boolean} preferences.autoSkipDisliked - whether to automatically skip disliked songs
  647. * @param {Function} cb - gets called with the result
  648. */
  649. updatePreferences: isLoginRequired(async function updatePreferences(session, preferences, cb) {
  650. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  651. async.waterfall(
  652. [
  653. next => {
  654. userModel.updateOne(
  655. { _id: session.userId },
  656. {
  657. $set: {
  658. preferences: {
  659. nightmode: preferences.nightmode,
  660. autoSkipDisliked: preferences.autoSkipDisliked
  661. }
  662. }
  663. },
  664. { runValidators: true },
  665. next
  666. );
  667. }
  668. ],
  669. async err => {
  670. if (err) {
  671. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  672. this.log(
  673. "ERROR",
  674. "UPDATE_USER_PREFERENCES",
  675. `Couldn't update preferences for user "${session.userId}" to "${JSON.stringify(
  676. preferences
  677. )}". "${err}"`
  678. );
  679. return cb({ status: "failure", message: err });
  680. }
  681. CacheModule.runJob("PUB", {
  682. channel: "user.updatePreferences",
  683. value: {
  684. preferences,
  685. userId: session.userId
  686. }
  687. });
  688. this.log(
  689. "SUCCESS",
  690. "UPDATE_USER_PREFERENCES",
  691. `Updated preferences for user "${session.userId}" to "${JSON.stringify(preferences)}".`
  692. );
  693. return cb({
  694. status: "success",
  695. message: "Preferences successfully updated"
  696. });
  697. }
  698. );
  699. }),
  700. /**
  701. * Retrieves a user's preferences
  702. *
  703. * @param {object} session - the session object automatically added by socket.io
  704. * @param {Function} cb - gets called with the result
  705. */
  706. getPreferences: isLoginRequired(async function updatePreferences(session, cb) {
  707. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  708. async.waterfall(
  709. [
  710. next => {
  711. userModel.findById(session.userId).select({ preferences: -1 }).exec(next);
  712. }
  713. ],
  714. async (err, { preferences }) => {
  715. if (err) {
  716. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  717. this.log(
  718. "ERROR",
  719. "GET_USER_PREFERENCES",
  720. `Couldn't retrieve preferences for user "${session.userId}". "${err}"`
  721. );
  722. return cb({ status: "failure", message: err });
  723. }
  724. this.log(
  725. "SUCCESS",
  726. "GET_USER_PREFERENCES",
  727. `Successfully obtained preferences for user "${session.userId}".`
  728. );
  729. return cb({
  730. status: "success",
  731. message: "Preferences successfully retrieved",
  732. data: preferences
  733. });
  734. }
  735. );
  736. }),
  737. /**
  738. * Gets user object from username (only a few properties)
  739. *
  740. * @param {object} session - the session object automatically added by socket.io
  741. * @param {string} username - the username of the user we are trying to find
  742. * @param {Function} cb - gets called with the result
  743. */
  744. findByUsername: async function findByUsername(session, username, cb) {
  745. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  746. async.waterfall(
  747. [
  748. next => {
  749. userModel.findOne({ username: new RegExp(`^${username}$`, "i") }, next);
  750. },
  751. (account, next) => {
  752. if (!account) return next("User not found.");
  753. return next(null, account);
  754. }
  755. ],
  756. async (err, account) => {
  757. if (err && err !== true) {
  758. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  759. this.log("ERROR", "FIND_BY_USERNAME", `User not found for username "${username}". "${err}"`);
  760. return cb({ status: "failure", message: err });
  761. }
  762. this.log("SUCCESS", "FIND_BY_USERNAME", `User found for username "${username}".`);
  763. return cb({
  764. status: "success",
  765. data: {
  766. _id: account._id,
  767. name: account.name,
  768. username: account.username,
  769. location: account.location,
  770. bio: account.bio,
  771. role: account.role,
  772. avatar: account.avatar,
  773. createdAt: account.createdAt
  774. }
  775. });
  776. }
  777. );
  778. },
  779. /**
  780. * Gets a username from an userId
  781. *
  782. * @param {object} session - the session object automatically added by socket.io
  783. * @param {string} userId - the userId of the person we are trying to get the username from
  784. * @param {Function} cb - gets called with the result
  785. */
  786. async getUsernameFromId(session, userId, cb) {
  787. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  788. userModel
  789. .findById(userId)
  790. .then(user => {
  791. if (user) {
  792. this.log("SUCCESS", "GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
  793. return cb({
  794. status: "success",
  795. data: user.username
  796. });
  797. }
  798. this.log(
  799. "ERROR",
  800. "GET_USERNAME_FROM_ID",
  801. `Getting the username from userId "${userId}" failed. User not found.`
  802. );
  803. return cb({
  804. status: "failure",
  805. message: "Couldn't find the user."
  806. });
  807. })
  808. .catch(async err => {
  809. if (err && err !== true) {
  810. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  811. this.log(
  812. "ERROR",
  813. "GET_USERNAME_FROM_ID",
  814. `Getting the username from userId "${userId}" failed. "${err}"`
  815. );
  816. cb({ status: "failure", message: err });
  817. }
  818. });
  819. },
  820. /**
  821. * Gets a user from a userId
  822. *
  823. * @param {object} session - the session object automatically added by socket.io
  824. * @param {string} userId - the userId of the person we are trying to get the username from
  825. * @param {Function} cb - gets called with the result
  826. */
  827. getUserFromId: isAdminRequired(async function getUserFromId(session, userId, cb) {
  828. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  829. userModel
  830. .findById(userId)
  831. .then(user => {
  832. if (user) {
  833. this.log("SUCCESS", "GET_USER_FROM_ID", `Found user for userId "${userId}".`);
  834. return cb({
  835. status: "success",
  836. data: {
  837. _id: user._id,
  838. username: user.username,
  839. role: user.role,
  840. liked: user.liked,
  841. disliked: user.disliked,
  842. songsRequested: user.statistics.songsRequested,
  843. email: {
  844. address: user.email.address,
  845. verified: user.email.verified
  846. },
  847. hasPassword: !!user.services.password,
  848. services: { github: user.services.github }
  849. }
  850. });
  851. }
  852. this.log(
  853. "ERROR",
  854. "GET_USER_FROM_ID",
  855. `Getting the user from userId "${userId}" failed. User not found.`
  856. );
  857. return cb({
  858. status: "failure",
  859. message: "Couldn't find the user."
  860. });
  861. })
  862. .catch(async err => {
  863. if (err && err !== true) {
  864. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  865. this.log("ERROR", "GET_USER_FROM_ID", `Getting the user from userId "${userId}" failed. "${err}"`);
  866. cb({ status: "failure", message: err });
  867. }
  868. });
  869. }),
  870. // TODO Fix security issues
  871. /**
  872. * Gets user info from session
  873. *
  874. * @param {object} session - the session object automatically added by socket.io
  875. * @param {Function} cb - gets called with the result
  876. */
  877. async findBySession(session, cb) {
  878. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  879. async.waterfall(
  880. [
  881. next => {
  882. CacheModule.runJob(
  883. "HGET",
  884. {
  885. table: "sessions",
  886. key: session.sessionId
  887. },
  888. this
  889. )
  890. .then(session => {
  891. next(null, session);
  892. })
  893. .catch(next);
  894. },
  895. (session, next) => {
  896. if (!session) return next("Session not found.");
  897. return next(null, session);
  898. },
  899. (session, next) => {
  900. userModel.findOne({ _id: session.userId }, next);
  901. },
  902. (user, next) => {
  903. if (!user) return next("User not found.");
  904. return next(null, user);
  905. }
  906. ],
  907. async (err, user) => {
  908. if (err && err !== true) {
  909. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  910. this.log("ERROR", "FIND_BY_SESSION", `User not found. "${err}"`);
  911. return cb({ status: "failure", message: err });
  912. }
  913. const data = {
  914. email: {
  915. address: user.email.address
  916. },
  917. avatar: user.avatar,
  918. username: user.username,
  919. name: user.name,
  920. location: user.location,
  921. bio: user.bio
  922. };
  923. if (user.services.password && user.services.password.password) data.password = true;
  924. if (user.services.github && user.services.github.id) data.github = true;
  925. this.log("SUCCESS", "FIND_BY_SESSION", `User found. "${user.username}".`);
  926. return cb({
  927. status: "success",
  928. data
  929. });
  930. }
  931. );
  932. },
  933. /**
  934. * Updates a user's username
  935. *
  936. * @param {object} session - the session object automatically added by socket.io
  937. * @param {string} updatingUserId - the updating user's id
  938. * @param {string} newUsername - the new username
  939. * @param {Function} cb - gets called with the result
  940. */
  941. updateUsername: isLoginRequired(async function updateUsername(session, updatingUserId, newUsername, cb) {
  942. const userModel = await DBModule.runJob(
  943. "GET_MODEL",
  944. {
  945. modelName: "user"
  946. },
  947. this
  948. );
  949. async.waterfall(
  950. [
  951. next => {
  952. if (updatingUserId === session.userId) return next(null, true);
  953. return userModel.findOne({ _id: session.userId }, next);
  954. },
  955. (user, next) => {
  956. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  957. return userModel.findOne({ _id: updatingUserId }, next);
  958. },
  959. (user, next) => {
  960. if (!user) return next("User not found.");
  961. if (user.username === newUsername)
  962. return next("New username can't be the same as the old username.");
  963. return next(null);
  964. },
  965. next => {
  966. userModel.findOne({ username: new RegExp(`^${newUsername}$`, "i") }, next);
  967. },
  968. (user, next) => {
  969. if (!user) return next();
  970. if (user._id === updatingUserId) return next();
  971. return next("That username is already in use.");
  972. },
  973. next => {
  974. userModel.updateOne(
  975. { _id: updatingUserId },
  976. { $set: { username: newUsername } },
  977. { runValidators: true },
  978. next
  979. );
  980. }
  981. ],
  982. async err => {
  983. if (err && err !== true) {
  984. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  985. this.log(
  986. "ERROR",
  987. "UPDATE_USERNAME",
  988. `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`
  989. );
  990. return cb({ status: "failure", message: err });
  991. }
  992. CacheModule.runJob("PUB", {
  993. channel: "user.updateUsername",
  994. value: {
  995. username: newUsername,
  996. _id: updatingUserId
  997. }
  998. });
  999. this.log(
  1000. "SUCCESS",
  1001. "UPDATE_USERNAME",
  1002. `Updated username for user "${updatingUserId}" to username "${newUsername}".`
  1003. );
  1004. return cb({
  1005. status: "success",
  1006. message: "Username updated successfully"
  1007. });
  1008. }
  1009. );
  1010. }),
  1011. /**
  1012. * Updates a user's email
  1013. *
  1014. * @param {object} session - the session object automatically added by socket.io
  1015. * @param {string} updatingUserId - the updating user's id
  1016. * @param {string} newEmail - the new email
  1017. * @param {Function} cb - gets called with the result
  1018. */
  1019. updateEmail: isLoginRequired(async function updateEmail(session, updatingUserId, newEmail, cb) {
  1020. newEmail = newEmail.toLowerCase();
  1021. const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 }, this);
  1022. const userModel = await DBModule.runJob(
  1023. "GET_MODEL",
  1024. {
  1025. modelName: "user"
  1026. },
  1027. this
  1028. );
  1029. const verifyEmailSchema = await MailModule.runJob(
  1030. "GET_SCHEMA",
  1031. {
  1032. schemaName: "verifyEmail"
  1033. },
  1034. this
  1035. );
  1036. async.waterfall(
  1037. [
  1038. next => {
  1039. if (updatingUserId === session.userId) return next(null, true);
  1040. return userModel.findOne({ _id: session.userId }, next);
  1041. },
  1042. (user, next) => {
  1043. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1044. return userModel.findOne({ _id: updatingUserId }, next);
  1045. },
  1046. (user, next) => {
  1047. if (!user) return next("User not found.");
  1048. if (user.email.address === newEmail)
  1049. return next("New email can't be the same as your the old email.");
  1050. return next();
  1051. },
  1052. next => {
  1053. userModel.findOne({ "email.address": newEmail }, next);
  1054. },
  1055. (user, next) => {
  1056. if (!user) return next();
  1057. if (user._id === updatingUserId) return next();
  1058. return next("That email is already in use.");
  1059. },
  1060. // regenerate the url for gravatar avatar
  1061. next => {
  1062. UtilsModule.runJob("CREATE_GRAVATAR", { email: newEmail }, this).then(url => {
  1063. next(null, url);
  1064. });
  1065. },
  1066. (newAvatarUrl, next) => {
  1067. userModel.updateOne(
  1068. { _id: updatingUserId },
  1069. {
  1070. $set: {
  1071. "avatar.url": newAvatarUrl,
  1072. "email.address": newEmail,
  1073. "email.verified": false,
  1074. "email.verificationToken": verificationToken
  1075. }
  1076. },
  1077. { runValidators: true },
  1078. next
  1079. );
  1080. },
  1081. (res, next) => {
  1082. userModel.findOne({ _id: updatingUserId }, next);
  1083. },
  1084. (user, next) => {
  1085. verifyEmailSchema(newEmail, user.username, verificationToken, err => {
  1086. next(err);
  1087. });
  1088. }
  1089. ],
  1090. async err => {
  1091. if (err && err !== true) {
  1092. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1093. this.log(
  1094. "ERROR",
  1095. "UPDATE_EMAIL",
  1096. `Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`
  1097. );
  1098. return cb({ status: "failure", message: err });
  1099. }
  1100. this.log(
  1101. "SUCCESS",
  1102. "UPDATE_EMAIL",
  1103. `Updated email for user "${updatingUserId}" to email "${newEmail}".`
  1104. );
  1105. return cb({
  1106. status: "success",
  1107. message: "Email updated successfully."
  1108. });
  1109. }
  1110. );
  1111. }),
  1112. /**
  1113. * Updates a user's name
  1114. *
  1115. * @param {object} session - the session object automatically added by socket.io
  1116. * @param {string} updatingUserId - the updating user's id
  1117. * @param {string} newBio - the new name
  1118. * @param {Function} cb - gets called with the result
  1119. */
  1120. updateName: isLoginRequired(async function updateName(session, updatingUserId, newName, cb) {
  1121. const userModel = await DBModule.runJob(
  1122. "GET_MODEL",
  1123. {
  1124. modelName: "user"
  1125. },
  1126. this
  1127. );
  1128. async.waterfall(
  1129. [
  1130. next => {
  1131. if (updatingUserId === session.userId) return next(null, true);
  1132. return userModel.findOne({ _id: session.userId }, next);
  1133. },
  1134. (user, next) => {
  1135. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1136. return userModel.findOne({ _id: updatingUserId }, next);
  1137. },
  1138. (user, next) => {
  1139. if (!user) return next("User not found.");
  1140. return userModel.updateOne(
  1141. { _id: updatingUserId },
  1142. { $set: { name: newName } },
  1143. { runValidators: true },
  1144. next
  1145. );
  1146. }
  1147. ],
  1148. async err => {
  1149. if (err && err !== true) {
  1150. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1151. this.log(
  1152. "ERROR",
  1153. "UPDATE_NAME",
  1154. `Couldn't update name for user "${updatingUserId}" to name "${newName}". "${err}"`
  1155. );
  1156. cb({ status: "failure", message: err });
  1157. } else {
  1158. this.log(
  1159. "SUCCESS",
  1160. "UPDATE_NAME",
  1161. `Updated name for user "${updatingUserId}" to name "${newName}".`
  1162. );
  1163. cb({
  1164. status: "success",
  1165. message: "Name updated successfully"
  1166. });
  1167. }
  1168. }
  1169. );
  1170. }),
  1171. /**
  1172. * Updates a user's location
  1173. *
  1174. * @param {object} session - the session object automatically added by socket.io
  1175. * @param {string} updatingUserId - the updating user's id
  1176. * @param {string} newLocation - the new location
  1177. * @param {Function} cb - gets called with the result
  1178. */
  1179. updateLocation: isLoginRequired(async function updateLocation(session, updatingUserId, newLocation, cb) {
  1180. const userModel = await DBModule.runJob(
  1181. "GET_MODEL",
  1182. {
  1183. modelName: "user"
  1184. },
  1185. this
  1186. );
  1187. async.waterfall(
  1188. [
  1189. next => {
  1190. if (updatingUserId === session.userId) return next(null, true);
  1191. return userModel.findOne({ _id: session.userId }, next);
  1192. },
  1193. (user, next) => {
  1194. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1195. return userModel.findOne({ _id: updatingUserId }, next);
  1196. },
  1197. (user, next) => {
  1198. if (!user) return next("User not found.");
  1199. return userModel.updateOne(
  1200. { _id: updatingUserId },
  1201. { $set: { location: newLocation } },
  1202. { runValidators: true },
  1203. next
  1204. );
  1205. }
  1206. ],
  1207. async err => {
  1208. if (err && err !== true) {
  1209. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1210. this.log(
  1211. "ERROR",
  1212. "UPDATE_LOCATION",
  1213. `Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`
  1214. );
  1215. return cb({ status: "failure", message: err });
  1216. }
  1217. this.log(
  1218. "SUCCESS",
  1219. "UPDATE_LOCATION",
  1220. `Updated location for user "${updatingUserId}" to location "${newLocation}".`
  1221. );
  1222. return cb({
  1223. status: "success",
  1224. message: "Location updated successfully"
  1225. });
  1226. }
  1227. );
  1228. }),
  1229. /**
  1230. * Updates a user's bio
  1231. *
  1232. * @param {object} session - the session object automatically added by socket.io
  1233. * @param {string} updatingUserId - the updating user's id
  1234. * @param {string} newBio - the new bio
  1235. * @param {Function} cb - gets called with the result
  1236. */
  1237. updateBio: isLoginRequired(async function updateBio(session, updatingUserId, newBio, cb) {
  1238. const userModel = await DBModule.runJob(
  1239. "GET_MODEL",
  1240. {
  1241. modelName: "user"
  1242. },
  1243. this
  1244. );
  1245. async.waterfall(
  1246. [
  1247. next => {
  1248. if (updatingUserId === session.userId) return next(null, true);
  1249. return userModel.findOne({ _id: session.userId }, next);
  1250. },
  1251. (user, next) => {
  1252. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1253. return userModel.findOne({ _id: updatingUserId }, next);
  1254. },
  1255. (user, next) => {
  1256. if (!user) return next("User not found.");
  1257. return userModel.updateOne(
  1258. { _id: updatingUserId },
  1259. { $set: { bio: newBio } },
  1260. { runValidators: true },
  1261. next
  1262. );
  1263. }
  1264. ],
  1265. async err => {
  1266. if (err && err !== true) {
  1267. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1268. this.log(
  1269. "ERROR",
  1270. "UPDATE_BIO",
  1271. `Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`
  1272. );
  1273. cb({ status: "failure", message: err });
  1274. } else {
  1275. this.log("SUCCESS", "UPDATE_BIO", `Updated bio for user "${updatingUserId}" to bio "${newBio}".`);
  1276. cb({
  1277. status: "success",
  1278. message: "Bio updated successfully"
  1279. });
  1280. }
  1281. }
  1282. );
  1283. }),
  1284. /**
  1285. * Updates the type of a user's avatar
  1286. *
  1287. * @param {object} session - the session object automatically added by socket.io
  1288. * @param {string} updatingUserId - the updating user's id
  1289. * @param {string} newType - the new type
  1290. * @param {Function} cb - gets called with the result
  1291. */
  1292. updateAvatarType: isLoginRequired(async function updateAvatarType(session, updatingUserId, newType, cb) {
  1293. const userModel = await DBModule.runJob(
  1294. "GET_MODEL",
  1295. {
  1296. modelName: "user"
  1297. },
  1298. this
  1299. );
  1300. async.waterfall(
  1301. [
  1302. next => {
  1303. if (updatingUserId === session.userId) return next(null, true);
  1304. return userModel.findOne({ _id: session.userId }, next);
  1305. },
  1306. (user, next) => {
  1307. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1308. return userModel.findOne({ _id: updatingUserId }, next);
  1309. },
  1310. (user, next) => {
  1311. if (!user) return next("User not found.");
  1312. return userModel.findOneAndUpdate(
  1313. { _id: updatingUserId },
  1314. { $set: { "avatar.type": newType } },
  1315. { new: true, runValidators: true },
  1316. next
  1317. );
  1318. }
  1319. ],
  1320. async err => {
  1321. if (err && err !== true) {
  1322. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1323. this.log(
  1324. "ERROR",
  1325. "UPDATE_AVATAR_TYPE",
  1326. `Couldn't update avatar type for user "${updatingUserId}" to type "${newType}". "${err}"`
  1327. );
  1328. return cb({ status: "failure", message: err });
  1329. }
  1330. this.log(
  1331. "SUCCESS",
  1332. "UPDATE_AVATAR_TYPE",
  1333. `Updated avatar type for user "${updatingUserId}" to type "${newType}".`
  1334. );
  1335. return cb({
  1336. status: "success",
  1337. message: "Avatar type updated successfully"
  1338. });
  1339. }
  1340. );
  1341. }),
  1342. /**
  1343. * Updates a user's role
  1344. *
  1345. * @param {object} session - the session object automatically added by socket.io
  1346. * @param {string} updatingUserId - the updating user's id
  1347. * @param {string} newRole - the new role
  1348. * @param {Function} cb - gets called with the result
  1349. */
  1350. updateRole: isAdminRequired(async function updateRole(session, updatingUserId, newRole, cb) {
  1351. newRole = newRole.toLowerCase();
  1352. const userModel = await DBModule.runJob(
  1353. "GET_MODEL",
  1354. {
  1355. modelName: "user"
  1356. },
  1357. this
  1358. );
  1359. async.waterfall(
  1360. [
  1361. next => {
  1362. userModel.findOne({ _id: updatingUserId }, next);
  1363. },
  1364. (user, next) => {
  1365. if (!user) return next("User not found.");
  1366. if (user.role === newRole) return next("New role can't be the same as the old role.");
  1367. return next();
  1368. },
  1369. next => {
  1370. userModel.updateOne(
  1371. { _id: updatingUserId },
  1372. { $set: { role: newRole } },
  1373. { runValidators: true },
  1374. next
  1375. );
  1376. }
  1377. ],
  1378. async err => {
  1379. if (err && err !== true) {
  1380. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1381. this.log(
  1382. "ERROR",
  1383. "UPDATE_ROLE",
  1384. `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`
  1385. );
  1386. return cb({ status: "failure", message: err });
  1387. }
  1388. this.log(
  1389. "SUCCESS",
  1390. "UPDATE_ROLE",
  1391. `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`
  1392. );
  1393. return cb({
  1394. status: "success",
  1395. message: "Role successfully updated."
  1396. });
  1397. }
  1398. );
  1399. }),
  1400. /**
  1401. * Updates a user's password
  1402. *
  1403. * @param {object} session - the session object automatically added by socket.io
  1404. * @param {string} previousPassword - the previous password
  1405. * @param {string} newPassword - the new password
  1406. * @param {Function} cb - gets called with the result
  1407. */
  1408. updatePassword: isLoginRequired(async function updatePassword(session, previousPassword, newPassword, cb) {
  1409. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1410. async.waterfall(
  1411. [
  1412. next => {
  1413. userModel.findOne({ _id: session.userId }, next);
  1414. },
  1415. (user, next) => {
  1416. if (!user.services.password) return next("This account does not have a password set.");
  1417. return next(null, user.services.password.password);
  1418. },
  1419. (storedPassword, next) => {
  1420. bcrypt.compare(sha256(previousPassword), storedPassword).then(res => {
  1421. if (res) return next();
  1422. return next("Please enter the correct previous password.");
  1423. });
  1424. },
  1425. next => {
  1426. if (!DBModule.passwordValid(newPassword))
  1427. return next("Invalid new password. Check if it meets all the requirements.");
  1428. return next();
  1429. },
  1430. next => {
  1431. bcrypt.genSalt(10, next);
  1432. },
  1433. // hash the password
  1434. (salt, next) => {
  1435. bcrypt.hash(sha256(newPassword), salt, next);
  1436. },
  1437. (hashedPassword, next) => {
  1438. userModel.updateOne(
  1439. { _id: session.userId },
  1440. {
  1441. $set: {
  1442. "services.password.password": hashedPassword
  1443. }
  1444. },
  1445. next
  1446. );
  1447. }
  1448. ],
  1449. async err => {
  1450. if (err) {
  1451. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1452. this.log(
  1453. "ERROR",
  1454. "UPDATE_PASSWORD",
  1455. `Failed updating user password of user '${session.userId}'. '${err}'.`
  1456. );
  1457. return cb({ status: "failure", message: err });
  1458. }
  1459. this.log("SUCCESS", "UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
  1460. return cb({
  1461. status: "success",
  1462. message: "Password successfully updated."
  1463. });
  1464. }
  1465. );
  1466. }),
  1467. /**
  1468. * Requests a password for a session
  1469. *
  1470. * @param {object} session - the session object automatically added by socket.io
  1471. * @param {string} email - the email of the user that requests a password reset
  1472. * @param {Function} cb - gets called with the result
  1473. */
  1474. requestPassword: isLoginRequired(async function requestPassword(session, cb) {
  1475. const code = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 8 }, this);
  1476. const passwordRequestSchema = await MailModule.runJob(
  1477. "GET_SCHEMA",
  1478. {
  1479. schemaName: "passwordRequest"
  1480. },
  1481. this
  1482. );
  1483. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1484. async.waterfall(
  1485. [
  1486. next => {
  1487. userModel.findOne({ _id: session.userId }, next);
  1488. },
  1489. (user, next) => {
  1490. if (!user) return next("User not found.");
  1491. if (user.services.password && user.services.password.password)
  1492. return next("You already have a password set.");
  1493. return next(null, user);
  1494. },
  1495. (user, next) => {
  1496. const expires = new Date();
  1497. expires.setDate(expires.getDate() + 1);
  1498. userModel.findOneAndUpdate(
  1499. { "email.address": user.email.address },
  1500. {
  1501. $set: {
  1502. "services.password": {
  1503. set: { code, expires }
  1504. }
  1505. }
  1506. },
  1507. { runValidators: true },
  1508. next
  1509. );
  1510. },
  1511. (user, next) => {
  1512. passwordRequestSchema(user.email.address, user.username, code, next);
  1513. }
  1514. ],
  1515. async err => {
  1516. if (err && err !== true) {
  1517. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1518. this.log(
  1519. "ERROR",
  1520. "REQUEST_PASSWORD",
  1521. `UserId '${session.userId}' failed to request password. '${err}'`
  1522. );
  1523. return cb({ status: "failure", message: err });
  1524. }
  1525. this.log(
  1526. "SUCCESS",
  1527. "REQUEST_PASSWORD",
  1528. `UserId '${session.userId}' successfully requested a password.`
  1529. );
  1530. return cb({
  1531. status: "success",
  1532. message: "Successfully requested password."
  1533. });
  1534. }
  1535. );
  1536. }),
  1537. /**
  1538. * Verifies a password code
  1539. *
  1540. * @param {object} session - the session object automatically added by socket.io
  1541. * @param {string} code - the password code
  1542. * @param {Function} cb - gets called with the result
  1543. */
  1544. verifyPasswordCode: isLoginRequired(async function verifyPasswordCode(session, code, cb) {
  1545. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1546. async.waterfall(
  1547. [
  1548. next => {
  1549. if (!code || typeof code !== "string") return next("Invalid code.");
  1550. return userModel.findOne(
  1551. {
  1552. "services.password.set.code": code,
  1553. _id: session.userId
  1554. },
  1555. next
  1556. );
  1557. },
  1558. (user, next) => {
  1559. if (!user) return next("Invalid code.");
  1560. if (user.services.password.set.expires < new Date()) return next("That code has expired.");
  1561. return next(null);
  1562. }
  1563. ],
  1564. async err => {
  1565. if (err && err !== true) {
  1566. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1567. this.log("ERROR", "VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
  1568. cb({ status: "failure", message: err });
  1569. } else {
  1570. this.log("SUCCESS", "VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
  1571. cb({
  1572. status: "success",
  1573. message: "Successfully verified password code."
  1574. });
  1575. }
  1576. }
  1577. );
  1578. }),
  1579. /**
  1580. * Adds a password to a user with a code
  1581. *
  1582. * @param {object} session - the session object automatically added by socket.io
  1583. * @param {string} code - the password code
  1584. * @param {string} newPassword - the new password code
  1585. * @param {Function} cb - gets called with the result
  1586. */
  1587. changePasswordWithCode: isLoginRequired(async function changePasswordWithCode(session, code, newPassword, cb) {
  1588. const userModel = await DBModule.runJob(
  1589. "GET_MODEL",
  1590. {
  1591. modelName: "user"
  1592. },
  1593. this
  1594. );
  1595. async.waterfall(
  1596. [
  1597. next => {
  1598. if (!code || typeof code !== "string") return next("Invalid code.");
  1599. return userModel.findOne({ "services.password.set.code": code }, next);
  1600. },
  1601. (user, next) => {
  1602. if (!user) return next("Invalid code.");
  1603. if (!user.services.password.set.expires > new Date()) return next("That code has expired.");
  1604. return next();
  1605. },
  1606. next => {
  1607. if (!DBModule.passwordValid(newPassword))
  1608. return next("Invalid password. Check if it meets all the requirements.");
  1609. return next();
  1610. },
  1611. next => {
  1612. bcrypt.genSalt(10, next);
  1613. },
  1614. // hash the password
  1615. (salt, next) => {
  1616. bcrypt.hash(sha256(newPassword), salt, next);
  1617. },
  1618. (hashedPassword, next) => {
  1619. userModel.updateOne(
  1620. { "services.password.set.code": code },
  1621. {
  1622. $set: {
  1623. "services.password.password": hashedPassword
  1624. },
  1625. $unset: { "services.password.set": "" }
  1626. },
  1627. { runValidators: true },
  1628. next
  1629. );
  1630. }
  1631. ],
  1632. async err => {
  1633. if (err && err !== true) {
  1634. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1635. this.log("ERROR", "ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
  1636. cb({ status: "failure", message: err });
  1637. } else {
  1638. this.log("SUCCESS", "ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
  1639. CacheModule.runJob("PUB", {
  1640. channel: "user.linkPassword",
  1641. value: session.userId
  1642. });
  1643. cb({
  1644. status: "success",
  1645. message: "Successfully added password."
  1646. });
  1647. }
  1648. }
  1649. );
  1650. }),
  1651. /**
  1652. * Unlinks password from user
  1653. *
  1654. * @param {object} session - the session object automatically added by socket.io
  1655. * @param {Function} cb - gets called with the result
  1656. */
  1657. unlinkPassword: isLoginRequired(async function unlinkPassword(session, cb) {
  1658. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1659. async.waterfall(
  1660. [
  1661. next => {
  1662. userModel.findOne({ _id: session.userId }, next);
  1663. },
  1664. (user, next) => {
  1665. if (!user) return next("Not logged in.");
  1666. if (!user.services.github || !user.services.github.id)
  1667. return next("You can't remove password login without having GitHub login.");
  1668. return userModel.updateOne({ _id: session.userId }, { $unset: { "services.password": "" } }, next);
  1669. }
  1670. ],
  1671. async err => {
  1672. if (err && err !== true) {
  1673. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1674. this.log(
  1675. "ERROR",
  1676. "UNLINK_PASSWORD",
  1677. `Unlinking password failed for userId '${session.userId}'. '${err}'`
  1678. );
  1679. cb({ status: "failure", message: err });
  1680. } else {
  1681. this.log(
  1682. "SUCCESS",
  1683. "UNLINK_PASSWORD",
  1684. `Unlinking password successful for userId '${session.userId}'.`
  1685. );
  1686. CacheModule.runJob("PUB", {
  1687. channel: "user.unlinkPassword",
  1688. value: session.userId
  1689. });
  1690. cb({
  1691. status: "success",
  1692. message: "Successfully unlinked password."
  1693. });
  1694. }
  1695. }
  1696. );
  1697. }),
  1698. /**
  1699. * Unlinks GitHub from user
  1700. *
  1701. * @param {object} session - the session object automatically added by socket.io
  1702. * @param {Function} cb - gets called with the result
  1703. */
  1704. unlinkGitHub: isLoginRequired(async function unlinkGitHub(session, cb) {
  1705. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1706. async.waterfall(
  1707. [
  1708. next => {
  1709. userModel.findOne({ _id: session.userId }, next);
  1710. },
  1711. (user, next) => {
  1712. if (!user) return next("Not logged in.");
  1713. if (!user.services.password || !user.services.password.password)
  1714. return next("You can't remove GitHub login without having password login.");
  1715. return userModel.updateOne({ _id: session.userId }, { $unset: { "services.github": "" } }, next);
  1716. }
  1717. ],
  1718. async err => {
  1719. if (err && err !== true) {
  1720. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1721. this.log(
  1722. "ERROR",
  1723. "UNLINK_GITHUB",
  1724. `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`
  1725. );
  1726. cb({ status: "failure", message: err });
  1727. } else {
  1728. this.log("SUCCESS", "UNLINK_GITHUB", `Unlinking GitHub successful for userId '${session.userId}'.`);
  1729. CacheModule.runJob("PUB", {
  1730. channel: "user.unlinkGithub",
  1731. value: session.userId
  1732. });
  1733. cb({
  1734. status: "success",
  1735. message: "Successfully unlinked GitHub."
  1736. });
  1737. }
  1738. }
  1739. );
  1740. }),
  1741. /**
  1742. * Requests a password reset for an email
  1743. *
  1744. * @param {object} session - the session object automatically added by socket.io
  1745. * @param {string} email - the email of the user that requests a password reset
  1746. * @param {Function} cb - gets called with the result
  1747. */
  1748. async requestPasswordReset(session, email, cb) {
  1749. const code = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 8 }, this);
  1750. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1751. const resetPasswordRequestSchema = await MailModule.runJob(
  1752. "GET_SCHEMA",
  1753. {
  1754. schemaName: "resetPasswordRequest"
  1755. },
  1756. this
  1757. );
  1758. async.waterfall(
  1759. [
  1760. next => {
  1761. if (!email || typeof email !== "string") return next("Invalid email.");
  1762. email = email.toLowerCase();
  1763. return userModel.findOne({ "email.address": email }, next);
  1764. },
  1765. (user, next) => {
  1766. if (!user) return next("User not found.");
  1767. if (!user.services.password || !user.services.password.password)
  1768. return next("User does not have a password set, and probably uses GitHub to log in.");
  1769. return next(null, user);
  1770. },
  1771. (user, next) => {
  1772. const expires = new Date();
  1773. expires.setDate(expires.getDate() + 1);
  1774. userModel.findOneAndUpdate(
  1775. { "email.address": email },
  1776. {
  1777. $set: {
  1778. "services.password.reset": {
  1779. code,
  1780. expires
  1781. }
  1782. }
  1783. },
  1784. { runValidators: true },
  1785. next
  1786. );
  1787. },
  1788. (user, next) => {
  1789. resetPasswordRequestSchema(user.email.address, user.username, code, next);
  1790. }
  1791. ],
  1792. async err => {
  1793. if (err && err !== true) {
  1794. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1795. this.log(
  1796. "ERROR",
  1797. "REQUEST_PASSWORD_RESET",
  1798. `Email '${email}' failed to request password reset. '${err}'`
  1799. );
  1800. cb({ status: "failure", message: err });
  1801. } else {
  1802. this.log(
  1803. "SUCCESS",
  1804. "REQUEST_PASSWORD_RESET",
  1805. `Email '${email}' successfully requested a password reset.`
  1806. );
  1807. cb({
  1808. status: "success",
  1809. message: "Successfully requested password reset."
  1810. });
  1811. }
  1812. }
  1813. );
  1814. },
  1815. /**
  1816. * Verifies a reset code
  1817. *
  1818. * @param {object} session - the session object automatically added by socket.io
  1819. * @param {string} code - the password reset code
  1820. * @param {Function} cb - gets called with the result
  1821. */
  1822. async verifyPasswordResetCode(session, code, cb) {
  1823. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1824. async.waterfall(
  1825. [
  1826. next => {
  1827. if (!code || typeof code !== "string") return next("Invalid code.");
  1828. return userModel.findOne({ "services.password.reset.code": code }, next);
  1829. },
  1830. (user, next) => {
  1831. if (!user) return next("Invalid code.");
  1832. if (!user.services.password.reset.expires > new Date()) return next("That code has expired.");
  1833. return next(null);
  1834. }
  1835. ],
  1836. async err => {
  1837. if (err && err !== true) {
  1838. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1839. this.log("ERROR", "VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
  1840. cb({ status: "failure", message: err });
  1841. } else {
  1842. this.log("SUCCESS", "VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  1843. cb({
  1844. status: "success",
  1845. message: "Successfully verified password reset code."
  1846. });
  1847. }
  1848. }
  1849. );
  1850. },
  1851. /**
  1852. * Changes a user's password with a reset code
  1853. *
  1854. * @param {object} session - the session object automatically added by socket.io
  1855. * @param {string} code - the password reset code
  1856. * @param {string} newPassword - the new password reset code
  1857. * @param {Function} cb - gets called with the result
  1858. */
  1859. async changePasswordWithResetCode(session, code, newPassword, cb) {
  1860. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1861. async.waterfall(
  1862. [
  1863. next => {
  1864. if (!code || typeof code !== "string") return next("Invalid code.");
  1865. return userModel.findOne({ "services.password.reset.code": code }, next);
  1866. },
  1867. (user, next) => {
  1868. if (!user) return next("Invalid code.");
  1869. if (!user.services.password.reset.expires > new Date()) return next("That code has expired.");
  1870. return next();
  1871. },
  1872. next => {
  1873. if (!DBModule.passwordValid(newPassword))
  1874. return next("Invalid password. Check if it meets all the requirements.");
  1875. return next();
  1876. },
  1877. next => {
  1878. bcrypt.genSalt(10, next);
  1879. },
  1880. // hash the password
  1881. (salt, next) => {
  1882. bcrypt.hash(sha256(newPassword), salt, next);
  1883. },
  1884. (hashedPassword, next) => {
  1885. userModel.updateOne(
  1886. { "services.password.reset.code": code },
  1887. {
  1888. $set: {
  1889. "services.password.password": hashedPassword
  1890. },
  1891. $unset: { "services.password.reset": "" }
  1892. },
  1893. { runValidators: true },
  1894. next
  1895. );
  1896. }
  1897. ],
  1898. async err => {
  1899. if (err && err !== true) {
  1900. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1901. this.log(
  1902. "ERROR",
  1903. "CHANGE_PASSWORD_WITH_RESET_CODE",
  1904. `Code '${code}' failed to change password. '${err}'`
  1905. );
  1906. cb({ status: "failure", message: err });
  1907. } else {
  1908. this.log(
  1909. "SUCCESS",
  1910. "CHANGE_PASSWORD_WITH_RESET_CODE",
  1911. `Code '${code}' successfully changed password.`
  1912. );
  1913. cb({
  1914. status: "success",
  1915. message: "Successfully changed password."
  1916. });
  1917. }
  1918. }
  1919. );
  1920. },
  1921. /**
  1922. * Bans a user by userId
  1923. *
  1924. * @param {object} session - the session object automatically added by socket.io
  1925. * @param {string} value - the user id that is going to be banned
  1926. * @param {string} reason - the reason for the ban
  1927. * @param {string} expiresAt - the time the ban expires
  1928. * @param {Function} cb - gets called with the result
  1929. */
  1930. banUserById: isAdminRequired(function banUserById(session, userId, reason, expiresAt, cb) {
  1931. async.waterfall(
  1932. [
  1933. next => {
  1934. if (!userId) return next("You must provide a userId to ban.");
  1935. if (!reason) return next("You must provide a reason for the ban.");
  1936. return next();
  1937. },
  1938. next => {
  1939. if (!expiresAt || typeof expiresAt !== "string") return next("Invalid expire date.");
  1940. const date = new Date();
  1941. switch (expiresAt) {
  1942. case "1h":
  1943. expiresAt = date.setHours(date.getHours() + 1);
  1944. break;
  1945. case "12h":
  1946. expiresAt = date.setHours(date.getHours() + 12);
  1947. break;
  1948. case "1d":
  1949. expiresAt = date.setDate(date.getDate() + 1);
  1950. break;
  1951. case "1w":
  1952. expiresAt = date.setDate(date.getDate() + 7);
  1953. break;
  1954. case "1m":
  1955. expiresAt = date.setMonth(date.getMonth() + 1);
  1956. break;
  1957. case "3m":
  1958. expiresAt = date.setMonth(date.getMonth() + 3);
  1959. break;
  1960. case "6m":
  1961. expiresAt = date.setMonth(date.getMonth() + 6);
  1962. break;
  1963. case "1y":
  1964. expiresAt = date.setFullYear(date.getFullYear() + 1);
  1965. break;
  1966. case "never":
  1967. expiresAt = new Date(3093527980800000);
  1968. break;
  1969. default:
  1970. return next("Invalid expire date.");
  1971. }
  1972. return next();
  1973. },
  1974. next => {
  1975. PunishmentsModule.runJob(
  1976. "ADD_PUNISHMENT",
  1977. {
  1978. type: "banUserId",
  1979. value: userId,
  1980. reason,
  1981. expiresAt,
  1982. punishedBy: "" // needs changed
  1983. },
  1984. this
  1985. )
  1986. .then(punishment => next(null, punishment))
  1987. .catch(next);
  1988. },
  1989. (punishment, next) => {
  1990. CacheModule.runJob("PUB", {
  1991. channel: "user.ban",
  1992. value: { userId, punishment }
  1993. });
  1994. next();
  1995. }
  1996. ],
  1997. async err => {
  1998. if (err && err !== true) {
  1999. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2000. this.log(
  2001. "ERROR",
  2002. "BAN_USER_BY_ID",
  2003. `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`
  2004. );
  2005. cb({ status: "failure", message: err });
  2006. } else {
  2007. this.log(
  2008. "SUCCESS",
  2009. "BAN_USER_BY_ID",
  2010. `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`
  2011. );
  2012. cb({
  2013. status: "success",
  2014. message: "Successfully banned user."
  2015. });
  2016. }
  2017. }
  2018. );
  2019. })
  2020. };