users.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233
  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. */
  301. removeSessions: hooks.loginRequired((session, userId, cb) => {
  302. async.waterfall([
  303. (next) => {
  304. db.models.user.findOne({ _id: session.userId }, (err, user) => {
  305. if (err) return next(err);
  306. if (user.role !== 'admin' && session.userId !== 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. location: user.location,
  449. bio: user.bio
  450. };
  451. if (user.services.password && user.services.password.password) data.password = true;
  452. if (user.services.github && user.services.github.id) data.github = true;
  453. logger.success("FIND_BY_SESSION", `User found. "${user.username}".`);
  454. return cb({
  455. status: 'success',
  456. data
  457. });
  458. }
  459. });
  460. },
  461. /**
  462. * Updates a user's username
  463. *
  464. * @param {Object} session - the session object automatically added by socket.io
  465. * @param {String} updatingUserId - the updating user's id
  466. * @param {String} newUsername - the new username
  467. * @param {Function} cb - gets called with the result
  468. */
  469. updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb) => {
  470. async.waterfall([
  471. (next) => {
  472. if (updatingUserId === session.userId) return next(null, true);
  473. db.models.user.findOne({_id: session.userId}, next);
  474. },
  475. (user, next) => {
  476. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  477. db.models.user.findOne({ _id: updatingUserId }, next);
  478. },
  479. (user, next) => {
  480. if (!user) return next('User not found.');
  481. if (user.username === newUsername) return next('New username can\'t be the same as the old username.');
  482. next(null);
  483. },
  484. (next) => {
  485. db.models.user.findOne({ username: new RegExp(`^${newUsername}$`, 'i') }, next);
  486. },
  487. (user, next) => {
  488. if (!user) return next();
  489. if (user._id === updatingUserId) return next();
  490. next('That username is already in use.');
  491. },
  492. (next) => {
  493. db.models.user.updateOne({ _id: updatingUserId }, {$set: {username: newUsername}}, {runValidators: true}, next);
  494. }
  495. ], async (err) => {
  496. if (err && err !== true) {
  497. err = await utils.getError(err);
  498. logger.error("UPDATE_USERNAME", `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`);
  499. cb({status: 'failure', message: err});
  500. } else {
  501. cache.pub('user.updateUsername', {
  502. username: newUsername,
  503. _id: updatingUserId
  504. });
  505. logger.success("UPDATE_USERNAME", `Updated username for user "${updatingUserId}" to username "${newUsername}".`);
  506. cb({ status: 'success', message: 'Username updated successfully' });
  507. }
  508. });
  509. }),
  510. /**
  511. * Updates a user's email
  512. *
  513. * @param {Object} session - the session object automatically added by socket.io
  514. * @param {String} updatingUserId - the updating user's id
  515. * @param {String} newEmail - the new email
  516. * @param {Function} cb - gets called with the result
  517. */
  518. updateEmail: hooks.loginRequired(async (session, updatingUserId, newEmail, cb) => {
  519. newEmail = newEmail.toLowerCase();
  520. let verificationToken = await utils.generateRandomString(64);
  521. async.waterfall([
  522. (next) => {
  523. if (updatingUserId === session.userId) return next(null, true);
  524. db.models.user.findOne({_id: session.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 location
  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} newLocation - the new location
  571. * @param {Function} cb - gets called with the result
  572. */
  573. updateLocation: hooks.loginRequired((session, updatingUserId, newLocation, cb) => {
  574. async.waterfall([
  575. (next) => {
  576. if (updatingUserId === session.userId) return next(null, true);
  577. db.models.user.findOne({_id: session.userId}, next);
  578. },
  579. (user, next) => {
  580. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  581. db.models.user.findOne({ _id: updatingUserId }, next);
  582. },
  583. (user, next) => {
  584. if (!user) return next('User not found.');
  585. db.models.user.updateOne({ _id: updatingUserId }, {$set: {location: newLocation}}, {runValidators: true}, next);
  586. }
  587. ], async (err) => {
  588. if (err && err !== true) {
  589. err = await utils.getError(err);
  590. logger.error("UPDATE_LOCATION", `Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`);
  591. cb({status: 'failure', message: err});
  592. } else {
  593. logger.success("UPDATE_LOCATION", `Updated location for user "${updatingUserId}" to location "${newLocation}".`);
  594. cb({ status: 'success', message: 'Location updated successfully' });
  595. }
  596. });
  597. }),
  598. /**
  599. * Updates a user's bio
  600. *
  601. * @param {Object} session - the session object automatically added by socket.io
  602. * @param {String} updatingUserId - the updating user's id
  603. * @param {String} newBio - the new bio
  604. * @param {Function} cb - gets called with the result
  605. */
  606. updateBio: hooks.loginRequired((session, updatingUserId, newBio, cb) => {
  607. async.waterfall([
  608. (next) => {
  609. if (updatingUserId === session.userId) return next(null, true);
  610. db.models.user.findOne({_id: session.userId}, next);
  611. },
  612. (user, next) => {
  613. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  614. db.models.user.findOne({ _id: updatingUserId }, next);
  615. },
  616. (user, next) => {
  617. if (!user) return next('User not found.');
  618. db.models.user.updateOne({ _id: updatingUserId }, {$set: {bio: newBio}}, {runValidators: true}, next);
  619. }
  620. ], async (err) => {
  621. if (err && err !== true) {
  622. err = await utils.getError(err);
  623. logger.error("UPDATE_BIO", `Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`);
  624. cb({status: 'failure', message: err});
  625. } else {
  626. logger.success("UPDATE_BIO", `Updated bio for user "${updatingUserId}" to bio "${newBio}".`);
  627. cb({ status: 'success', message: 'Bio updated successfully' });
  628. }
  629. });
  630. }),
  631. /**
  632. * Updates a user's role
  633. *
  634. * @param {Object} session - the session object automatically added by socket.io
  635. * @param {String} updatingUserId - the updating user's id
  636. * @param {String} newRole - the new role
  637. * @param {Function} cb - gets called with the result
  638. */
  639. updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb) => {
  640. newRole = newRole.toLowerCase();
  641. async.waterfall([
  642. (next) => {
  643. db.models.user.findOne({ _id: updatingUserId }, next);
  644. },
  645. (user, next) => {
  646. if (!user) return next('User not found.');
  647. else if (user.role === newRole) return next('New role can\'t be the same as the old role.');
  648. else return next();
  649. },
  650. (next) => {
  651. db.models.user.updateOne({_id: updatingUserId}, {$set: {role: newRole}}, {runValidators: true}, next);
  652. }
  653. ], async (err) => {
  654. if (err && err !== true) {
  655. err = await utils.getError(err);
  656. logger.error("UPDATE_ROLE", `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`);
  657. cb({status: 'failure', message: err});
  658. } else {
  659. logger.success("UPDATE_ROLE", `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`);
  660. cb({
  661. status: 'success',
  662. message: 'Role successfully updated.'
  663. });
  664. }
  665. });
  666. }),
  667. /**
  668. * Updates a user's password
  669. *
  670. * @param {Object} session - the session object automatically added by socket.io
  671. * @param {String} newPassword - the new password
  672. * @param {Function} cb - gets called with the result
  673. */
  674. updatePassword: hooks.loginRequired((session, newPassword, cb) => {
  675. async.waterfall([
  676. (next) => {
  677. db.models.user.findOne({_id: session.userId}, next);
  678. },
  679. (user, next) => {
  680. if (!user.services.password) return next('This account does not have a password set.');
  681. next();
  682. },
  683. (next) => {
  684. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  685. return next();
  686. },
  687. (next) => {
  688. bcrypt.genSalt(10, next);
  689. },
  690. // hash the password
  691. (salt, next) => {
  692. bcrypt.hash(sha256(newPassword), salt, next);
  693. },
  694. (hashedPassword, next) => {
  695. db.models.user.updateOne({_id: session.userId}, {$set: {"services.password.password": hashedPassword}}, next);
  696. }
  697. ], async (err) => {
  698. if (err) {
  699. err = await utils.getError(err);
  700. logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${session.userId}'. '${err}'.`);
  701. return cb({ status: 'failure', message: err });
  702. }
  703. logger.success("UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
  704. cb({
  705. status: 'success',
  706. message: 'Password successfully updated.'
  707. });
  708. });
  709. }),
  710. /**
  711. * Requests a password for a session
  712. *
  713. * @param {Object} session - the session object automatically added by socket.io
  714. * @param {String} email - the email of the user that requests a password reset
  715. * @param {Function} cb - gets called with the result
  716. */
  717. requestPassword: hooks.loginRequired(async (session, cb) => {
  718. let code = await utils.generateRandomString(8);
  719. async.waterfall([
  720. (next) => {
  721. db.models.user.findOne({_id: session.userId}, next);
  722. },
  723. (user, next) => {
  724. if (!user) return next('User not found.');
  725. if (user.services.password && user.services.password.password) return next('You already have a password set.');
  726. next(null, user);
  727. },
  728. (user, next) => {
  729. let expires = new Date();
  730. expires.setDate(expires.getDate() + 1);
  731. db.models.user.findOneAndUpdate({"email.address": user.email.address}, {$set: {"services.password": {set: {code: code, expires}}}}, {runValidators: true}, next);
  732. },
  733. (user, next) => {
  734. mail.schemas.passwordRequest(user.email.address, user.username, code, next);
  735. }
  736. ], async (err) => {
  737. if (err && err !== true) {
  738. err = await utils.getError(err);
  739. logger.error("REQUEST_PASSWORD", `UserId '${session.userId}' failed to request password. '${err}'`);
  740. cb({status: 'failure', message: err});
  741. } else {
  742. logger.success("REQUEST_PASSWORD", `UserId '${session.userId}' successfully requested a password.`);
  743. cb({
  744. status: 'success',
  745. message: 'Successfully requested password.'
  746. });
  747. }
  748. });
  749. }),
  750. /**
  751. * Verifies a password code
  752. *
  753. * @param {Object} session - the session object automatically added by socket.io
  754. * @param {String} code - the password code
  755. * @param {Function} cb - gets called with the result
  756. */
  757. verifyPasswordCode: hooks.loginRequired((session, code, cb) => {
  758. async.waterfall([
  759. (next) => {
  760. if (!code || typeof code !== 'string') return next('Invalid code1.');
  761. db.models.user.findOne({"services.password.set.code": code, _id: session.userId}, next);
  762. },
  763. (user, next) => {
  764. if (!user) return next('Invalid code2.');
  765. if (user.services.password.set.expires < new Date()) return next('That code has expired.');
  766. next(null);
  767. }
  768. ], async(err) => {
  769. if (err && err !== true) {
  770. err = await utils.getError(err);
  771. logger.error("VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
  772. cb({status: 'failure', message: err});
  773. } else {
  774. logger.success("VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
  775. cb({
  776. status: 'success',
  777. message: 'Successfully verified password code.'
  778. });
  779. }
  780. });
  781. }),
  782. /**
  783. * Adds a password to a user with a code
  784. *
  785. * @param {Object} session - the session object automatically added by socket.io
  786. * @param {String} code - the password code
  787. * @param {String} newPassword - the new password code
  788. * @param {Function} cb - gets called with the result
  789. */
  790. changePasswordWithCode: hooks.loginRequired((session, code, newPassword, cb) => {
  791. async.waterfall([
  792. (next) => {
  793. if (!code || typeof code !== 'string') return next('Invalid code1.');
  794. db.models.user.findOne({"services.password.set.code": code}, next);
  795. },
  796. (user, next) => {
  797. if (!user) return next('Invalid code2.');
  798. if (!user.services.password.set.expires > new Date()) return next('That code has expired.');
  799. next();
  800. },
  801. (next) => {
  802. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  803. return next();
  804. },
  805. (next) => {
  806. bcrypt.genSalt(10, next);
  807. },
  808. // hash the password
  809. (salt, next) => {
  810. bcrypt.hash(sha256(newPassword), salt, next);
  811. },
  812. (hashedPassword, next) => {
  813. db.models.user.updateOne({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, {runValidators: true}, next);
  814. }
  815. ], async (err) => {
  816. if (err && err !== true) {
  817. err = await utils.getError(err);
  818. logger.error("ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
  819. cb({status: 'failure', message: err});
  820. } else {
  821. logger.success("ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
  822. cache.pub('user.linkPassword', session.userId);
  823. cb({
  824. status: 'success',
  825. message: 'Successfully added password.'
  826. });
  827. }
  828. });
  829. }),
  830. /**
  831. * Unlinks password from user
  832. *
  833. * @param {Object} session - the session object automatically added by socket.io
  834. * @param {Function} cb - gets called with the result
  835. */
  836. unlinkPassword: hooks.loginRequired((session, cb) => {
  837. async.waterfall([
  838. (next) => {
  839. db.models.user.findOne({_id: session.userId}, next);
  840. },
  841. (user, next) => {
  842. if (!user) return next('Not logged in.');
  843. if (!user.services.github || !user.services.github.id) return next('You can\'t remove password login without having GitHub login.');
  844. db.models.user.updateOne({_id: session.userId}, {$unset: {"services.password": ''}}, next);
  845. }
  846. ], async (err) => {
  847. if (err && err !== true) {
  848. err = await utils.getError(err);
  849. logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${session.userId}'. '${err}'`);
  850. cb({status: 'failure', message: err});
  851. } else {
  852. logger.success("UNLINK_PASSWORD", `Unlinking password successful for userId '${session.userId}'.`);
  853. cache.pub('user.unlinkPassword', session.userId);
  854. cb({
  855. status: 'success',
  856. message: 'Successfully unlinked password.'
  857. });
  858. }
  859. });
  860. }),
  861. /**
  862. * Unlinks GitHub from user
  863. *
  864. * @param {Object} session - the session object automatically added by socket.io
  865. * @param {Function} cb - gets called with the result
  866. */
  867. unlinkGitHub: hooks.loginRequired((session, cb) => {
  868. async.waterfall([
  869. (next) => {
  870. db.models.user.findOne({_id: session.userId}, next);
  871. },
  872. (user, next) => {
  873. if (!user) return next('Not logged in.');
  874. if (!user.services.password || !user.services.password.password) return next('You can\'t remove GitHub login without having password login.');
  875. db.models.user.updateOne({_id: session.userId}, {$unset: {"services.github": ''}}, next);
  876. }
  877. ], async (err) => {
  878. if (err && err !== true) {
  879. err = await utils.getError(err);
  880. logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`);
  881. cb({status: 'failure', message: err});
  882. } else {
  883. logger.success("UNLINK_GITHUB", `Unlinking GitHub successful for userId '${session.userId}'.`);
  884. cache.pub('user.unlinkGitHub', session.userId);
  885. cb({
  886. status: 'success',
  887. message: 'Successfully unlinked GitHub.'
  888. });
  889. }
  890. });
  891. }),
  892. /**
  893. * Requests a password reset for an email
  894. *
  895. * @param {Object} session - the session object automatically added by socket.io
  896. * @param {String} email - the email of the user that requests a password reset
  897. * @param {Function} cb - gets called with the result
  898. */
  899. requestPasswordReset: async (session, email, cb) => {
  900. let code = await utils.generateRandomString(8);
  901. async.waterfall([
  902. (next) => {
  903. if (!email || typeof email !== 'string') return next('Invalid email.');
  904. email = email.toLowerCase();
  905. db.models.user.findOne({"email.address": email}, next);
  906. },
  907. (user, next) => {
  908. if (!user) return next('User not found.');
  909. if (!user.services.password || !user.services.password.password) return next('User does not have a password set, and probably uses GitHub to log in.');
  910. next(null, user);
  911. },
  912. (user, next) => {
  913. let expires = new Date();
  914. expires.setDate(expires.getDate() + 1);
  915. db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, {runValidators: true}, next);
  916. },
  917. (user, next) => {
  918. mail.schemas.resetPasswordRequest(user.email.address, user.username, code, next);
  919. }
  920. ], async (err) => {
  921. if (err && err !== true) {
  922. err = await utils.getError(err);
  923. logger.error("REQUEST_PASSWORD_RESET", `Email '${email}' failed to request password reset. '${err}'`);
  924. cb({status: 'failure', message: err});
  925. } else {
  926. logger.success("REQUEST_PASSWORD_RESET", `Email '${email}' successfully requested a password reset.`);
  927. cb({
  928. status: 'success',
  929. message: 'Successfully requested password reset.'
  930. });
  931. }
  932. });
  933. },
  934. /**
  935. * Verifies a reset code
  936. *
  937. * @param {Object} session - the session object automatically added by socket.io
  938. * @param {String} code - the password reset code
  939. * @param {Function} cb - gets called with the result
  940. */
  941. verifyPasswordResetCode: (session, code, cb) => {
  942. async.waterfall([
  943. (next) => {
  944. if (!code || typeof code !== 'string') return next('Invalid code.');
  945. db.models.user.findOne({"services.password.reset.code": code}, next);
  946. },
  947. (user, next) => {
  948. if (!user) return next('Invalid code.');
  949. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  950. next(null);
  951. }
  952. ], async (err) => {
  953. if (err && err !== true) {
  954. err = await utils.getError(err);
  955. logger.error("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
  956. cb({status: 'failure', message: err});
  957. } else {
  958. logger.success("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  959. cb({
  960. status: 'success',
  961. message: 'Successfully verified password reset code.'
  962. });
  963. }
  964. });
  965. },
  966. /**
  967. * Changes a user's password with a reset code
  968. *
  969. * @param {Object} session - the session object automatically added by socket.io
  970. * @param {String} code - the password reset code
  971. * @param {String} newPassword - the new password reset code
  972. * @param {Function} cb - gets called with the result
  973. */
  974. changePasswordWithResetCode: (session, code, newPassword, cb) => {
  975. async.waterfall([
  976. (next) => {
  977. if (!code || typeof code !== 'string') return next('Invalid code.');
  978. db.models.user.findOne({"services.password.reset.code": code}, next);
  979. },
  980. (user, next) => {
  981. if (!user) return next('Invalid code.');
  982. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  983. next();
  984. },
  985. (next) => {
  986. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  987. return next();
  988. },
  989. (next) => {
  990. bcrypt.genSalt(10, next);
  991. },
  992. // hash the password
  993. (salt, next) => {
  994. bcrypt.hash(sha256(newPassword), salt, next);
  995. },
  996. (hashedPassword, next) => {
  997. db.models.user.updateOne({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, {runValidators: true}, next);
  998. }
  999. ], async (err) => {
  1000. if (err && err !== true) {
  1001. err = await utils.getError(err);
  1002. logger.error("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' failed to change password. '${err}'`);
  1003. cb({status: 'failure', message: err});
  1004. } else {
  1005. logger.success("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' successfully changed password.`);
  1006. cb({
  1007. status: 'success',
  1008. message: 'Successfully changed password.'
  1009. });
  1010. }
  1011. });
  1012. },
  1013. /**
  1014. * Bans a user by userId
  1015. *
  1016. * @param {Object} session - the session object automatically added by socket.io
  1017. * @param {String} value - the user id that is going to be banned
  1018. * @param {String} reason - the reason for the ban
  1019. * @param {String} expiresAt - the time the ban expires
  1020. * @param {Function} cb - gets called with the result
  1021. */
  1022. banUserById: hooks.adminRequired((session, userId, reason, expiresAt, cb) => {
  1023. async.waterfall([
  1024. (next) => {
  1025. if (!userId) return next('You must provide a userId to ban.');
  1026. else if (!reason) return next('You must provide a reason for the ban.');
  1027. else return next();
  1028. },
  1029. (next) => {
  1030. if (!expiresAt || typeof expiresAt !== 'string') return next('Invalid expire date.');
  1031. let date = new Date();
  1032. switch(expiresAt) {
  1033. case '1h':
  1034. expiresAt = date.setHours(date.getHours() + 1);
  1035. break;
  1036. case '12h':
  1037. expiresAt = date.setHours(date.getHours() + 12);
  1038. break;
  1039. case '1d':
  1040. expiresAt = date.setDate(date.getDate() + 1);
  1041. break;
  1042. case '1w':
  1043. expiresAt = date.setDate(date.getDate() + 7);
  1044. break;
  1045. case '1m':
  1046. expiresAt = date.setMonth(date.getMonth() + 1);
  1047. break;
  1048. case '3m':
  1049. expiresAt = date.setMonth(date.getMonth() + 3);
  1050. break;
  1051. case '6m':
  1052. expiresAt = date.setMonth(date.getMonth() + 6);
  1053. break;
  1054. case '1y':
  1055. expiresAt = date.setFullYear(date.getFullYear() + 1);
  1056. break;
  1057. case 'never':
  1058. expiresAt = new Date(3093527980800000);
  1059. break;
  1060. default:
  1061. return next('Invalid expire date.');
  1062. }
  1063. next();
  1064. },
  1065. (next) => {
  1066. punishments.addPunishment('banUserId', userId, reason, expiresAt, userId, next)
  1067. },
  1068. (punishment, next) => {
  1069. cache.pub('user.ban', { userId, punishment });
  1070. next();
  1071. },
  1072. ], async (err) => {
  1073. if (err && err !== true) {
  1074. err = await utils.getError(err);
  1075. logger.error("BAN_USER_BY_ID", `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`);
  1076. cb({status: 'failure', message: err});
  1077. } else {
  1078. logger.success("BAN_USER_BY_ID", `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`);
  1079. cb({
  1080. status: 'success',
  1081. message: 'Successfully banned user.'
  1082. });
  1083. }
  1084. });
  1085. }),
  1086. getFavoriteStations: hooks.loginRequired((session, cb) => {
  1087. async.waterfall([
  1088. (next) => {
  1089. db.models.user.findOne({ _id: session.userId }, next);
  1090. },
  1091. (user, next) => {
  1092. if (!user) return next("User not found.");
  1093. next(null, user);
  1094. }
  1095. ], async (err, user) => {
  1096. if (err && err !== true) {
  1097. err = await utils.getError(err);
  1098. logger.error("GET_FAVORITE_STATIONS", `User ${session.userId} failed to get favorite stations. '${err}'`);
  1099. cb({status: 'failure', message: err});
  1100. } else {
  1101. logger.success("GET_FAVORITE_STATIONS", `User ${session.userId} got favorite stations.`);
  1102. cb({
  1103. status: 'success',
  1104. favoriteStations: user.favoriteStations
  1105. });
  1106. }
  1107. });
  1108. })
  1109. };