users.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
  1. 'use strict';
  2. const async = require('async');
  3. const config = require('config');
  4. const request = require('request');
  5. const bcrypt = require('bcrypt');
  6. const sha256 = require('sha256');
  7. const hooks = require('./hooks');
  8. const moduleManager = require("../../index");
  9. const db = moduleManager.modules["db"];
  10. const mail = moduleManager.modules["mail"];
  11. const cache = moduleManager.modules["cache"];
  12. const punishments = moduleManager.modules["punishments"];
  13. const utils = moduleManager.modules["utils"];
  14. const logger = moduleManager.modules["logger"];
  15. cache.sub('user.updateUsername', user => {
  16. utils.socketsFromUser(user._id, sockets => {
  17. sockets.forEach(socket => {
  18. socket.emit('event:user.username.changed', user.username);
  19. });
  20. });
  21. });
  22. cache.sub('user.removeSessions', userId => {
  23. utils.socketsFromUserWithoutCache(userId, sockets => {
  24. sockets.forEach(socket => {
  25. socket.emit('keep.event:user.session.removed');
  26. });
  27. });
  28. });
  29. cache.sub('user.linkPassword', userId => {
  30. utils.socketsFromUser(userId, sockets => {
  31. sockets.forEach(socket => {
  32. socket.emit('event:user.linkPassword');
  33. });
  34. });
  35. });
  36. cache.sub('user.linkGitHub', userId => {
  37. utils.socketsFromUser(userId, sockets => {
  38. sockets.forEach(socket => {
  39. socket.emit('event:user.linkGitHub');
  40. });
  41. });
  42. });
  43. cache.sub('user.unlinkPassword', userId => {
  44. utils.socketsFromUser(userId, sockets => {
  45. sockets.forEach(socket => {
  46. socket.emit('event:user.unlinkPassword');
  47. });
  48. });
  49. });
  50. cache.sub('user.unlinkGitHub', userId => {
  51. utils.socketsFromUser(userId, sockets => {
  52. sockets.forEach(socket => {
  53. socket.emit('event:user.unlinkGitHub');
  54. });
  55. });
  56. });
  57. cache.sub('user.ban', data => {
  58. utils.socketsFromUser(data.userId, sockets => {
  59. sockets.forEach(socket => {
  60. socket.emit('keep.event:banned', data.punishment);
  61. socket.disconnect(true);
  62. });
  63. });
  64. });
  65. cache.sub('user.favoritedStation', data => {
  66. utils.socketsFromUser(data.userId, sockets => {
  67. sockets.forEach(socket => {
  68. socket.emit('event:user.favoritedStation', data.stationId);
  69. });
  70. });
  71. });
  72. cache.sub('user.unfavoritedStation', data => {
  73. utils.socketsFromUser(data.userId, sockets => {
  74. sockets.forEach(socket => {
  75. socket.emit('event:user.unfavoritedStation', data.stationId);
  76. });
  77. });
  78. });
  79. module.exports = {
  80. /**
  81. * Lists all Users
  82. *
  83. * @param {Object} session - the session object automatically added by socket.io
  84. * @param {Function} cb - gets called with the result
  85. */
  86. index: hooks.adminRequired((session, cb) => {
  87. async.waterfall([
  88. (next) => {
  89. db.models.user.find({}).exec(next);
  90. }
  91. ], async (err, users) => {
  92. if (err) {
  93. err = await utils.getError(err);
  94. logger.error("USER_INDEX", `Indexing users failed. "${err}"`);
  95. return cb({status: 'failure', message: err});
  96. } else {
  97. logger.success("USER_INDEX", `Indexing users successful.`);
  98. let filteredUsers = [];
  99. users.forEach(user => {
  100. filteredUsers.push({
  101. _id: user._id,
  102. username: user.username,
  103. role: user.role,
  104. liked: user.liked,
  105. disliked: user.disliked,
  106. songsRequested: user.statistics.songsRequested,
  107. email: {
  108. address: user.email.address,
  109. verified: user.email.verified
  110. },
  111. hasPassword: !!user.services.password,
  112. services: { github: user.services.github }
  113. });
  114. });
  115. return cb({ status: 'success', data: filteredUsers });
  116. }
  117. });
  118. }),
  119. /**
  120. * Logs user in
  121. *
  122. * @param {Object} session - the session object automatically added by socket.io
  123. * @param {String} identifier - the email of the user
  124. * @param {String} password - the plaintext of the user
  125. * @param {Function} cb - gets called with the result
  126. */
  127. login: (session, identifier, password, cb) => {
  128. identifier = identifier.toLowerCase();
  129. async.waterfall([
  130. // check if a user with the requested identifier exists
  131. (next) => {
  132. db.models.user.findOne({
  133. $or: [{ 'email.address': identifier }]
  134. }, next)
  135. },
  136. // if the user doesn't exist, respond with a failure
  137. // otherwise compare the requested password and the actual users password
  138. (user, next) => {
  139. if (!user) return next('User not found');
  140. if (!user.services.password || !user.services.password.password) return next('The account you are trying to access uses GitHub to log in.');
  141. bcrypt.compare(sha256(password), user.services.password.password, (err, match) => {
  142. if (err) return next(err);
  143. if (!match) return next('Incorrect password');
  144. next(null, user);
  145. });
  146. },
  147. (user, next) => {
  148. utils.guid().then((sessionId) => {
  149. next(null, user, sessionId);
  150. });
  151. },
  152. (user, sessionId, next) => {
  153. cache.hset('sessions', sessionId, cache.schemas.session(sessionId, user._id), (err) => {
  154. if (err) return next(err);
  155. next(null, sessionId);
  156. });
  157. }
  158. ], async (err, sessionId) => {
  159. if (err && err !== true) {
  160. err = await utils.getError(err);
  161. logger.error("USER_PASSWORD_LOGIN", `Login failed with password for user "${identifier}". "${err}"`);
  162. return cb({status: 'failure', message: err});
  163. }
  164. logger.success("USER_PASSWORD_LOGIN", `Login successful with password for user "${identifier}"`);
  165. cb({ status: 'success', message: 'Login successful', user: {}, SID: sessionId });
  166. });
  167. },
  168. /**
  169. * Registers a new user
  170. *
  171. * @param {Object} session - the session object automatically added by socket.io
  172. * @param {String} username - the username for the new user
  173. * @param {String} email - the email for the new user
  174. * @param {String} password - the plaintext password for the new user
  175. * @param {Object} recaptcha - the recaptcha data
  176. * @param {Function} cb - gets called with the result
  177. */
  178. register: async function(session, username, email, password, recaptcha, cb) {
  179. email = email.toLowerCase();
  180. let verificationToken = await utils.generateRandomString(64);
  181. async.waterfall([
  182. // verify the request with google recaptcha
  183. (next) => {
  184. if (!db.passwordValid(password)) return next('Invalid password. Check if it meets all the requirements.');
  185. return next();
  186. },
  187. (next) => {
  188. request({
  189. url: 'https://www.google.com/recaptcha/api/siteverify',
  190. method: 'POST',
  191. form: {
  192. 'secret': config.get("apis").recaptcha.secret,
  193. 'response': recaptcha
  194. }
  195. }, next);
  196. },
  197. // check if the response from Google recaptcha is successful
  198. // if it is, we check if a user with the requested username already exists
  199. (response, body, next) => {
  200. let json = JSON.parse(body);
  201. if (json.success !== true) return next('Response from recaptcha was not successful.');
  202. db.models.user.findOne({ username: new RegExp(`^${username}$`, 'i') }, next);
  203. },
  204. // if the user already exists, respond with that
  205. // otherwise check if a user with the requested email already exists
  206. (user, next) => {
  207. if (user) return next('A user with that username already exists.');
  208. db.models.user.findOne({ 'email.address': email }, next);
  209. },
  210. // if the user already exists, respond with that
  211. // otherwise, generate a salt to use with hashing the new users password
  212. (user, next) => {
  213. if (user) return next('A user with that email already exists.');
  214. bcrypt.genSalt(10, next);
  215. },
  216. // hash the password
  217. (salt, next) => {
  218. bcrypt.hash(sha256(password), salt, next)
  219. },
  220. (hash, next) => {
  221. utils.generateRandomString(12).then((_id) => {
  222. next(null, hash, _id);
  223. });
  224. },
  225. // create the user object
  226. (hash, _id, next) => {
  227. next(null, {
  228. _id,
  229. username,
  230. email: {
  231. address: email,
  232. verificationToken
  233. },
  234. services: {
  235. password: {
  236. password: hash
  237. }
  238. }
  239. });
  240. },
  241. // generate the url for gravatar avatar
  242. (user, next) => {
  243. this.utils.createGravatar(user.email.address).then(url => {
  244. user.avatar = url;
  245. next(null, user);
  246. });
  247. },
  248. // save the new user to the database
  249. (user, next) => {
  250. db.models.user.create(user, next);
  251. },
  252. // respond with the new user
  253. (newUser, next) => {
  254. mail.schemas.verifyEmail(email, username, verificationToken, () => {
  255. next();
  256. });
  257. }
  258. ], async (err) => {
  259. if (err && err !== true) {
  260. err = await utils.getError(err);
  261. logger.error("USER_PASSWORD_REGISTER", `Register failed with password for user "${username}"."${err}"`);
  262. cb({status: 'failure', message: err});
  263. } else {
  264. module.exports.login(session, email, password, (result) => {
  265. let obj = {status: 'success', message: 'Successfully registered.'};
  266. if (result.status === 'success') {
  267. obj.SID = result.SID;
  268. }
  269. logger.success("USER_PASSWORD_REGISTER", `Register successful with password for user "${username}".`);
  270. cb(obj);
  271. });
  272. }
  273. });
  274. },
  275. /**
  276. * Logs out a user
  277. *
  278. * @param {Object} session - the session object automatically added by socket.io
  279. * @param {Function} cb - gets called with the result
  280. */
  281. logout: (session, cb) => {
  282. async.waterfall([
  283. (next) => {
  284. cache.hget('sessions', session.sessionId, next);
  285. },
  286. (session, next) => {
  287. if (!session) return next('Session not found');
  288. next(null, session);
  289. },
  290. (session, next) => {
  291. cache.hdel('sessions', session.sessionId, next);
  292. }
  293. ], async (err) => {
  294. if (err && err !== true) {
  295. err = await utils.getError(err);
  296. logger.error("USER_LOGOUT", `Logout failed. "${err}" `);
  297. cb({ status: 'failure', message: err });
  298. } else {
  299. logger.success("USER_LOGOUT", `Logout successful.`);
  300. cb({ status: 'success', message: 'Successfully logged out.' });
  301. }
  302. });
  303. },
  304. /**
  305. * Removes all sessions for a user
  306. *
  307. * @param {Object} session - the session object automatically added by socket.io
  308. * @param {String} userId - the id of the user we are trying to delete the sessions of
  309. * @param {Function} cb - gets called with the result
  310. */
  311. removeSessions: hooks.loginRequired((session, userId, cb) => {
  312. async.waterfall([
  313. (next) => {
  314. db.models.user.findOne({ _id: session.userId }, (err, user) => {
  315. if (err) return next(err);
  316. if (user.role !== 'admin' && session.userId !== userId) return next('Only admins and the owner of the account can remove their sessions.');
  317. else return next();
  318. });
  319. },
  320. (next) => {
  321. cache.hgetall('sessions', next);
  322. },
  323. (sessions, next) => {
  324. if (!sessions) return next('There are no sessions for this user to remove.');
  325. else {
  326. let keys = Object.keys(sessions);
  327. next(null, keys, sessions);
  328. }
  329. },
  330. (keys, sessions, next) => {
  331. cache.pub('user.removeSessions', userId);
  332. async.each(keys, (sessionId, callback) => {
  333. let session = sessions[sessionId];
  334. if (session.userId === userId) {
  335. cache.hdel('sessions', sessionId, err => {
  336. if (err) return callback(err);
  337. else callback(null);
  338. });
  339. }
  340. }, err => {
  341. next(err);
  342. });
  343. }
  344. ], async err => {
  345. if (err) {
  346. err = await utils.getError(err);
  347. logger.error("REMOVE_SESSIONS_FOR_USER", `Couldn't remove all sessions for user "${userId}". "${err}"`);
  348. return cb({ status: 'failure', message: err });
  349. } else {
  350. logger.success("REMOVE_SESSIONS_FOR_USER", `Removed all sessions for user "${userId}".`);
  351. return cb({ status: 'success', message: 'Successfully removed all sessions.' });
  352. }
  353. });
  354. }),
  355. /**
  356. * Gets user object from username (only a few properties)
  357. *
  358. * @param {Object} session - the session object automatically added by socket.io
  359. * @param {String} username - the username of the user we are trying to find
  360. * @param {Function} cb - gets called with the result
  361. */
  362. findByUsername: (session, username, cb) => {
  363. async.waterfall([
  364. (next) => {
  365. db.models.user.findOne({ username: new RegExp(`^${username}$`, 'i') }, next);
  366. },
  367. (account, next) => {
  368. if (!account) return next('User not found.');
  369. next(null, account);
  370. }
  371. ], async (err, account) => {
  372. if (err && err !== true) {
  373. err = await utils.getError(err);
  374. logger.error("FIND_BY_USERNAME", `User not found for username "${username}". "${err}"`);
  375. cb({status: 'failure', message: err});
  376. } else {
  377. logger.success("FIND_BY_USERNAME", `User found for username "${username}".`);
  378. return cb({
  379. status: 'success',
  380. data: {
  381. _id: account._id,
  382. username: account.username,
  383. role: account.role,
  384. email: account.email.address,
  385. avatar: account.avatar,
  386. createdAt: account.createdAt,
  387. statistics: account.statistics,
  388. liked: account.liked,
  389. disliked: account.disliked
  390. }
  391. });
  392. }
  393. });
  394. },
  395. /**
  396. * Gets a username from an userId
  397. *
  398. * @param {Object} session - the session object automatically added by socket.io
  399. * @param {String} userId - the userId of the person we are trying to get the username from
  400. * @param {Function} cb - gets called with the result
  401. */
  402. getUsernameFromId: (session, userId, cb) => {
  403. db.models.user.findById(userId).then(user => {
  404. if (user) {
  405. logger.success("GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
  406. return cb({
  407. status: 'success',
  408. data: user.username
  409. });
  410. } else {
  411. logger.error("GET_USERNAME_FROM_ID", `Getting the username from userId "${userId}" failed. User not found.`);
  412. cb({
  413. status: 'failure',
  414. message: "Couldn't find the user."
  415. });
  416. }
  417. }).catch(async err => {
  418. if (err && err !== true) {
  419. err = await utils.getError(err);
  420. logger.error("GET_USERNAME_FROM_ID", `Getting the username from userId "${userId}" failed. "${err}"`);
  421. cb({ status: 'failure', message: err });
  422. }
  423. });
  424. },
  425. //TODO Fix security issues
  426. /**
  427. * Gets user info from session
  428. *
  429. * @param {Object} session - the session object automatically added by socket.io
  430. * @param {Function} cb - gets called with the result
  431. */
  432. findBySession: (session, cb) => {
  433. async.waterfall([
  434. (next) => {
  435. cache.hget('sessions', session.sessionId, next);
  436. },
  437. (session, next) => {
  438. if (!session) return next('Session not found.');
  439. next(null, session);
  440. },
  441. (session, next) => {
  442. db.models.user.findOne({ _id: session.userId }, next);
  443. },
  444. (user, next) => {
  445. if (!user) return next('User not found.');
  446. next(null, user);
  447. }
  448. ], async (err, user) => {
  449. if (err && err !== true) {
  450. err = await utils.getError(err);
  451. logger.error("FIND_BY_SESSION", `User not found. "${err}"`);
  452. cb({status: 'failure', message: err});
  453. } else {
  454. let data = {
  455. email: {
  456. address: user.email.address
  457. },
  458. username: user.username,
  459. location: user.location,
  460. bio: user.bio
  461. };
  462. if (user.services.password && user.services.password.password) data.password = true;
  463. if (user.services.github && user.services.github.id) data.github = true;
  464. logger.success("FIND_BY_SESSION", `User found. "${user.username}".`);
  465. return cb({
  466. status: 'success',
  467. data
  468. });
  469. }
  470. });
  471. },
  472. /**
  473. * Updates a user's username
  474. *
  475. * @param {Object} session - the session object automatically added by socket.io
  476. * @param {String} updatingUserId - the updating user's id
  477. * @param {String} newUsername - the new username
  478. * @param {Function} cb - gets called with the result
  479. */
  480. updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb) => {
  481. async.waterfall([
  482. (next) => {
  483. if (updatingUserId === session.userId) return next(null, true);
  484. db.models.user.findOne({_id: session.userId}, next);
  485. },
  486. (user, next) => {
  487. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  488. db.models.user.findOne({ _id: updatingUserId }, next);
  489. },
  490. (user, next) => {
  491. if (!user) return next('User not found.');
  492. if (user.username === newUsername) return next('New username can\'t be the same as the old username.');
  493. next(null);
  494. },
  495. (next) => {
  496. db.models.user.findOne({ username: new RegExp(`^${newUsername}$`, 'i') }, next);
  497. },
  498. (user, next) => {
  499. if (!user) return next();
  500. if (user._id === updatingUserId) return next();
  501. next('That username is already in use.');
  502. },
  503. (next) => {
  504. db.models.user.updateOne({ _id: updatingUserId }, {$set: {username: newUsername}}, {runValidators: true}, next);
  505. }
  506. ], async (err) => {
  507. if (err && err !== true) {
  508. err = await utils.getError(err);
  509. logger.error("UPDATE_USERNAME", `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`);
  510. cb({status: 'failure', message: err});
  511. } else {
  512. cache.pub('user.updateUsername', {
  513. username: newUsername,
  514. _id: updatingUserId
  515. });
  516. logger.success("UPDATE_USERNAME", `Updated username for user "${updatingUserId}" to username "${newUsername}".`);
  517. cb({ status: 'success', message: 'Username updated successfully' });
  518. }
  519. });
  520. }),
  521. /**
  522. * Updates a user's email
  523. *
  524. * @param {Object} session - the session object automatically added by socket.io
  525. * @param {String} updatingUserId - the updating user's id
  526. * @param {String} newEmail - the new email
  527. * @param {Function} cb - gets called with the result
  528. */
  529. updateEmail: hooks.loginRequired(async (session, updatingUserId, newEmail, cb) => {
  530. newEmail = newEmail.toLowerCase();
  531. let verificationToken = await utils.generateRandomString(64);
  532. async.waterfall([
  533. (next) => {
  534. if (updatingUserId === session.userId) return next(null, true);
  535. db.models.user.findOne({_id: session.userId}, next);
  536. },
  537. (user, next) => {
  538. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  539. db.models.user.findOne({ _id: updatingUserId }, next);
  540. },
  541. (user, next) => {
  542. if (!user) return next('User not found.');
  543. if (user.email.address === newEmail) return next('New email can\'t be the same as your the old email.');
  544. next();
  545. },
  546. (next) => {
  547. db.models.user.findOne({"email.address": newEmail}, next);
  548. },
  549. (user, next) => {
  550. if (!user) return next();
  551. if (user._id === updatingUserId) return next();
  552. next('That email is already in use.');
  553. },
  554. // regenerate the url for gravatar avatar
  555. (next) => {
  556. utils.createGravatar(newEmail).then(url => next(null, url));
  557. },
  558. (avatar, next) => {
  559. db.models.user.updateOne({ _id: updatingUserId }, {
  560. $set: {
  561. "avatar": avatar,
  562. "email.address": newEmail,
  563. "email.verified": false,
  564. "email.verificationToken": verificationToken
  565. }
  566. }, { runValidators: true }, next);
  567. },
  568. (res, next) => {
  569. db.models.user.findOne({ _id: updatingUserId }, next);
  570. },
  571. (user, next) => {
  572. mail.schemas.verifyEmail(newEmail, user.username, verificationToken, () => {
  573. next();
  574. });
  575. }
  576. ], async (err) => {
  577. if (err && err !== true) {
  578. err = await utils.getError(err);
  579. logger.error("UPDATE_EMAIL", `Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`);
  580. cb({status: 'failure', message: err});
  581. } else {
  582. logger.success("UPDATE_EMAIL", `Updated email for user "${updatingUserId}" to email "${newEmail}".`);
  583. cb({ status: 'success', message: 'Email updated successfully.' });
  584. }
  585. });
  586. }),
  587. /**
  588. * Updates a user's location
  589. *
  590. * @param {Object} session - the session object automatically added by socket.io
  591. * @param {String} updatingUserId - the updating user's id
  592. * @param {String} newLocation - the new location
  593. * @param {Function} cb - gets called with the result
  594. */
  595. updateLocation: hooks.loginRequired((session, updatingUserId, newLocation, cb) => {
  596. async.waterfall([
  597. (next) => {
  598. if (updatingUserId === session.userId) return next(null, true);
  599. db.models.user.findOne({_id: session.userId}, next);
  600. },
  601. (user, next) => {
  602. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  603. db.models.user.findOne({ _id: updatingUserId }, next);
  604. },
  605. (user, next) => {
  606. if (!user) return next('User not found.');
  607. db.models.user.updateOne({ _id: updatingUserId }, {$set: {location: newLocation}}, {runValidators: true}, next);
  608. }
  609. ], async (err) => {
  610. if (err && err !== true) {
  611. err = await utils.getError(err);
  612. logger.error("UPDATE_LOCATION", `Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`);
  613. cb({status: 'failure', message: err});
  614. } else {
  615. logger.success("UPDATE_LOCATION", `Updated location for user "${updatingUserId}" to location "${newLocation}".`);
  616. cb({ status: 'success', message: 'Location updated successfully' });
  617. }
  618. });
  619. }),
  620. /**
  621. * Updates a user's bio
  622. *
  623. * @param {Object} session - the session object automatically added by socket.io
  624. * @param {String} updatingUserId - the updating user's id
  625. * @param {String} newBio - the new bio
  626. * @param {Function} cb - gets called with the result
  627. */
  628. updateBio: hooks.loginRequired((session, updatingUserId, newBio, cb) => {
  629. async.waterfall([
  630. (next) => {
  631. if (updatingUserId === session.userId) return next(null, true);
  632. db.models.user.findOne({_id: session.userId}, next);
  633. },
  634. (user, next) => {
  635. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  636. db.models.user.findOne({ _id: updatingUserId }, next);
  637. },
  638. (user, next) => {
  639. if (!user) return next('User not found.');
  640. db.models.user.updateOne({ _id: updatingUserId }, {$set: {bio: newBio}}, {runValidators: true}, next);
  641. }
  642. ], async (err) => {
  643. if (err && err !== true) {
  644. err = await utils.getError(err);
  645. logger.error("UPDATE_BIO", `Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`);
  646. cb({status: 'failure', message: err});
  647. } else {
  648. logger.success("UPDATE_BIO", `Updated bio for user "${updatingUserId}" to bio "${newBio}".`);
  649. cb({ status: 'success', message: 'Bio updated successfully' });
  650. }
  651. });
  652. }),
  653. /**
  654. * Updates a user's role
  655. *
  656. * @param {Object} session - the session object automatically added by socket.io
  657. * @param {String} updatingUserId - the updating user's id
  658. * @param {String} newRole - the new role
  659. * @param {Function} cb - gets called with the result
  660. */
  661. updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb) => {
  662. newRole = newRole.toLowerCase();
  663. async.waterfall([
  664. (next) => {
  665. db.models.user.findOne({ _id: updatingUserId }, next);
  666. },
  667. (user, next) => {
  668. if (!user) return next('User not found.');
  669. else if (user.role === newRole) return next('New role can\'t be the same as the old role.');
  670. else return next();
  671. },
  672. (next) => {
  673. db.models.user.updateOne({_id: updatingUserId}, {$set: {role: newRole}}, {runValidators: true}, next);
  674. }
  675. ], async (err) => {
  676. if (err && err !== true) {
  677. err = await utils.getError(err);
  678. logger.error("UPDATE_ROLE", `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`);
  679. cb({status: 'failure', message: err});
  680. } else {
  681. logger.success("UPDATE_ROLE", `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`);
  682. cb({
  683. status: 'success',
  684. message: 'Role successfully updated.'
  685. });
  686. }
  687. });
  688. }),
  689. /**
  690. * Updates a user's password
  691. *
  692. * @param {Object} session - the session object automatically added by socket.io
  693. * @param {String} newPassword - the new password
  694. * @param {Function} cb - gets called with the result
  695. */
  696. updatePassword: hooks.loginRequired((session, newPassword, cb) => {
  697. async.waterfall([
  698. (next) => {
  699. db.models.user.findOne({_id: session.userId}, next);
  700. },
  701. (user, next) => {
  702. if (!user.services.password) return next('This account does not have a password set.');
  703. next();
  704. },
  705. (next) => {
  706. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  707. return next();
  708. },
  709. (next) => {
  710. bcrypt.genSalt(10, next);
  711. },
  712. // hash the password
  713. (salt, next) => {
  714. bcrypt.hash(sha256(newPassword), salt, next);
  715. },
  716. (hashedPassword, next) => {
  717. db.models.user.updateOne({_id: session.userId}, {$set: {"services.password.password": hashedPassword}}, next);
  718. }
  719. ], async (err) => {
  720. if (err) {
  721. err = await utils.getError(err);
  722. logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${session.userId}'. '${err}'.`);
  723. return cb({ status: 'failure', message: err });
  724. }
  725. logger.success("UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
  726. cb({
  727. status: 'success',
  728. message: 'Password successfully updated.'
  729. });
  730. });
  731. }),
  732. /**
  733. * Requests a password for a session
  734. *
  735. * @param {Object} session - the session object automatically added by socket.io
  736. * @param {String} email - the email of the user that requests a password reset
  737. * @param {Function} cb - gets called with the result
  738. */
  739. requestPassword: hooks.loginRequired(async (session, cb) => {
  740. let code = await utils.generateRandomString(8);
  741. async.waterfall([
  742. (next) => {
  743. db.models.user.findOne({_id: session.userId}, next);
  744. },
  745. (user, next) => {
  746. if (!user) return next('User not found.');
  747. if (user.services.password && user.services.password.password) return next('You already have a password set.');
  748. next(null, user);
  749. },
  750. (user, next) => {
  751. let expires = new Date();
  752. expires.setDate(expires.getDate() + 1);
  753. db.models.user.findOneAndUpdate({"email.address": user.email.address}, {$set: {"services.password": {set: {code: code, expires}}}}, {runValidators: true}, next);
  754. },
  755. (user, next) => {
  756. mail.schemas.passwordRequest(user.email.address, user.username, code, next);
  757. }
  758. ], async (err) => {
  759. if (err && err !== true) {
  760. err = await utils.getError(err);
  761. logger.error("REQUEST_PASSWORD", `UserId '${session.userId}' failed to request password. '${err}'`);
  762. cb({status: 'failure', message: err});
  763. } else {
  764. logger.success("REQUEST_PASSWORD", `UserId '${session.userId}' successfully requested a password.`);
  765. cb({
  766. status: 'success',
  767. message: 'Successfully requested password.'
  768. });
  769. }
  770. });
  771. }),
  772. /**
  773. * Verifies a password code
  774. *
  775. * @param {Object} session - the session object automatically added by socket.io
  776. * @param {String} code - the password code
  777. * @param {Function} cb - gets called with the result
  778. */
  779. verifyPasswordCode: hooks.loginRequired((session, code, cb) => {
  780. async.waterfall([
  781. (next) => {
  782. if (!code || typeof code !== 'string') return next('Invalid code1.');
  783. db.models.user.findOne({"services.password.set.code": code, _id: session.userId}, next);
  784. },
  785. (user, next) => {
  786. if (!user) return next('Invalid code2.');
  787. if (user.services.password.set.expires < new Date()) return next('That code has expired.');
  788. next(null);
  789. }
  790. ], async(err) => {
  791. if (err && err !== true) {
  792. err = await utils.getError(err);
  793. logger.error("VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
  794. cb({status: 'failure', message: err});
  795. } else {
  796. logger.success("VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
  797. cb({
  798. status: 'success',
  799. message: 'Successfully verified password code.'
  800. });
  801. }
  802. });
  803. }),
  804. /**
  805. * Adds a password to a user with a code
  806. *
  807. * @param {Object} session - the session object automatically added by socket.io
  808. * @param {String} code - the password code
  809. * @param {String} newPassword - the new password code
  810. * @param {Function} cb - gets called with the result
  811. */
  812. changePasswordWithCode: hooks.loginRequired((session, code, newPassword, cb) => {
  813. async.waterfall([
  814. (next) => {
  815. if (!code || typeof code !== 'string') return next('Invalid code1.');
  816. db.models.user.findOne({"services.password.set.code": code}, next);
  817. },
  818. (user, next) => {
  819. if (!user) return next('Invalid code2.');
  820. if (!user.services.password.set.expires > new Date()) return next('That code has expired.');
  821. next();
  822. },
  823. (next) => {
  824. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  825. return next();
  826. },
  827. (next) => {
  828. bcrypt.genSalt(10, next);
  829. },
  830. // hash the password
  831. (salt, next) => {
  832. bcrypt.hash(sha256(newPassword), salt, next);
  833. },
  834. (hashedPassword, next) => {
  835. db.models.user.updateOne({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, {runValidators: true}, next);
  836. }
  837. ], async (err) => {
  838. if (err && err !== true) {
  839. err = await utils.getError(err);
  840. logger.error("ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
  841. cb({status: 'failure', message: err});
  842. } else {
  843. logger.success("ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
  844. cache.pub('user.linkPassword', session.userId);
  845. cb({
  846. status: 'success',
  847. message: 'Successfully added password.'
  848. });
  849. }
  850. });
  851. }),
  852. /**
  853. * Unlinks password from user
  854. *
  855. * @param {Object} session - the session object automatically added by socket.io
  856. * @param {Function} cb - gets called with the result
  857. */
  858. unlinkPassword: hooks.loginRequired((session, cb) => {
  859. async.waterfall([
  860. (next) => {
  861. db.models.user.findOne({_id: session.userId}, next);
  862. },
  863. (user, next) => {
  864. if (!user) return next('Not logged in.');
  865. if (!user.services.github || !user.services.github.id) return next('You can\'t remove password login without having GitHub login.');
  866. db.models.user.updateOne({_id: session.userId}, {$unset: {"services.password": ''}}, next);
  867. }
  868. ], async (err) => {
  869. if (err && err !== true) {
  870. err = await utils.getError(err);
  871. logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${session.userId}'. '${err}'`);
  872. cb({status: 'failure', message: err});
  873. } else {
  874. logger.success("UNLINK_PASSWORD", `Unlinking password successful for userId '${session.userId}'.`);
  875. cache.pub('user.unlinkPassword', session.userId);
  876. cb({
  877. status: 'success',
  878. message: 'Successfully unlinked password.'
  879. });
  880. }
  881. });
  882. }),
  883. /**
  884. * Unlinks GitHub from user
  885. *
  886. * @param {Object} session - the session object automatically added by socket.io
  887. * @param {Function} cb - gets called with the result
  888. */
  889. unlinkGitHub: hooks.loginRequired((session, cb) => {
  890. async.waterfall([
  891. (next) => {
  892. db.models.user.findOne({_id: session.userId}, next);
  893. },
  894. (user, next) => {
  895. if (!user) return next('Not logged in.');
  896. if (!user.services.password || !user.services.password.password) return next('You can\'t remove GitHub login without having password login.');
  897. db.models.user.updateOne({_id: session.userId}, {$unset: {"services.github": ''}}, next);
  898. }
  899. ], async (err) => {
  900. if (err && err !== true) {
  901. err = await utils.getError(err);
  902. logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`);
  903. cb({status: 'failure', message: err});
  904. } else {
  905. logger.success("UNLINK_GITHUB", `Unlinking GitHub successful for userId '${session.userId}'.`);
  906. cache.pub('user.unlinkGitHub', session.userId);
  907. cb({
  908. status: 'success',
  909. message: 'Successfully unlinked GitHub.'
  910. });
  911. }
  912. });
  913. }),
  914. /**
  915. * Requests a password reset for an email
  916. *
  917. * @param {Object} session - the session object automatically added by socket.io
  918. * @param {String} email - the email of the user that requests a password reset
  919. * @param {Function} cb - gets called with the result
  920. */
  921. requestPasswordReset: async (session, email, cb) => {
  922. let code = await utils.generateRandomString(8);
  923. async.waterfall([
  924. (next) => {
  925. if (!email || typeof email !== 'string') return next('Invalid email.');
  926. email = email.toLowerCase();
  927. db.models.user.findOne({"email.address": email}, next);
  928. },
  929. (user, next) => {
  930. if (!user) return next('User not found.');
  931. if (!user.services.password || !user.services.password.password) return next('User does not have a password set, and probably uses GitHub to log in.');
  932. next(null, user);
  933. },
  934. (user, next) => {
  935. let expires = new Date();
  936. expires.setDate(expires.getDate() + 1);
  937. db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, {runValidators: true}, next);
  938. },
  939. (user, next) => {
  940. mail.schemas.resetPasswordRequest(user.email.address, user.username, code, next);
  941. }
  942. ], async (err) => {
  943. if (err && err !== true) {
  944. err = await utils.getError(err);
  945. logger.error("REQUEST_PASSWORD_RESET", `Email '${email}' failed to request password reset. '${err}'`);
  946. cb({status: 'failure', message: err});
  947. } else {
  948. logger.success("REQUEST_PASSWORD_RESET", `Email '${email}' successfully requested a password reset.`);
  949. cb({
  950. status: 'success',
  951. message: 'Successfully requested password reset.'
  952. });
  953. }
  954. });
  955. },
  956. /**
  957. * Verifies a reset code
  958. *
  959. * @param {Object} session - the session object automatically added by socket.io
  960. * @param {String} code - the password reset code
  961. * @param {Function} cb - gets called with the result
  962. */
  963. verifyPasswordResetCode: (session, code, cb) => {
  964. async.waterfall([
  965. (next) => {
  966. if (!code || typeof code !== 'string') return next('Invalid code.');
  967. db.models.user.findOne({"services.password.reset.code": code}, next);
  968. },
  969. (user, next) => {
  970. if (!user) return next('Invalid code.');
  971. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  972. next(null);
  973. }
  974. ], async (err) => {
  975. if (err && err !== true) {
  976. err = await utils.getError(err);
  977. logger.error("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
  978. cb({status: 'failure', message: err});
  979. } else {
  980. logger.success("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  981. cb({
  982. status: 'success',
  983. message: 'Successfully verified password reset code.'
  984. });
  985. }
  986. });
  987. },
  988. /**
  989. * Changes a user's password with a reset code
  990. *
  991. * @param {Object} session - the session object automatically added by socket.io
  992. * @param {String} code - the password reset code
  993. * @param {String} newPassword - the new password reset code
  994. * @param {Function} cb - gets called with the result
  995. */
  996. changePasswordWithResetCode: (session, code, newPassword, cb) => {
  997. async.waterfall([
  998. (next) => {
  999. if (!code || typeof code !== 'string') return next('Invalid code.');
  1000. db.models.user.findOne({"services.password.reset.code": code}, next);
  1001. },
  1002. (user, next) => {
  1003. if (!user) return next('Invalid code.');
  1004. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  1005. next();
  1006. },
  1007. (next) => {
  1008. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  1009. return next();
  1010. },
  1011. (next) => {
  1012. bcrypt.genSalt(10, next);
  1013. },
  1014. // hash the password
  1015. (salt, next) => {
  1016. bcrypt.hash(sha256(newPassword), salt, next);
  1017. },
  1018. (hashedPassword, next) => {
  1019. db.models.user.updateOne({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, {runValidators: true}, next);
  1020. }
  1021. ], async (err) => {
  1022. if (err && err !== true) {
  1023. err = await utils.getError(err);
  1024. logger.error("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' failed to change password. '${err}'`);
  1025. cb({status: 'failure', message: err});
  1026. } else {
  1027. logger.success("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' successfully changed password.`);
  1028. cb({
  1029. status: 'success',
  1030. message: 'Successfully changed password.'
  1031. });
  1032. }
  1033. });
  1034. },
  1035. /**
  1036. * Bans a user by userId
  1037. *
  1038. * @param {Object} session - the session object automatically added by socket.io
  1039. * @param {String} value - the user id that is going to be banned
  1040. * @param {String} reason - the reason for the ban
  1041. * @param {String} expiresAt - the time the ban expires
  1042. * @param {Function} cb - gets called with the result
  1043. */
  1044. banUserById: hooks.adminRequired((session, userId, reason, expiresAt, cb) => {
  1045. async.waterfall([
  1046. (next) => {
  1047. if (!userId) return next('You must provide a userId to ban.');
  1048. else if (!reason) return next('You must provide a reason for the ban.');
  1049. else return next();
  1050. },
  1051. (next) => {
  1052. if (!expiresAt || typeof expiresAt !== 'string') return next('Invalid expire date.');
  1053. let date = new Date();
  1054. switch(expiresAt) {
  1055. case '1h':
  1056. expiresAt = date.setHours(date.getHours() + 1);
  1057. break;
  1058. case '12h':
  1059. expiresAt = date.setHours(date.getHours() + 12);
  1060. break;
  1061. case '1d':
  1062. expiresAt = date.setDate(date.getDate() + 1);
  1063. break;
  1064. case '1w':
  1065. expiresAt = date.setDate(date.getDate() + 7);
  1066. break;
  1067. case '1m':
  1068. expiresAt = date.setMonth(date.getMonth() + 1);
  1069. break;
  1070. case '3m':
  1071. expiresAt = date.setMonth(date.getMonth() + 3);
  1072. break;
  1073. case '6m':
  1074. expiresAt = date.setMonth(date.getMonth() + 6);
  1075. break;
  1076. case '1y':
  1077. expiresAt = date.setFullYear(date.getFullYear() + 1);
  1078. break;
  1079. case 'never':
  1080. expiresAt = new Date(3093527980800000);
  1081. break;
  1082. default:
  1083. return next('Invalid expire date.');
  1084. }
  1085. next();
  1086. },
  1087. (next) => {
  1088. punishments.addPunishment('banUserId', userId, reason, expiresAt, userId, next)
  1089. },
  1090. (punishment, next) => {
  1091. cache.pub('user.ban', { userId, punishment });
  1092. next();
  1093. },
  1094. ], async (err) => {
  1095. if (err && err !== true) {
  1096. err = await utils.getError(err);
  1097. logger.error("BAN_USER_BY_ID", `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`);
  1098. cb({status: 'failure', message: err});
  1099. } else {
  1100. logger.success("BAN_USER_BY_ID", `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`);
  1101. cb({
  1102. status: 'success',
  1103. message: 'Successfully banned user.'
  1104. });
  1105. }
  1106. });
  1107. }),
  1108. getFavoriteStations: hooks.loginRequired((session, cb) => {
  1109. async.waterfall([
  1110. (next) => {
  1111. db.models.user.findOne({ _id: session.userId }, next);
  1112. },
  1113. (user, next) => {
  1114. if (!user) return next("User not found.");
  1115. next(null, user);
  1116. }
  1117. ], async (err, user) => {
  1118. if (err && err !== true) {
  1119. err = await utils.getError(err);
  1120. logger.error("GET_FAVORITE_STATIONS", `User ${session.userId} failed to get favorite stations. '${err}'`);
  1121. cb({status: 'failure', message: err});
  1122. } else {
  1123. logger.success("GET_FAVORITE_STATIONS", `User ${session.userId} got favorite stations.`);
  1124. cb({
  1125. status: 'success',
  1126. favoriteStations: user.favoriteStations
  1127. });
  1128. }
  1129. });
  1130. })
  1131. };