users.js 78 KB

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