users.js 35 KB

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