users.js 50 KB

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