users.js 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584
  1. import config from "config";
  2. import async from "async";
  3. import axios from "axios";
  4. import bcrypt from "bcrypt";
  5. import sha256 from "sha256";
  6. import { isAdminRequired, isLoginRequired } from "./hooks";
  7. import moduleManager from "../../index";
  8. const DBModule = moduleManager.modules.db;
  9. const UtilsModule = moduleManager.modules.utils;
  10. const WSModule = moduleManager.modules.ws;
  11. const CacheModule = moduleManager.modules.cache;
  12. const MailModule = moduleManager.modules.mail;
  13. const PunishmentsModule = moduleManager.modules.punishments;
  14. const SongsModule = moduleManager.modules.songs;
  15. const ActivitiesModule = moduleManager.modules.activities;
  16. const PlaylistsModule = moduleManager.modules.playlists;
  17. CacheModule.runJob("SUB", {
  18. channel: "user.updatePreferences",
  19. cb: res => {
  20. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  21. sockets.forEach(socket => {
  22. socket.dispatch("keep.event:user.preferences.updated", { data: { preferences: res.preferences } });
  23. });
  24. });
  25. }
  26. });
  27. CacheModule.runJob("SUB", {
  28. channel: "user.updateOrderOfFavoriteStations",
  29. cb: res => {
  30. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  31. sockets.forEach(socket => {
  32. socket.dispatch("event:user.orderOfFavoriteStations.updated", {
  33. data: { order: res.favoriteStations }
  34. });
  35. });
  36. });
  37. }
  38. });
  39. CacheModule.runJob("SUB", {
  40. channel: "user.updateOrderOfPlaylists",
  41. cb: res => {
  42. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  43. sockets.forEach(socket => {
  44. socket.dispatch("event:user.orderOfPlaylists.updated", { data: { order: res.orderOfPlaylists } });
  45. });
  46. });
  47. WSModule.runJob("EMIT_TO_ROOM", {
  48. room: `profile.${res.userId}.playlists`,
  49. args: ["event:user.orderOfPlaylists.updated", { data: { order: res.orderOfPlaylists } }]
  50. });
  51. }
  52. });
  53. CacheModule.runJob("SUB", {
  54. channel: "user.updateUsername",
  55. cb: user => {
  56. WSModule.runJob("SOCKETS_FROM_USER", { userId: user._id }).then(sockets => {
  57. sockets.forEach(socket => {
  58. socket.dispatch("event:user.username.updated", { data: { username: user.username } });
  59. });
  60. });
  61. }
  62. });
  63. CacheModule.runJob("SUB", {
  64. channel: "user.removeSessions",
  65. cb: userId => {
  66. WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets =>
  67. sockets.forEach(socket => socket.dispatch("keep.event:user.session.deleted"))
  68. );
  69. }
  70. });
  71. CacheModule.runJob("SUB", {
  72. channel: "user.linkPassword",
  73. cb: userId => {
  74. WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
  75. sockets.forEach(socket => {
  76. socket.dispatch("event:user.password.linked");
  77. });
  78. });
  79. }
  80. });
  81. CacheModule.runJob("SUB", {
  82. channel: "user.unlinkPassword",
  83. cb: userId => {
  84. WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
  85. sockets.forEach(socket => {
  86. socket.dispatch("event:user.password.unlinked");
  87. });
  88. });
  89. }
  90. });
  91. CacheModule.runJob("SUB", {
  92. channel: "user.linkGithub",
  93. cb: userId => {
  94. WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
  95. sockets.forEach(socket => {
  96. socket.dispatch("event:user.github.linked");
  97. });
  98. });
  99. }
  100. });
  101. CacheModule.runJob("SUB", {
  102. channel: "user.unlinkGithub",
  103. cb: userId => {
  104. WSModule.runJob("SOCKETS_FROM_USER", { userId }).then(sockets => {
  105. sockets.forEach(socket => {
  106. socket.dispatch("event:user.github.unlinked");
  107. });
  108. });
  109. }
  110. });
  111. CacheModule.runJob("SUB", {
  112. channel: "user.ban",
  113. cb: data => {
  114. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  115. sockets.forEach(socket => {
  116. socket.dispatch("keep.event:user.banned", { data: { ban: data.punishment } });
  117. socket.disconnect(true);
  118. });
  119. });
  120. }
  121. });
  122. CacheModule.runJob("SUB", {
  123. channel: "user.favoritedStation",
  124. cb: data => {
  125. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  126. sockets.forEach(socket => {
  127. socket.dispatch("event:user.station.favorited", { data: { stationId: data.stationId } });
  128. });
  129. });
  130. }
  131. });
  132. CacheModule.runJob("SUB", {
  133. channel: "user.unfavoritedStation",
  134. cb: data => {
  135. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  136. sockets.forEach(socket => {
  137. socket.dispatch("event:user.station.unfavorited", { data: { stationId: data.stationId } });
  138. });
  139. });
  140. }
  141. });
  142. export default {
  143. /**
  144. * Lists all Users
  145. *
  146. * @param {object} session - the session object automatically added by the websocket
  147. * @param {Function} cb - gets called with the result
  148. */
  149. index: isAdminRequired(async function index(session, cb) {
  150. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  151. async.waterfall(
  152. [
  153. next => {
  154. userModel.find({}).exec(next);
  155. }
  156. ],
  157. async (err, users) => {
  158. if (err) {
  159. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  160. this.log("ERROR", "USER_INDEX", `Indexing users failed. "${err}"`);
  161. return cb({ status: "error", message: err });
  162. }
  163. this.log("SUCCESS", "USER_INDEX", `Indexing users successful.`);
  164. const filteredUsers = [];
  165. users.forEach(user => {
  166. filteredUsers.push({
  167. _id: user._id,
  168. name: user.name,
  169. username: user.username,
  170. role: user.role,
  171. liked: user.liked,
  172. disliked: user.disliked,
  173. songsRequested: user.statistics.songsRequested,
  174. email: {
  175. address: user.email.address,
  176. verified: user.email.verified
  177. },
  178. avatar: {
  179. type: user.avatar.type,
  180. url: user.avatar.url,
  181. color: user.avatar.color
  182. },
  183. hasPassword: !!user.services.password,
  184. services: { github: user.services.github }
  185. });
  186. });
  187. return cb({ status: "success", data: { users: filteredUsers } });
  188. }
  189. );
  190. }),
  191. /**
  192. * Removes all data held on a user, including their ability to login
  193. *
  194. * @param {object} session - the session object automatically added by the websocket
  195. * @param {Function} cb - gets called with the result
  196. */
  197. remove: isLoginRequired(async function remove(session, cb) {
  198. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  199. const dataRequestModel = await DBModule.runJob("GET_MODEL", { modelName: "dataRequest" }, this);
  200. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  201. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  202. const activityModel = await DBModule.runJob("GET_MODEL", { modelName: "activity" }, this);
  203. const dataRequestEmail = await MailModule.runJob("GET_SCHEMA", { schemaName: "dataRequest" }, this);
  204. const songsToAdjustRatings = [];
  205. async.waterfall(
  206. [
  207. // activities related to the user
  208. next => {
  209. activityModel.deleteMany({ userId: session.userId }, next);
  210. },
  211. // user's stations
  212. (res, next) => {
  213. stationModel.find({ owner: session.userId }, (err, stations) => {
  214. if (err) return next(err);
  215. return async.each(
  216. stations,
  217. (station, callback) => {
  218. // delete the station
  219. stationModel.deleteOne({ _id: station._id }, err => {
  220. if (err) return callback(err);
  221. // if applicable, delete the corresponding playlist for the station
  222. if (station.playlist)
  223. return PlaylistsModule.runJob("DELETE_PLAYLIST", {
  224. playlistId: station.playlist
  225. })
  226. .then(() => callback())
  227. .catch(callback);
  228. return callback();
  229. });
  230. },
  231. err => next(err)
  232. );
  233. });
  234. },
  235. next => {
  236. playlistModel.findOne({ createdBy: session.userId, displayName: "Liked Songs" }, next);
  237. },
  238. // get all liked songs (as the global rating values for these songs will need adjusted)
  239. (playlist, next) => {
  240. if (!playlist) return next();
  241. playlist.songs.forEach(song =>
  242. songsToAdjustRatings.push({ songId: song._id, youtubeId: song.youtubeId })
  243. );
  244. return next();
  245. },
  246. next => {
  247. playlistModel.findOne({ createdBy: session.userId, displayName: "Disliked Songs" }, next);
  248. },
  249. // get all disliked songs (as the global rating values for these songs will need adjusted)
  250. (playlist, next) => {
  251. if (!playlist) return next();
  252. playlist.songs.forEach(song =>
  253. songsToAdjustRatings.push({ songId: song._id, youtubeId: song.youtubeId })
  254. );
  255. return next();
  256. },
  257. // user's playlists
  258. next => {
  259. playlistModel.deleteMany({ createdBy: session.userId }, next);
  260. },
  261. (res, next) => {
  262. async.each(
  263. songsToAdjustRatings,
  264. (song, next) => {
  265. const { songId, youtubeId } = song;
  266. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, youtubeId })
  267. .then(() => next())
  268. .catch(next);
  269. },
  270. err => next(err)
  271. );
  272. },
  273. // user object
  274. next => {
  275. userModel.deleteMany({ _id: session.userId }, next);
  276. },
  277. // request data removal for user
  278. (res, next) => {
  279. dataRequestModel.create({ userId: session.userId, type: "remove" }, next);
  280. },
  281. (request, next) => {
  282. WSModule.runJob("EMIT_TO_ROOM", {
  283. room: "admin.users",
  284. args: ["event:admin.dataRequests.created", { data: { request } }]
  285. });
  286. return next();
  287. },
  288. next => userModel.find({ role: "admin" }, next),
  289. // send email to all admins of a data removal request
  290. (users, next) => {
  291. if (!config.get("sendDataRequestEmails")) return next();
  292. if (users.length === 0) return next();
  293. const to = [];
  294. users.forEach(user => to.push(user.email.address));
  295. return dataRequestEmail(to, session.userId, "remove", err => next(err));
  296. }
  297. ],
  298. async err => {
  299. if (err && err !== true) {
  300. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  301. this.log(
  302. "ERROR",
  303. "USER_REMOVE",
  304. `Removing data and account for user "${session.userId}" failed. "${err}"`
  305. );
  306. return cb({ status: "error", message: err });
  307. }
  308. this.log(
  309. "SUCCESS",
  310. "USER_REMOVE",
  311. `Successfully removed data and account for user "${session.userId}"`
  312. );
  313. return cb({
  314. status: "success",
  315. message: "Successfully removed data and account."
  316. });
  317. }
  318. );
  319. }),
  320. /**
  321. * Logs user in
  322. *
  323. * @param {object} session - the session object automatically added by the websocket
  324. * @param {string} identifier - the email of the user
  325. * @param {string} password - the plaintext of the user
  326. * @param {Function} cb - gets called with the result
  327. */
  328. async login(session, identifier, password, cb) {
  329. identifier = identifier.toLowerCase();
  330. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  331. const sessionSchema = await CacheModule.runJob("GET_SCHEMA", { schemaName: "session" }, this);
  332. async.waterfall(
  333. [
  334. // check if a user with the requested identifier exists
  335. next => {
  336. userModel.findOne(
  337. {
  338. $or: [{ "email.address": identifier }]
  339. },
  340. next
  341. );
  342. },
  343. // if the user doesn't exist, respond with a failure
  344. // otherwise compare the requested password and the actual users password
  345. (user, next) => {
  346. if (!user) return next("User not found");
  347. if (!user.services.password || !user.services.password.password)
  348. return next("The account you are trying to access uses GitHub to log in.");
  349. return bcrypt.compare(sha256(password), user.services.password.password, (err, match) => {
  350. if (err) return next(err);
  351. if (!match) return next("Incorrect password");
  352. return next(null, user);
  353. });
  354. },
  355. (user, next) => {
  356. UtilsModule.runJob("GUID", {}, this).then(sessionId => {
  357. next(null, user, sessionId);
  358. });
  359. },
  360. (user, sessionId, next) => {
  361. CacheModule.runJob(
  362. "HSET",
  363. {
  364. table: "sessions",
  365. key: sessionId,
  366. value: sessionSchema(sessionId, user._id)
  367. },
  368. this
  369. )
  370. .then(() => next(null, sessionId))
  371. .catch(next);
  372. }
  373. ],
  374. async (err, sessionId) => {
  375. if (err && err !== true) {
  376. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  377. this.log(
  378. "ERROR",
  379. "USER_PASSWORD_LOGIN",
  380. `Login failed with password for user "${identifier}". "${err}"`
  381. );
  382. return cb({ status: "error", message: err });
  383. }
  384. this.log("SUCCESS", "USER_PASSWORD_LOGIN", `Login successful with password for user "${identifier}"`);
  385. return cb({
  386. status: "success",
  387. message: "Login successful",
  388. data: { SID: sessionId }
  389. });
  390. }
  391. );
  392. },
  393. /**
  394. * Registers a new user
  395. *
  396. * @param {object} session - the session object automatically added by the websocket
  397. * @param {string} username - the username for the new user
  398. * @param {string} email - the email for the new user
  399. * @param {string} password - the plaintext password for the new user
  400. * @param {object} recaptcha - the recaptcha data
  401. * @param {Function} cb - gets called with the result
  402. */
  403. async register(session, username, email, password, recaptcha, cb) {
  404. email = email.toLowerCase();
  405. const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 }, this);
  406. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  407. const verifyEmailSchema = await MailModule.runJob("GET_SCHEMA", { schemaName: "verifyEmail" }, this);
  408. async.waterfall(
  409. [
  410. next => {
  411. if (config.get("registrationDisabled") === true)
  412. return next("Registration is not allowed at this time.");
  413. return next();
  414. },
  415. next => {
  416. if (!DBModule.passwordValid(password))
  417. return next("Invalid password. Check if it meets all the requirements.");
  418. return next();
  419. },
  420. // verify the request with google recaptcha
  421. next => {
  422. if (config.get("apis.recaptcha.enabled") === true)
  423. axios
  424. .post("https://www.google.com/recaptcha/api/siteverify", {
  425. data: {
  426. secret: config.get("apis").recaptcha.secret,
  427. response: recaptcha
  428. }
  429. })
  430. .then(res => next(null, res.data))
  431. .catch(err => next(err));
  432. else next(null, null);
  433. },
  434. // check if the response from Google recaptcha is successful
  435. // if it is, we check if a user with the requested username already exists
  436. (body, next) => {
  437. if (config.get("apis.recaptcha.enabled") === true)
  438. if (body.success !== true) return next("Response from recaptcha was not successful.");
  439. return userModel.findOne({ username: new RegExp(`^${username}$`, "i") }, next);
  440. },
  441. // if the user already exists, respond with that
  442. // otherwise check if a user with the requested email already exists
  443. (user, next) => {
  444. if (user) return next("A user with that username already exists.");
  445. return userModel.findOne({ "email.address": email }, next);
  446. },
  447. // if the user already exists, respond with that
  448. // otherwise, generate a salt to use with hashing the new users password
  449. (user, next) => {
  450. if (user) return next("A user with that email already exists.");
  451. return bcrypt.genSalt(10, next);
  452. },
  453. // hash the password
  454. (salt, next) => {
  455. bcrypt.hash(sha256(password), salt, next);
  456. },
  457. (hash, next) => {
  458. UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 12 }, this).then(_id => {
  459. next(null, hash, _id);
  460. });
  461. },
  462. // create the user object
  463. (hash, _id, next) => {
  464. next(null, {
  465. _id,
  466. name: username,
  467. username,
  468. email: {
  469. address: email,
  470. verificationToken
  471. },
  472. services: {
  473. password: {
  474. password: hash
  475. }
  476. }
  477. });
  478. },
  479. // generate the url for gravatar avatar
  480. (user, next) => {
  481. UtilsModule.runJob("CREATE_GRAVATAR", { email: user.email.address }, this).then(url => {
  482. const avatarColors = ["blue", "orange", "green", "purple", "teal"];
  483. user.avatar = {
  484. type: "initials",
  485. color: avatarColors[Math.floor(Math.random() * avatarColors.length)],
  486. url
  487. };
  488. next(null, user);
  489. });
  490. },
  491. // save the new user to the database
  492. (user, next) => {
  493. userModel.create(user, next);
  494. },
  495. // respond with the new user
  496. (user, next) => {
  497. verifyEmailSchema(email, username, verificationToken, err => {
  498. next(err, user._id);
  499. });
  500. },
  501. // create a liked songs playlist for the new user
  502. (userId, next) => {
  503. PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
  504. userId,
  505. displayName: "Liked Songs",
  506. type: "user"
  507. })
  508. .then(likedSongsPlaylist => {
  509. next(null, likedSongsPlaylist, userId);
  510. })
  511. .catch(err => next(err));
  512. },
  513. // create a disliked songs playlist for the new user
  514. (likedSongsPlaylist, userId, next) => {
  515. PlaylistsModule.runJob("CREATE_READ_ONLY_PLAYLIST", {
  516. userId,
  517. displayName: "Disliked Songs",
  518. type: "user"
  519. })
  520. .then(dislikedSongsPlaylist => {
  521. next(null, { likedSongsPlaylist, dislikedSongsPlaylist }, userId);
  522. })
  523. .catch(err => next(err));
  524. },
  525. // associate liked + disliked songs playlist to the user object
  526. ({ likedSongsPlaylist, dislikedSongsPlaylist }, userId, next) => {
  527. userModel.updateOne(
  528. { _id: userId },
  529. { $set: { likedSongsPlaylist, dislikedSongsPlaylist } },
  530. { runValidators: true },
  531. err => {
  532. if (err) return next(err);
  533. return next(null, userId);
  534. }
  535. );
  536. }
  537. ],
  538. async (err, userId) => {
  539. if (err && err !== true) {
  540. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  541. this.log(
  542. "ERROR",
  543. "USER_PASSWORD_REGISTER",
  544. `Register failed with password for user "${username}"."${err}"`
  545. );
  546. return cb({ status: "error", message: err });
  547. }
  548. ActivitiesModule.runJob("ADD_ACTIVITY", {
  549. userId,
  550. type: "user__joined",
  551. payload: { message: "Welcome to Musare!" }
  552. });
  553. this.log(
  554. "SUCCESS",
  555. "USER_PASSWORD_REGISTER",
  556. `Register successful with password for user "${username}".`
  557. );
  558. const res = await this.module.runJob(
  559. "RUN_ACTION2",
  560. {
  561. session,
  562. namespace: "users",
  563. action: "login",
  564. args: [email, password]
  565. },
  566. this
  567. );
  568. const obj = {
  569. status: "success",
  570. message: "Successfully registered."
  571. };
  572. if (res.status === "success") {
  573. obj.SID = res.data.SID;
  574. }
  575. return cb(obj);
  576. }
  577. );
  578. },
  579. /**
  580. * Logs out a user
  581. *
  582. * @param {object} session - the session object automatically added by the websocket
  583. * @param {Function} cb - gets called with the result
  584. */
  585. logout(session, cb) {
  586. async.waterfall(
  587. [
  588. next => {
  589. CacheModule.runJob("HGET", { table: "sessions", key: session.sessionId }, this)
  590. .then(session => next(null, session))
  591. .catch(next);
  592. },
  593. (session, next) => {
  594. if (!session) return next("Session not found");
  595. return next(null, session);
  596. },
  597. (session, next) => {
  598. CacheModule.runJob("PUB", {
  599. channel: "user.removeSessions",
  600. value: session.userId
  601. });
  602. // temp fix, need to wait properly for the SUB/PUB refactor (on wekan)
  603. setTimeout(() => {
  604. CacheModule.runJob("HDEL", { table: "sessions", key: session.sessionId }, this)
  605. .then(() => next())
  606. .catch(next);
  607. }, 50);
  608. }
  609. ],
  610. async err => {
  611. if (err && err !== true) {
  612. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  613. this.log("ERROR", "USER_LOGOUT", `Logout failed. "${err}" `);
  614. return cb({ status: "error", message: err });
  615. }
  616. this.log("SUCCESS", "USER_LOGOUT", `Logout successful.`);
  617. return cb({
  618. status: "success",
  619. message: "Successfully logged out."
  620. });
  621. }
  622. );
  623. },
  624. /**
  625. * Checks if user's password is correct (e.g. before a sensitive action)
  626. *
  627. * @param {object} session - the session object automatically added by the websocket
  628. * @param {string} password - the password the user entered that we need to validate
  629. * @param {Function} cb - gets called with the result
  630. */
  631. confirmPasswordMatch: isLoginRequired(async function confirmPasswordMatch(session, password, cb) {
  632. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  633. return async.waterfall(
  634. [
  635. next => {
  636. if (!password || password === "") return next("Please provide a valid password.");
  637. return next();
  638. },
  639. next => {
  640. userModel.findOne({ _id: session.userId }, (err, user) =>
  641. next(err, user.services.password.password)
  642. );
  643. },
  644. (passwordHash, next) => {
  645. if (!passwordHash) return next("Your account doesn't have a password linked.");
  646. return bcrypt.compare(sha256(password), passwordHash, (err, match) => {
  647. if (err) return next(err);
  648. if (!match) return next(null, false);
  649. return next(null, true);
  650. });
  651. }
  652. ],
  653. async (err, match) => {
  654. if (err) {
  655. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  656. this.log(
  657. "ERROR",
  658. "USER_CONFIRM_PASSWORD",
  659. `Couldn't confirm password for user "${session.userId}". "${err}"`
  660. );
  661. return cb({ status: "error", message: err });
  662. }
  663. if (match) {
  664. this.log(
  665. "SUCCESS",
  666. "USER_CONFIRM_PASSWORD",
  667. `Successfully checked for password match (it matched) for user "${session.userId}".`
  668. );
  669. return cb({
  670. status: "success",
  671. message: "Your password matches."
  672. });
  673. }
  674. this.log(
  675. "SUCCESS",
  676. "USER_CONFIRM_PASSWORD",
  677. `Successfully checked for password match (it didn't match) for user "${session.userId}".`
  678. );
  679. return cb({
  680. status: "error",
  681. message: "Unfortunately your password doesn't match."
  682. });
  683. }
  684. );
  685. }),
  686. /**
  687. * Checks if user's github access token has expired or not (ie. if their github account is still linked)
  688. *
  689. * @param {object} session - the session object automatically added by the websocket
  690. * @param {Function} cb - gets called with the result
  691. */
  692. confirmGithubLink: isLoginRequired(async function confirmGithubLink(session, cb) {
  693. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  694. return async.waterfall(
  695. [
  696. next => {
  697. userModel.findOne({ _id: session.userId }, (err, user) => next(err, user));
  698. },
  699. (user, next) => {
  700. if (!user.services.github) return next("You don't have GitHub linked to your account.");
  701. return axios
  702. .get(`https://api.github.com/user/emails`, {
  703. headers: {
  704. "User-Agent": "request",
  705. Authorization: `token ${user.services.github.access_token}`
  706. }
  707. })
  708. .then(res => next(null, res))
  709. .catch(err => next(err));
  710. },
  711. (res, next) => next(null, res.status === 200)
  712. ],
  713. async (err, linked) => {
  714. if (err) {
  715. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  716. this.log(
  717. "ERROR",
  718. "USER_CONFIRM_GITHUB_LINK",
  719. `Couldn't confirm github link for user "${session.userId}". "${err}"`
  720. );
  721. return cb({ status: "error", message: err });
  722. }
  723. this.log(
  724. "SUCCESS",
  725. "USER_CONFIRM_GITHUB_LINK",
  726. `GitHub is ${linked ? "linked" : "not linked"} for user "${session.userId}".`
  727. );
  728. return cb({
  729. status: "success",
  730. data: { linked },
  731. message: "Successfully checked if GitHub accounty was linked."
  732. });
  733. }
  734. );
  735. }),
  736. /**
  737. * Removes all sessions for a user
  738. *
  739. * @param {object} session - the session object automatically added by the websocket
  740. * @param {string} userId - the id of the user we are trying to delete the sessions of
  741. * @param {Function} cb - gets called with the result
  742. */
  743. removeSessions: isLoginRequired(async function removeSessions(session, userId, cb) {
  744. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  745. async.waterfall(
  746. [
  747. next => {
  748. userModel.findOne({ _id: session.userId }, (err, user) => {
  749. if (err) return next(err);
  750. if (user.role !== "admin" && session.userId !== userId)
  751. return next("Only admins and the owner of the account can remove their sessions.");
  752. return next();
  753. });
  754. },
  755. next => {
  756. CacheModule.runJob("HGETALL", { table: "sessions" }, this)
  757. .then(sessions => {
  758. next(null, sessions);
  759. })
  760. .catch(next);
  761. },
  762. (sessions, next) => {
  763. if (!sessions) return next("There are no sessions for this user to remove.");
  764. const keys = Object.keys(sessions);
  765. return next(null, keys, sessions);
  766. },
  767. (keys, sessions, next) => {
  768. CacheModule.runJob("PUB", {
  769. channel: "user.removeSessions",
  770. value: userId
  771. });
  772. // temp fix, need to wait properly for the SUB/PUB refactor (on wekan)
  773. setTimeout(
  774. () =>
  775. async.each(
  776. keys,
  777. (sessionId, callback) => {
  778. const session = sessions[sessionId];
  779. if (session.userId === userId) {
  780. // TODO Also maybe add this to this runJob
  781. CacheModule.runJob("HDEL", {
  782. table: "sessions",
  783. key: sessionId
  784. })
  785. .then(() => callback(null))
  786. .catch(callback);
  787. }
  788. },
  789. err => {
  790. next(err);
  791. }
  792. ),
  793. 50
  794. );
  795. }
  796. ],
  797. async err => {
  798. if (err) {
  799. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  800. this.log(
  801. "ERROR",
  802. "REMOVE_SESSIONS_FOR_USER",
  803. `Couldn't remove all sessions for user "${userId}". "${err}"`
  804. );
  805. return cb({ status: "error", message: err });
  806. }
  807. this.log("SUCCESS", "REMOVE_SESSIONS_FOR_USER", `Removed all sessions for user "${userId}".`);
  808. return cb({
  809. status: "success",
  810. message: "Successfully removed all sessions."
  811. });
  812. }
  813. );
  814. }),
  815. /**
  816. * Updates the order of a user's favorite stations
  817. *
  818. * @param {object} session - the session object automatically added by the websocket
  819. * @param {Array} favoriteStations - array of station ids (with a specific order)
  820. * @param {Function} cb - gets called with the result
  821. */
  822. updateOrderOfFavoriteStations: isLoginRequired(async function updateOrderOfFavoriteStations(
  823. session,
  824. favoriteStations,
  825. cb
  826. ) {
  827. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  828. async.waterfall(
  829. [
  830. next => {
  831. userModel.updateOne(
  832. { _id: session.userId },
  833. { $set: { favoriteStations } },
  834. { runValidators: true },
  835. next
  836. );
  837. }
  838. ],
  839. async err => {
  840. if (err) {
  841. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  842. this.log(
  843. "ERROR",
  844. "UPDATE_ORDER_OF_USER_FAVORITE_STATIONS",
  845. `Couldn't update order of favorite stations for user "${session.userId}" to "${favoriteStations}". "${err}"`
  846. );
  847. return cb({ status: "error", message: err });
  848. }
  849. CacheModule.runJob("PUB", {
  850. channel: "user.updateOrderOfFavoriteStations",
  851. value: {
  852. favoriteStations,
  853. userId: session.userId
  854. }
  855. });
  856. this.log(
  857. "SUCCESS",
  858. "UPDATE_ORDER_OF_USER_FAVORITE_STATIONS",
  859. `Updated order of favorite stations for user "${session.userId}" to "${favoriteStations}".`
  860. );
  861. return cb({
  862. status: "success",
  863. message: "Order of favorite stations successfully updated"
  864. });
  865. }
  866. );
  867. }),
  868. /**
  869. * Updates the order of a user's playlists
  870. *
  871. * @param {object} session - the session object automatically added by the websocket
  872. * @param {Array} orderOfPlaylists - array of playlist ids (with a specific order)
  873. * @param {Function} cb - gets called with the result
  874. */
  875. updateOrderOfPlaylists: isLoginRequired(async function updateOrderOfPlaylists(session, orderOfPlaylists, cb) {
  876. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  877. async.waterfall(
  878. [
  879. next => {
  880. userModel.updateOne(
  881. { _id: session.userId },
  882. { $set: { "preferences.orderOfPlaylists": orderOfPlaylists } },
  883. { runValidators: true },
  884. next
  885. );
  886. }
  887. ],
  888. async err => {
  889. if (err) {
  890. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  891. this.log(
  892. "ERROR",
  893. "UPDATE_ORDER_OF_USER_PLAYLISTS",
  894. `Couldn't update order of playlists for user "${session.userId}" to "${orderOfPlaylists}". "${err}"`
  895. );
  896. return cb({ status: "error", message: err });
  897. }
  898. CacheModule.runJob("PUB", {
  899. channel: "user.updateOrderOfPlaylists",
  900. value: {
  901. orderOfPlaylists,
  902. userId: session.userId
  903. }
  904. });
  905. this.log(
  906. "SUCCESS",
  907. "UPDATE_ORDER_OF_USER_PLAYLISTS",
  908. `Updated order of playlists for user "${session.userId}" to "${orderOfPlaylists}".`
  909. );
  910. return cb({
  911. status: "success",
  912. message: "Order of playlists successfully updated"
  913. });
  914. }
  915. );
  916. }),
  917. /**
  918. * Updates a user's preferences
  919. *
  920. * @param {object} session - the session object automatically added by the websocket
  921. * @param {object} preferences - object containing preferences
  922. * @param {boolean} preferences.nightmode - whether or not the user is using the night mode theme
  923. * @param {boolean} preferences.autoSkipDisliked - whether to automatically skip disliked songs
  924. * @param {boolean} preferences.activityLogPublic - whether or not a user's activity log can be publicly viewed
  925. * @param {boolean} preferences.anonymousSongRequests - whether or not a user's requested songs will be anonymous
  926. * @param {boolean} preferences.activityWatch - whether or not a user is using the ActivityWatch integration
  927. * @param {Function} cb - gets called with the result
  928. */
  929. updatePreferences: isLoginRequired(async function updatePreferences(session, preferences, cb) {
  930. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  931. async.waterfall(
  932. [
  933. next => {
  934. const $set = {};
  935. Object.keys(preferences).forEach(preference => {
  936. $set[`preferences.${preference}`] = preferences[preference];
  937. });
  938. return next(null, $set);
  939. },
  940. ($set, next) => {
  941. userModel.findByIdAndUpdate(session.userId, { $set }, { new: false, upsert: true }, next);
  942. }
  943. ],
  944. async (err, user) => {
  945. if (err) {
  946. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  947. this.log(
  948. "ERROR",
  949. "UPDATE_USER_PREFERENCES",
  950. `Couldn't update preferences for user "${session.userId}" to "${JSON.stringify(
  951. preferences
  952. )}". "${err}"`
  953. );
  954. return cb({ status: "error", message: err });
  955. }
  956. CacheModule.runJob("PUB", {
  957. channel: "user.updatePreferences",
  958. value: {
  959. preferences,
  960. userId: session.userId
  961. }
  962. });
  963. if (preferences.nightmode !== undefined && preferences.nightmode !== user.preferences.nightmode)
  964. ActivitiesModule.runJob("ADD_ACTIVITY", {
  965. userId: session.userId,
  966. type: "user__toggle_nightmode",
  967. payload: { message: preferences.nightmode ? "Enabled nightmode" : "Disabled nightmode" }
  968. });
  969. if (
  970. preferences.autoSkipDisliked !== undefined &&
  971. preferences.autoSkipDisliked !== user.preferences.autoSkipDisliked
  972. )
  973. ActivitiesModule.runJob("ADD_ACTIVITY", {
  974. userId: session.userId,
  975. type: "user__toggle_autoskip_disliked_songs",
  976. payload: {
  977. message: preferences.autoSkipDisliked
  978. ? "Enabled the autoskipping of disliked songs"
  979. : "Disabled the autoskipping of disliked songs"
  980. }
  981. });
  982. if (
  983. preferences.activityWatch !== undefined &&
  984. preferences.activityWatch !== user.preferences.activityWatch
  985. )
  986. ActivitiesModule.runJob("ADD_ACTIVITY", {
  987. userId: session.userId,
  988. type: "user__toggle_activity_watch",
  989. payload: {
  990. message: preferences.activityWatch
  991. ? "Enabled ActivityWatch integration"
  992. : "Disabled ActivityWatch integration"
  993. }
  994. });
  995. this.log(
  996. "SUCCESS",
  997. "UPDATE_USER_PREFERENCES",
  998. `Updated preferences for user "${session.userId}" to "${JSON.stringify(preferences)}".`
  999. );
  1000. return cb({
  1001. status: "success",
  1002. message: "Preferences successfully updated"
  1003. });
  1004. }
  1005. );
  1006. }),
  1007. /**
  1008. * Retrieves a user's preferences
  1009. *
  1010. * @param {object} session - the session object automatically added by the websocket
  1011. * @param {Function} cb - gets called with the result
  1012. */
  1013. getPreferences: isLoginRequired(async function updatePreferences(session, cb) {
  1014. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1015. async.waterfall(
  1016. [
  1017. next => {
  1018. userModel.findById(session.userId).select({ preferences: -1 }).exec(next);
  1019. }
  1020. ],
  1021. async (err, { preferences }) => {
  1022. if (err) {
  1023. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1024. this.log(
  1025. "ERROR",
  1026. "GET_USER_PREFERENCES",
  1027. `Couldn't retrieve preferences for user "${session.userId}". "${err}"`
  1028. );
  1029. return cb({ status: "error", message: err });
  1030. }
  1031. this.log(
  1032. "SUCCESS",
  1033. "GET_USER_PREFERENCES",
  1034. `Successfully obtained preferences for user "${session.userId}".`
  1035. );
  1036. return cb({
  1037. status: "success",
  1038. message: "Preferences successfully retrieved",
  1039. data: { preferences }
  1040. });
  1041. }
  1042. );
  1043. }),
  1044. /**
  1045. * Gets user object from username (only a few properties)
  1046. *
  1047. * @param {object} session - the session object automatically added by the websocket
  1048. * @param {string} username - the username of the user we are trying to find
  1049. * @param {Function} cb - gets called with the result
  1050. */
  1051. findByUsername: async function findByUsername(session, username, cb) {
  1052. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1053. async.waterfall(
  1054. [
  1055. next => {
  1056. userModel.findOne({ username: new RegExp(`^${username}$`, "i") }, next);
  1057. },
  1058. (account, next) => {
  1059. if (!account) return next("User not found.");
  1060. return next(null, account);
  1061. }
  1062. ],
  1063. async (err, account) => {
  1064. if (err && err !== true) {
  1065. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1066. this.log("ERROR", "FIND_BY_USERNAME", `User not found for username "${username}". "${err}"`);
  1067. return cb({ status: "error", message: err });
  1068. }
  1069. this.log("SUCCESS", "FIND_BY_USERNAME", `User found for username "${username}".`);
  1070. return cb({
  1071. status: "success",
  1072. data: {
  1073. _id: account._id,
  1074. name: account.name,
  1075. username: account.username,
  1076. location: account.location,
  1077. bio: account.bio,
  1078. role: account.role,
  1079. avatar: account.avatar,
  1080. createdAt: account.createdAt
  1081. }
  1082. });
  1083. }
  1084. );
  1085. },
  1086. /**
  1087. * Gets a username from an userId
  1088. *
  1089. * @param {object} session - the session object automatically added by the websocket
  1090. * @param {string} userId - the userId of the person we are trying to get the username from
  1091. * @param {Function} cb - gets called with the result
  1092. */
  1093. async getUsernameFromId(session, userId, cb) {
  1094. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1095. userModel
  1096. .findById(userId)
  1097. .then(user => {
  1098. if (user) {
  1099. this.log("SUCCESS", "GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
  1100. return cb({
  1101. status: "success",
  1102. data: { username: user.username }
  1103. });
  1104. }
  1105. this.log(
  1106. "ERROR",
  1107. "GET_USERNAME_FROM_ID",
  1108. `Getting the username from userId "${userId}" failed. User not found.`
  1109. );
  1110. return cb({
  1111. status: "error",
  1112. message: "Couldn't find the user."
  1113. });
  1114. })
  1115. .catch(async err => {
  1116. if (err && err !== true) {
  1117. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1118. this.log(
  1119. "ERROR",
  1120. "GET_USERNAME_FROM_ID",
  1121. `Getting the username from userId "${userId}" failed. "${err}"`
  1122. );
  1123. cb({ status: "error", message: err });
  1124. }
  1125. });
  1126. },
  1127. /**
  1128. * Gets a user from a userId
  1129. *
  1130. * @param {object} session - the session object automatically added by the websocket
  1131. * @param {string} userId - the userId of the person we are trying to get the username from
  1132. * @param {Function} cb - gets called with the result
  1133. */
  1134. getUserFromId: isAdminRequired(async function getUserFromId(session, userId, cb) {
  1135. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1136. userModel
  1137. .findById(userId)
  1138. .then(user => {
  1139. if (user) {
  1140. this.log("SUCCESS", "GET_USER_FROM_ID", `Found user for userId "${userId}".`);
  1141. return cb({
  1142. status: "success",
  1143. data: {
  1144. _id: user._id,
  1145. username: user.username,
  1146. role: user.role,
  1147. liked: user.liked,
  1148. disliked: user.disliked,
  1149. songsRequested: user.statistics.songsRequested,
  1150. email: {
  1151. address: user.email.address,
  1152. verified: user.email.verified
  1153. },
  1154. hasPassword: !!user.services.password,
  1155. services: { github: user.services.github }
  1156. }
  1157. });
  1158. }
  1159. this.log(
  1160. "ERROR",
  1161. "GET_USER_FROM_ID",
  1162. `Getting the user from userId "${userId}" failed. User not found.`
  1163. );
  1164. return cb({
  1165. status: "error",
  1166. message: "Couldn't find the user."
  1167. });
  1168. })
  1169. .catch(async err => {
  1170. if (err && err !== true) {
  1171. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1172. this.log("ERROR", "GET_USER_FROM_ID", `Getting the user from userId "${userId}" failed. "${err}"`);
  1173. cb({ status: "error", message: err });
  1174. }
  1175. });
  1176. }),
  1177. /**
  1178. * Gets user info from session
  1179. *
  1180. * @param {object} session - the session object automatically added by the websocket
  1181. * @param {Function} cb - gets called with the result
  1182. */
  1183. findBySession: isLoginRequired(async function findBySession(session, cb) {
  1184. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1185. async.waterfall(
  1186. [
  1187. next => {
  1188. CacheModule.runJob(
  1189. "HGET",
  1190. {
  1191. table: "sessions",
  1192. key: session.sessionId
  1193. },
  1194. this
  1195. )
  1196. .then(session => next(null, session))
  1197. .catch(next);
  1198. },
  1199. (session, next) => {
  1200. if (!session) return next("Session not found.");
  1201. return next(null, session);
  1202. },
  1203. (session, next) => {
  1204. userModel.findOne({ _id: session.userId }, next);
  1205. },
  1206. (user, next) => {
  1207. if (!user) return next("User not found.");
  1208. return next(null, user);
  1209. }
  1210. ],
  1211. async (err, user) => {
  1212. if (err && err !== true) {
  1213. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1214. this.log("ERROR", "FIND_BY_SESSION", `User not found. "${err}"`);
  1215. return cb({ status: "error", message: err });
  1216. }
  1217. const sanitisedUser = {
  1218. email: {
  1219. address: user.email.address
  1220. },
  1221. avatar: user.avatar,
  1222. username: user.username,
  1223. name: user.name,
  1224. location: user.location,
  1225. bio: user.bio
  1226. };
  1227. if (user.services.password && user.services.password.password) sanitisedUser.password = true;
  1228. if (user.services.github && user.services.github.id) sanitisedUser.github = true;
  1229. this.log("SUCCESS", "FIND_BY_SESSION", `User found. "${user.username}".`);
  1230. return cb({
  1231. status: "success",
  1232. data: { user: sanitisedUser }
  1233. });
  1234. }
  1235. );
  1236. }),
  1237. /**
  1238. * Updates a user's username
  1239. *
  1240. * @param {object} session - the session object automatically added by the websocket
  1241. * @param {string} updatingUserId - the updating user's id
  1242. * @param {string} newUsername - the new username
  1243. * @param {Function} cb - gets called with the result
  1244. */
  1245. updateUsername: isLoginRequired(async function updateUsername(session, updatingUserId, newUsername, cb) {
  1246. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1247. async.waterfall(
  1248. [
  1249. next => {
  1250. if (updatingUserId === session.userId) return next(null, true);
  1251. return userModel.findOne({ _id: session.userId }, next);
  1252. },
  1253. (user, next) => {
  1254. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1255. return userModel.findOne({ _id: updatingUserId }, next);
  1256. },
  1257. (user, next) => {
  1258. if (!user) return next("User not found.");
  1259. if (user.username === newUsername)
  1260. return next("New username can't be the same as the old username.");
  1261. return next(null);
  1262. },
  1263. next => {
  1264. userModel.findOne({ username: new RegExp(`^${newUsername}$`, "i") }, next);
  1265. },
  1266. (user, next) => {
  1267. if (!user) return next();
  1268. if (user._id === updatingUserId) return next();
  1269. return next("That username is already in use.");
  1270. },
  1271. next => {
  1272. userModel.updateOne(
  1273. { _id: updatingUserId },
  1274. { $set: { username: newUsername } },
  1275. { runValidators: true },
  1276. next
  1277. );
  1278. }
  1279. ],
  1280. async err => {
  1281. if (err && err !== true) {
  1282. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1283. this.log(
  1284. "ERROR",
  1285. "UPDATE_USERNAME",
  1286. `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`
  1287. );
  1288. return cb({ status: "error", message: err });
  1289. }
  1290. CacheModule.runJob("PUB", {
  1291. channel: "user.updateUsername",
  1292. value: {
  1293. username: newUsername,
  1294. _id: updatingUserId
  1295. }
  1296. });
  1297. this.log(
  1298. "SUCCESS",
  1299. "UPDATE_USERNAME",
  1300. `Updated username for user "${updatingUserId}" to username "${newUsername}".`
  1301. );
  1302. return cb({
  1303. status: "success",
  1304. message: "Username updated successfully"
  1305. });
  1306. }
  1307. );
  1308. }),
  1309. /**
  1310. * Updates a user's email
  1311. *
  1312. * @param {object} session - the session object automatically added by the websocket
  1313. * @param {string} updatingUserId - the updating user's id
  1314. * @param {string} newEmail - the new email
  1315. * @param {Function} cb - gets called with the result
  1316. */
  1317. updateEmail: isLoginRequired(async function updateEmail(session, updatingUserId, newEmail, cb) {
  1318. newEmail = newEmail.toLowerCase();
  1319. const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 }, this);
  1320. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1321. const verifyEmailSchema = await MailModule.runJob("GET_SCHEMA", { schemaName: "verifyEmail" }, this);
  1322. async.waterfall(
  1323. [
  1324. next => {
  1325. if (updatingUserId === session.userId) return next(null, true);
  1326. return userModel.findOne({ _id: session.userId }, next);
  1327. },
  1328. (user, next) => {
  1329. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1330. return userModel.findOne({ _id: updatingUserId }, next);
  1331. },
  1332. (user, next) => {
  1333. if (!user) return next("User not found.");
  1334. if (user.email.address === newEmail)
  1335. return next("New email can't be the same as your the old email.");
  1336. return next();
  1337. },
  1338. next => {
  1339. userModel.findOne({ "email.address": newEmail }, next);
  1340. },
  1341. (user, next) => {
  1342. if (!user) return next();
  1343. if (user._id === updatingUserId) return next();
  1344. return next("That email is already in use.");
  1345. },
  1346. // regenerate the url for gravatar avatar
  1347. next => {
  1348. UtilsModule.runJob("CREATE_GRAVATAR", { email: newEmail }, this).then(url => {
  1349. next(null, url);
  1350. });
  1351. },
  1352. (newAvatarUrl, next) => {
  1353. userModel.updateOne(
  1354. { _id: updatingUserId },
  1355. {
  1356. $set: {
  1357. "avatar.url": newAvatarUrl,
  1358. "email.address": newEmail,
  1359. "email.verified": false,
  1360. "email.verificationToken": verificationToken
  1361. }
  1362. },
  1363. { runValidators: true },
  1364. next
  1365. );
  1366. },
  1367. (res, next) => {
  1368. userModel.findOne({ _id: updatingUserId }, next);
  1369. },
  1370. (user, next) => {
  1371. verifyEmailSchema(newEmail, user.username, verificationToken, err => {
  1372. next(err);
  1373. });
  1374. }
  1375. ],
  1376. async err => {
  1377. if (err && err !== true) {
  1378. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1379. this.log(
  1380. "ERROR",
  1381. "UPDATE_EMAIL",
  1382. `Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`
  1383. );
  1384. return cb({ status: "error", message: err });
  1385. }
  1386. this.log(
  1387. "SUCCESS",
  1388. "UPDATE_EMAIL",
  1389. `Updated email for user "${updatingUserId}" to email "${newEmail}".`
  1390. );
  1391. return cb({
  1392. status: "success",
  1393. message: "Email updated successfully."
  1394. });
  1395. }
  1396. );
  1397. }),
  1398. /**
  1399. * Updates a user's name
  1400. *
  1401. * @param {object} session - the session object automatically added by the websocket
  1402. * @param {string} updatingUserId - the updating user's id
  1403. * @param {string} newBio - the new name
  1404. * @param {Function} cb - gets called with the result
  1405. */
  1406. updateName: isLoginRequired(async function updateName(session, updatingUserId, newName, cb) {
  1407. const userModel = await DBModule.runJob(
  1408. "GET_MODEL",
  1409. {
  1410. modelName: "user"
  1411. },
  1412. this
  1413. );
  1414. async.waterfall(
  1415. [
  1416. next => {
  1417. if (updatingUserId === session.userId) return next(null, true);
  1418. return userModel.findOne({ _id: session.userId }, next);
  1419. },
  1420. (user, next) => {
  1421. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1422. return userModel.findOne({ _id: updatingUserId }, next);
  1423. },
  1424. (user, next) => {
  1425. if (!user) return next("User not found.");
  1426. return userModel.updateOne(
  1427. { _id: updatingUserId },
  1428. { $set: { name: newName } },
  1429. { runValidators: true },
  1430. next
  1431. );
  1432. }
  1433. ],
  1434. async err => {
  1435. if (err && err !== true) {
  1436. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1437. this.log(
  1438. "ERROR",
  1439. "UPDATE_NAME",
  1440. `Couldn't update name for user "${updatingUserId}" to name "${newName}". "${err}"`
  1441. );
  1442. return cb({ status: "error", message: err });
  1443. }
  1444. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1445. userId: updatingUserId,
  1446. type: "user__edit_name",
  1447. payload: { message: `Changed name to ${newName}` }
  1448. });
  1449. this.log("SUCCESS", "UPDATE_NAME", `Updated name for user "${updatingUserId}" to name "${newName}".`);
  1450. return cb({
  1451. status: "success",
  1452. message: "Name updated successfully"
  1453. });
  1454. }
  1455. );
  1456. }),
  1457. /**
  1458. * Updates a user's location
  1459. *
  1460. * @param {object} session - the session object automatically added by the websocket
  1461. * @param {string} updatingUserId - the updating user's id
  1462. * @param {string} newLocation - the new location
  1463. * @param {Function} cb - gets called with the result
  1464. */
  1465. updateLocation: isLoginRequired(async function updateLocation(session, updatingUserId, newLocation, cb) {
  1466. const userModel = await DBModule.runJob(
  1467. "GET_MODEL",
  1468. {
  1469. modelName: "user"
  1470. },
  1471. this
  1472. );
  1473. async.waterfall(
  1474. [
  1475. next => {
  1476. if (updatingUserId === session.userId) return next(null, true);
  1477. return userModel.findOne({ _id: session.userId }, next);
  1478. },
  1479. (user, next) => {
  1480. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1481. return userModel.findOne({ _id: updatingUserId }, next);
  1482. },
  1483. (user, next) => {
  1484. if (!user) return next("User not found.");
  1485. return userModel.updateOne(
  1486. { _id: updatingUserId },
  1487. { $set: { location: newLocation } },
  1488. { runValidators: true },
  1489. next
  1490. );
  1491. }
  1492. ],
  1493. async err => {
  1494. if (err && err !== true) {
  1495. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1496. this.log(
  1497. "ERROR",
  1498. "UPDATE_LOCATION",
  1499. `Couldn't update location for user "${updatingUserId}" to location "${newLocation}". "${err}"`
  1500. );
  1501. return cb({ status: "error", message: err });
  1502. }
  1503. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1504. userId: updatingUserId,
  1505. type: "user__edit_location",
  1506. payload: { message: `Changed location to ${newLocation}` }
  1507. });
  1508. this.log(
  1509. "SUCCESS",
  1510. "UPDATE_LOCATION",
  1511. `Updated location for user "${updatingUserId}" to location "${newLocation}".`
  1512. );
  1513. return cb({
  1514. status: "success",
  1515. message: "Location updated successfully"
  1516. });
  1517. }
  1518. );
  1519. }),
  1520. /**
  1521. * Updates a user's bio
  1522. *
  1523. * @param {object} session - the session object automatically added by the websocket
  1524. * @param {string} updatingUserId - the updating user's id
  1525. * @param {string} newBio - the new bio
  1526. * @param {Function} cb - gets called with the result
  1527. */
  1528. updateBio: isLoginRequired(async function updateBio(session, updatingUserId, newBio, cb) {
  1529. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1530. async.waterfall(
  1531. [
  1532. next => {
  1533. if (updatingUserId === session.userId) return next(null, true);
  1534. return userModel.findOne({ _id: session.userId }, next);
  1535. },
  1536. (user, next) => {
  1537. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1538. return userModel.findOne({ _id: updatingUserId }, next);
  1539. },
  1540. (user, next) => {
  1541. if (!user) return next("User not found.");
  1542. return userModel.updateOne(
  1543. { _id: updatingUserId },
  1544. { $set: { bio: newBio } },
  1545. { runValidators: true },
  1546. next
  1547. );
  1548. }
  1549. ],
  1550. async err => {
  1551. if (err && err !== true) {
  1552. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1553. this.log(
  1554. "ERROR",
  1555. "UPDATE_BIO",
  1556. `Couldn't update bio for user "${updatingUserId}" to bio "${newBio}". "${err}"`
  1557. );
  1558. return cb({ status: "error", message: err });
  1559. }
  1560. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1561. userId: updatingUserId,
  1562. type: "user__edit_bio",
  1563. payload: { message: `Changed bio to ${newBio}` }
  1564. });
  1565. this.log("SUCCESS", "UPDATE_BIO", `Updated bio for user "${updatingUserId}" to bio "${newBio}".`);
  1566. return cb({
  1567. status: "success",
  1568. message: "Bio updated successfully"
  1569. });
  1570. }
  1571. );
  1572. }),
  1573. /**
  1574. * Updates a user's avatar
  1575. *
  1576. * @param {object} session - the session object automatically added by the websocket
  1577. * @param {string} updatingUserId - the updating user's id
  1578. * @param {string} newAvatar - the new avatar object
  1579. * @param {Function} cb - gets called with the result
  1580. */
  1581. updateAvatar: isLoginRequired(async function updateAvatarType(session, updatingUserId, newAvatar, cb) {
  1582. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1583. async.waterfall(
  1584. [
  1585. next => {
  1586. if (updatingUserId === session.userId) return next(null, true);
  1587. return userModel.findOne({ _id: session.userId }, next);
  1588. },
  1589. (user, next) => {
  1590. if (user !== true && (!user || user.role !== "admin")) return next("Invalid permissions.");
  1591. return userModel.findOne({ _id: updatingUserId }, next);
  1592. },
  1593. (user, next) => {
  1594. if (!user) return next("User not found.");
  1595. return userModel.findOneAndUpdate(
  1596. { _id: updatingUserId },
  1597. { $set: { "avatar.type": newAvatar.type, "avatar.color": newAvatar.color } },
  1598. { new: true, runValidators: true },
  1599. next
  1600. );
  1601. }
  1602. ],
  1603. async err => {
  1604. if (err && err !== true) {
  1605. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1606. this.log(
  1607. "ERROR",
  1608. "UPDATE_AVATAR",
  1609. `Couldn't update avatar for user "${updatingUserId}" to type "${newAvatar.type}" and color "${newAvatar.color}". "${err}"`
  1610. );
  1611. return cb({ status: "error", message: err });
  1612. }
  1613. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1614. userId: updatingUserId,
  1615. type: "user__edit_avatar",
  1616. payload: { message: `Changed avatar to use ${newAvatar.type} and ${newAvatar.color}` }
  1617. });
  1618. this.log(
  1619. "SUCCESS",
  1620. "UPDATE_AVATAR",
  1621. `Updated avatar for user "${updatingUserId}" to type "${newAvatar.type} and color ${newAvatar.color}".`
  1622. );
  1623. return cb({
  1624. status: "success",
  1625. message: "Avatar updated successfully"
  1626. });
  1627. }
  1628. );
  1629. }),
  1630. /**
  1631. * Updates a user's role
  1632. *
  1633. * @param {object} session - the session object automatically added by the websocket
  1634. * @param {string} updatingUserId - the updating user's id
  1635. * @param {string} newRole - the new role
  1636. * @param {Function} cb - gets called with the result
  1637. */
  1638. updateRole: isAdminRequired(async function updateRole(session, updatingUserId, newRole, cb) {
  1639. newRole = newRole.toLowerCase();
  1640. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1641. async.waterfall(
  1642. [
  1643. next => {
  1644. userModel.findOne({ _id: updatingUserId }, next);
  1645. },
  1646. (user, next) => {
  1647. if (!user) return next("User not found.");
  1648. if (user.role === newRole) return next("New role can't be the same as the old role.");
  1649. return next();
  1650. },
  1651. next => {
  1652. userModel.updateOne(
  1653. { _id: updatingUserId },
  1654. { $set: { role: newRole } },
  1655. { runValidators: true },
  1656. next
  1657. );
  1658. }
  1659. ],
  1660. async err => {
  1661. if (err && err !== true) {
  1662. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1663. this.log(
  1664. "ERROR",
  1665. "UPDATE_ROLE",
  1666. `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`
  1667. );
  1668. return cb({ status: "error", message: err });
  1669. }
  1670. this.log(
  1671. "SUCCESS",
  1672. "UPDATE_ROLE",
  1673. `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`
  1674. );
  1675. return cb({
  1676. status: "success",
  1677. message: "Role successfully updated."
  1678. });
  1679. }
  1680. );
  1681. }),
  1682. /**
  1683. * Updates a user's password
  1684. *
  1685. * @param {object} session - the session object automatically added by the websocket
  1686. * @param {string} previousPassword - the previous password
  1687. * @param {string} newPassword - the new password
  1688. * @param {Function} cb - gets called with the result
  1689. */
  1690. updatePassword: isLoginRequired(async function updatePassword(session, previousPassword, newPassword, cb) {
  1691. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1692. async.waterfall(
  1693. [
  1694. next => {
  1695. userModel.findOne({ _id: session.userId }, next);
  1696. },
  1697. (user, next) => {
  1698. if (!user.services.password) return next("This account does not have a password set.");
  1699. return next(null, user.services.password.password);
  1700. },
  1701. (storedPassword, next) => {
  1702. bcrypt.compare(sha256(previousPassword), storedPassword).then(res => {
  1703. if (res) return next();
  1704. return next("Please enter the correct previous password.");
  1705. });
  1706. },
  1707. next => {
  1708. if (!DBModule.passwordValid(newPassword))
  1709. return next("Invalid new password. Check if it meets all the requirements.");
  1710. return next();
  1711. },
  1712. next => {
  1713. bcrypt.genSalt(10, next);
  1714. },
  1715. // hash the password
  1716. (salt, next) => {
  1717. bcrypt.hash(sha256(newPassword), salt, next);
  1718. },
  1719. (hashedPassword, next) => {
  1720. userModel.updateOne(
  1721. { _id: session.userId },
  1722. {
  1723. $set: {
  1724. "services.password.password": hashedPassword
  1725. }
  1726. },
  1727. next
  1728. );
  1729. }
  1730. ],
  1731. async err => {
  1732. if (err) {
  1733. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1734. this.log(
  1735. "ERROR",
  1736. "UPDATE_PASSWORD",
  1737. `Failed updating user password of user '${session.userId}'. '${err}'.`
  1738. );
  1739. return cb({ status: "error", message: err });
  1740. }
  1741. this.log("SUCCESS", "UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
  1742. return cb({
  1743. status: "success",
  1744. message: "Password successfully updated."
  1745. });
  1746. }
  1747. );
  1748. }),
  1749. /**
  1750. * Requests a password for a session
  1751. *
  1752. * @param {object} session - the session object automatically added by the websocket
  1753. * @param {string} email - the email of the user that requests a password reset
  1754. * @param {Function} cb - gets called with the result
  1755. */
  1756. requestPassword: isLoginRequired(async function requestPassword(session, cb) {
  1757. const code = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 8 }, this);
  1758. const passwordRequestSchema = await MailModule.runJob(
  1759. "GET_SCHEMA",
  1760. {
  1761. schemaName: "passwordRequest"
  1762. },
  1763. this
  1764. );
  1765. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1766. async.waterfall(
  1767. [
  1768. next => {
  1769. userModel.findOne({ _id: session.userId }, next);
  1770. },
  1771. (user, next) => {
  1772. if (!user) return next("User not found.");
  1773. if (user.services.password && user.services.password.password)
  1774. return next("You already have a password set.");
  1775. return next(null, user);
  1776. },
  1777. (user, next) => {
  1778. const expires = new Date();
  1779. expires.setDate(expires.getDate() + 1);
  1780. userModel.findOneAndUpdate(
  1781. { "email.address": user.email.address },
  1782. {
  1783. $set: {
  1784. "services.password": {
  1785. set: { code, expires }
  1786. }
  1787. }
  1788. },
  1789. { runValidators: true },
  1790. next
  1791. );
  1792. },
  1793. (user, next) => {
  1794. passwordRequestSchema(user.email.address, user.username, code, next);
  1795. }
  1796. ],
  1797. async err => {
  1798. if (err && err !== true) {
  1799. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1800. this.log(
  1801. "ERROR",
  1802. "REQUEST_PASSWORD",
  1803. `UserId '${session.userId}' failed to request password. '${err}'`
  1804. );
  1805. return cb({ status: "error", message: err });
  1806. }
  1807. this.log(
  1808. "SUCCESS",
  1809. "REQUEST_PASSWORD",
  1810. `UserId '${session.userId}' successfully requested a password.`
  1811. );
  1812. return cb({
  1813. status: "success",
  1814. message: "Successfully requested password."
  1815. });
  1816. }
  1817. );
  1818. }),
  1819. /**
  1820. * Verifies a password code
  1821. *
  1822. * @param {object} session - the session object automatically added by the websocket
  1823. * @param {string} code - the password code
  1824. * @param {Function} cb - gets called with the result
  1825. */
  1826. verifyPasswordCode: isLoginRequired(async function verifyPasswordCode(session, code, cb) {
  1827. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1828. async.waterfall(
  1829. [
  1830. next => {
  1831. if (!code || typeof code !== "string") return next("Invalid code.");
  1832. return userModel.findOne(
  1833. {
  1834. "services.password.set.code": code,
  1835. _id: session.userId
  1836. },
  1837. next
  1838. );
  1839. },
  1840. (user, next) => {
  1841. if (!user) return next("Invalid code.");
  1842. if (user.services.password.set.expires < new Date()) return next("That code has expired.");
  1843. return next(null);
  1844. }
  1845. ],
  1846. async err => {
  1847. if (err && err !== true) {
  1848. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1849. this.log("ERROR", "VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
  1850. cb({ status: "error", message: err });
  1851. } else {
  1852. this.log("SUCCESS", "VERIFY_PASSWORD_CODE", `Code '${code}' successfully verified.`);
  1853. cb({
  1854. status: "success",
  1855. message: "Successfully verified password code."
  1856. });
  1857. }
  1858. }
  1859. );
  1860. }),
  1861. /**
  1862. * Adds a password to a user with a code
  1863. *
  1864. * @param {object} session - the session object automatically added by the websocket
  1865. * @param {string} code - the password code
  1866. * @param {string} newPassword - the new password code
  1867. * @param {Function} cb - gets called with the result
  1868. */
  1869. changePasswordWithCode: isLoginRequired(async function changePasswordWithCode(session, code, newPassword, cb) {
  1870. const userModel = await DBModule.runJob(
  1871. "GET_MODEL",
  1872. {
  1873. modelName: "user"
  1874. },
  1875. this
  1876. );
  1877. async.waterfall(
  1878. [
  1879. next => {
  1880. if (!code || typeof code !== "string") return next("Invalid code.");
  1881. return userModel.findOne({ "services.password.set.code": code }, next);
  1882. },
  1883. (user, next) => {
  1884. if (!user) return next("Invalid code.");
  1885. if (!user.services.password.set.expires > new Date()) return next("That code has expired.");
  1886. return next();
  1887. },
  1888. next => {
  1889. if (!DBModule.passwordValid(newPassword))
  1890. return next("Invalid password. Check if it meets all the requirements.");
  1891. return next();
  1892. },
  1893. next => {
  1894. bcrypt.genSalt(10, next);
  1895. },
  1896. // hash the password
  1897. (salt, next) => {
  1898. bcrypt.hash(sha256(newPassword), salt, next);
  1899. },
  1900. (hashedPassword, next) => {
  1901. userModel.updateOne(
  1902. { "services.password.set.code": code },
  1903. {
  1904. $set: {
  1905. "services.password.password": hashedPassword
  1906. },
  1907. $unset: { "services.password.set": "" }
  1908. },
  1909. { runValidators: true },
  1910. next
  1911. );
  1912. }
  1913. ],
  1914. async err => {
  1915. if (err && err !== true) {
  1916. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1917. this.log("ERROR", "ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
  1918. return cb({ status: "error", message: err });
  1919. }
  1920. this.log("SUCCESS", "ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
  1921. CacheModule.runJob("PUB", {
  1922. channel: "user.linkPassword",
  1923. value: session.userId
  1924. });
  1925. return cb({
  1926. status: "success",
  1927. message: "Successfully added password."
  1928. });
  1929. }
  1930. );
  1931. }),
  1932. /**
  1933. * Unlinks password from user
  1934. *
  1935. * @param {object} session - the session object automatically added by the websocket
  1936. * @param {Function} cb - gets called with the result
  1937. */
  1938. unlinkPassword: isLoginRequired(async function unlinkPassword(session, cb) {
  1939. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1940. async.waterfall(
  1941. [
  1942. next => {
  1943. userModel.findOne({ _id: session.userId }, next);
  1944. },
  1945. (user, next) => {
  1946. if (!user) return next("Not logged in.");
  1947. if (!user.services.github || !user.services.github.id)
  1948. return next("You can't remove password login without having GitHub login.");
  1949. return userModel.updateOne({ _id: session.userId }, { $unset: { "services.password": "" } }, next);
  1950. }
  1951. ],
  1952. async err => {
  1953. if (err && err !== true) {
  1954. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1955. this.log(
  1956. "ERROR",
  1957. "UNLINK_PASSWORD",
  1958. `Unlinking password failed for userId '${session.userId}'. '${err}'`
  1959. );
  1960. return cb({ status: "error", message: err });
  1961. }
  1962. this.log("SUCCESS", "UNLINK_PASSWORD", `Unlinking password successful for userId '${session.userId}'.`);
  1963. CacheModule.runJob("PUB", {
  1964. channel: "user.unlinkPassword",
  1965. value: session.userId
  1966. });
  1967. return cb({
  1968. status: "success",
  1969. message: "Successfully unlinked password."
  1970. });
  1971. }
  1972. );
  1973. }),
  1974. /**
  1975. * Unlinks GitHub from user
  1976. *
  1977. * @param {object} session - the session object automatically added by the websocket
  1978. * @param {Function} cb - gets called with the result
  1979. */
  1980. unlinkGitHub: isLoginRequired(async function unlinkGitHub(session, cb) {
  1981. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1982. async.waterfall(
  1983. [
  1984. next => {
  1985. userModel.findOne({ _id: session.userId }, next);
  1986. },
  1987. (user, next) => {
  1988. if (!user) return next("Not logged in.");
  1989. if (!user.services.password || !user.services.password.password)
  1990. return next("You can't remove GitHub login without having password login.");
  1991. return userModel.updateOne({ _id: session.userId }, { $unset: { "services.github": "" } }, next);
  1992. }
  1993. ],
  1994. async err => {
  1995. if (err && err !== true) {
  1996. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1997. this.log(
  1998. "ERROR",
  1999. "UNLINK_GITHUB",
  2000. `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`
  2001. );
  2002. return cb({ status: "error", message: err });
  2003. }
  2004. this.log("SUCCESS", "UNLINK_GITHUB", `Unlinking GitHub successful for userId '${session.userId}'.`);
  2005. CacheModule.runJob("PUB", {
  2006. channel: "user.unlinkGithub",
  2007. value: session.userId
  2008. });
  2009. return cb({
  2010. status: "success",
  2011. message: "Successfully unlinked GitHub."
  2012. });
  2013. }
  2014. );
  2015. }),
  2016. /**
  2017. * Requests a password reset for an email
  2018. *
  2019. * @param {object} session - the session object automatically added by the websocket
  2020. * @param {string} email - the email of the user that requests a password reset
  2021. * @param {Function} cb - gets called with the result
  2022. */
  2023. async requestPasswordReset(session, email, cb) {
  2024. const code = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 8 }, this);
  2025. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  2026. const resetPasswordRequestSchema = await MailModule.runJob(
  2027. "GET_SCHEMA",
  2028. { schemaName: "resetPasswordRequest" },
  2029. this
  2030. );
  2031. async.waterfall(
  2032. [
  2033. next => {
  2034. if (!email || typeof email !== "string") return next("Invalid email.");
  2035. email = email.toLowerCase();
  2036. return userModel.findOne({ "email.address": email }, next);
  2037. },
  2038. (user, next) => {
  2039. if (!user) return next("User not found.");
  2040. if (!user.services.password || !user.services.password.password)
  2041. return next("User does not have a password set, and probably uses GitHub to log in.");
  2042. return next(null, user);
  2043. },
  2044. (user, next) => {
  2045. const expires = new Date();
  2046. expires.setDate(expires.getDate() + 1);
  2047. userModel.findOneAndUpdate(
  2048. { "email.address": email },
  2049. {
  2050. $set: {
  2051. "services.password.reset": {
  2052. code,
  2053. expires
  2054. }
  2055. }
  2056. },
  2057. { runValidators: true },
  2058. next
  2059. );
  2060. },
  2061. (user, next) => {
  2062. resetPasswordRequestSchema(user.email.address, user.username, code, next);
  2063. }
  2064. ],
  2065. async err => {
  2066. if (err && err !== true) {
  2067. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2068. this.log(
  2069. "ERROR",
  2070. "REQUEST_PASSWORD_RESET",
  2071. `Email '${email}' failed to request password reset. '${err}'`
  2072. );
  2073. return cb({ status: "error", message: err });
  2074. }
  2075. this.log(
  2076. "SUCCESS",
  2077. "REQUEST_PASSWORD_RESET",
  2078. `Email '${email}' successfully requested a password reset.`
  2079. );
  2080. return cb({
  2081. status: "success",
  2082. message: "Successfully requested password reset."
  2083. });
  2084. }
  2085. );
  2086. },
  2087. /**
  2088. * Verifies a reset code
  2089. *
  2090. * @param {object} session - the session object automatically added by the websocket
  2091. * @param {string} code - the password reset code
  2092. * @param {Function} cb - gets called with the result
  2093. */
  2094. async verifyPasswordResetCode(session, code, cb) {
  2095. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  2096. async.waterfall(
  2097. [
  2098. next => {
  2099. if (!code || typeof code !== "string") return next("Invalid code.");
  2100. return userModel.findOne({ "services.password.reset.code": code }, next);
  2101. },
  2102. (user, next) => {
  2103. if (!user) return next("Invalid code.");
  2104. if (!user.services.password.reset.expires > new Date()) return next("That code has expired.");
  2105. return next(null);
  2106. }
  2107. ],
  2108. async err => {
  2109. if (err && err !== true) {
  2110. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2111. this.log("ERROR", "VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
  2112. return cb({ status: "error", message: err });
  2113. }
  2114. this.log("SUCCESS", "VERIFY_PASSWORD_RESET_CODE", `Code '${code}' successfully verified.`);
  2115. return cb({
  2116. status: "success",
  2117. message: "Successfully verified password reset code."
  2118. });
  2119. }
  2120. );
  2121. },
  2122. /**
  2123. * Changes a user's password with a reset code
  2124. *
  2125. * @param {object} session - the session object automatically added by the websocket
  2126. * @param {string} code - the password reset code
  2127. * @param {string} newPassword - the new password reset code
  2128. * @param {Function} cb - gets called with the result
  2129. */
  2130. async changePasswordWithResetCode(session, code, newPassword, cb) {
  2131. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  2132. async.waterfall(
  2133. [
  2134. next => {
  2135. if (!code || typeof code !== "string") return next("Invalid code.");
  2136. return userModel.findOne({ "services.password.reset.code": code }, next);
  2137. },
  2138. (user, next) => {
  2139. if (!user) return next("Invalid code.");
  2140. if (!user.services.password.reset.expires > new Date()) return next("That code has expired.");
  2141. return next();
  2142. },
  2143. next => {
  2144. if (!DBModule.passwordValid(newPassword))
  2145. return next("Invalid password. Check if it meets all the requirements.");
  2146. return next();
  2147. },
  2148. next => {
  2149. bcrypt.genSalt(10, next);
  2150. },
  2151. // hash the password
  2152. (salt, next) => {
  2153. bcrypt.hash(sha256(newPassword), salt, next);
  2154. },
  2155. (hashedPassword, next) => {
  2156. userModel.updateOne(
  2157. { "services.password.reset.code": code },
  2158. {
  2159. $set: {
  2160. "services.password.password": hashedPassword
  2161. },
  2162. $unset: { "services.password.reset": "" }
  2163. },
  2164. { runValidators: true },
  2165. next
  2166. );
  2167. }
  2168. ],
  2169. async err => {
  2170. if (err && err !== true) {
  2171. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2172. this.log(
  2173. "ERROR",
  2174. "CHANGE_PASSWORD_WITH_RESET_CODE",
  2175. `Code '${code}' failed to change password. '${err}'`
  2176. );
  2177. return cb({ status: "error", message: err });
  2178. }
  2179. this.log("SUCCESS", "CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' successfully changed password.`);
  2180. return cb({
  2181. status: "success",
  2182. message: "Successfully changed password."
  2183. });
  2184. }
  2185. );
  2186. },
  2187. /**
  2188. * Bans a user by userId
  2189. *
  2190. * @param {object} session - the session object automatically added by the websocket
  2191. * @param {string} value - the user id that is going to be banned
  2192. * @param {string} reason - the reason for the ban
  2193. * @param {string} expiresAt - the time the ban expires
  2194. * @param {Function} cb - gets called with the result
  2195. */
  2196. banUserById: isAdminRequired(function banUserById(session, userId, reason, expiresAt, cb) {
  2197. async.waterfall(
  2198. [
  2199. next => {
  2200. if (!userId) return next("You must provide a userId to ban.");
  2201. if (!reason) return next("You must provide a reason for the ban.");
  2202. return next();
  2203. },
  2204. next => {
  2205. if (!expiresAt || typeof expiresAt !== "string") return next("Invalid expire date.");
  2206. const date = new Date();
  2207. switch (expiresAt) {
  2208. case "1h":
  2209. expiresAt = date.setHours(date.getHours() + 1);
  2210. break;
  2211. case "12h":
  2212. expiresAt = date.setHours(date.getHours() + 12);
  2213. break;
  2214. case "1d":
  2215. expiresAt = date.setDate(date.getDate() + 1);
  2216. break;
  2217. case "1w":
  2218. expiresAt = date.setDate(date.getDate() + 7);
  2219. break;
  2220. case "1m":
  2221. expiresAt = date.setMonth(date.getMonth() + 1);
  2222. break;
  2223. case "3m":
  2224. expiresAt = date.setMonth(date.getMonth() + 3);
  2225. break;
  2226. case "6m":
  2227. expiresAt = date.setMonth(date.getMonth() + 6);
  2228. break;
  2229. case "1y":
  2230. expiresAt = date.setFullYear(date.getFullYear() + 1);
  2231. break;
  2232. case "never":
  2233. expiresAt = new Date(3093527980800000);
  2234. break;
  2235. default:
  2236. return next("Invalid expire date.");
  2237. }
  2238. return next();
  2239. },
  2240. next => {
  2241. PunishmentsModule.runJob(
  2242. "ADD_PUNISHMENT",
  2243. {
  2244. type: "banUserId",
  2245. value: userId,
  2246. reason,
  2247. expiresAt,
  2248. punishedBy: session.userId
  2249. },
  2250. this
  2251. )
  2252. .then(punishment => next(null, punishment))
  2253. .catch(next);
  2254. },
  2255. (punishment, next) => {
  2256. CacheModule.runJob("PUB", {
  2257. channel: "user.ban",
  2258. value: { userId, punishment }
  2259. });
  2260. next();
  2261. }
  2262. ],
  2263. async err => {
  2264. if (err && err !== true) {
  2265. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2266. this.log(
  2267. "ERROR",
  2268. "BAN_USER_BY_ID",
  2269. `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`
  2270. );
  2271. return cb({ status: "error", message: err });
  2272. }
  2273. this.log(
  2274. "SUCCESS",
  2275. "BAN_USER_BY_ID",
  2276. `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`
  2277. );
  2278. return cb({
  2279. status: "success",
  2280. message: "Successfully banned user."
  2281. });
  2282. }
  2283. );
  2284. })
  2285. };