users.js 84 KB

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