users.js 78 KB

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