users.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  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. if (user.services.github && user.services.github.id) data.github = true;
  307. logger.success("FIND_BY_SESSION", `User found. '${user.username}'.`);
  308. return cb({
  309. status: 'success',
  310. data
  311. });
  312. }
  313. });
  314. },
  315. /**
  316. * Updates a user's username
  317. *
  318. * @param {Object} session - the session object automatically added by socket.io
  319. * @param {String} updatingUserId - the updating user's id
  320. * @param {String} newUsername - the new username
  321. * @param {Function} cb - gets called with the result
  322. * @param {String} userId - the userId automatically added by hooks
  323. */
  324. updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb, userId) => {
  325. async.waterfall([
  326. (next) => {
  327. db.models.user.findOne({ _id: updatingUserId }, next);
  328. },
  329. (user, next) => {
  330. if (!user) return next('User not found.');
  331. if (user.username === newUsername) return next('New username can\'t be the same as the old username.');
  332. next(null);
  333. },
  334. (next) => {
  335. db.models.user.findOne({ username: new RegExp(`^${newUsername}$`, 'i') }, next);
  336. },
  337. (user, next) => {
  338. if (!user) return next();
  339. if (user._id === updatingUserId) return next();
  340. next('That username is already in use.');
  341. },
  342. (next) => {
  343. db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, next);
  344. }
  345. ], (err) => {
  346. if (err && err !== true) {
  347. let error = 'An error occurred.';
  348. if (typeof err === "string") error = err;
  349. else if (err.message) error = err.message;
  350. logger.error("UPDATE_USERNAME", `Couldn't update username for user '${updatingUserId}' to username '${newUsername}'. '${error}'`);
  351. cb({status: 'failure', message: error});
  352. } else {
  353. cache.pub('user.updateUsername', {
  354. username: newUsername,
  355. _id: updatingUserId
  356. });
  357. logger.success("UPDATE_USERNAME", `Updated username for user '${updatingUserId}' to username '${newUsername}'.`);
  358. cb({ status: 'success', message: 'Username updated successfully' });
  359. }
  360. });
  361. }),
  362. /**
  363. * Updates a user's email
  364. *
  365. * @param {Object} session - the session object automatically added by socket.io
  366. * @param {String} updatingUserId - the updating user's id
  367. * @param {String} newEmail - the new email
  368. * @param {Function} cb - gets called with the result
  369. * @param {String} userId - the userId automatically added by hooks
  370. */
  371. updateEmail: hooks.loginRequired((session, updatingUserId, newEmail, cb, userId) => {
  372. newEmail = newEmail.toLowerCase();
  373. let verificationToken = utils.generateRandomString(64);
  374. async.waterfall([
  375. (next) => {
  376. db.models.user.findOne({ _id: updatingUserId }, next);
  377. },
  378. (user, next) => {
  379. if (!user) return next('User not found.');
  380. if (user.email.address === newEmail) return next('New email can\'t be the same as your the old email.');
  381. next();
  382. },
  383. (next) => {
  384. db.models.user.findOne({"email.address": newEmail}, next);
  385. },
  386. (user, next) => {
  387. if (!user) return next();
  388. if (user._id === updatingUserId) return next();
  389. next('That email is already in use.');
  390. },
  391. (next) => {
  392. db.models.user.update({_id: updatingUserId}, {$set: {"email.address": newEmail, "email.verified": false, "email.verificationToken": verificationToken}}, next);
  393. },
  394. (res, next) => {
  395. db.models.user.findOne({ _id: updatingUserId }, next);
  396. },
  397. (user, next) => {
  398. mail.schemas.verifyEmail(newEmail, user.username, verificationToken, () => {
  399. next();
  400. });
  401. }
  402. ], (err) => {
  403. if (err && err !== true) {
  404. let error = 'An error occurred.';
  405. if (typeof err === "string") error = err;
  406. else if (err.message) error = err.message;
  407. logger.error("UPDATE_EMAIL", `Couldn't update email for user '${updatingUserId}' to email '${newEmail}'. '${error}'`);
  408. cb({status: 'failure', message: error});
  409. } else {
  410. logger.success("UPDATE_EMAIL", `Updated email for user '${updatingUserId}' to email '${newEmail}'.`);
  411. cb({ status: 'success', message: 'Email updated successfully.' });
  412. }
  413. });
  414. }),
  415. /**
  416. * Updates a user's role
  417. *
  418. * @param {Object} session - the session object automatically added by socket.io
  419. * @param {String} updatingUserId - the updating user's id
  420. * @param {String} newRole - the new role
  421. * @param {Function} cb - gets called with the result
  422. * @param {String} userId - the userId automatically added by hooks
  423. */
  424. updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb, userId) => {
  425. newRole = newRole.toLowerCase();
  426. async.waterfall([
  427. (next) => {
  428. db.models.user.findOne({ _id: updatingUserId }, next);
  429. },
  430. (user, next) => {
  431. if (!user) return next('User not found.');
  432. else if (user.role === newRole) return next('New role can\'t be the same as the old role.');
  433. else return next();
  434. },
  435. (next) => {
  436. db.models.user.update({_id: updatingUserId}, {$set: {role: newRole}}, next);
  437. }
  438. ], (err) => {
  439. if (err && err !== true) {
  440. let error = 'An error occurred.';
  441. if (typeof err === "string") error = err;
  442. else if (err.message) error = err.message;
  443. logger.error("UPDATE_ROLE", `User '${userId}' couldn't update role for user '${updatingUserId}' to role '${newRole}'. '${error}'`);
  444. cb({status: 'failure', message: error});
  445. } else {
  446. logger.success("UPDATE_ROLE", `User '${userId}' updated the role of user '${updatingUserId}' to role '${newRole}'.`);
  447. cb({
  448. status: 'success',
  449. message: 'Role successfully updated.'
  450. });
  451. }
  452. });
  453. }),
  454. /**
  455. * Updates a user's password
  456. *
  457. * @param {Object} session - the session object automatically added by socket.io
  458. * @param {String} newPassword - the new password
  459. * @param {Function} cb - gets called with the result
  460. * @param {String} userId - the userId automatically added by hooks
  461. */
  462. updatePassword: hooks.loginRequired((session, newPassword, cb, userId) => {
  463. async.waterfall([
  464. (next) => {
  465. db.models.user.findOne({_id: userId}, next);
  466. },
  467. (user, next) => {
  468. if (!user.services.password) return next('This account does not have a password set.');
  469. next();
  470. },
  471. (next) => {
  472. bcrypt.genSalt(10, next);
  473. },
  474. // hash the password
  475. (salt, next) => {
  476. bcrypt.hash(sha256(newPassword), salt, next);
  477. },
  478. (hashedPassword, next) => {
  479. db.models.user.update({_id: userId}, {$set: {"services.password.password": hashedPassword}}, next);
  480. }
  481. ], (err) => {
  482. if (err) {
  483. let error = 'An error occurred.';
  484. if (typeof err === "string") error = err;
  485. else if (err.message) error = err.message;
  486. logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${userId}'. '${error}'.`);
  487. return cb({ status: 'failure', message: error });
  488. }
  489. logger.error("UPDATE_PASSWORD", `User '${userId}' updated their password.`);
  490. cb({
  491. status: 'success',
  492. message: 'Password successfully updated.'
  493. });
  494. });
  495. }),
  496. /**
  497. * Requests a password for a session
  498. *
  499. * @param {Object} session - the session object automatically added by socket.io
  500. * @param {String} email - the email of the user that requests a password reset
  501. * @param {Function} cb - gets called with the result
  502. * @param {String} userId - the userId automatically added by hooks
  503. */
  504. requestPassword: hooks.loginRequired((session, cb, userId) => {
  505. let code = utils.generateRandomString(8);
  506. async.waterfall([
  507. (next) => {
  508. db.models.user.findOne({_id: userId}, next);
  509. },
  510. (user, next) => {
  511. if (!user) return next('User not found.');
  512. if (user.services.password && user.services.password.password) return next('You already have a password set.');
  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": user.email.address}, {$set: {"services.password": {set: {code: code, expires}}}}, next);
  519. },
  520. (user, next) => {
  521. mail.schemas.passwordRequest(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", `UserId '${userId}' failed to request password. '${error}'`);
  529. cb({status: 'failure', message: error});
  530. } else {
  531. logger.success("REQUEST_PASSWORD", `UserId '${userId}' successfully requested a password.`);
  532. cb({
  533. status: 'success',
  534. message: 'Successfully requested password.'
  535. });
  536. }
  537. });
  538. }),
  539. /**
  540. * Verifies a password code
  541. *
  542. * @param {Object} session - the session object automatically added by socket.io
  543. * @param {String} code - the password code
  544. * @param {Function} cb - gets called with the result
  545. * @param {String} userId - the userId automatically added by hooks
  546. */
  547. verifyPasswordCode: hooks.loginRequired((session, code, cb, userId) => {
  548. async.waterfall([
  549. (next) => {
  550. if (!code || typeof code !== 'string') return next('Invalid code1.');
  551. db.models.user.findOne({"services.password.set.code": code, _id: userId}, next);
  552. },
  553. (user, next) => {
  554. if (!user) return next('Invalid code2.');
  555. if (user.services.password.set.expires < new Date()) return next('That code has expired.');
  556. next(null);
  557. }
  558. ], (err) => {
  559. if (err && err !== true) {
  560. let error = 'An error occurred.';
  561. if (typeof err === "string") error = err;
  562. else if (err.message) error = err.message;
  563. logger.error("VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${error}'`);
  564. cb({status: 'failure', message: error});
  565. } else {
  566. logger.success("VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
  567. cb({
  568. status: 'success',
  569. message: 'Successfully verified password code.'
  570. });
  571. }
  572. });
  573. }),
  574. /**
  575. * Adds a password to a user with a code
  576. *
  577. * @param {Object} session - the session object automatically added by socket.io
  578. * @param {String} code - the password code
  579. * @param {String} newPassword - the new password code
  580. * @param {Function} cb - gets called with the result
  581. * @param {String} userId - the userId automatically added by hooks
  582. */
  583. changePasswordWithCode: hooks.loginRequired((session, code, newPassword, cb) => {
  584. async.waterfall([
  585. (next) => {
  586. if (!code || typeof code !== 'string') return next('Invalid code1.');
  587. db.models.user.findOne({"services.password.set.code": code}, next);
  588. },
  589. (user, next) => {
  590. if (!user) return next('Invalid code2.');
  591. if (!user.services.password.set.expires > new Date()) return next('That code has expired.');
  592. next();
  593. },
  594. (next) => {
  595. bcrypt.genSalt(10, next);
  596. },
  597. // hash the password
  598. (salt, next) => {
  599. bcrypt.hash(sha256(newPassword), salt, next);
  600. },
  601. (hashedPassword, next) => {
  602. db.models.user.update({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, next);
  603. }
  604. ], (err) => {
  605. if (err && err !== true) {
  606. let error = 'An error occurred.';
  607. if (typeof err === "string") error = err;
  608. else if (err.message) error = err.message;
  609. logger.error("ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${error}'`);
  610. cb({status: 'failure', message: error});
  611. } else {
  612. logger.success("ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
  613. cb({
  614. status: 'success',
  615. message: 'Successfully added password.'
  616. });
  617. }
  618. });
  619. }),
  620. /**
  621. * Unlinks password from user
  622. *
  623. * @param {Object} session - the session object automatically added by socket.io
  624. * @param {Function} cb - gets called with the result
  625. * @param {String} userId - the userId automatically added by hooks
  626. */
  627. unlinkPassword: hooks.loginRequired((session, cb, userId) => {
  628. async.waterfall([
  629. (next) => {
  630. db.models.user.findOne({_id: userId}, next);
  631. },
  632. (user, next) => {
  633. if (!user) return next('Not logged in.');
  634. if (!user.services.github || !user.services.github.id) return next('You can\'t remove password login without having GitHub login.');
  635. db.models.user.update({_id: userId}, {$unset: {"services.password": ''}}, next);
  636. }
  637. ], (err) => {
  638. if (err && err !== true) {
  639. let error = 'An error occurred.';
  640. if (typeof err === "string") error = err;
  641. else if (err.message) error = err.message;
  642. logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${userId}'. '${error}'`);
  643. cb({status: 'failure', message: error});
  644. } else {
  645. logger.success("UNLINK_PASSWORD", `Unlinking password successful for userId '${userId}'.`);
  646. cb({
  647. status: 'success',
  648. message: 'Successfully unlinked password.'
  649. });
  650. }
  651. });
  652. }),
  653. /**
  654. * Unlinks GitHub from user
  655. *
  656. * @param {Object} session - the session object automatically added by socket.io
  657. * @param {Function} cb - gets called with the result
  658. * @param {String} userId - the userId automatically added by hooks
  659. */
  660. unlinkGitHub: hooks.loginRequired((session, cb, userId) => {
  661. async.waterfall([
  662. (next) => {
  663. db.models.user.findOne({_id: userId}, next);
  664. },
  665. (user, next) => {
  666. if (!user) return next('Not logged in.');
  667. if (!user.services.password || !user.services.password.password) return next('You can\'t remove GitHub login without having password login.');
  668. db.models.user.update({_id: userId}, {$unset: {"services.github": ''}}, next);
  669. }
  670. ], (err) => {
  671. if (err && err !== true) {
  672. let error = 'An error occurred.';
  673. if (typeof err === "string") error = err;
  674. else if (err.message) error = err.message;
  675. logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${userId}'. '${error}'`);
  676. cb({status: 'failure', message: error});
  677. } else {
  678. logger.success("UNLINK_GITHUB", `Unlinking GitHub successful for userId '${userId}'.`);
  679. cb({
  680. status: 'success',
  681. message: 'Successfully unlinked GitHub.'
  682. });
  683. }
  684. });
  685. }),
  686. /**
  687. * Requests a password reset for an email
  688. *
  689. * @param {Object} session - the session object automatically added by socket.io
  690. * @param {String} email - the email of the user that requests a password reset
  691. * @param {Function} cb - gets called with the result
  692. */
  693. requestPasswordReset: (session, email, cb) => {
  694. let code = utils.generateRandomString(8);
  695. async.waterfall([
  696. (next) => {
  697. if (!email || typeof email !== 'string') return next('Invalid email.');
  698. email = email.toLowerCase();
  699. db.models.user.findOne({"email.address": email}, next);
  700. },
  701. (user, next) => {
  702. if (!user) return next('User not found.');
  703. if (!user.services.password || !user.services.password.password) return next('User does not have a password set, and probably uses GitHub to log in.');
  704. next(null, user);
  705. },
  706. (user, next) => {
  707. let expires = new Date();
  708. expires.setDate(expires.getDate() + 1);
  709. db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, next);
  710. },
  711. (user, next) => {
  712. mail.schemas.resetPasswordRequest(user.email.address, user.username, code, next);
  713. }
  714. ], (err) => {
  715. if (err && err !== true) {
  716. let error = 'An error occurred.';
  717. if (typeof err === "string") error = err;
  718. else if (err.message) error = err.message;
  719. logger.error("REQUEST_PASSWORD_RESET", `Email '${email}' failed to request password reset. '${error}'`);
  720. cb({status: 'failure', message: error});
  721. } else {
  722. logger.success("REQUEST_PASSWORD_RESET", `Email '${email}' successfully requested a password reset.`);
  723. cb({
  724. status: 'success',
  725. message: 'Successfully requested password reset.'
  726. });
  727. }
  728. });
  729. },
  730. /**
  731. * Verifies a reset code
  732. *
  733. * @param {Object} session - the session object automatically added by socket.io
  734. * @param {String} code - the password reset code
  735. * @param {Function} cb - gets called with the result
  736. */
  737. verifyPasswordResetCode: (session, code, cb) => {
  738. async.waterfall([
  739. (next) => {
  740. if (!code || typeof code !== 'string') return next('Invalid code.');
  741. db.models.user.findOne({"services.password.reset.code": code}, next);
  742. },
  743. (user, next) => {
  744. if (!user) return next('Invalid code.');
  745. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  746. next(null);
  747. }
  748. ], (err) => {
  749. if (err && err !== true) {
  750. let error = 'An error occurred.';
  751. if (typeof err === "string") error = err;
  752. else if (err.message) error = err.message;
  753. logger.error("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${error}'`);
  754. cb({status: 'failure', message: error});
  755. } else {
  756. logger.success("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  757. cb({
  758. status: 'success',
  759. message: 'Successfully verified password reset code.'
  760. });
  761. }
  762. });
  763. },
  764. /**
  765. * Changes a user's password with a reset code
  766. *
  767. * @param {Object} session - the session object automatically added by socket.io
  768. * @param {String} code - the password reset code
  769. * @param {String} newPassword - the new password reset code
  770. * @param {Function} cb - gets called with the result
  771. */
  772. changePasswordWithResetCode: (session, code, newPassword, cb) => {
  773. async.waterfall([
  774. (next) => {
  775. if (!code || typeof code !== 'string') return next('Invalid code.');
  776. db.models.user.findOne({"services.password.reset.code": code}, next);
  777. },
  778. (user, next) => {
  779. if (!user) return next('Invalid code.');
  780. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  781. next();
  782. },
  783. (next) => {
  784. bcrypt.genSalt(10, next);
  785. },
  786. // hash the password
  787. (salt, next) => {
  788. bcrypt.hash(sha256(newPassword), salt, next);
  789. },
  790. (hashedPassword, next) => {
  791. db.models.user.update({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, next);
  792. }
  793. ], (err) => {
  794. if (err && err !== true) {
  795. let error = 'An error occurred.';
  796. if (typeof err === "string") error = err;
  797. else if (err.message) error = err.message;
  798. logger.error("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' failed to change password. '${error}'`);
  799. cb({status: 'failure', message: error});
  800. } else {
  801. logger.success("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' successfully changed password.`);
  802. cb({
  803. status: 'success',
  804. message: 'Successfully changed password.'
  805. });
  806. }
  807. });
  808. }
  809. };