users.js 82 KB

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