users.js 34 KB

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