users.js 58 KB

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