users.js 84 KB

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