users.js 50 KB

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