users.js 34 KB

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