users.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  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. // create the user object
  226. (hash, _id, next) => {
  227. next(null, {
  228. _id,
  229. username,
  230. email: {
  231. address: email,
  232. verificationToken
  233. },
  234. services: {
  235. password: {
  236. password: hash
  237. }
  238. }
  239. });
  240. },
  241. // generate the url for gravatar avatar
  242. (user, next) => {
  243. this.utils.createGravatar(user.email.address).then(url => {
  244. user.avatar = url;
  245. next(null, user);
  246. });
  247. },
  248. // save the new user to the database
  249. (user, next) => {
  250. db.models.user.create(user, next);
  251. },
  252. // respond with the new user
  253. (newUser, next) => {
  254. mail.schemas.verifyEmail(email, username, verificationToken, () => {
  255. next();
  256. });
  257. }
  258. ], async (err) => {
  259. if (err && err !== true) {
  260. err = await utils.getError(err);
  261. logger.error("USER_PASSWORD_REGISTER", `Register failed with password for user "${username}"."${err}"`);
  262. cb({status: 'failure', message: err});
  263. } else {
  264. module.exports.login(session, email, password, (result) => {
  265. let obj = {status: 'success', message: 'Successfully registered.'};
  266. if (result.status === 'success') {
  267. obj.SID = result.SID;
  268. }
  269. logger.success("USER_PASSWORD_REGISTER", `Register successful with password for user "${username}".`);
  270. cb(obj);
  271. });
  272. }
  273. });
  274. },
  275. /**
  276. * Logs out a user
  277. *
  278. * @param {Object} session - the session object automatically added by socket.io
  279. * @param {Function} cb - gets called with the result
  280. */
  281. logout: (session, cb) => {
  282. async.waterfall([
  283. (next) => {
  284. cache.hget('sessions', session.sessionId, next);
  285. },
  286. (session, next) => {
  287. if (!session) return next('Session not found');
  288. next(null, session);
  289. },
  290. (session, next) => {
  291. cache.hdel('sessions', session.sessionId, next);
  292. }
  293. ], async (err) => {
  294. if (err && err !== true) {
  295. err = await utils.getError(err);
  296. logger.error("USER_LOGOUT", `Logout failed. "${err}" `);
  297. cb({ status: 'failure', message: err });
  298. } else {
  299. logger.success("USER_LOGOUT", `Logout successful.`);
  300. cb({ status: 'success', message: 'Successfully logged out.' });
  301. }
  302. });
  303. },
  304. /**
  305. * Removes all sessions for a user
  306. *
  307. * @param {Object} session - the session object automatically added by socket.io
  308. * @param {String} userId - the id of the user we are trying to delete the sessions of
  309. * @param {Function} cb - gets called with the result
  310. */
  311. removeSessions: hooks.loginRequired((session, userId, cb) => {
  312. async.waterfall([
  313. (next) => {
  314. db.models.user.findOne({ _id: session.userId }, (err, user) => {
  315. if (err) return next(err);
  316. if (user.role !== 'admin' && session.userId !== userId) return next('Only admins and the owner of the account can remove their sessions.');
  317. else return next();
  318. });
  319. },
  320. (next) => {
  321. cache.hgetall('sessions', next);
  322. },
  323. (sessions, next) => {
  324. if (!sessions) return next('There are no sessions for this user to remove.');
  325. else {
  326. let keys = Object.keys(sessions);
  327. next(null, keys, sessions);
  328. }
  329. },
  330. (keys, sessions, next) => {
  331. cache.pub('user.removeSessions', userId);
  332. async.each(keys, (sessionId, callback) => {
  333. let session = sessions[sessionId];
  334. if (session.userId === userId) {
  335. cache.hdel('sessions', sessionId, err => {
  336. if (err) return callback(err);
  337. else callback(null);
  338. });
  339. }
  340. }, err => {
  341. next(err);
  342. });
  343. }
  344. ], async err => {
  345. if (err) {
  346. err = await utils.getError(err);
  347. logger.error("REMOVE_SESSIONS_FOR_USER", `Couldn't remove all sessions for user "${userId}". "${err}"`);
  348. return cb({ status: 'failure', message: err });
  349. } else {
  350. logger.success("REMOVE_SESSIONS_FOR_USER", `Removed all sessions for user "${userId}".`);
  351. return cb({ status: 'success', message: 'Successfully removed all sessions.' });
  352. }
  353. });
  354. }),
  355. /**
  356. * Gets user object from username (only a few properties)
  357. *
  358. * @param {Object} session - the session object automatically added by socket.io
  359. * @param {String} username - the username of the user we are trying to find
  360. * @param {Function} cb - gets called with the result
  361. */
  362. findByUsername: (session, username, cb) => {
  363. async.waterfall([
  364. (next) => {
  365. db.models.user.findOne({ username: new RegExp(`^${username}$`, 'i') }, next);
  366. },
  367. (account, next) => {
  368. if (!account) return next('User not found.');
  369. next(null, account);
  370. }
  371. ], async (err, account) => {
  372. if (err && err !== true) {
  373. err = await utils.getError(err);
  374. logger.error("FIND_BY_USERNAME", `User not found for username "${username}". "${err}"`);
  375. cb({status: 'failure', message: err});
  376. } else {
  377. logger.success("FIND_BY_USERNAME", `User found for username "${username}".`);
  378. return cb({
  379. status: 'success',
  380. data: {
  381. _id: account._id,
  382. name: account.name,
  383. username: account.username,
  384. location: account.location,
  385. bio: account.bio,
  386. role: account.role,
  387. avatar: account.avatar,
  388. createdAt: account.createdAt
  389. }
  390. });
  391. }
  392. });
  393. },
  394. /**
  395. * Gets a username from an userId
  396. *
  397. * @param {Object} session - the session object automatically added by socket.io
  398. * @param {String} userId - the userId of the person we are trying to get the username from
  399. * @param {Function} cb - gets called with the result
  400. */
  401. getUsernameFromId: (session, userId, cb) => {
  402. db.models.user.findById(userId).then(user => {
  403. if (user) {
  404. logger.success("GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
  405. return cb({
  406. status: 'success',
  407. data: user.username
  408. });
  409. } else {
  410. logger.error("GET_USERNAME_FROM_ID", `Getting the username from userId "${userId}" failed. User not found.`);
  411. cb({
  412. status: 'failure',
  413. message: "Couldn't find the user."
  414. });
  415. }
  416. }).catch(async err => {
  417. if (err && err !== true) {
  418. err = await utils.getError(err);
  419. logger.error("GET_USERNAME_FROM_ID", `Getting the username from userId "${userId}" failed. "${err}"`);
  420. cb({ status: 'failure', message: err });
  421. }
  422. });
  423. },
  424. //TODO Fix security issues
  425. /**
  426. * Gets user info from session
  427. *
  428. * @param {Object} session - the session object automatically added by socket.io
  429. * @param {Function} cb - gets called with the result
  430. */
  431. findBySession: (session, cb) => {
  432. async.waterfall([
  433. (next) => {
  434. cache.hget('sessions', session.sessionId, next);
  435. },
  436. (session, next) => {
  437. if (!session) return next('Session not found.');
  438. next(null, session);
  439. },
  440. (session, next) => {
  441. db.models.user.findOne({ _id: session.userId }, next);
  442. },
  443. (user, next) => {
  444. if (!user) return next('User not found.');
  445. next(null, user);
  446. }
  447. ], async (err, user) => {
  448. if (err && err !== true) {
  449. err = await utils.getError(err);
  450. logger.error("FIND_BY_SESSION", `User not found. "${err}"`);
  451. cb({status: 'failure', message: err});
  452. } else {
  453. let data = {
  454. email: {
  455. address: user.email.address
  456. },
  457. username: user.username,
  458. name: user.name,
  459. location: user.location,
  460. bio: user.bio
  461. };
  462. if (user.services.password && user.services.password.password) data.password = true;
  463. if (user.services.github && user.services.github.id) data.github = true;
  464. logger.success("FIND_BY_SESSION", `User found. "${user.username}".`);
  465. return cb({
  466. status: 'success',
  467. data
  468. });
  469. }
  470. });
  471. },
  472. /**
  473. * Updates a user's username
  474. *
  475. * @param {Object} session - the session object automatically added by socket.io
  476. * @param {String} updatingUserId - the updating user's id
  477. * @param {String} newUsername - the new username
  478. * @param {Function} cb - gets called with the result
  479. */
  480. updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb) => {
  481. async.waterfall([
  482. (next) => {
  483. if (updatingUserId === session.userId) return next(null, true);
  484. db.models.user.findOne({_id: session.userId}, next);
  485. },
  486. (user, next) => {
  487. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  488. db.models.user.findOne({ _id: updatingUserId }, next);
  489. },
  490. (user, next) => {
  491. if (!user) return next('User not found.');
  492. if (user.username === newUsername) return next('New username can\'t be the same as the old username.');
  493. next(null);
  494. },
  495. (next) => {
  496. db.models.user.findOne({ username: new RegExp(`^${newUsername}$`, 'i') }, next);
  497. },
  498. (user, next) => {
  499. if (!user) return next();
  500. if (user._id === updatingUserId) return next();
  501. next('That username is already in use.');
  502. },
  503. (next) => {
  504. db.models.user.updateOne({ _id: updatingUserId }, {$set: {username: newUsername}}, {runValidators: true}, next);
  505. }
  506. ], async (err) => {
  507. if (err && err !== true) {
  508. err = await utils.getError(err);
  509. logger.error("UPDATE_USERNAME", `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`);
  510. cb({status: 'failure', message: err});
  511. } else {
  512. cache.pub('user.updateUsername', {
  513. username: newUsername,
  514. _id: updatingUserId
  515. });
  516. logger.success("UPDATE_USERNAME", `Updated username for user "${updatingUserId}" to username "${newUsername}".`);
  517. cb({ status: 'success', message: 'Username updated successfully' });
  518. }
  519. });
  520. }),
  521. /**
  522. * Updates a user's email
  523. *
  524. * @param {Object} session - the session object automatically added by socket.io
  525. * @param {String} updatingUserId - the updating user's id
  526. * @param {String} newEmail - the new email
  527. * @param {Function} cb - gets called with the result
  528. */
  529. updateEmail: hooks.loginRequired(async (session, updatingUserId, newEmail, cb) => {
  530. newEmail = newEmail.toLowerCase();
  531. let verificationToken = await utils.generateRandomString(64);
  532. async.waterfall([
  533. (next) => {
  534. if (updatingUserId === session.userId) return next(null, true);
  535. db.models.user.findOne({_id: session.userId}, next);
  536. },
  537. (user, next) => {
  538. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  539. db.models.user.findOne({ _id: updatingUserId }, next);
  540. },
  541. (user, next) => {
  542. if (!user) return next('User not found.');
  543. if (user.email.address === newEmail) return next('New email can\'t be the same as your the old email.');
  544. next();
  545. },
  546. (next) => {
  547. db.models.user.findOne({"email.address": newEmail}, next);
  548. },
  549. (user, next) => {
  550. if (!user) return next();
  551. if (user._id === updatingUserId) return next();
  552. next('That email is already in use.');
  553. },
  554. // regenerate the url for gravatar avatar
  555. (next) => {
  556. utils.createGravatar(newEmail).then(url => next(null, url));
  557. },
  558. (avatar, next) => {
  559. db.models.user.updateOne({ _id: updatingUserId }, {
  560. $set: {
  561. "avatar": avatar,
  562. "email.address": newEmail,
  563. "email.verified": false,
  564. "email.verificationToken": verificationToken
  565. }
  566. }, { runValidators: true }, next);
  567. },
  568. (res, next) => {
  569. db.models.user.findOne({ _id: updatingUserId }, next);
  570. },
  571. (user, next) => {
  572. mail.schemas.verifyEmail(newEmail, user.username, verificationToken, () => {
  573. next();
  574. });
  575. }
  576. ], async (err) => {
  577. if (err && err !== true) {
  578. err = await utils.getError(err);
  579. logger.error("UPDATE_EMAIL", `Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`);
  580. cb({status: 'failure', message: err});
  581. } else {
  582. logger.success("UPDATE_EMAIL", `Updated email for user "${updatingUserId}" to email "${newEmail}".`);
  583. cb({ status: 'success', message: 'Email updated successfully.' });
  584. }
  585. });
  586. }),
  587. /**
  588. * Updates a user's name
  589. *
  590. * @param {Object} session - the session object automatically added by socket.io
  591. * @param {String} updatingUserId - the updating user's id
  592. * @param {String} newBio - the new name
  593. * @param {Function} cb - gets called with the result
  594. */
  595. updateName: hooks.loginRequired((session, updatingUserId, newName, cb) => {
  596. async.waterfall([
  597. (next) => {
  598. if (updatingUserId === session.userId) return next(null, true);
  599. db.models.user.findOne({_id: session.userId}, next);
  600. },
  601. (user, next) => {
  602. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  603. db.models.user.findOne({ _id: updatingUserId }, next);
  604. },
  605. (user, next) => {
  606. if (!user) return next('User not found.');
  607. db.models.user.updateOne({ _id: updatingUserId }, {$set: { name: newName }}, {runValidators: true}, next);
  608. }
  609. ], async (err) => {
  610. if (err && err !== true) {
  611. err = await utils.getError(err);
  612. logger.error("UPDATE_NAME", `Couldn't update name for user "${updatingUserId}" to name "${newName}". "${err}"`);
  613. cb({status: 'failure', message: err});
  614. } else {
  615. logger.success("UPDATE_NAME", `Updated name for user "${updatingUserId}" to name "${newName}".`);
  616. cb({ status: 'success', message: 'Name updated successfully' });
  617. }
  618. });
  619. }),
  620. /**
  621. * Updates a user's location
  622. *
  623. * @param {Object} session - the session object automatically added by socket.io
  624. * @param {String} updatingUserId - the updating user's id
  625. * @param {String} newLocation - the new location
  626. * @param {Function} cb - gets called with the result
  627. */
  628. updateLocation: hooks.loginRequired((session, updatingUserId, newLocation, cb) => {
  629. async.waterfall([
  630. (next) => {
  631. if (updatingUserId === session.userId) return next(null, true);
  632. db.models.user.findOne({_id: session.userId}, next);
  633. },
  634. (user, next) => {
  635. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  636. db.models.user.findOne({ _id: updatingUserId }, next);
  637. },
  638. (user, next) => {
  639. if (!user) return next('User not found.');
  640. db.models.user.updateOne({ _id: updatingUserId }, {$set: {location: newLocation}}, {runValidators: true}, next);
  641. }
  642. ], async (err) => {
  643. if (err && err !== true) {
  644. err = await utils.getError(err);
  645. logger.error("UPDATE_LOCATION", `Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`);
  646. cb({status: 'failure', message: err});
  647. } else {
  648. logger.success("UPDATE_LOCATION", `Updated location for user "${updatingUserId}" to location "${newLocation}".`);
  649. cb({ status: 'success', message: 'Location updated successfully' });
  650. }
  651. });
  652. }),
  653. /**
  654. * Updates a user's bio
  655. *
  656. * @param {Object} session - the session object automatically added by socket.io
  657. * @param {String} updatingUserId - the updating user's id
  658. * @param {String} newBio - the new bio
  659. * @param {Function} cb - gets called with the result
  660. */
  661. updateBio: hooks.loginRequired((session, updatingUserId, newBio, cb) => {
  662. async.waterfall([
  663. (next) => {
  664. if (updatingUserId === session.userId) return next(null, true);
  665. db.models.user.findOne({_id: session.userId}, next);
  666. },
  667. (user, next) => {
  668. if (user !== true && (!user || user.role !== 'admin')) return next('Invalid permissions.');
  669. db.models.user.findOne({ _id: updatingUserId }, next);
  670. },
  671. (user, next) => {
  672. if (!user) return next('User not found.');
  673. db.models.user.updateOne({ _id: updatingUserId }, {$set: {bio: newBio}}, {runValidators: true}, next);
  674. }
  675. ], async (err) => {
  676. if (err && err !== true) {
  677. err = await utils.getError(err);
  678. logger.error("UPDATE_BIO", `Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`);
  679. cb({status: 'failure', message: err});
  680. } else {
  681. logger.success("UPDATE_BIO", `Updated bio for user "${updatingUserId}" to bio "${newBio}".`);
  682. cb({ status: 'success', message: 'Bio updated successfully' });
  683. }
  684. });
  685. }),
  686. /**
  687. * Updates a user's role
  688. *
  689. * @param {Object} session - the session object automatically added by socket.io
  690. * @param {String} updatingUserId - the updating user's id
  691. * @param {String} newRole - the new role
  692. * @param {Function} cb - gets called with the result
  693. */
  694. updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb) => {
  695. newRole = newRole.toLowerCase();
  696. async.waterfall([
  697. (next) => {
  698. db.models.user.findOne({ _id: updatingUserId }, next);
  699. },
  700. (user, next) => {
  701. if (!user) return next('User not found.');
  702. else if (user.role === newRole) return next('New role can\'t be the same as the old role.');
  703. else return next();
  704. },
  705. (next) => {
  706. db.models.user.updateOne({_id: updatingUserId}, {$set: {role: newRole}}, {runValidators: true}, next);
  707. }
  708. ], async (err) => {
  709. if (err && err !== true) {
  710. err = await utils.getError(err);
  711. logger.error("UPDATE_ROLE", `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`);
  712. cb({status: 'failure', message: err});
  713. } else {
  714. logger.success("UPDATE_ROLE", `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`);
  715. cb({
  716. status: 'success',
  717. message: 'Role successfully updated.'
  718. });
  719. }
  720. });
  721. }),
  722. /**
  723. * Updates a user's password
  724. *
  725. * @param {Object} session - the session object automatically added by socket.io
  726. * @param {String} newPassword - the new password
  727. * @param {Function} cb - gets called with the result
  728. */
  729. updatePassword: hooks.loginRequired((session, newPassword, cb) => {
  730. async.waterfall([
  731. (next) => {
  732. db.models.user.findOne({_id: session.userId}, next);
  733. },
  734. (user, next) => {
  735. if (!user.services.password) return next('This account does not have a password set.');
  736. next();
  737. },
  738. (next) => {
  739. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  740. return next();
  741. },
  742. (next) => {
  743. bcrypt.genSalt(10, next);
  744. },
  745. // hash the password
  746. (salt, next) => {
  747. bcrypt.hash(sha256(newPassword), salt, next);
  748. },
  749. (hashedPassword, next) => {
  750. db.models.user.updateOne({_id: session.userId}, {$set: {"services.password.password": hashedPassword}}, next);
  751. }
  752. ], async (err) => {
  753. if (err) {
  754. err = await utils.getError(err);
  755. logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${session.userId}'. '${err}'.`);
  756. return cb({ status: 'failure', message: err });
  757. }
  758. logger.success("UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
  759. cb({
  760. status: 'success',
  761. message: 'Password successfully updated.'
  762. });
  763. });
  764. }),
  765. /**
  766. * Requests a password for a session
  767. *
  768. * @param {Object} session - the session object automatically added by socket.io
  769. * @param {String} email - the email of the user that requests a password reset
  770. * @param {Function} cb - gets called with the result
  771. */
  772. requestPassword: hooks.loginRequired(async (session, cb) => {
  773. let code = await utils.generateRandomString(8);
  774. async.waterfall([
  775. (next) => {
  776. db.models.user.findOne({_id: session.userId}, next);
  777. },
  778. (user, next) => {
  779. if (!user) return next('User not found.');
  780. if (user.services.password && user.services.password.password) return next('You already have a password set.');
  781. next(null, user);
  782. },
  783. (user, next) => {
  784. let expires = new Date();
  785. expires.setDate(expires.getDate() + 1);
  786. db.models.user.findOneAndUpdate({"email.address": user.email.address}, {$set: {"services.password": {set: {code: code, expires}}}}, {runValidators: true}, next);
  787. },
  788. (user, next) => {
  789. mail.schemas.passwordRequest(user.email.address, user.username, code, next);
  790. }
  791. ], async (err) => {
  792. if (err && err !== true) {
  793. err = await utils.getError(err);
  794. logger.error("REQUEST_PASSWORD", `UserId '${session.userId}' failed to request password. '${err}'`);
  795. cb({status: 'failure', message: err});
  796. } else {
  797. logger.success("REQUEST_PASSWORD", `UserId '${session.userId}' successfully requested a password.`);
  798. cb({
  799. status: 'success',
  800. message: 'Successfully requested password.'
  801. });
  802. }
  803. });
  804. }),
  805. /**
  806. * Verifies a password code
  807. *
  808. * @param {Object} session - the session object automatically added by socket.io
  809. * @param {String} code - the password code
  810. * @param {Function} cb - gets called with the result
  811. */
  812. verifyPasswordCode: hooks.loginRequired((session, code, cb) => {
  813. async.waterfall([
  814. (next) => {
  815. if (!code || typeof code !== 'string') return next('Invalid code1.');
  816. db.models.user.findOne({"services.password.set.code": code, _id: session.userId}, next);
  817. },
  818. (user, next) => {
  819. if (!user) return next('Invalid code2.');
  820. if (user.services.password.set.expires < new Date()) return next('That code has expired.');
  821. next(null);
  822. }
  823. ], async(err) => {
  824. if (err && err !== true) {
  825. err = await utils.getError(err);
  826. logger.error("VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
  827. cb({status: 'failure', message: err});
  828. } else {
  829. logger.success("VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
  830. cb({
  831. status: 'success',
  832. message: 'Successfully verified password code.'
  833. });
  834. }
  835. });
  836. }),
  837. /**
  838. * Adds a password to a user with a code
  839. *
  840. * @param {Object} session - the session object automatically added by socket.io
  841. * @param {String} code - the password code
  842. * @param {String} newPassword - the new password code
  843. * @param {Function} cb - gets called with the result
  844. */
  845. changePasswordWithCode: hooks.loginRequired((session, code, newPassword, cb) => {
  846. async.waterfall([
  847. (next) => {
  848. if (!code || typeof code !== 'string') return next('Invalid code1.');
  849. db.models.user.findOne({"services.password.set.code": code}, next);
  850. },
  851. (user, next) => {
  852. if (!user) return next('Invalid code2.');
  853. if (!user.services.password.set.expires > new Date()) return next('That code has expired.');
  854. next();
  855. },
  856. (next) => {
  857. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  858. return next();
  859. },
  860. (next) => {
  861. bcrypt.genSalt(10, next);
  862. },
  863. // hash the password
  864. (salt, next) => {
  865. bcrypt.hash(sha256(newPassword), salt, next);
  866. },
  867. (hashedPassword, next) => {
  868. db.models.user.updateOne({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, {runValidators: true}, next);
  869. }
  870. ], async (err) => {
  871. if (err && err !== true) {
  872. err = await utils.getError(err);
  873. logger.error("ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
  874. cb({status: 'failure', message: err});
  875. } else {
  876. logger.success("ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
  877. cache.pub('user.linkPassword', session.userId);
  878. cb({
  879. status: 'success',
  880. message: 'Successfully added password.'
  881. });
  882. }
  883. });
  884. }),
  885. /**
  886. * Unlinks password from user
  887. *
  888. * @param {Object} session - the session object automatically added by socket.io
  889. * @param {Function} cb - gets called with the result
  890. */
  891. unlinkPassword: hooks.loginRequired((session, cb) => {
  892. async.waterfall([
  893. (next) => {
  894. db.models.user.findOne({_id: session.userId}, next);
  895. },
  896. (user, next) => {
  897. if (!user) return next('Not logged in.');
  898. if (!user.services.github || !user.services.github.id) return next('You can\'t remove password login without having GitHub login.');
  899. db.models.user.updateOne({_id: session.userId}, {$unset: {"services.password": ''}}, next);
  900. }
  901. ], async (err) => {
  902. if (err && err !== true) {
  903. err = await utils.getError(err);
  904. logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${session.userId}'. '${err}'`);
  905. cb({status: 'failure', message: err});
  906. } else {
  907. logger.success("UNLINK_PASSWORD", `Unlinking password successful for userId '${session.userId}'.`);
  908. cache.pub('user.unlinkPassword', session.userId);
  909. cb({
  910. status: 'success',
  911. message: 'Successfully unlinked password.'
  912. });
  913. }
  914. });
  915. }),
  916. /**
  917. * Unlinks GitHub from user
  918. *
  919. * @param {Object} session - the session object automatically added by socket.io
  920. * @param {Function} cb - gets called with the result
  921. */
  922. unlinkGitHub: hooks.loginRequired((session, cb) => {
  923. async.waterfall([
  924. (next) => {
  925. db.models.user.findOne({_id: session.userId}, next);
  926. },
  927. (user, next) => {
  928. if (!user) return next('Not logged in.');
  929. if (!user.services.password || !user.services.password.password) return next('You can\'t remove GitHub login without having password login.');
  930. db.models.user.updateOne({_id: session.userId}, {$unset: {"services.github": ''}}, next);
  931. }
  932. ], async (err) => {
  933. if (err && err !== true) {
  934. err = await utils.getError(err);
  935. logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`);
  936. cb({status: 'failure', message: err});
  937. } else {
  938. logger.success("UNLINK_GITHUB", `Unlinking GitHub successful for userId '${session.userId}'.`);
  939. cache.pub('user.unlinkGitHub', session.userId);
  940. cb({
  941. status: 'success',
  942. message: 'Successfully unlinked GitHub.'
  943. });
  944. }
  945. });
  946. }),
  947. /**
  948. * Requests a password reset for an email
  949. *
  950. * @param {Object} session - the session object automatically added by socket.io
  951. * @param {String} email - the email of the user that requests a password reset
  952. * @param {Function} cb - gets called with the result
  953. */
  954. requestPasswordReset: async (session, email, cb) => {
  955. let code = await utils.generateRandomString(8);
  956. async.waterfall([
  957. (next) => {
  958. if (!email || typeof email !== 'string') return next('Invalid email.');
  959. email = email.toLowerCase();
  960. db.models.user.findOne({"email.address": email}, next);
  961. },
  962. (user, next) => {
  963. if (!user) return next('User not found.');
  964. if (!user.services.password || !user.services.password.password) return next('User does not have a password set, and probably uses GitHub to log in.');
  965. next(null, user);
  966. },
  967. (user, next) => {
  968. let expires = new Date();
  969. expires.setDate(expires.getDate() + 1);
  970. db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, {runValidators: true}, next);
  971. },
  972. (user, next) => {
  973. mail.schemas.resetPasswordRequest(user.email.address, user.username, code, next);
  974. }
  975. ], async (err) => {
  976. if (err && err !== true) {
  977. err = await utils.getError(err);
  978. logger.error("REQUEST_PASSWORD_RESET", `Email '${email}' failed to request password reset. '${err}'`);
  979. cb({status: 'failure', message: err});
  980. } else {
  981. logger.success("REQUEST_PASSWORD_RESET", `Email '${email}' successfully requested a password reset.`);
  982. cb({
  983. status: 'success',
  984. message: 'Successfully requested password reset.'
  985. });
  986. }
  987. });
  988. },
  989. /**
  990. * Verifies a reset code
  991. *
  992. * @param {Object} session - the session object automatically added by socket.io
  993. * @param {String} code - the password reset code
  994. * @param {Function} cb - gets called with the result
  995. */
  996. verifyPasswordResetCode: (session, code, cb) => {
  997. async.waterfall([
  998. (next) => {
  999. if (!code || typeof code !== 'string') return next('Invalid code.');
  1000. db.models.user.findOne({"services.password.reset.code": code}, next);
  1001. },
  1002. (user, next) => {
  1003. if (!user) return next('Invalid code.');
  1004. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  1005. next(null);
  1006. }
  1007. ], async (err) => {
  1008. if (err && err !== true) {
  1009. err = await utils.getError(err);
  1010. logger.error("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
  1011. cb({status: 'failure', message: err});
  1012. } else {
  1013. logger.success("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  1014. cb({
  1015. status: 'success',
  1016. message: 'Successfully verified password reset code.'
  1017. });
  1018. }
  1019. });
  1020. },
  1021. /**
  1022. * Changes a user's password with a reset code
  1023. *
  1024. * @param {Object} session - the session object automatically added by socket.io
  1025. * @param {String} code - the password reset code
  1026. * @param {String} newPassword - the new password reset code
  1027. * @param {Function} cb - gets called with the result
  1028. */
  1029. changePasswordWithResetCode: (session, code, newPassword, cb) => {
  1030. async.waterfall([
  1031. (next) => {
  1032. if (!code || typeof code !== 'string') return next('Invalid code.');
  1033. db.models.user.findOne({"services.password.reset.code": code}, next);
  1034. },
  1035. (user, next) => {
  1036. if (!user) return next('Invalid code.');
  1037. if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
  1038. next();
  1039. },
  1040. (next) => {
  1041. if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
  1042. return next();
  1043. },
  1044. (next) => {
  1045. bcrypt.genSalt(10, next);
  1046. },
  1047. // hash the password
  1048. (salt, next) => {
  1049. bcrypt.hash(sha256(newPassword), salt, next);
  1050. },
  1051. (hashedPassword, next) => {
  1052. db.models.user.updateOne({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, {runValidators: true}, next);
  1053. }
  1054. ], async (err) => {
  1055. if (err && err !== true) {
  1056. err = await utils.getError(err);
  1057. logger.error("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' failed to change password. '${err}'`);
  1058. cb({status: 'failure', message: err});
  1059. } else {
  1060. logger.success("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' successfully changed password.`);
  1061. cb({
  1062. status: 'success',
  1063. message: 'Successfully changed password.'
  1064. });
  1065. }
  1066. });
  1067. },
  1068. /**
  1069. * Bans a user by userId
  1070. *
  1071. * @param {Object} session - the session object automatically added by socket.io
  1072. * @param {String} value - the user id that is going to be banned
  1073. * @param {String} reason - the reason for the ban
  1074. * @param {String} expiresAt - the time the ban expires
  1075. * @param {Function} cb - gets called with the result
  1076. */
  1077. banUserById: hooks.adminRequired((session, userId, reason, expiresAt, cb) => {
  1078. async.waterfall([
  1079. (next) => {
  1080. if (!userId) return next('You must provide a userId to ban.');
  1081. else if (!reason) return next('You must provide a reason for the ban.');
  1082. else return next();
  1083. },
  1084. (next) => {
  1085. if (!expiresAt || typeof expiresAt !== 'string') return next('Invalid expire date.');
  1086. let date = new Date();
  1087. switch(expiresAt) {
  1088. case '1h':
  1089. expiresAt = date.setHours(date.getHours() + 1);
  1090. break;
  1091. case '12h':
  1092. expiresAt = date.setHours(date.getHours() + 12);
  1093. break;
  1094. case '1d':
  1095. expiresAt = date.setDate(date.getDate() + 1);
  1096. break;
  1097. case '1w':
  1098. expiresAt = date.setDate(date.getDate() + 7);
  1099. break;
  1100. case '1m':
  1101. expiresAt = date.setMonth(date.getMonth() + 1);
  1102. break;
  1103. case '3m':
  1104. expiresAt = date.setMonth(date.getMonth() + 3);
  1105. break;
  1106. case '6m':
  1107. expiresAt = date.setMonth(date.getMonth() + 6);
  1108. break;
  1109. case '1y':
  1110. expiresAt = date.setFullYear(date.getFullYear() + 1);
  1111. break;
  1112. case 'never':
  1113. expiresAt = new Date(3093527980800000);
  1114. break;
  1115. default:
  1116. return next('Invalid expire date.');
  1117. }
  1118. next();
  1119. },
  1120. (next) => {
  1121. punishments.addPunishment('banUserId', userId, reason, expiresAt, userId, next)
  1122. },
  1123. (punishment, next) => {
  1124. cache.pub('user.ban', { userId, punishment });
  1125. next();
  1126. },
  1127. ], async (err) => {
  1128. if (err && err !== true) {
  1129. err = await utils.getError(err);
  1130. logger.error("BAN_USER_BY_ID", `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`);
  1131. cb({status: 'failure', message: err});
  1132. } else {
  1133. logger.success("BAN_USER_BY_ID", `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`);
  1134. cb({
  1135. status: 'success',
  1136. message: 'Successfully banned user.'
  1137. });
  1138. }
  1139. });
  1140. }),
  1141. getFavoriteStations: hooks.loginRequired((session, cb) => {
  1142. async.waterfall([
  1143. (next) => {
  1144. db.models.user.findOne({ _id: session.userId }, next);
  1145. },
  1146. (user, next) => {
  1147. if (!user) return next("User not found.");
  1148. next(null, user);
  1149. }
  1150. ], async (err, user) => {
  1151. if (err && err !== true) {
  1152. err = await utils.getError(err);
  1153. logger.error("GET_FAVORITE_STATIONS", `User ${session.userId} failed to get favorite stations. '${err}'`);
  1154. cb({status: 'failure', message: err});
  1155. } else {
  1156. logger.success("GET_FAVORITE_STATIONS", `User ${session.userId} got favorite stations.`);
  1157. cb({
  1158. status: 'success',
  1159. favoriteStations: user.favoriteStations
  1160. });
  1161. }
  1162. });
  1163. })
  1164. };