users.js 58 KB

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