users.js 40 KB

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