users.js 61 KB

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