users.js 33 KB

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