users.js 64 KB

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