users.js 52 KB

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