users.js 57 KB

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