users.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  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 db = require('../db');
  7. const mail = require('../mail');
  8. const cache = require('../cache');
  9. const utils = require('../utils');
  10. const hooks = require('./hooks');
  11. const sha256 = require('sha256');
  12. const logger = require('../logger');
  13. cache.sub('user.updateUsername', user => {
  14. utils.socketsFromUser(user._id, sockets => {
  15. sockets.forEach(socket => {
  16. socket.emit('event:user.username.changed', user.username);
  17. });
  18. });
  19. });
  20. module.exports = {
  21. /**
  22. * Lists all Users
  23. *
  24. * @param {Object} session - the session object automatically added by socket.io
  25. * @param {Function} cb - gets called with the result
  26. */
  27. index: hooks.adminRequired((session, cb) => {
  28. async.waterfall([
  29. (next) => {
  30. db.models.user.find({}).exec(next);
  31. }
  32. ], (err, users) => {
  33. if (err) {
  34. logger.error("USER_INDEX", `Indexing users failed. "${err.message}"`);
  35. return cb({status: 'failure', message: 'Something went wrong.'});
  36. } else {
  37. logger.success("USER_INDEX", `Indexing users successful.`);
  38. let filteredUsers = [];
  39. users.forEach(user => {
  40. filteredUsers.push({
  41. _id: user._id,
  42. username: user.username,
  43. role: user.role,
  44. liked: user.liked,
  45. disliked: user.disliked,
  46. songsRequested: user.statistics.songsRequested,
  47. email: {
  48. address: user.email.address,
  49. verified: user.email.verified
  50. },
  51. hasPassword: !!user.services.password,
  52. services: { github: user.services.github }
  53. });
  54. });
  55. return cb({ status: 'success', data: filteredUsers });
  56. }
  57. });
  58. }),
  59. /**
  60. * Logs user in
  61. *
  62. * @param {Object} session - the session object automatically added by socket.io
  63. * @param {String} identifier - the email of the user
  64. * @param {String} password - the plaintext of the user
  65. * @param {Function} cb - gets called with the result
  66. */
  67. login: (session, identifier, password, cb) => {
  68. identifier = identifier.toLowerCase();
  69. async.waterfall([
  70. // check if a user with the requested identifier exists
  71. (next) => {
  72. db.models.user.findOne({
  73. $or: [{ 'email.address': identifier }]
  74. }, next)
  75. },
  76. // if the user doesn't exist, respond with a failure
  77. // otherwise compare the requested password and the actual users password
  78. (user, next) => {
  79. if (!user) return next('User not found');
  80. if (!user.services.password || !user.services.password.password) return next('The account you are trying to access uses GitHub to log in.');
  81. bcrypt.compare(sha256(password), user.services.password.password, (err, match) => {
  82. if (err) return next(err);
  83. if (!match) return next('Incorrect password');
  84. next(null, user);
  85. });
  86. },
  87. (user, next) => {
  88. let sessionId = utils.guid();
  89. cache.hset('sessions', sessionId, cache.schemas.session(sessionId, user._id), (err) => {
  90. if (err) return next(err);
  91. next(null, sessionId);
  92. });
  93. }
  94. ], (err, sessionId) => {
  95. if (err && err !== true) {
  96. let error = 'An error occurred.';
  97. if (typeof err === "string") error = err;
  98. else if (err.message) error = err.message;
  99. logger.error("USER_PASSWORD_LOGIN", "Login failed with password for user " + identifier + '. "' + error + '"');
  100. return cb({ status: 'failure', message: error });
  101. }
  102. logger.success("USER_PASSWORD_LOGIN", "Login successful with password for user " + identifier);
  103. cb({ status: 'success', message: 'Login successful', user: {}, SID: sessionId });
  104. });
  105. },
  106. /**
  107. * Registers a new user
  108. *
  109. * @param {Object} session - the session object automatically added by socket.io
  110. * @param {String} username - the username for the new user
  111. * @param {String} email - the email for the new user
  112. * @param {String} password - the plaintext password for the new user
  113. * @param {Object} recaptcha - the recaptcha data
  114. * @param {Function} cb - gets called with the result
  115. */
  116. register: function(session, username, email, password, recaptcha, cb) {
  117. email = email.toLowerCase();
  118. let verificationToken = utils.generateRandomString(64);
  119. async.waterfall([
  120. // verify the request with google recaptcha
  121. (next) => {
  122. request({
  123. url: 'https://www.google.com/recaptcha/api/siteverify',
  124. method: 'POST',
  125. form: {
  126. 'secret': config.get("apis").recaptcha.secret,
  127. 'response': recaptcha
  128. }
  129. }, next);
  130. },
  131. // check if the response from Google recaptcha is successful
  132. // if it is, we check if a user with the requested username already exists
  133. (response, body, next) => {
  134. let json = JSON.parse(body);
  135. if (json.success !== true) return next('Response from recaptcha was not successful.');
  136. db.models.user.findOne({ username: new RegExp(`^${username}$`, 'i') }, next);
  137. },
  138. // if the user already exists, respond with that
  139. // otherwise check if a user with the requested email already exists
  140. (user, next) => {
  141. if (user) return next('A user with that username already exists.');
  142. db.models.user.findOne({ 'email.address': email }, next);
  143. },
  144. // if the user already exists, respond with that
  145. // otherwise, generate a salt to use with hashing the new users password
  146. (user, next) => {
  147. if (user) return next('A user with that email already exists.');
  148. bcrypt.genSalt(10, next);
  149. },
  150. // hash the password
  151. (salt, next) => {
  152. bcrypt.hash(sha256(password), salt, next)
  153. },
  154. // save the new user to the database
  155. (hash, next) => {
  156. db.models.user.create({
  157. _id: utils.generateRandomString(12),//TODO Check if exists
  158. username,
  159. email: {
  160. address: email,
  161. verificationToken
  162. },
  163. services: {
  164. password: {
  165. password: hash
  166. }
  167. }
  168. }, next);
  169. },
  170. // respond with the new user
  171. (newUser, next) => {
  172. //TODO Send verification email
  173. mail.schemas.verifyEmail(email, username, verificationToken, () => {
  174. next();
  175. });
  176. }
  177. ], (err) => {
  178. if (err && err !== true) {
  179. let error = 'An error occurred.';
  180. if (typeof err === "string") error = err;
  181. else if (err.message) error = err.message;
  182. logger.error("USER_PASSWORD_REGISTER", "Register failed with password for user. " + '"' + error + '"');
  183. cb({status: 'failure', message: error});
  184. } else {
  185. module.exports.login(session, email, password, (result) => {
  186. let obj = {status: 'success', message: 'Successfully registered.'};
  187. if (result.status === 'success') {
  188. obj.SID = result.SID;
  189. }
  190. logger.success("USER_PASSWORD_REGISTER", "Register successful with password for user '" + username + "'.");
  191. cb(obj);
  192. });
  193. }
  194. });
  195. },
  196. /**
  197. * Logs out a user
  198. *
  199. * @param {Object} session - the session object automatically added by socket.io
  200. * @param {Function} cb - gets called with the result
  201. */
  202. logout: (session, cb) => {
  203. async.waterfall([
  204. (next) => {
  205. cache.hget('sessions', session.sessionId, next);
  206. },
  207. (session, next) => {
  208. if (!session) return next('Session not found');
  209. next(null, session);
  210. },
  211. (session, next) => {
  212. cache.hdel('sessions', session.sessionId, next);
  213. }
  214. ], (err) => {
  215. if (err && err !== true) {
  216. let error = 'An error occurred.';
  217. if (typeof err === "string") error = err;
  218. else if (err.message) error = err.message;
  219. logger.error("USER_LOGOUT", `Logout failed. ${error}`);
  220. cb({status: 'failure', message: error});
  221. } else {
  222. logger.success("USER_LOGOUT", `Logout successful.`);
  223. cb({status: 'success', message: 'Successfully logged out.'});
  224. }
  225. });
  226. },
  227. /**
  228. * Gets user object from username (only a few properties)
  229. *
  230. * @param {Object} session - the session object automatically added by socket.io
  231. * @param {String} username - the username of the user we are trying to find
  232. * @param {Function} cb - gets called with the result
  233. */
  234. findByUsername: (session, username, cb) => {
  235. async.waterfall([
  236. (next) => {
  237. db.models.user.findOne({ username: new RegExp(`^${username}$`, 'i') }, next);
  238. },
  239. (account, next) => {
  240. if (!account) return next('User not found.');
  241. next(null, account);
  242. }
  243. ], (err, account) => {
  244. if (err && err !== true) {
  245. let error = 'An error occurred.';
  246. if (typeof err === "string") error = err;
  247. else if (err.message) error = err.message;
  248. logger.error("FIND_BY_USERNAME", `User not found for username '${username}'. ${error}`);
  249. cb({status: 'failure', message: error});
  250. } else {
  251. logger.success("FIND_BY_USERNAME", `User found for username '${username}'.`);
  252. return cb({
  253. status: 'success',
  254. data: {
  255. _id: account._id,
  256. username: account.username,
  257. role: account.role,
  258. email: account.email.address,
  259. createdAt: account.createdAt,
  260. statistics: account.statistics,
  261. liked: account.liked,
  262. disliked: account.disliked
  263. }
  264. });
  265. }
  266. });
  267. },
  268. //TODO Fix security issues
  269. /**
  270. * Gets user info from session
  271. *
  272. * @param {Object} session - the session object automatically added by socket.io
  273. * @param {Function} cb - gets called with the result
  274. */
  275. findBySession: (session, cb) => {
  276. async.waterfall([
  277. (next) => {
  278. cache.hget('sessions', session.sessionId, next);
  279. },
  280. (session, next) => {
  281. if (!session) return next('Session not found.');
  282. next(null, session);
  283. },
  284. (session, next) => {
  285. db.models.user.findOne({ _id: session.userId }, next);
  286. },
  287. (user, next) => {
  288. if (!user) return next('User not found.');
  289. next(null, user);
  290. }
  291. ], (err, user) => {
  292. if (err && err !== true) {
  293. let error = 'An error occurred.';
  294. if (typeof err === "string") error = err;
  295. else if (err.message) error = err.message;
  296. logger.error("FIND_BY_SESSION", `User not found. ${error}`);
  297. cb({status: 'failure', message: error});
  298. } else {
  299. let data = {
  300. email: {
  301. address: user.email.address
  302. },
  303. username: user.username
  304. };
  305. if (user.services.password && user.services.password.password) data.password = true;
  306. logger.success("FIND_BY_SESSION", `User found. '${user.username}'.`);
  307. return cb({
  308. status: 'success',
  309. data
  310. });
  311. }
  312. });
  313. },
  314. /**
  315. * Updates a user's username
  316. *
  317. * @param {Object} session - the session object automatically added by socket.io
  318. * @param {String} updatingUserId - the updating user's id
  319. * @param {String} newUsername - the new username
  320. * @param {Function} cb - gets called with the result
  321. * @param {String} userId - the userId automatically added by hooks
  322. */
  323. updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb, userId) => {
  324. async.waterfall([
  325. (next) => {
  326. db.models.user.findOne({ _id: updatingUserId }, next);
  327. },
  328. (user, next) => {
  329. if (!user) return next('User not found.');
  330. if (user.username === newUsername) return next('New username can\'t be the same as the old username.');
  331. next(null);
  332. },
  333. (next) => {
  334. db.models.user.findOne({ username: new RegExp(`^${newUsername}$`, 'i') }, next);
  335. },
  336. (user, next) => {
  337. if (!user) return next();
  338. if (user._id === updatingUserId) return next();
  339. next('That username is already in use.');
  340. },
  341. (next) => {
  342. db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, next);
  343. }
  344. ], (err) => {
  345. if (err && err !== true) {
  346. let error = 'An error occurred.';
  347. if (typeof err === "string") error = err;
  348. else if (err.message) error = err.message;
  349. logger.error("UPDATE_USERNAME", `Couldn't update username for user '${updatingUserId}' to username '${newUsername}'. '${error}'`);
  350. cb({status: 'failure', message: error});
  351. } else {
  352. cache.pub('user.updateUsername', {
  353. username: newUsername,
  354. _id: updatingUserId
  355. });
  356. logger.success("UPDATE_USERNAME", `Updated username for user '${updatingUserId}' to username '${newUsername}'.`);
  357. cb({ status: 'success', message: 'Username updated successfully' });
  358. }
  359. });
  360. }),
  361. /**
  362. * Updates a user's email
  363. *
  364. * @param {Object} session - the session object automatically added by socket.io
  365. * @param {String} updatingUserId - the updating user's id
  366. * @param {String} newEmail - the new email
  367. * @param {Function} cb - gets called with the result
  368. * @param {String} userId - the userId automatically added by hooks
  369. */
  370. updateEmail: hooks.loginRequired((session, updatingUserId, newEmail, cb, userId) => {
  371. newEmail = newEmail.toLowerCase();
  372. let verificationToken = utils.generateRandomString(64);
  373. async.waterfall([
  374. (next) => {
  375. db.models.user.findOne({ _id: updatingUserId }, next);
  376. },
  377. (user, next) => {
  378. if (!user) return next('User not found.');
  379. if (user.email.address === newEmail) return next('New email can\'t be the same as your the old email.');
  380. next();
  381. },
  382. (next) => {
  383. db.models.user.findOne({"email.address": newEmail}, next);
  384. },
  385. (user, next) => {
  386. if (!user) return next();
  387. if (user._id === updatingUserId) return next();
  388. next('That email is already in use.');
  389. },
  390. (next) => {
  391. db.models.user.update({_id: updatingUserId}, {$set: {"email.address": newEmail, "email.verified": false, "email.verificationToken": verificationToken}}, next);
  392. },
  393. (res, next) => {
  394. db.models.user.findOne({ _id: updatingUserId }, next);
  395. },
  396. (user, next) => {
  397. mail.schemas.verifyEmail(newEmail, user.username, verificationToken, () => {
  398. next();
  399. });
  400. }
  401. ], (err) => {
  402. if (err && err !== true) {
  403. let error = 'An error occurred.';
  404. if (typeof err === "string") error = err;
  405. else if (err.message) error = err.message;
  406. logger.error("UPDATE_EMAIL", `Couldn't update email for user '${updatingUserId}' to email '${newEmail}'. '${error}'`);
  407. cb({status: 'failure', message: error});
  408. } else {
  409. logger.success("UPDATE_EMAIL", `Updated email for user '${updatingUserId}' to email '${newEmail}'.`);
  410. cb({ status: 'success', message: 'Email updated successfully.' });
  411. }
  412. });
  413. }),
  414. /**
  415. * Updates a user's role
  416. *
  417. * @param {Object} session - the session object automatically added by socket.io
  418. * @param {String} updatingUserId - the updating user's id
  419. * @param {String} newRole - the new role
  420. * @param {Function} cb - gets called with the result
  421. * @param {String} userId - the userId automatically added by hooks
  422. */
  423. updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb, userId) => {
  424. newRole = newRole.toLowerCase();
  425. async.waterfall([
  426. (next) => {
  427. db.models.user.findOne({ _id: updatingUserId }, next);
  428. },
  429. (user, next) => {
  430. if (!user) return next('User not found.');
  431. else if (user.role === newRole) return next('New role can\'t be the same as the old role.');
  432. else return next();
  433. },
  434. (next) => {
  435. db.models.user.update({_id: updatingUserId}, {$set: {role: newRole}}, next);
  436. }
  437. ], (err) => {
  438. if (err && err !== true) {
  439. let error = 'An error occurred.';
  440. if (typeof err === "string") error = err;
  441. else if (err.message) error = err.message;
  442. logger.error("UPDATE_ROLE", `User '${userId}' couldn't update role for user '${updatingUserId}' to role '${newRole}'. '${error}'`);
  443. cb({status: 'failure', message: error});
  444. } else {
  445. logger.success("UPDATE_ROLE", `User '${userId}' updated the role of user '${updatingUserId}' to role '${newRole}'.`);
  446. cb({
  447. status: 'success',
  448. message: 'Role successfully updated.'
  449. });
  450. }
  451. });
  452. }),
  453. /**
  454. * Updates a user's password
  455. *
  456. * @param {Object} session - the session object automatically added by socket.io
  457. * @param {String} newPassword - the new password
  458. * @param {Function} cb - gets called with the result
  459. * @param {String} userId - the userId automatically added by hooks
  460. */
  461. updatePassword: hooks.loginRequired((session, newPassword, cb, userId) => {
  462. async.waterfall([
  463. (next) => {
  464. db.models.user.findOne({_id: userId}, next);
  465. },
  466. (user, next) => {
  467. if (!user.services.password) return next('This account does not have a password set.');
  468. next();
  469. },
  470. (next) => {
  471. bcrypt.genSalt(10, next);
  472. },
  473. // hash the password
  474. (salt, next) => {
  475. bcrypt.hash(sha256(newPassword), salt, next);
  476. },
  477. (hashedPassword, next) => {
  478. db.models.user.update({_id: userId}, {$set: {"services.password.password": hashedPassword}}, next);
  479. }
  480. ], (err) => {
  481. if (err) {
  482. let error = 'An error occurred.';
  483. if (typeof err === "string") error = err;
  484. else if (err.message) error = err.message;
  485. logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${userId}'. '${error}'.`);
  486. return cb({ status: 'failure', message: error });
  487. }
  488. logger.error("UPDATE_PASSWORD", `User '${userId}' updated their password.`);
  489. cb({
  490. status: 'success',
  491. message: 'Password successfully updated.'
  492. });
  493. });
  494. }),
  495. /**
  496. * Requests a password reset for an email
  497. *
  498. * @param {Object} session - the session object automatically added by socket.io
  499. * @param {String} email - the email of the user that requests a password reset
  500. * @param {Function} cb - gets called with the result
  501. */
  502. requestPasswordReset: (session, email, cb) => {
  503. let code = utils.generateRandomString(8);
  504. async.waterfall([
  505. (next) => {
  506. if (!email || typeof email !== 'string') return next('Invalid code.');
  507. email = email.toLowerCase();
  508. db.models.user.findOne({"email.address": email}, next);
  509. },
  510. (user, next) => {
  511. if (!user) return next('User not found.');
  512. if (!user.services.password || !user.services.password.password) return next('User does not have a password set, and probably uses GitHub to log in.');
  513. next(null, user);
  514. },
  515. (user, next) => {
  516. let expires = new Date();
  517. expires.setDate(expires.getDate() + 1);
  518. db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, next);
  519. },
  520. (user, next) => {
  521. mail.schemas.resetPasswordRequest(user.email.address, user.username, code, next);
  522. }
  523. ], (err) => {
  524. if (err && err !== true) {
  525. let error = 'An error occurred.';
  526. if (typeof err === "string") error = err;
  527. else if (err.message) error = err.message;
  528. logger.error("REQUEST_PASSWORD_RESET", `Email '${email}' failed to request password reset. '${error}'`);
  529. cb({status: 'failure', message: error});
  530. } else {
  531. logger.success("REQUEST_PASSWORD_RESET", `Email '${email}' successfully requested a password reset.`);
  532. cb({
  533. status: 'success',
  534. message: 'Successfully requested password reset.'
  535. });
  536. }
  537. });
  538. },
  539. /**
  540. * Verifies a reset code
  541. *
  542. * @param {Object} session - the session object automatically added by socket.io
  543. * @param {String} code - the password reset code
  544. * @param {Function} cb - gets called with the result
  545. */
  546. verifyPasswordResetCode: (session, code, cb) => {
  547. async.waterfall([
  548. (next) => {
  549. if (!code || typeof code !== 'string') return next('Invalid code.');
  550. db.models.user.findOne({"services.password.reset.code": code}, next);
  551. },
  552. (user, next) => {
  553. if (!user) return next('Invalid code.');
  554. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  555. next(null);
  556. }
  557. ], (err) => {
  558. if (err && err !== true) {
  559. let error = 'An error occurred.';
  560. if (typeof err === "string") error = err;
  561. else if (err.message) error = err.message;
  562. logger.error("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${error}'`);
  563. cb({status: 'failure', message: error});
  564. } else {
  565. logger.success("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  566. cb({
  567. status: 'success',
  568. message: 'Successfully verified password reset code.'
  569. });
  570. }
  571. });
  572. },
  573. /**
  574. * Changes a user's password with a reset code
  575. *
  576. * @param {Object} session - the session object automatically added by socket.io
  577. * @param {String} code - the password reset code
  578. * @param {String} newPassword - the new password reset code
  579. * @param {Function} cb - gets called with the result
  580. */
  581. changePasswordWithResetCode: (session, code, newPassword, cb) => {
  582. async.waterfall([
  583. (next) => {
  584. if (!code || typeof code !== 'string') return next('Invalid code.');
  585. db.models.user.findOne({"services.password.reset.code": code}, next);
  586. },
  587. (user, next) => {
  588. if (!user) return next('Invalid code.');
  589. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  590. next();
  591. },
  592. (next) => {
  593. bcrypt.genSalt(10, next);
  594. },
  595. // hash the password
  596. (salt, next) => {
  597. bcrypt.hash(sha256(newPassword), salt, next);
  598. },
  599. (hashedPassword, next) => {
  600. db.models.user.update({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, next);
  601. }
  602. ], (err) => {
  603. if (err && err !== true) {
  604. let error = 'An error occurred.';
  605. if (typeof err === "string") error = err;
  606. else if (err.message) error = err.message;
  607. logger.error("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' failed to change password. '${error}'`);
  608. cb({status: 'failure', message: error});
  609. } else {
  610. logger.success("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' successfully changed password.`);
  611. cb({
  612. status: 'success',
  613. message: 'Successfully changed password.'
  614. });
  615. }
  616. });
  617. }
  618. };