users.js 83 KB

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