users.js 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037
  1. import { SyncedCron } from 'meteor/percolate:synced-cron';
  2. import ImpersonatedUsers from './impersonatedUsers';
  3. // Sandstorm context is detected using the METEOR_SETTINGS environment variable
  4. // in the package definition.
  5. const isSandstorm =
  6. Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
  7. Users = Meteor.users;
  8. const allowedSortValues = [
  9. '-modifiedAt',
  10. 'modifiedAt',
  11. '-title',
  12. 'title',
  13. '-sort',
  14. 'sort',
  15. ];
  16. const defaultSortBy = allowedSortValues[0];
  17. /**
  18. * A User in wekan
  19. */
  20. Users.attachSchema(
  21. new SimpleSchema({
  22. username: {
  23. /**
  24. * the username of the user
  25. */
  26. type: String,
  27. optional: true,
  28. // eslint-disable-next-line consistent-return
  29. autoValue() {
  30. if (this.isInsert && !this.isSet) {
  31. const name = this.field('profile.fullname');
  32. if (name.isSet) {
  33. return name.value.toLowerCase().replace(/\s/g, '');
  34. }
  35. }
  36. },
  37. },
  38. emails: {
  39. /**
  40. * the list of emails attached to a user
  41. */
  42. type: [Object],
  43. optional: true,
  44. },
  45. 'emails.$.address': {
  46. /**
  47. * The email address
  48. */
  49. type: String,
  50. regEx: SimpleSchema.RegEx.Email,
  51. },
  52. 'emails.$.verified': {
  53. /**
  54. * Has the email been verified
  55. */
  56. type: Boolean,
  57. },
  58. createdAt: {
  59. /**
  60. * creation date of the user
  61. */
  62. type: Date,
  63. // eslint-disable-next-line consistent-return
  64. autoValue() {
  65. if (this.isInsert) {
  66. return new Date();
  67. } else if (this.isUpsert) {
  68. return {
  69. $setOnInsert: new Date(),
  70. };
  71. } else {
  72. this.unset();
  73. }
  74. },
  75. },
  76. modifiedAt: {
  77. type: Date,
  78. denyUpdate: false,
  79. // eslint-disable-next-line consistent-return
  80. autoValue() {
  81. if (this.isInsert || this.isUpsert || this.isUpdate) {
  82. return new Date();
  83. } else {
  84. this.unset();
  85. }
  86. },
  87. },
  88. profile: {
  89. /**
  90. * profile settings
  91. */
  92. type: Object,
  93. optional: true,
  94. // eslint-disable-next-line consistent-return
  95. autoValue() {
  96. if (this.isInsert && !this.isSet) {
  97. return {
  98. boardView: 'board-view-swimlanes',
  99. };
  100. }
  101. },
  102. },
  103. 'profile.avatarUrl': {
  104. /**
  105. * URL of the avatar of the user
  106. */
  107. type: String,
  108. optional: true,
  109. },
  110. 'profile.emailBuffer': {
  111. /**
  112. * list of email buffers of the user
  113. */
  114. type: [String],
  115. optional: true,
  116. },
  117. 'profile.fullname': {
  118. /**
  119. * full name of the user
  120. */
  121. type: String,
  122. optional: true,
  123. },
  124. 'profile.showDesktopDragHandles': {
  125. /**
  126. * does the user want to hide system messages?
  127. */
  128. type: Boolean,
  129. optional: true,
  130. },
  131. 'profile.hideCheckedItems': {
  132. /**
  133. * does the user want to hide checked checklist items?
  134. */
  135. type: Boolean,
  136. optional: true,
  137. },
  138. 'profile.hiddenSystemMessages': {
  139. /**
  140. * does the user want to hide system messages?
  141. */
  142. type: Boolean,
  143. optional: true,
  144. },
  145. 'profile.hiddenMinicardLabelText': {
  146. /**
  147. * does the user want to hide minicard label texts?
  148. */
  149. type: Boolean,
  150. optional: true,
  151. },
  152. 'profile.initials': {
  153. /**
  154. * initials of the user
  155. */
  156. type: String,
  157. optional: true,
  158. },
  159. 'profile.invitedBoards': {
  160. /**
  161. * board IDs the user has been invited to
  162. */
  163. type: [String],
  164. optional: true,
  165. },
  166. 'profile.language': {
  167. /**
  168. * language of the user
  169. */
  170. type: String,
  171. optional: true,
  172. },
  173. 'profile.notifications': {
  174. /**
  175. * enabled notifications for the user
  176. */
  177. type: [Object],
  178. optional: true,
  179. },
  180. 'profile.notifications.$.activity': {
  181. /**
  182. * The id of the activity this notification references
  183. */
  184. type: String,
  185. },
  186. 'profile.notifications.$.read': {
  187. /**
  188. * the date on which this notification was read
  189. */
  190. type: Date,
  191. optional: true,
  192. },
  193. 'profile.showCardsCountAt': {
  194. /**
  195. * showCardCountAt field of the user
  196. */
  197. type: Number,
  198. optional: true,
  199. },
  200. 'profile.startDayOfWeek': {
  201. /**
  202. * startDayOfWeek field of the user
  203. */
  204. type: Number,
  205. optional: true,
  206. },
  207. 'profile.starredBoards': {
  208. /**
  209. * list of starred board IDs
  210. */
  211. type: [String],
  212. optional: true,
  213. },
  214. 'profile.icode': {
  215. /**
  216. * icode
  217. */
  218. type: String,
  219. optional: true,
  220. },
  221. 'profile.boardView': {
  222. /**
  223. * boardView field of the user
  224. */
  225. type: String,
  226. optional: true,
  227. allowedValues: [
  228. 'board-view-swimlanes',
  229. 'board-view-lists',
  230. 'board-view-cal',
  231. ],
  232. },
  233. 'profile.listSortBy': {
  234. /**
  235. * default sort list for user
  236. */
  237. type: String,
  238. optional: true,
  239. defaultValue: defaultSortBy,
  240. allowedValues: allowedSortValues,
  241. },
  242. 'profile.templatesBoardId': {
  243. /**
  244. * Reference to the templates board
  245. */
  246. type: String,
  247. defaultValue: '',
  248. },
  249. 'profile.cardTemplatesSwimlaneId': {
  250. /**
  251. * Reference to the card templates swimlane Id
  252. */
  253. type: String,
  254. defaultValue: '',
  255. },
  256. 'profile.listTemplatesSwimlaneId': {
  257. /**
  258. * Reference to the list templates swimlane Id
  259. */
  260. type: String,
  261. defaultValue: '',
  262. },
  263. 'profile.boardTemplatesSwimlaneId': {
  264. /**
  265. * Reference to the board templates swimlane Id
  266. */
  267. type: String,
  268. defaultValue: '',
  269. },
  270. services: {
  271. /**
  272. * services field of the user
  273. */
  274. type: Object,
  275. optional: true,
  276. blackbox: true,
  277. },
  278. heartbeat: {
  279. /**
  280. * last time the user has been seen
  281. */
  282. type: Date,
  283. optional: true,
  284. },
  285. isAdmin: {
  286. /**
  287. * is the user an admin of the board?
  288. */
  289. type: Boolean,
  290. optional: true,
  291. },
  292. createdThroughApi: {
  293. /**
  294. * was the user created through the API?
  295. */
  296. type: Boolean,
  297. optional: true,
  298. },
  299. loginDisabled: {
  300. /**
  301. * loginDisabled field of the user
  302. */
  303. type: Boolean,
  304. optional: true,
  305. },
  306. authenticationMethod: {
  307. /**
  308. * authentication method of the user
  309. */
  310. type: String,
  311. optional: false,
  312. defaultValue: 'password',
  313. },
  314. sessionData: {
  315. /**
  316. * profile settings
  317. */
  318. type: Object,
  319. optional: true,
  320. // eslint-disable-next-line consistent-return
  321. autoValue() {
  322. if (this.isInsert && !this.isSet) {
  323. return {};
  324. }
  325. },
  326. },
  327. 'sessionData.totalHits': {
  328. /**
  329. * Total hits from last search
  330. */
  331. type: Number,
  332. optional: true,
  333. },
  334. 'sessionData.lastHit': {
  335. /**
  336. * last hit that was returned
  337. */
  338. type: Number,
  339. optional: true,
  340. },
  341. importUsernames: {
  342. /**
  343. * username for imported
  344. */
  345. type: [String],
  346. optional: true,
  347. },
  348. }),
  349. );
  350. Users.allow({
  351. update(userId, doc) {
  352. const user = Users.findOne({
  353. _id: userId,
  354. });
  355. if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
  356. return true;
  357. if (!user) {
  358. return false;
  359. }
  360. return doc._id === userId;
  361. },
  362. remove(userId, doc) {
  363. const adminsNumber = Users.find({
  364. isAdmin: true,
  365. }).count();
  366. const { isAdmin } = Users.findOne(
  367. {
  368. _id: userId,
  369. },
  370. {
  371. fields: {
  372. isAdmin: 1,
  373. },
  374. },
  375. );
  376. // Prevents remove of the only one administrator
  377. if (adminsNumber === 1 && isAdmin && userId === doc._id) {
  378. return false;
  379. }
  380. // If it's the user or an admin
  381. return userId === doc._id || isAdmin;
  382. },
  383. fetch: [],
  384. });
  385. // Search a user in the complete server database by its name or username. This
  386. // is used for instance to add a new user to a board.
  387. const searchInFields = ['username', 'profile.fullname'];
  388. Users.initEasySearch(searchInFields, {
  389. use: 'mongo-db',
  390. returnFields: [...searchInFields, 'profile.avatarUrl'],
  391. });
  392. Users.safeFields = {
  393. _id: 1,
  394. username: 1,
  395. 'profile.fullname': 1,
  396. 'profile.avatarUrl': 1,
  397. 'profile.initials': 1,
  398. };
  399. if (Meteor.isClient) {
  400. Users.helpers({
  401. isBoardMember() {
  402. const board = Boards.findOne(Session.get('currentBoard'));
  403. return board && board.hasMember(this._id);
  404. },
  405. isNotNoComments() {
  406. const board = Boards.findOne(Session.get('currentBoard'));
  407. return (
  408. board && board.hasMember(this._id) && !board.hasNoComments(this._id)
  409. );
  410. },
  411. isNoComments() {
  412. const board = Boards.findOne(Session.get('currentBoard'));
  413. return board && board.hasNoComments(this._id);
  414. },
  415. isNotCommentOnly() {
  416. const board = Boards.findOne(Session.get('currentBoard'));
  417. return (
  418. board && board.hasMember(this._id) && !board.hasCommentOnly(this._id)
  419. );
  420. },
  421. isCommentOnly() {
  422. const board = Boards.findOne(Session.get('currentBoard'));
  423. return board && board.hasCommentOnly(this._id);
  424. },
  425. isNotWorker() {
  426. const board = Boards.findOne(Session.get('currentBoard'));
  427. return board && board.hasMember(this._id) && !board.hasWorker(this._id);
  428. },
  429. isWorker() {
  430. const board = Boards.findOne(Session.get('currentBoard'));
  431. return board && board.hasWorker(this._id);
  432. },
  433. isBoardAdmin(boardId = Session.get('currentBoard')) {
  434. const board = Boards.findOne(boardId);
  435. return board && board.hasAdmin(this._id);
  436. },
  437. });
  438. }
  439. Users.parseImportUsernames = (usernamesString) => {
  440. return usernamesString.trim().split(new RegExp('\\s*[,;]\\s*'));
  441. };
  442. Users.helpers({
  443. importUsernamesString() {
  444. if (this.importUsernames) {
  445. return this.importUsernames.join(', ');
  446. }
  447. return '';
  448. },
  449. boards() {
  450. return Boards.find(
  451. {
  452. 'members.userId': this._id,
  453. },
  454. {
  455. sort: {
  456. sort: 1 /* boards default sorting */,
  457. },
  458. },
  459. );
  460. },
  461. starredBoards() {
  462. const { starredBoards = [] } = this.profile || {};
  463. return Boards.find(
  464. {
  465. archived: false,
  466. _id: {
  467. $in: starredBoards,
  468. },
  469. },
  470. {
  471. sort: {
  472. sort: 1 /* boards default sorting */,
  473. },
  474. },
  475. );
  476. },
  477. hasStarred(boardId) {
  478. const { starredBoards = [] } = this.profile || {};
  479. return _.contains(starredBoards, boardId);
  480. },
  481. invitedBoards() {
  482. const { invitedBoards = [] } = this.profile || {};
  483. return Boards.find(
  484. {
  485. archived: false,
  486. _id: {
  487. $in: invitedBoards,
  488. },
  489. },
  490. {
  491. sort: {
  492. sort: 1 /* boards default sorting */,
  493. },
  494. },
  495. );
  496. },
  497. isInvitedTo(boardId) {
  498. const { invitedBoards = [] } = this.profile || {};
  499. return _.contains(invitedBoards, boardId);
  500. },
  501. _getListSortBy() {
  502. const profile = this.profile || {};
  503. const sortBy = profile.listSortBy || defaultSortBy;
  504. const keyPattern = /^(-{0,1})(.*$)/;
  505. const ret = [];
  506. if (keyPattern.exec(sortBy)) {
  507. ret[0] = RegExp.$2;
  508. ret[1] = RegExp.$1 ? -1 : 1;
  509. }
  510. return ret;
  511. },
  512. hasSortBy() {
  513. // if use doesn't have dragHandle, then we can let user to choose sort list by different order
  514. return !this.hasShowDesktopDragHandles();
  515. },
  516. getListSortBy() {
  517. return this._getListSortBy()[0];
  518. },
  519. getListSortTypes() {
  520. return allowedSortValues;
  521. },
  522. getListSortByDirection() {
  523. return this._getListSortBy()[1];
  524. },
  525. hasTag(tag) {
  526. const { tags = [] } = this.profile || {};
  527. return _.contains(tags, tag);
  528. },
  529. hasNotification(activityId) {
  530. const { notifications = [] } = this.profile || {};
  531. return _.contains(notifications, activityId);
  532. },
  533. notifications() {
  534. const { notifications = [] } = this.profile || {};
  535. for (const index in notifications) {
  536. if (!notifications.hasOwnProperty(index)) continue;
  537. const notification = notifications[index];
  538. // this preserves their db sort order for editing
  539. notification.dbIndex = index;
  540. notification.activity = Activities.findOne(notification.activity);
  541. }
  542. // this sorts them newest to oldest to match Trello's behavior
  543. notifications.reverse();
  544. return notifications;
  545. },
  546. hasShowDesktopDragHandles() {
  547. const profile = this.profile || {};
  548. return profile.showDesktopDragHandles || false;
  549. },
  550. hasHideCheckedItems() {
  551. const profile = this.profile || {};
  552. return profile.hideCheckedItems || false;
  553. },
  554. hasHiddenSystemMessages() {
  555. const profile = this.profile || {};
  556. return profile.hiddenSystemMessages || false;
  557. },
  558. hasHiddenMinicardLabelText() {
  559. const profile = this.profile || {};
  560. return profile.hiddenMinicardLabelText || false;
  561. },
  562. getEmailBuffer() {
  563. const { emailBuffer = [] } = this.profile || {};
  564. return emailBuffer;
  565. },
  566. getInitials() {
  567. const profile = this.profile || {};
  568. if (profile.initials) return profile.initials;
  569. else if (profile.fullname) {
  570. return profile.fullname
  571. .split(/\s+/)
  572. .reduce((memo, word) => {
  573. return memo + word[0];
  574. }, '')
  575. .toUpperCase();
  576. } else {
  577. return this.username[0].toUpperCase();
  578. }
  579. },
  580. getLimitToShowCardsCount() {
  581. const profile = this.profile || {};
  582. return profile.showCardsCountAt;
  583. },
  584. getName() {
  585. const profile = this.profile || {};
  586. return profile.fullname || this.username;
  587. },
  588. getLanguage() {
  589. const profile = this.profile || {};
  590. return profile.language || 'en';
  591. },
  592. getStartDayOfWeek() {
  593. const profile = this.profile || {};
  594. if (typeof profile.startDayOfWeek === 'undefined') {
  595. // default is 'Monday' (1)
  596. return 1;
  597. }
  598. return profile.startDayOfWeek;
  599. },
  600. getTemplatesBoardId() {
  601. return (this.profile || {}).templatesBoardId;
  602. },
  603. getTemplatesBoardSlug() {
  604. return (Boards.findOne((this.profile || {}).templatesBoardId) || {}).slug;
  605. },
  606. remove() {
  607. User.remove({
  608. _id: this._id,
  609. });
  610. },
  611. });
  612. Users.mutations({
  613. toggleBoardStar(boardId) {
  614. const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
  615. return {
  616. [queryKind]: {
  617. 'profile.starredBoards': boardId,
  618. },
  619. };
  620. },
  621. addInvite(boardId) {
  622. return {
  623. $addToSet: {
  624. 'profile.invitedBoards': boardId,
  625. },
  626. };
  627. },
  628. removeInvite(boardId) {
  629. return {
  630. $pull: {
  631. 'profile.invitedBoards': boardId,
  632. },
  633. };
  634. },
  635. addTag(tag) {
  636. return {
  637. $addToSet: {
  638. 'profile.tags': tag,
  639. },
  640. };
  641. },
  642. removeTag(tag) {
  643. return {
  644. $pull: {
  645. 'profile.tags': tag,
  646. },
  647. };
  648. },
  649. toggleTag(tag) {
  650. if (this.hasTag(tag)) this.removeTag(tag);
  651. else this.addTag(tag);
  652. },
  653. setListSortBy(value) {
  654. return {
  655. $set: {
  656. 'profile.listSortBy': value,
  657. },
  658. };
  659. },
  660. setName(value) {
  661. return {
  662. $set: {
  663. 'profile.fullname': value,
  664. },
  665. };
  666. },
  667. toggleDesktopHandles(value = false) {
  668. return {
  669. $set: {
  670. 'profile.showDesktopDragHandles': !value,
  671. },
  672. };
  673. },
  674. toggleHideCheckedItems() {
  675. const value = this.hasHideCheckedItems();
  676. return {
  677. $set: {
  678. 'profile.hideCheckedItems': !value,
  679. },
  680. };
  681. },
  682. toggleSystem(value = false) {
  683. return {
  684. $set: {
  685. 'profile.hiddenSystemMessages': !value,
  686. },
  687. };
  688. },
  689. toggleLabelText(value = false) {
  690. return {
  691. $set: {
  692. 'profile.hiddenMinicardLabelText': !value,
  693. },
  694. };
  695. },
  696. addNotification(activityId) {
  697. return {
  698. $addToSet: {
  699. 'profile.notifications': {
  700. activity: activityId,
  701. },
  702. },
  703. };
  704. },
  705. removeNotification(activityId) {
  706. return {
  707. $pull: {
  708. 'profile.notifications': {
  709. activity: activityId,
  710. },
  711. },
  712. };
  713. },
  714. addEmailBuffer(text) {
  715. return {
  716. $addToSet: {
  717. 'profile.emailBuffer': text,
  718. },
  719. };
  720. },
  721. clearEmailBuffer() {
  722. return {
  723. $set: {
  724. 'profile.emailBuffer': [],
  725. },
  726. };
  727. },
  728. setAvatarUrl(avatarUrl) {
  729. return {
  730. $set: {
  731. 'profile.avatarUrl': avatarUrl,
  732. },
  733. };
  734. },
  735. setShowCardsCountAt(limit) {
  736. return {
  737. $set: {
  738. 'profile.showCardsCountAt': limit,
  739. },
  740. };
  741. },
  742. setStartDayOfWeek(startDay) {
  743. return {
  744. $set: {
  745. 'profile.startDayOfWeek': startDay,
  746. },
  747. };
  748. },
  749. setBoardView(view) {
  750. return {
  751. $set: {
  752. 'profile.boardView': view,
  753. },
  754. };
  755. },
  756. });
  757. Meteor.methods({
  758. setListSortBy(value) {
  759. check(value, String);
  760. Meteor.user().setListSortBy(value);
  761. },
  762. toggleDesktopDragHandles() {
  763. const user = Meteor.user();
  764. user.toggleDesktopHandles(user.hasShowDesktopDragHandles());
  765. },
  766. toggleHideCheckedItems() {
  767. const user = Meteor.user();
  768. user.toggleHideCheckedItems();
  769. },
  770. toggleSystemMessages() {
  771. const user = Meteor.user();
  772. user.toggleSystem(user.hasHiddenSystemMessages());
  773. },
  774. toggleMinicardLabelText() {
  775. const user = Meteor.user();
  776. user.toggleLabelText(user.hasHiddenMinicardLabelText());
  777. },
  778. changeLimitToShowCardsCount(limit) {
  779. check(limit, Number);
  780. Meteor.user().setShowCardsCountAt(limit);
  781. },
  782. changeStartDayOfWeek(startDay) {
  783. check(startDay, Number);
  784. Meteor.user().setStartDayOfWeek(startDay);
  785. },
  786. });
  787. if (Meteor.isServer) {
  788. Meteor.methods({
  789. setAllUsersHideSystemMessages() {
  790. if (Meteor.user() && Meteor.user().isAdmin) {
  791. // If setting is missing, add it
  792. Users.update(
  793. {
  794. 'profile.hiddenSystemMessages': {
  795. $exists: false,
  796. },
  797. },
  798. {
  799. $set: {
  800. 'profile.hiddenSystemMessages': true,
  801. },
  802. },
  803. {
  804. multi: true,
  805. },
  806. );
  807. // If setting is false, set it to true
  808. Users.update(
  809. {
  810. 'profile.hiddenSystemMessages': false,
  811. },
  812. {
  813. $set: {
  814. 'profile.hiddenSystemMessages': true,
  815. },
  816. },
  817. {
  818. multi: true,
  819. },
  820. );
  821. return true;
  822. } else {
  823. return false;
  824. }
  825. },
  826. setCreateUser(
  827. fullname,
  828. username,
  829. initials,
  830. password,
  831. isAdmin,
  832. isActive,
  833. email,
  834. importUsernames,
  835. ) {
  836. if (Meteor.user() && Meteor.user().isAdmin) {
  837. check(fullname, String);
  838. check(username, String);
  839. check(initials, String);
  840. check(password, String);
  841. check(isAdmin, String);
  842. check(isActive, String);
  843. check(email, String);
  844. check(importUsernames, Array);
  845. const nUsersWithUsername = Users.find({
  846. username,
  847. }).count();
  848. const nUsersWithEmail = Users.find({
  849. email,
  850. }).count();
  851. if (nUsersWithUsername > 0) {
  852. throw new Meteor.Error('username-already-taken');
  853. } else if (nUsersWithEmail > 0) {
  854. throw new Meteor.Error('email-already-taken');
  855. } else {
  856. Accounts.createUser({
  857. username,
  858. password,
  859. isAdmin,
  860. isActive,
  861. email: email.toLowerCase(),
  862. from: 'admin',
  863. });
  864. const user =
  865. Users.findOne(username) ||
  866. Users.findOne({
  867. username,
  868. });
  869. if (user) {
  870. Users.update(user._id, {
  871. $set: {
  872. 'profile.fullname': fullname,
  873. importUsernames,
  874. 'profile.initials': initials,
  875. },
  876. });
  877. }
  878. }
  879. }
  880. },
  881. setUsername(username, userId) {
  882. if (Meteor.user() && Meteor.user().isAdmin) {
  883. check(username, String);
  884. check(userId, String);
  885. const nUsersWithUsername = Users.find({
  886. username,
  887. }).count();
  888. if (nUsersWithUsername > 0) {
  889. throw new Meteor.Error('username-already-taken');
  890. } else {
  891. Users.update(userId, {
  892. $set: {
  893. username,
  894. },
  895. });
  896. }
  897. }
  898. },
  899. setEmail(email, userId) {
  900. if (Meteor.user() && Meteor.user().isAdmin) {
  901. if (Array.isArray(email)) {
  902. email = email.shift();
  903. }
  904. check(email, String);
  905. const existingUser = Users.findOne(
  906. {
  907. 'emails.address': email,
  908. },
  909. {
  910. fields: {
  911. _id: 1,
  912. },
  913. },
  914. );
  915. if (existingUser) {
  916. throw new Meteor.Error('email-already-taken');
  917. } else {
  918. Users.update(userId, {
  919. $set: {
  920. emails: [
  921. {
  922. address: email,
  923. verified: false,
  924. },
  925. ],
  926. },
  927. });
  928. }
  929. }
  930. },
  931. setUsernameAndEmail(username, email, userId) {
  932. if (Meteor.user() && Meteor.user().isAdmin) {
  933. check(username, String);
  934. if (Array.isArray(email)) {
  935. email = email.shift();
  936. }
  937. check(email, String);
  938. check(userId, String);
  939. Meteor.call('setUsername', username, userId);
  940. Meteor.call('setEmail', email, userId);
  941. }
  942. },
  943. setPassword(newPassword, userId) {
  944. if (Meteor.user() && Meteor.user().isAdmin) {
  945. check(userId, String);
  946. check(newPassword, String);
  947. if (Meteor.user().isAdmin) {
  948. Accounts.setPassword(userId, newPassword);
  949. }
  950. }
  951. },
  952. setEmailVerified(email, verified, userId) {
  953. if (Meteor.user() && Meteor.user().isAdmin) {
  954. check(email, String);
  955. check(verified, Boolean);
  956. check(userId, String);
  957. Users.update(userId, {
  958. $set: {
  959. emails: [
  960. {
  961. address: email,
  962. verified,
  963. },
  964. ],
  965. },
  966. });
  967. }
  968. },
  969. setInitials(initials, userId) {
  970. if (Meteor.user() && Meteor.user().isAdmin) {
  971. check(initials, String);
  972. check(userId, String);
  973. Users.update(userId, {
  974. $set: {
  975. 'profile.initials': initials,
  976. },
  977. });
  978. }
  979. },
  980. // we accept userId, username, email
  981. inviteUserToBoard(username, boardId) {
  982. check(username, String);
  983. check(boardId, String);
  984. const inviter = Meteor.user();
  985. const board = Boards.findOne(boardId);
  986. const allowInvite =
  987. inviter &&
  988. board &&
  989. board.members &&
  990. _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
  991. _.where(board.members, {
  992. userId: inviter._id,
  993. })[0].isActive;
  994. // GitHub issue 2060
  995. //_.where(board.members, { userId: inviter._id })[0].isAdmin;
  996. if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
  997. this.unblock();
  998. const posAt = username.indexOf('@');
  999. let user = null;
  1000. if (posAt >= 0) {
  1001. user = Users.findOne({
  1002. emails: {
  1003. $elemMatch: {
  1004. address: username,
  1005. },
  1006. },
  1007. });
  1008. } else {
  1009. user =
  1010. Users.findOne(username) ||
  1011. Users.findOne({
  1012. username,
  1013. });
  1014. }
  1015. if (user) {
  1016. if (user._id === inviter._id)
  1017. throw new Meteor.Error('error-user-notAllowSelf');
  1018. } else {
  1019. if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
  1020. if (
  1021. Settings.findOne({
  1022. disableRegistration: true,
  1023. })
  1024. ) {
  1025. throw new Meteor.Error('error-user-notCreated');
  1026. }
  1027. // Set in lowercase email before creating account
  1028. const email = username.toLowerCase();
  1029. username = email.substring(0, posAt);
  1030. const newUserId = Accounts.createUser({
  1031. username,
  1032. email,
  1033. });
  1034. if (!newUserId) throw new Meteor.Error('error-user-notCreated');
  1035. // assume new user speak same language with inviter
  1036. if (inviter.profile && inviter.profile.language) {
  1037. Users.update(newUserId, {
  1038. $set: {
  1039. 'profile.language': inviter.profile.language,
  1040. },
  1041. });
  1042. }
  1043. Accounts.sendEnrollmentEmail(newUserId);
  1044. user = Users.findOne(newUserId);
  1045. }
  1046. board.addMember(user._id);
  1047. user.addInvite(boardId);
  1048. //Check if there is a subtasks board
  1049. if (board.subtasksDefaultBoardId) {
  1050. const subBoard = Boards.findOne(board.subtasksDefaultBoardId);
  1051. //If there is, also add user to that board
  1052. if (subBoard) {
  1053. subBoard.addMember(user._id);
  1054. user.addInvite(subBoard._id);
  1055. }
  1056. }
  1057. try {
  1058. const params = {
  1059. user: user.username,
  1060. inviter: inviter.username,
  1061. board: board.title,
  1062. url: board.absoluteUrl(),
  1063. };
  1064. const lang = user.getLanguage();
  1065. Email.send({
  1066. to: user.emails[0].address.toLowerCase(),
  1067. from: Accounts.emailTemplates.from,
  1068. subject: TAPi18n.__('email-invite-subject', params, lang),
  1069. text: TAPi18n.__('email-invite-text', params, lang),
  1070. });
  1071. } catch (e) {
  1072. throw new Meteor.Error('email-fail', e.message);
  1073. }
  1074. return {
  1075. username: user.username,
  1076. email: user.emails[0].address,
  1077. };
  1078. },
  1079. impersonate(userId) {
  1080. check(userId, String);
  1081. if (!Meteor.users.findOne(userId))
  1082. throw new Meteor.Error(404, 'User not found');
  1083. if (!Meteor.user().isAdmin)
  1084. throw new Meteor.Error(403, 'Permission denied');
  1085. ImpersonatedUsers.insert({ adminId: Meteor.user()._id, userId: userId, reason: 'clickedImpersonate' });
  1086. this.setUserId(userId);
  1087. },
  1088. isImpersonated(userId) {
  1089. check(userId, String);
  1090. const isImpersonated = ImpersonatedUsers.findOne({
  1091. userId: userId,
  1092. });
  1093. return isImpersonated;
  1094. },
  1095. });
  1096. Accounts.onCreateUser((options, user) => {
  1097. const userCount = Users.find().count();
  1098. if (userCount === 0) {
  1099. user.isAdmin = true;
  1100. return user;
  1101. }
  1102. if (user.services.oidc) {
  1103. let email = user.services.oidc.email;
  1104. if (Array.isArray(email)) {
  1105. email = email.shift();
  1106. }
  1107. email = email.toLowerCase();
  1108. user.username = user.services.oidc.username;
  1109. user.emails = [
  1110. {
  1111. address: email,
  1112. verified: true,
  1113. },
  1114. ];
  1115. const initials = user.services.oidc.fullname
  1116. .split(/\s+/)
  1117. .reduce((memo, word) => {
  1118. return memo + word[0];
  1119. }, '')
  1120. .toUpperCase();
  1121. user.profile = {
  1122. initials,
  1123. fullname: user.services.oidc.fullname,
  1124. boardView: 'board-view-swimlanes',
  1125. };
  1126. user.authenticationMethod = 'oauth2';
  1127. // see if any existing user has this email address or username, otherwise create new
  1128. const existingUser = Meteor.users.findOne({
  1129. $or: [
  1130. {
  1131. 'emails.address': email,
  1132. },
  1133. {
  1134. username: user.username,
  1135. },
  1136. ],
  1137. });
  1138. if (!existingUser) return user;
  1139. // copy across new service info
  1140. const service = _.keys(user.services)[0];
  1141. existingUser.services[service] = user.services[service];
  1142. existingUser.emails = user.emails;
  1143. existingUser.username = user.username;
  1144. existingUser.profile = user.profile;
  1145. existingUser.authenticationMethod = user.authenticationMethod;
  1146. Meteor.users.remove({
  1147. _id: user._id,
  1148. });
  1149. Meteor.users.remove({
  1150. _id: existingUser._id,
  1151. }); // is going to be created again
  1152. return existingUser;
  1153. }
  1154. if (options.from === 'admin') {
  1155. user.createdThroughApi = true;
  1156. return user;
  1157. }
  1158. const disableRegistration = Settings.findOne().disableRegistration;
  1159. // If this is the first Authentication by the ldap and self registration disabled
  1160. if (disableRegistration && options && options.ldap) {
  1161. user.authenticationMethod = 'ldap';
  1162. return user;
  1163. }
  1164. // If self registration enabled
  1165. if (!disableRegistration) {
  1166. return user;
  1167. }
  1168. if (!options || !options.profile) {
  1169. throw new Meteor.Error(
  1170. 'error-invitation-code-blank',
  1171. 'The invitation code is required',
  1172. );
  1173. }
  1174. const invitationCode = InvitationCodes.findOne({
  1175. code: options.profile.invitationcode,
  1176. email: options.email,
  1177. valid: true,
  1178. });
  1179. if (!invitationCode) {
  1180. throw new Meteor.Error(
  1181. 'error-invitation-code-not-exist',
  1182. // eslint-disable-next-line quotes
  1183. "The invitation code doesn't exist",
  1184. );
  1185. } else {
  1186. user.profile = {
  1187. icode: options.profile.invitationcode,
  1188. };
  1189. user.profile.boardView = 'board-view-swimlanes';
  1190. // Deletes the invitation code after the user was created successfully.
  1191. setTimeout(
  1192. Meteor.bindEnvironment(() => {
  1193. InvitationCodes.remove({
  1194. _id: invitationCode._id,
  1195. });
  1196. }),
  1197. 200,
  1198. );
  1199. return user;
  1200. }
  1201. });
  1202. }
  1203. const addCronJob = _.debounce(
  1204. Meteor.bindEnvironment(function notificationCleanupDebounced() {
  1205. // passed in the removeAge has to be a number standing for the number of days after a notification is read before we remove it
  1206. const envRemoveAge =
  1207. process.env.NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE;
  1208. // default notifications will be removed 2 days after they are read
  1209. const defaultRemoveAge = 2;
  1210. const removeAge = parseInt(envRemoveAge, 10) || defaultRemoveAge;
  1211. SyncedCron.add({
  1212. name: 'notification_cleanup',
  1213. schedule: (parser) => parser.text('every 1 days'),
  1214. job: () => {
  1215. for (const user of Users.find()) {
  1216. if (!user.profile || !user.profile.notifications) continue;
  1217. for (const notification of user.profile.notifications) {
  1218. if (notification.read) {
  1219. const removeDate = new Date(notification.read);
  1220. removeDate.setDate(removeDate.getDate() + removeAge);
  1221. if (removeDate <= new Date()) {
  1222. user.removeNotification(notification.activity);
  1223. }
  1224. }
  1225. }
  1226. }
  1227. },
  1228. });
  1229. SyncedCron.start();
  1230. }),
  1231. 500,
  1232. );
  1233. if (Meteor.isServer) {
  1234. // Let mongoDB ensure username unicity
  1235. Meteor.startup(() => {
  1236. allowedSortValues.forEach((value) => {
  1237. Lists._collection._ensureIndex(value);
  1238. });
  1239. Users._collection._ensureIndex({
  1240. modifiedAt: -1,
  1241. });
  1242. Users._collection._ensureIndex(
  1243. {
  1244. username: 1,
  1245. },
  1246. {
  1247. unique: true,
  1248. },
  1249. );
  1250. Meteor.defer(() => {
  1251. addCronJob();
  1252. });
  1253. });
  1254. // OLD WAY THIS CODE DID WORK: When user is last admin of board,
  1255. // if admin is removed, board is removed.
  1256. // NOW THIS IS COMMENTED OUT, because other board users still need to be able
  1257. // to use that board, and not have board deleted.
  1258. // Someone can be later changed to be admin of board, by making change to database.
  1259. // TODO: Add UI for changing someone as board admin.
  1260. //Users.before.remove((userId, doc) => {
  1261. // Boards
  1262. // .find({members: {$elemMatch: {userId: doc._id, isAdmin: true}}})
  1263. // .forEach((board) => {
  1264. // // If only one admin for the board
  1265. // if (board.members.filter((e) => e.isAdmin).length === 1) {
  1266. // Boards.remove(board._id);
  1267. // }
  1268. // });
  1269. //});
  1270. // Each board document contains the de-normalized number of users that have
  1271. // starred it. If the user star or unstar a board, we need to update this
  1272. // counter.
  1273. // We need to run this code on the server only, otherwise the incrementation
  1274. // will be done twice.
  1275. Users.after.update(function (userId, user, fieldNames) {
  1276. // The `starredBoards` list is hosted on the `profile` field. If this
  1277. // field hasn't been modificated we don't need to run this hook.
  1278. if (!_.contains(fieldNames, 'profile')) return;
  1279. // To calculate a diff of board starred ids, we get both the previous
  1280. // and the newly board ids list
  1281. function getStarredBoardsIds(doc) {
  1282. return doc.profile && doc.profile.starredBoards;
  1283. }
  1284. const oldIds = getStarredBoardsIds(this.previous);
  1285. const newIds = getStarredBoardsIds(user);
  1286. // The _.difference(a, b) method returns the values from a that are not in
  1287. // b. We use it to find deleted and newly inserted ids by using it in one
  1288. // direction and then in the other.
  1289. function incrementBoards(boardsIds, inc) {
  1290. boardsIds.forEach((boardId) => {
  1291. Boards.update(boardId, {
  1292. $inc: {
  1293. stars: inc,
  1294. },
  1295. });
  1296. });
  1297. }
  1298. incrementBoards(_.difference(oldIds, newIds), -1);
  1299. incrementBoards(_.difference(newIds, oldIds), +1);
  1300. });
  1301. // Override getUserId so that we can TODO get the current userId
  1302. const fakeUserId = new Meteor.EnvironmentVariable();
  1303. const getUserId = CollectionHooks.getUserId;
  1304. CollectionHooks.getUserId = () => {
  1305. return fakeUserId.get() || getUserId();
  1306. };
  1307. if (!isSandstorm) {
  1308. Users.after.insert((userId, doc) => {
  1309. const fakeUser = {
  1310. extendAutoValueContext: {
  1311. userId: doc._id,
  1312. },
  1313. };
  1314. fakeUserId.withValue(doc._id, () => {
  1315. /*
  1316. // Insert the Welcome Board
  1317. Boards.insert({
  1318. title: TAPi18n.__('welcome-board'),
  1319. permission: 'private',
  1320. }, fakeUser, (err, boardId) => {
  1321. Swimlanes.insert({
  1322. title: TAPi18n.__('welcome-swimlane'),
  1323. boardId,
  1324. sort: 1,
  1325. }, fakeUser);
  1326. ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
  1327. Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
  1328. });
  1329. });
  1330. */
  1331. const Future = require('fibers/future');
  1332. const future1 = new Future();
  1333. const future2 = new Future();
  1334. const future3 = new Future();
  1335. Boards.insert(
  1336. {
  1337. title: TAPi18n.__('templates'),
  1338. permission: 'private',
  1339. type: 'template-container',
  1340. },
  1341. fakeUser,
  1342. (err, boardId) => {
  1343. // Insert the reference to our templates board
  1344. Users.update(fakeUserId.get(), {
  1345. $set: {
  1346. 'profile.templatesBoardId': boardId,
  1347. },
  1348. });
  1349. // Insert the card templates swimlane
  1350. Swimlanes.insert(
  1351. {
  1352. title: TAPi18n.__('card-templates-swimlane'),
  1353. boardId,
  1354. sort: 1,
  1355. type: 'template-container',
  1356. },
  1357. fakeUser,
  1358. (err, swimlaneId) => {
  1359. // Insert the reference to out card templates swimlane
  1360. Users.update(fakeUserId.get(), {
  1361. $set: {
  1362. 'profile.cardTemplatesSwimlaneId': swimlaneId,
  1363. },
  1364. });
  1365. future1.return();
  1366. },
  1367. );
  1368. // Insert the list templates swimlane
  1369. Swimlanes.insert(
  1370. {
  1371. title: TAPi18n.__('list-templates-swimlane'),
  1372. boardId,
  1373. sort: 2,
  1374. type: 'template-container',
  1375. },
  1376. fakeUser,
  1377. (err, swimlaneId) => {
  1378. // Insert the reference to out list templates swimlane
  1379. Users.update(fakeUserId.get(), {
  1380. $set: {
  1381. 'profile.listTemplatesSwimlaneId': swimlaneId,
  1382. },
  1383. });
  1384. future2.return();
  1385. },
  1386. );
  1387. // Insert the board templates swimlane
  1388. Swimlanes.insert(
  1389. {
  1390. title: TAPi18n.__('board-templates-swimlane'),
  1391. boardId,
  1392. sort: 3,
  1393. type: 'template-container',
  1394. },
  1395. fakeUser,
  1396. (err, swimlaneId) => {
  1397. // Insert the reference to out board templates swimlane
  1398. Users.update(fakeUserId.get(), {
  1399. $set: {
  1400. 'profile.boardTemplatesSwimlaneId': swimlaneId,
  1401. },
  1402. });
  1403. future3.return();
  1404. },
  1405. );
  1406. },
  1407. );
  1408. // HACK
  1409. future1.wait();
  1410. future2.wait();
  1411. future3.wait();
  1412. });
  1413. });
  1414. }
  1415. Users.after.insert((userId, doc) => {
  1416. // HACK
  1417. doc = Users.findOne({
  1418. _id: doc._id,
  1419. });
  1420. if (doc.createdThroughApi) {
  1421. // The admin user should be able to create a user despite disabling registration because
  1422. // it is two different things (registration and creation).
  1423. // So, when a new user is created via the api (only admin user can do that) one must avoid
  1424. // the disableRegistration check.
  1425. // Issue : https://github.com/wekan/wekan/issues/1232
  1426. // PR : https://github.com/wekan/wekan/pull/1251
  1427. Users.update(doc._id, {
  1428. $set: {
  1429. createdThroughApi: '',
  1430. },
  1431. });
  1432. return;
  1433. }
  1434. //invite user to corresponding boards
  1435. const disableRegistration = Settings.findOne().disableRegistration;
  1436. // If ldap, bypass the inviation code if the self registration isn't allowed.
  1437. // TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type
  1438. if (doc.authenticationMethod !== 'ldap' && disableRegistration) {
  1439. const invitationCode = InvitationCodes.findOne({
  1440. code: doc.profile.icode,
  1441. valid: true,
  1442. });
  1443. if (!invitationCode) {
  1444. throw new Meteor.Error('error-invitation-code-not-exist');
  1445. } else {
  1446. invitationCode.boardsToBeInvited.forEach((boardId) => {
  1447. const board = Boards.findOne(boardId);
  1448. board.addMember(doc._id);
  1449. });
  1450. if (!doc.profile) {
  1451. doc.profile = {};
  1452. }
  1453. doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
  1454. Users.update(doc._id, {
  1455. $set: {
  1456. profile: doc.profile,
  1457. },
  1458. });
  1459. InvitationCodes.update(invitationCode._id, {
  1460. $set: {
  1461. valid: false,
  1462. },
  1463. });
  1464. }
  1465. }
  1466. });
  1467. }
  1468. // USERS REST API
  1469. if (Meteor.isServer) {
  1470. // Middleware which checks that API is enabled.
  1471. JsonRoutes.Middleware.use(function (req, res, next) {
  1472. const api = req.url.startsWith('/api');
  1473. if ((api === true && process.env.WITH_API === 'true') || api === false) {
  1474. return next();
  1475. } else {
  1476. res.writeHead(301, {
  1477. Location: '/',
  1478. });
  1479. return res.end();
  1480. }
  1481. });
  1482. /**
  1483. * @operation get_current_user
  1484. *
  1485. * @summary returns the current user
  1486. * @return_type Users
  1487. */
  1488. JsonRoutes.add('GET', '/api/user', function (req, res) {
  1489. try {
  1490. Authentication.checkLoggedIn(req.userId);
  1491. const data = Meteor.users.findOne({
  1492. _id: req.userId,
  1493. });
  1494. delete data.services;
  1495. // get all boards where the user is member of
  1496. let boards = Boards.find(
  1497. {
  1498. type: 'board',
  1499. 'members.userId': req.userId,
  1500. },
  1501. {
  1502. fields: {
  1503. _id: 1,
  1504. members: 1,
  1505. },
  1506. },
  1507. );
  1508. boards = boards.map((b) => {
  1509. const u = b.members.find((m) => m.userId === req.userId);
  1510. delete u.userId;
  1511. u.boardId = b._id;
  1512. return u;
  1513. });
  1514. data.boards = boards;
  1515. JsonRoutes.sendResult(res, {
  1516. code: 200,
  1517. data,
  1518. });
  1519. } catch (error) {
  1520. JsonRoutes.sendResult(res, {
  1521. code: 200,
  1522. data: error,
  1523. });
  1524. }
  1525. });
  1526. /**
  1527. * @operation get_all_users
  1528. *
  1529. * @summary return all the users
  1530. *
  1531. * @description Only the admin user (the first user) can call the REST API.
  1532. * @return_type [{ _id: string,
  1533. * username: string}]
  1534. */
  1535. JsonRoutes.add('GET', '/api/users', function (req, res) {
  1536. try {
  1537. Authentication.checkUserId(req.userId);
  1538. JsonRoutes.sendResult(res, {
  1539. code: 200,
  1540. data: Meteor.users.find({}).map(function (doc) {
  1541. return {
  1542. _id: doc._id,
  1543. username: doc.username,
  1544. };
  1545. }),
  1546. });
  1547. } catch (error) {
  1548. JsonRoutes.sendResult(res, {
  1549. code: 200,
  1550. data: error,
  1551. });
  1552. }
  1553. });
  1554. /**
  1555. * @operation get_user
  1556. *
  1557. * @summary get a given user
  1558. *
  1559. * @description Only the admin user (the first user) can call the REST API.
  1560. *
  1561. * @param {string} userId the user ID or username
  1562. * @return_type Users
  1563. */
  1564. JsonRoutes.add('GET', '/api/users/:userId', function (req, res) {
  1565. try {
  1566. Authentication.checkUserId(req.userId);
  1567. let id = req.params.userId;
  1568. let user = Meteor.users.findOne({
  1569. _id: id,
  1570. });
  1571. if (!user) {
  1572. user = Meteor.users.findOne({
  1573. username: id,
  1574. });
  1575. id = user._id;
  1576. }
  1577. // get all boards where the user is member of
  1578. let boards = Boards.find(
  1579. {
  1580. type: 'board',
  1581. 'members.userId': id,
  1582. },
  1583. {
  1584. fields: {
  1585. _id: 1,
  1586. members: 1,
  1587. },
  1588. },
  1589. );
  1590. boards = boards.map((b) => {
  1591. const u = b.members.find((m) => m.userId === id);
  1592. delete u.userId;
  1593. u.boardId = b._id;
  1594. return u;
  1595. });
  1596. user.boards = boards;
  1597. JsonRoutes.sendResult(res, {
  1598. code: 200,
  1599. data: user,
  1600. });
  1601. } catch (error) {
  1602. JsonRoutes.sendResult(res, {
  1603. code: 200,
  1604. data: error,
  1605. });
  1606. }
  1607. });
  1608. /**
  1609. * @operation edit_user
  1610. *
  1611. * @summary edit a given user
  1612. *
  1613. * @description Only the admin user (the first user) can call the REST API.
  1614. *
  1615. * Possible values for *action*:
  1616. * - `takeOwnership`: The admin takes the ownership of ALL boards of the user (archived and not archived) where the user is admin on.
  1617. * - `disableLogin`: Disable a user (the user is not allowed to login and his login tokens are purged)
  1618. * - `enableLogin`: Enable a user
  1619. *
  1620. * @param {string} userId the user ID
  1621. * @param {string} action the action
  1622. * @return_type {_id: string,
  1623. * title: string}
  1624. */
  1625. JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) {
  1626. try {
  1627. Authentication.checkUserId(req.userId);
  1628. const id = req.params.userId;
  1629. const action = req.body.action;
  1630. let data = Meteor.users.findOne({
  1631. _id: id,
  1632. });
  1633. if (data !== undefined) {
  1634. if (action === 'takeOwnership') {
  1635. data = Boards.find(
  1636. {
  1637. 'members.userId': id,
  1638. 'members.isAdmin': true,
  1639. },
  1640. {
  1641. sort: {
  1642. sort: 1 /* boards default sorting */,
  1643. },
  1644. },
  1645. ).map(function (board) {
  1646. if (board.hasMember(req.userId)) {
  1647. board.removeMember(req.userId);
  1648. }
  1649. board.changeOwnership(id, req.userId);
  1650. return {
  1651. _id: board._id,
  1652. title: board.title,
  1653. };
  1654. });
  1655. } else {
  1656. if (action === 'disableLogin' && id !== req.userId) {
  1657. Users.update(
  1658. {
  1659. _id: id,
  1660. },
  1661. {
  1662. $set: {
  1663. loginDisabled: true,
  1664. 'services.resume.loginTokens': '',
  1665. },
  1666. },
  1667. );
  1668. } else if (action === 'enableLogin') {
  1669. Users.update(
  1670. {
  1671. _id: id,
  1672. },
  1673. {
  1674. $set: {
  1675. loginDisabled: '',
  1676. },
  1677. },
  1678. );
  1679. }
  1680. data = Meteor.users.findOne({
  1681. _id: id,
  1682. });
  1683. }
  1684. }
  1685. JsonRoutes.sendResult(res, {
  1686. code: 200,
  1687. data,
  1688. });
  1689. } catch (error) {
  1690. JsonRoutes.sendResult(res, {
  1691. code: 200,
  1692. data: error,
  1693. });
  1694. }
  1695. });
  1696. /**
  1697. * @operation add_board_member
  1698. * @tag Boards
  1699. *
  1700. * @summary Add New Board Member with Role
  1701. *
  1702. * @description Only the admin user (the first user) can call the REST API.
  1703. *
  1704. * **Note**: see [Boards.set_board_member_permission](#set_board_member_permission)
  1705. * to later change the permissions.
  1706. *
  1707. * @param {string} boardId the board ID
  1708. * @param {string} userId the user ID
  1709. * @param {boolean} isAdmin is the user an admin of the board
  1710. * @param {boolean} isNoComments disable comments
  1711. * @param {boolean} isCommentOnly only enable comments
  1712. * @return_type {_id: string,
  1713. * title: string}
  1714. */
  1715. JsonRoutes.add(
  1716. 'POST',
  1717. '/api/boards/:boardId/members/:userId/add',
  1718. function (req, res) {
  1719. try {
  1720. Authentication.checkUserId(req.userId);
  1721. const userId = req.params.userId;
  1722. const boardId = req.params.boardId;
  1723. const action = req.body.action;
  1724. const { isAdmin, isNoComments, isCommentOnly } = req.body;
  1725. let data = Meteor.users.findOne({
  1726. _id: userId,
  1727. });
  1728. if (data !== undefined) {
  1729. if (action === 'add') {
  1730. data = Boards.find({
  1731. _id: boardId,
  1732. }).map(function (board) {
  1733. if (!board.hasMember(userId)) {
  1734. board.addMember(userId);
  1735. function isTrue(data) {
  1736. return data.toLowerCase() === 'true';
  1737. }
  1738. board.setMemberPermission(
  1739. userId,
  1740. isTrue(isAdmin),
  1741. isTrue(isNoComments),
  1742. isTrue(isCommentOnly),
  1743. userId,
  1744. );
  1745. }
  1746. return {
  1747. _id: board._id,
  1748. title: board.title,
  1749. };
  1750. });
  1751. }
  1752. }
  1753. JsonRoutes.sendResult(res, {
  1754. code: 200,
  1755. data: query,
  1756. });
  1757. } catch (error) {
  1758. JsonRoutes.sendResult(res, {
  1759. code: 200,
  1760. data: error,
  1761. });
  1762. }
  1763. },
  1764. );
  1765. /**
  1766. * @operation remove_board_member
  1767. * @tag Boards
  1768. *
  1769. * @summary Remove Member from Board
  1770. *
  1771. * @description Only the admin user (the first user) can call the REST API.
  1772. *
  1773. * @param {string} boardId the board ID
  1774. * @param {string} userId the user ID
  1775. * @param {string} action the action (needs to be `remove`)
  1776. * @return_type {_id: string,
  1777. * title: string}
  1778. */
  1779. JsonRoutes.add(
  1780. 'POST',
  1781. '/api/boards/:boardId/members/:userId/remove',
  1782. function (req, res) {
  1783. try {
  1784. Authentication.checkUserId(req.userId);
  1785. const userId = req.params.userId;
  1786. const boardId = req.params.boardId;
  1787. const action = req.body.action;
  1788. let data = Meteor.users.findOne({
  1789. _id: userId,
  1790. });
  1791. if (data !== undefined) {
  1792. if (action === 'remove') {
  1793. data = Boards.find({
  1794. _id: boardId,
  1795. }).map(function (board) {
  1796. if (board.hasMember(userId)) {
  1797. board.removeMember(userId);
  1798. }
  1799. return {
  1800. _id: board._id,
  1801. title: board.title,
  1802. };
  1803. });
  1804. }
  1805. }
  1806. JsonRoutes.sendResult(res, {
  1807. code: 200,
  1808. data: query,
  1809. });
  1810. } catch (error) {
  1811. JsonRoutes.sendResult(res, {
  1812. code: 200,
  1813. data: error,
  1814. });
  1815. }
  1816. },
  1817. );
  1818. /**
  1819. * @operation new_user
  1820. *
  1821. * @summary Create a new user
  1822. *
  1823. * @description Only the admin user (the first user) can call the REST API.
  1824. *
  1825. * @param {string} username the new username
  1826. * @param {string} email the email of the new user
  1827. * @param {string} password the password of the new user
  1828. * @return_type {_id: string}
  1829. */
  1830. JsonRoutes.add('POST', '/api/users/', function (req, res) {
  1831. try {
  1832. Authentication.checkUserId(req.userId);
  1833. const id = Accounts.createUser({
  1834. username: req.body.username,
  1835. email: req.body.email,
  1836. password: req.body.password,
  1837. from: 'admin',
  1838. });
  1839. JsonRoutes.sendResult(res, {
  1840. code: 200,
  1841. data: {
  1842. _id: id,
  1843. },
  1844. });
  1845. } catch (error) {
  1846. JsonRoutes.sendResult(res, {
  1847. code: 200,
  1848. data: error,
  1849. });
  1850. }
  1851. });
  1852. /**
  1853. * @operation delete_user
  1854. *
  1855. * @summary Delete a user
  1856. *
  1857. * @description Only the admin user (the first user) can call the REST API.
  1858. *
  1859. * @param {string} userId the ID of the user to delete
  1860. * @return_type {_id: string}
  1861. */
  1862. JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) {
  1863. try {
  1864. Authentication.checkUserId(req.userId);
  1865. const id = req.params.userId;
  1866. // Delete is not enabled yet, because it does leave empty user avatars
  1867. // to boards: boards members, card members and assignees have
  1868. // empty users. See:
  1869. // - wekan/client/components/settings/peopleBody.jade deleteButton
  1870. // - wekan/client/components/settings/peopleBody.js deleteButton
  1871. // - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
  1872. // that does now remove member from board, card members and assignees correctly,
  1873. // but that should be used to remove user from all boards similarly
  1874. // - wekan/models/users.js Delete is not enabled
  1875. // Meteor.users.remove({ _id: id });
  1876. JsonRoutes.sendResult(res, {
  1877. code: 200,
  1878. data: {
  1879. _id: id,
  1880. },
  1881. });
  1882. } catch (error) {
  1883. JsonRoutes.sendResult(res, {
  1884. code: 200,
  1885. data: error,
  1886. });
  1887. }
  1888. });
  1889. /**
  1890. * @operation create_user_token
  1891. *
  1892. * @summary Create a user token
  1893. *
  1894. * @description Only the admin user (the first user) can call the REST API.
  1895. *
  1896. * @param {string} userId the ID of the user to create token for.
  1897. * @return_type {_id: string}
  1898. */
  1899. JsonRoutes.add('POST', '/api/createtoken/:userId', function (req, res) {
  1900. try {
  1901. Authentication.checkUserId(req.userId);
  1902. const id = req.params.userId;
  1903. const token = Accounts._generateStampedLoginToken();
  1904. Accounts._insertLoginToken(id, token);
  1905. JsonRoutes.sendResult(res, {
  1906. code: 200,
  1907. data: {
  1908. _id: id,
  1909. authToken: token.token,
  1910. },
  1911. });
  1912. } catch (error) {
  1913. JsonRoutes.sendResult(res, {
  1914. code: 200,
  1915. data: error,
  1916. });
  1917. }
  1918. });
  1919. }
  1920. export default Users;