users.js 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730
  1. import { ReactiveCache, ReactiveMiniMongoIndex } from '/imports/reactiveCache';
  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. 'profile.listWidths': {
  423. /**
  424. * User-specified width of each list (or nothing if default).
  425. * profile[boardId][listId] = width;
  426. */
  427. type: Object,
  428. defaultValue: {},
  429. blackbox: true,
  430. },
  431. 'profile.swimlaneHeights': {
  432. /**
  433. * User-specified heights of each swimlane (or nothing if default).
  434. * profile[boardId][swimlaneId] = height;
  435. */
  436. type: Object,
  437. defaultValue: {},
  438. blackbox: true,
  439. },
  440. 'profile.listCollapsed': {
  441. /**
  442. * User-specific list of collapsed list IDs
  443. */
  444. type: [String],
  445. optional: true,
  446. },
  447. 'profile.swimlaneCollapsed': {
  448. /**
  449. * User-specific list of collapsed swimlane IDs
  450. */
  451. type: [String],
  452. optional: true,
  453. },
  454. services: {
  455. /**
  456. * services field of the user
  457. */
  458. type: Object,
  459. optional: true,
  460. blackbox: true,
  461. },
  462. heartbeat: {
  463. /**
  464. * last time the user has been seen
  465. */
  466. type: Date,
  467. optional: true,
  468. },
  469. isAdmin: {
  470. /**
  471. * is the user an admin of the board?
  472. */
  473. type: Boolean,
  474. optional: true,
  475. },
  476. createdThroughApi: {
  477. /**
  478. * was the user created through the API?
  479. */
  480. type: Boolean,
  481. optional: true,
  482. },
  483. loginDisabled: {
  484. /**
  485. * loginDisabled field of the user
  486. */
  487. type: Boolean,
  488. optional: true,
  489. },
  490. authenticationMethod: {
  491. /**
  492. * authentication method of the user
  493. */
  494. type: String,
  495. optional: false,
  496. defaultValue: 'password',
  497. },
  498. sessionData: {
  499. /**
  500. * profile settings
  501. */
  502. type: Object,
  503. optional: true,
  504. // eslint-disable-next-line consistent-return
  505. autoValue() {
  506. if (this.isInsert && !this.isSet) {
  507. return {};
  508. }
  509. },
  510. },
  511. 'sessionData.totalHits': {
  512. /**
  513. * Total hits from last searchquery['members.userId'] = Meteor.userId();
  514. * last hit that was returned
  515. */
  516. type: Number,
  517. optional: true,
  518. },
  519. importUsernames: {
  520. /**
  521. * username for imported
  522. */
  523. type: [String],
  524. optional: true,
  525. },
  526. lastConnectionDate: {
  527. type: Date,
  528. optional: true,
  529. },
  530. }),
  531. );
  532. Users.allow({
  533. update(userId, doc) {
  534. const user = ReactiveCache.getUser(userId) || ReactiveCache.getCurrentUser();
  535. if (user?.isAdmin)
  536. return true;
  537. if (!user) {
  538. return false;
  539. }
  540. return doc._id === userId;
  541. },
  542. remove(userId, doc) {
  543. const adminsNumber = ReactiveCache.getUsers({
  544. isAdmin: true,
  545. }).length;
  546. const isAdmin = ReactiveCache.getUser(
  547. {
  548. _id: userId,
  549. },
  550. {
  551. fields: {
  552. isAdmin: 1,
  553. },
  554. },
  555. );
  556. // Prevents remove of the only one administrator
  557. if (adminsNumber === 1 && isAdmin && userId === doc._id) {
  558. return false;
  559. }
  560. // If it's the user or an admin
  561. return userId === doc._id || isAdmin;
  562. },
  563. fetch: [],
  564. });
  565. // Non-Admin users can not change to Admin
  566. Users.deny({
  567. update(userId, board, fieldNames) {
  568. return _.contains(fieldNames, 'isAdmin') && !ReactiveCache.getCurrentUser().isAdmin;
  569. },
  570. fetch: [],
  571. });
  572. // Search a user in the complete server database by its name, username or emails adress. This
  573. // is used for instance to add a new user to a board.
  574. UserSearchIndex = new Index({
  575. collection: Users,
  576. fields: ['username', 'profile.fullname', 'profile.avatarUrl'],
  577. allowedFields: ['username', 'profile.fullname', 'profile.avatarUrl'],
  578. engine: new MongoDBEngine({
  579. fields: function (searchObject, options) {
  580. return {
  581. username: 1,
  582. 'profile.fullname': 1,
  583. 'profile.avatarUrl': 1,
  584. };
  585. },
  586. }),
  587. });
  588. Users.safeFields = {
  589. _id: 1,
  590. username: 1,
  591. 'profile.fullname': 1,
  592. 'profile.avatarUrl': 1,
  593. 'profile.initials': 1,
  594. orgs: 1,
  595. teams: 1,
  596. authenticationMethod: 1,
  597. lastConnectionDate: 1,
  598. };
  599. if (Meteor.isClient) {
  600. Users.helpers({
  601. isBoardMember() {
  602. const board = Utils.getCurrentBoard();
  603. return board && board.hasMember(this._id);
  604. },
  605. isNotNoComments() {
  606. const board = Utils.getCurrentBoard();
  607. return (
  608. board && board.hasMember(this._id) && !board.hasNoComments(this._id)
  609. );
  610. },
  611. isNoComments() {
  612. const board = Utils.getCurrentBoard();
  613. return board && board.hasNoComments(this._id);
  614. },
  615. isNotCommentOnly() {
  616. const board = Utils.getCurrentBoard();
  617. return (
  618. board && board.hasMember(this._id) && !board.hasCommentOnly(this._id)
  619. );
  620. },
  621. isCommentOnly() {
  622. const board = Utils.getCurrentBoard();
  623. return board && board.hasCommentOnly(this._id);
  624. },
  625. isNotWorker() {
  626. const board = Utils.getCurrentBoard();
  627. return board && board.hasMember(this._id) && !board.hasWorker(this._id);
  628. },
  629. isWorker() {
  630. const board = Utils.getCurrentBoard();
  631. return board && board.hasWorker(this._id);
  632. },
  633. isBoardAdmin(boardId) {
  634. let board;
  635. if (boardId) {
  636. board = ReactiveCache.getBoard(boardId);
  637. } else {
  638. board = Utils.getCurrentBoard();
  639. }
  640. return board && board.hasAdmin(this._id);
  641. },
  642. });
  643. }
  644. Users.parseImportUsernames = (usernamesString) => {
  645. return usernamesString.trim().split(new RegExp('\\s*[,;]\\s*'));
  646. };
  647. Users.helpers({
  648. importUsernamesString() {
  649. if (this.importUsernames) {
  650. return this.importUsernames.join(', ');
  651. }
  652. return '';
  653. },
  654. teamIds() {
  655. if (this.teams) {
  656. // TODO: Should the Team collection be queried to determine if the team isActive?
  657. return this.teams.map((team) => {
  658. return team.teamId;
  659. });
  660. }
  661. return [];
  662. },
  663. orgIds() {
  664. if (this.orgs) {
  665. // TODO: Should the Org collection be queried to determine if the organization isActive?
  666. return this.orgs.map((org) => {
  667. return org.orgId;
  668. });
  669. }
  670. return [];
  671. },
  672. orgsUserBelongs() {
  673. if (this.orgs) {
  674. return this.orgs
  675. .map(function (org) {
  676. return org.orgDisplayName;
  677. })
  678. .sort()
  679. .join(',');
  680. }
  681. return '';
  682. },
  683. orgIdsUserBelongs() {
  684. if (this.orgs) {
  685. return this.orgs
  686. .map(function (org) {
  687. return org.orgId;
  688. })
  689. .join(',');
  690. }
  691. return '';
  692. },
  693. teamsUserBelongs() {
  694. if (this.teams) {
  695. return this.teams
  696. .map(function (team) {
  697. return team.teamDisplayName;
  698. })
  699. .sort()
  700. .join(',');
  701. }
  702. return '';
  703. },
  704. teamIdsUserBelongs() {
  705. if (this.teams) {
  706. return this.teams
  707. .map(function (team) {
  708. return team.teamId;
  709. })
  710. .join(',');
  711. }
  712. return '';
  713. },
  714. boards() {
  715. return Boards.userBoards(this._id, null, {}, { sort: { sort: 1 } });
  716. },
  717. starredBoards() {
  718. const { starredBoards = [] } = this.profile || {};
  719. return Boards.userBoards(
  720. this._id,
  721. false,
  722. { _id: { $in: starredBoards } },
  723. { sort: { sort: 1 } },
  724. );
  725. },
  726. hasStarred(boardId) {
  727. const { starredBoards = [] } = this.profile || {};
  728. return _.contains(starredBoards, boardId);
  729. },
  730. collapsedLists() {
  731. const { collapsedLists = [] } = this.profile || {};
  732. return Lists.userLists(
  733. this._id,
  734. false,
  735. { _id: { $in: collapsedLists } },
  736. { sort: { sort: 1 } },
  737. );
  738. },
  739. hasCollapsedList(listId) {
  740. const { collapsedLists = [] } = this.profile || {};
  741. return _.contains(collapsedLists, listId);
  742. },
  743. collapsedSwimlanes() {
  744. const { collapsedSwimlanes = [] } = this.profile || {};
  745. return Swimlanes.userSwimlanes(
  746. this._id,
  747. false,
  748. { _id: { $in: collapsedSwimlanes } },
  749. { sort: { sort: 1 } },
  750. );
  751. },
  752. hasCollapsedSwimlane(swimlaneId) {
  753. const { collapsedSwimlanes = [] } = this.profile || {};
  754. return _.contains(collapsedSwimlanes, swimlaneId);
  755. },
  756. invitedBoards() {
  757. const { invitedBoards = [] } = this.profile || {};
  758. return Boards.userBoards(
  759. this._id,
  760. false,
  761. { _id: { $in: invitedBoards } },
  762. { sort: { sort: 1 } },
  763. );
  764. },
  765. isInvitedTo(boardId) {
  766. const { invitedBoards = [] } = this.profile || {};
  767. return _.contains(invitedBoards, boardId);
  768. },
  769. _getListSortBy() {
  770. const profile = this.profile || {};
  771. const sortBy = profile.listSortBy || defaultSortBy;
  772. const keyPattern = /^(-{0,1})(.*$)/;
  773. const ret = [];
  774. if (keyPattern.exec(sortBy)) {
  775. ret[0] = RegExp.$2;
  776. ret[1] = RegExp.$1 ? -1 : 1;
  777. }
  778. return ret;
  779. },
  780. hasSortBy() {
  781. // if use doesn't have dragHandle, then we can let user to choose sort list by different order
  782. return !this.hasShowDesktopDragHandles();
  783. },
  784. getListSortBy() {
  785. return this._getListSortBy()[0];
  786. },
  787. getListSortTypes() {
  788. return allowedSortValues;
  789. },
  790. getListSortByDirection() {
  791. return this._getListSortBy()[1];
  792. },
  793. getListWidths() {
  794. const { listWidths = {} } = this.profile || {};
  795. return listWidths;
  796. },
  797. getListWidth(boardId, listId) {
  798. const listWidths = this.getListWidths();
  799. if (listWidths[boardId] && listWidths[boardId][listId]) {
  800. return listWidths[boardId][listId];
  801. } else {
  802. return 270; //TODO(mark-i-m): default?
  803. }
  804. },
  805. getSwimlaneHeights() {
  806. const { swimlaneHeights = {} } = this.profile || {};
  807. return swimlaneHeights;
  808. },
  809. getSwimlaneHeight(boardId, listId) {
  810. const swimlaneHeights = this.getSwimlaneHeights();
  811. if (swimlaneHeights[boardId] && swimlaneHeights[boardId][listId]) {
  812. return swimlaneHeights[boardId][listId];
  813. } else {
  814. return -1;
  815. }
  816. },
  817. /** returns all confirmed move and copy dialog field values
  818. * <li> the board, swimlane and list id is stored for each board
  819. */
  820. getMoveAndCopyDialogOptions() {
  821. let _ret = {};
  822. if (this.profile && this.profile.moveAndCopyDialog) {
  823. _ret = this.profile.moveAndCopyDialog;
  824. }
  825. return _ret;
  826. },
  827. /** returns all confirmed move checklist dialog field values
  828. * <li> the board, swimlane, list and card id is stored for each board
  829. */
  830. getMoveChecklistDialogOptions() {
  831. let _ret = {};
  832. if (this.profile && this.profile.moveChecklistDialog) {
  833. _ret = this.profile.moveChecklistDialog;
  834. }
  835. return _ret;
  836. },
  837. /** returns all confirmed copy checklist dialog field values
  838. * <li> the board, swimlane, list and card id is stored for each board
  839. */
  840. getCopyChecklistDialogOptions() {
  841. let _ret = {};
  842. if (this.profile && this.profile.copyChecklistDialog) {
  843. _ret = this.profile.copyChecklistDialog;
  844. }
  845. return _ret;
  846. },
  847. hasTag(tag) {
  848. const { tags = [] } = this.profile || {};
  849. return _.contains(tags, tag);
  850. },
  851. hasNotification(activityId) {
  852. const { notifications = [] } = this.profile || {};
  853. return _.contains(notifications, activityId);
  854. },
  855. notifications() {
  856. const { notifications = [] } = this.profile || {};
  857. for (const index in notifications) {
  858. if (!notifications.hasOwnProperty(index)) continue;
  859. const notification = notifications[index];
  860. // this preserves their db sort order for editing
  861. notification.dbIndex = index;
  862. if (!notification.activityObj && typeof(notification.activity) === 'string') {
  863. notification.activityObj = ReactiveMiniMongoIndex.getActivityWithId(notification.activity);
  864. }
  865. }
  866. // newest first. don't use reverse() because it changes the array inplace, so sometimes the array is reversed twice and oldest items at top again
  867. const ret = notifications.toReversed();
  868. return ret;
  869. },
  870. hasShowDesktopDragHandles() {
  871. const profile = this.profile || {};
  872. return profile.showDesktopDragHandles || false;
  873. },
  874. hasHideCheckedItems() {
  875. const profile = this.profile || {};
  876. return profile.hideCheckedItems || false;
  877. },
  878. hasHiddenSystemMessages() {
  879. const profile = this.profile || {};
  880. return profile.hiddenSystemMessages || false;
  881. },
  882. hasCustomFieldsGrid() {
  883. const profile = this.profile || {};
  884. return profile.customFieldsGrid || false;
  885. },
  886. hasCardMaximized() {
  887. const profile = this.profile || {};
  888. return profile.cardMaximized || false;
  889. },
  890. hasHiddenMinicardLabelText() {
  891. const profile = this.profile || {};
  892. return profile.hiddenMinicardLabelText || false;
  893. },
  894. hasRescuedCardDescription() {
  895. const profile = this.profile || {};
  896. return profile.rescueCardDescription || false;
  897. },
  898. getEmailBuffer() {
  899. const { emailBuffer = [] } = this.profile || {};
  900. return emailBuffer;
  901. },
  902. getInitials() {
  903. const profile = this.profile || {};
  904. if (profile.initials) return profile.initials;
  905. else if (profile.fullname) {
  906. return profile.fullname
  907. .split(/\s+/)
  908. .reduce((memo, word) => {
  909. return memo + word[0];
  910. }, '')
  911. .toUpperCase();
  912. } else {
  913. return this.username[0].toUpperCase();
  914. }
  915. },
  916. getLimitToShowCardsCount() {
  917. const profile = this.profile || {};
  918. return profile.showCardsCountAt;
  919. },
  920. getName() {
  921. const profile = this.profile || {};
  922. return profile.fullname || this.username;
  923. },
  924. getLanguage() {
  925. const profile = this.profile || {};
  926. return profile.language || 'en';
  927. },
  928. getStartDayOfWeek() {
  929. const profile = this.profile || {};
  930. if (typeof profile.startDayOfWeek === 'undefined') {
  931. // default is 'Monday' (1)
  932. return 1;
  933. }
  934. return profile.startDayOfWeek;
  935. },
  936. getTemplatesBoardId() {
  937. return (this.profile || {}).templatesBoardId;
  938. },
  939. getTemplatesBoardSlug() {
  940. //return (ReactiveCache.getBoard((this.profile || {}).templatesBoardId) || {}).slug;
  941. return 'templates';
  942. },
  943. remove() {
  944. User.remove({
  945. _id: this._id,
  946. });
  947. },
  948. });
  949. Users.mutations({
  950. /** set the confirmed board id/swimlane id/list id of a board
  951. * @param boardId the current board id
  952. * @param options an object with the confirmed field values
  953. */
  954. setMoveAndCopyDialogOption(boardId, options) {
  955. let currentOptions = this.getMoveAndCopyDialogOptions();
  956. currentOptions[boardId] = options;
  957. return {
  958. $set: {
  959. 'profile.moveAndCopyDialog': currentOptions,
  960. },
  961. };
  962. },
  963. /** set the confirmed board id/swimlane id/list id/card id of a board (move checklist)
  964. * @param boardId the current board id
  965. * @param options an object with the confirmed field values
  966. */
  967. setMoveChecklistDialogOption(boardId, options) {
  968. let currentOptions = this.getMoveChecklistDialogOptions();
  969. currentOptions[boardId] = options;
  970. return {
  971. $set: {
  972. 'profile.moveChecklistDialog': currentOptions,
  973. },
  974. };
  975. },
  976. /** set the confirmed board id/swimlane id/list id/card id of a board (copy checklist)
  977. * @param boardId the current board id
  978. * @param options an object with the confirmed field values
  979. */
  980. setCopyChecklistDialogOption(boardId, options) {
  981. let currentOptions = this.getCopyChecklistDialogOptions();
  982. currentOptions[boardId] = options;
  983. return {
  984. $set: {
  985. 'profile.copyChecklistDialog': currentOptions,
  986. },
  987. };
  988. },
  989. toggleBoardStar(boardId) {
  990. const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
  991. return {
  992. [queryKind]: {
  993. 'profile.starredBoards': boardId,
  994. },
  995. };
  996. },
  997. toggleCollapseList(listId) {
  998. const queryKind = this.hasCollapsedList(listId) ? '$pull' : '$addToSet';
  999. return {
  1000. [queryKind]: {
  1001. 'profile.listCollapsed': listId,
  1002. },
  1003. };
  1004. },
  1005. toggleCollapseSwimlane(swimlaneId) {
  1006. const queryKind = this.hasCollapsedSwimlane(swimlaneId) ? '$pull' : '$addToSet';
  1007. return {
  1008. [queryKind]: {
  1009. 'profile.swimlaneCollapsed': swimlaneId,
  1010. },
  1011. };
  1012. },
  1013. addInvite(boardId) {
  1014. return {
  1015. $addToSet: {
  1016. 'profile.invitedBoards': boardId,
  1017. },
  1018. };
  1019. },
  1020. removeInvite(boardId) {
  1021. return {
  1022. $pull: {
  1023. 'profile.invitedBoards': boardId,
  1024. },
  1025. };
  1026. },
  1027. addTag(tag) {
  1028. return {
  1029. $addToSet: {
  1030. 'profile.tags': tag,
  1031. },
  1032. };
  1033. },
  1034. removeTag(tag) {
  1035. return {
  1036. $pull: {
  1037. 'profile.tags': tag,
  1038. },
  1039. };
  1040. },
  1041. toggleTag(tag) {
  1042. if (this.hasTag(tag)) this.removeTag(tag);
  1043. else this.addTag(tag);
  1044. },
  1045. setListSortBy(value) {
  1046. return {
  1047. $set: {
  1048. 'profile.listSortBy': value,
  1049. },
  1050. };
  1051. },
  1052. setName(value) {
  1053. return {
  1054. $set: {
  1055. 'profile.fullname': value,
  1056. },
  1057. };
  1058. },
  1059. toggleDesktopHandles(value = false) {
  1060. return {
  1061. $set: {
  1062. 'profile.showDesktopDragHandles': !value,
  1063. },
  1064. };
  1065. },
  1066. toggleHideCheckedItems() {
  1067. const value = this.hasHideCheckedItems();
  1068. return {
  1069. $set: {
  1070. 'profile.hideCheckedItems': !value,
  1071. },
  1072. };
  1073. },
  1074. toggleSystem(value = false) {
  1075. return {
  1076. $set: {
  1077. 'profile.hiddenSystemMessages': !value,
  1078. },
  1079. };
  1080. },
  1081. toggleFieldsGrid(value = false) {
  1082. return {
  1083. $set: {
  1084. 'profile.customFieldsGrid': !value,
  1085. },
  1086. };
  1087. },
  1088. toggleCardMaximized(value = false) {
  1089. return {
  1090. $set: {
  1091. 'profile.cardMaximized': !value,
  1092. },
  1093. };
  1094. },
  1095. toggleLabelText(value = false) {
  1096. return {
  1097. $set: {
  1098. 'profile.hiddenMinicardLabelText': !value,
  1099. },
  1100. };
  1101. },
  1102. toggleRescueCardDescription(value = false) {
  1103. return {
  1104. $set: {
  1105. 'profile.rescueCardDescription': !value,
  1106. },
  1107. };
  1108. },
  1109. addNotification(activityId) {
  1110. return {
  1111. $addToSet: {
  1112. 'profile.notifications': {
  1113. activity: activityId,
  1114. },
  1115. },
  1116. };
  1117. },
  1118. removeNotification(activityId) {
  1119. return {
  1120. $pull: {
  1121. 'profile.notifications': {
  1122. activity: activityId,
  1123. },
  1124. },
  1125. };
  1126. },
  1127. addEmailBuffer(text) {
  1128. return {
  1129. $addToSet: {
  1130. 'profile.emailBuffer': text,
  1131. },
  1132. };
  1133. },
  1134. clearEmailBuffer() {
  1135. return {
  1136. $set: {
  1137. 'profile.emailBuffer': [],
  1138. },
  1139. };
  1140. },
  1141. setAvatarUrl(avatarUrl) {
  1142. return {
  1143. $set: {
  1144. 'profile.avatarUrl': avatarUrl,
  1145. },
  1146. };
  1147. },
  1148. setShowCardsCountAt(limit) {
  1149. return {
  1150. $set: {
  1151. 'profile.showCardsCountAt': limit,
  1152. },
  1153. };
  1154. },
  1155. setStartDayOfWeek(startDay) {
  1156. return {
  1157. $set: {
  1158. 'profile.startDayOfWeek': startDay,
  1159. },
  1160. };
  1161. },
  1162. setBoardView(view) {
  1163. return {
  1164. $set: {
  1165. 'profile.boardView': view,
  1166. },
  1167. };
  1168. },
  1169. setListWidth(boardId, listId, width) {
  1170. let currentWidths = this.getListWidths();
  1171. if (!currentWidths[boardId]) {
  1172. currentWidths[boardId] = {};
  1173. }
  1174. currentWidths[boardId][listId] = width;
  1175. return {
  1176. $set: {
  1177. 'profile.listWidths': currentWidths,
  1178. },
  1179. };
  1180. },
  1181. setSwimlaneHeight(boardId, swimlaneId, height) {
  1182. let currentHeights = this.getSwimlaneHeights();
  1183. if (!currentHeights[boardId]) {
  1184. currentHeights[boardId] = {};
  1185. }
  1186. currentHeights[boardId][swimlaneId] = height;
  1187. return {
  1188. $set: {
  1189. 'profile.swimlaneHeights': currentHeights,
  1190. },
  1191. };
  1192. },
  1193. });
  1194. Meteor.methods({
  1195. setListSortBy(value) {
  1196. check(value, String);
  1197. ReactiveCache.getCurrentUser().setListSortBy(value);
  1198. },
  1199. toggleDesktopDragHandles() {
  1200. const user = ReactiveCache.getCurrentUser();
  1201. user.toggleDesktopHandles(user.hasShowDesktopDragHandles());
  1202. },
  1203. toggleHideCheckedItems() {
  1204. const user = ReactiveCache.getCurrentUser();
  1205. user.toggleHideCheckedItems();
  1206. },
  1207. toggleSystemMessages() {
  1208. const user = ReactiveCache.getCurrentUser();
  1209. user.toggleSystem(user.hasHiddenSystemMessages());
  1210. },
  1211. toggleCustomFieldsGrid() {
  1212. const user = ReactiveCache.getCurrentUser();
  1213. user.toggleFieldsGrid(user.hasCustomFieldsGrid());
  1214. },
  1215. toggleCardMaximized() {
  1216. const user = ReactiveCache.getCurrentUser();
  1217. user.toggleCardMaximized(user.hasCardMaximized());
  1218. },
  1219. toggleMinicardLabelText() {
  1220. const user = ReactiveCache.getCurrentUser();
  1221. user.toggleLabelText(user.hasHiddenMinicardLabelText());
  1222. },
  1223. toggleRescueCardDescription() {
  1224. const user = ReactiveCache.getCurrentUser();
  1225. user.toggleRescueCardDescription(user.hasRescuedCardDescription());
  1226. },
  1227. changeLimitToShowCardsCount(limit) {
  1228. check(limit, Number);
  1229. ReactiveCache.getCurrentUser().setShowCardsCountAt(limit);
  1230. },
  1231. changeStartDayOfWeek(startDay) {
  1232. check(startDay, Number);
  1233. ReactiveCache.getCurrentUser().setStartDayOfWeek(startDay);
  1234. },
  1235. applyListWidth(boardId, listId, width) {
  1236. check(boardId, String);
  1237. check(listId, String);
  1238. check(width, Number);
  1239. const user = Meteor.user();
  1240. user.setListWidth(boardId, listId, width);
  1241. },
  1242. applySwimlaneHeight(boardId, swimlaneId, height) {
  1243. check(boardId, String);
  1244. check(swimlaneId, String);
  1245. check(height, Number);
  1246. const user = Meteor.user();
  1247. user.setSwimlaneHeight(boardId, swimlaneId, height);
  1248. },
  1249. });
  1250. if (Meteor.isServer) {
  1251. Meteor.methods({
  1252. setAllUsersHideSystemMessages() {
  1253. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1254. // If setting is missing, add it
  1255. Users.update(
  1256. {
  1257. 'profile.hiddenSystemMessages': {
  1258. $exists: false,
  1259. },
  1260. },
  1261. {
  1262. $set: {
  1263. 'profile.hiddenSystemMessages': true,
  1264. },
  1265. },
  1266. {
  1267. multi: true,
  1268. },
  1269. );
  1270. // If setting is false, set it to true
  1271. Users.update(
  1272. {
  1273. 'profile.hiddenSystemMessages': false,
  1274. },
  1275. {
  1276. $set: {
  1277. 'profile.hiddenSystemMessages': true,
  1278. },
  1279. },
  1280. {
  1281. multi: true,
  1282. },
  1283. );
  1284. return true;
  1285. } else {
  1286. return false;
  1287. }
  1288. },
  1289. setCreateUser(
  1290. fullname,
  1291. username,
  1292. initials,
  1293. password,
  1294. isAdmin,
  1295. isActive,
  1296. email,
  1297. importUsernames,
  1298. userOrgsArray,
  1299. userTeamsArray,
  1300. ) {
  1301. check(fullname, String);
  1302. check(username, String);
  1303. check(initials, String);
  1304. check(password, String);
  1305. check(isAdmin, String);
  1306. check(isActive, String);
  1307. check(email, String);
  1308. check(importUsernames, Array);
  1309. check(userOrgsArray, Array);
  1310. check(userTeamsArray, Array);
  1311. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1312. // Thanks to mc-marcy and xet7 !
  1313. if (fullname.includes('/') ||
  1314. username.includes('/') ||
  1315. email.includes('/') ||
  1316. initials.includes('/')) {
  1317. return false;
  1318. }
  1319. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1320. const nUsersWithUsername = ReactiveCache.getUsers({
  1321. username,
  1322. }).length;
  1323. const nUsersWithEmail = ReactiveCache.getUsers({
  1324. email,
  1325. }).length;
  1326. if (nUsersWithUsername > 0) {
  1327. throw new Meteor.Error('username-already-taken');
  1328. } else if (nUsersWithEmail > 0) {
  1329. throw new Meteor.Error('email-already-taken');
  1330. } else {
  1331. Accounts.createUser({
  1332. username,
  1333. password,
  1334. isAdmin,
  1335. isActive,
  1336. email: email.toLowerCase(),
  1337. from: 'admin',
  1338. });
  1339. const user =
  1340. ReactiveCache.getUser(username) ||
  1341. ReactiveCache.getUser({ username });
  1342. if (user) {
  1343. Users.update(user._id, {
  1344. $set: {
  1345. 'profile.fullname': fullname,
  1346. importUsernames,
  1347. 'profile.initials': initials,
  1348. orgs: userOrgsArray,
  1349. teams: userTeamsArray,
  1350. },
  1351. });
  1352. }
  1353. }
  1354. }
  1355. },
  1356. setUsername(username, userId) {
  1357. check(username, String);
  1358. check(userId, String);
  1359. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1360. // Thanks to mc-marcy and xet7 !
  1361. if (username.includes('/') ||
  1362. userId.includes('/')) {
  1363. return false;
  1364. }
  1365. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1366. const nUsersWithUsername = ReactiveCache.getUsers({
  1367. username,
  1368. }).length;
  1369. if (nUsersWithUsername > 0) {
  1370. throw new Meteor.Error('username-already-taken');
  1371. } else {
  1372. Users.update(userId, {
  1373. $set: {
  1374. username,
  1375. },
  1376. });
  1377. }
  1378. }
  1379. },
  1380. setEmail(email, userId) {
  1381. check(email, String);
  1382. check(username, String);
  1383. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1384. // Thanks to mc-marcy and xet7 !
  1385. if (username.includes('/') ||
  1386. email.includes('/')) {
  1387. return false;
  1388. }
  1389. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1390. if (Array.isArray(email)) {
  1391. email = email.shift();
  1392. }
  1393. const existingUser = ReactiveCache.getUser(
  1394. {
  1395. 'emails.address': email,
  1396. },
  1397. {
  1398. fields: {
  1399. _id: 1,
  1400. },
  1401. },
  1402. );
  1403. if (existingUser) {
  1404. throw new Meteor.Error('email-already-taken');
  1405. } else {
  1406. Users.update(userId, {
  1407. $set: {
  1408. emails: [
  1409. {
  1410. address: email,
  1411. verified: false,
  1412. },
  1413. ],
  1414. },
  1415. });
  1416. }
  1417. }
  1418. },
  1419. setUsernameAndEmail(username, email, userId) {
  1420. check(username, String);
  1421. check(email, String);
  1422. check(userId, String);
  1423. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1424. // Thanks to mc-marcy and xet7 !
  1425. if (username.includes('/') ||
  1426. email.includes('/') ||
  1427. userId.includes('/')) {
  1428. return false;
  1429. }
  1430. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1431. if (Array.isArray(email)) {
  1432. email = email.shift();
  1433. }
  1434. Meteor.call('setUsername', username, userId);
  1435. Meteor.call('setEmail', email, userId);
  1436. }
  1437. },
  1438. setPassword(newPassword, userId) {
  1439. check(userId, String);
  1440. check(newPassword, String);
  1441. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1442. Accounts.setPassword(userId, newPassword);
  1443. }
  1444. },
  1445. setEmailVerified(email, verified, userId) {
  1446. check(email, String);
  1447. check(verified, Boolean);
  1448. check(userId, String);
  1449. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1450. // Thanks to mc-marcy and xet7 !
  1451. if (email.includes('/') ||
  1452. userId.includes('/')) {
  1453. return false;
  1454. }
  1455. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1456. Users.update(userId, {
  1457. $set: {
  1458. emails: [
  1459. {
  1460. address: email,
  1461. verified,
  1462. },
  1463. ],
  1464. },
  1465. });
  1466. }
  1467. },
  1468. setInitials(initials, userId) {
  1469. check(initials, String);
  1470. check(userId, String);
  1471. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1472. // Thanks to mc-marcy and xet7 !
  1473. if (initials.includes('/') ||
  1474. userId.includes('/')) {
  1475. return false;
  1476. }
  1477. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1478. Users.update(userId, {
  1479. $set: {
  1480. 'profile.initials': initials,
  1481. },
  1482. });
  1483. }
  1484. },
  1485. // we accept userId, username, email
  1486. inviteUserToBoard(username, boardId) {
  1487. check(username, String);
  1488. check(boardId, String);
  1489. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1490. // Thanks to mc-marcy and xet7 !
  1491. if (username.includes('/') ||
  1492. boardId.includes('/')) {
  1493. return false;
  1494. }
  1495. const inviter = ReactiveCache.getCurrentUser();
  1496. const board = ReactiveCache.getBoard(boardId);
  1497. const allowInvite =
  1498. inviter &&
  1499. board &&
  1500. board.members &&
  1501. _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
  1502. _.where(board.members, {
  1503. userId: inviter._id,
  1504. })[0].isActive;
  1505. // GitHub issue 2060
  1506. //_.where(board.members, { userId: inviter._id })[0].isAdmin;
  1507. if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
  1508. this.unblock();
  1509. const posAt = username.indexOf('@');
  1510. let user = null;
  1511. if (posAt >= 0) {
  1512. user = ReactiveCache.getUser({
  1513. emails: {
  1514. $elemMatch: {
  1515. address: username,
  1516. },
  1517. },
  1518. });
  1519. } else {
  1520. user =
  1521. ReactiveCache.getUser(username) ||
  1522. ReactiveCache.getUser({ username });
  1523. }
  1524. if (user) {
  1525. if (user._id === inviter._id)
  1526. throw new Meteor.Error('error-user-notAllowSelf');
  1527. } else {
  1528. if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
  1529. if (ReactiveCache.getCurrentSetting().disableRegistration) {
  1530. throw new Meteor.Error('error-user-notCreated');
  1531. }
  1532. // Set in lowercase email before creating account
  1533. const email = username.toLowerCase();
  1534. username = email.substring(0, posAt);
  1535. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1536. // Thanks to mc-marcy and xet7 !
  1537. if (username.includes('/') ||
  1538. email.includes('/')) {
  1539. return false;
  1540. }
  1541. const newUserId = Accounts.createUser({
  1542. username,
  1543. email,
  1544. });
  1545. if (!newUserId) throw new Meteor.Error('error-user-notCreated');
  1546. // assume new user speak same language with inviter
  1547. if (inviter.profile && inviter.profile.language) {
  1548. Users.update(newUserId, {
  1549. $set: {
  1550. 'profile.language': inviter.profile.language,
  1551. },
  1552. });
  1553. }
  1554. Accounts.sendEnrollmentEmail(newUserId);
  1555. user = ReactiveCache.getUser(newUserId);
  1556. }
  1557. board.addMember(user._id);
  1558. user.addInvite(boardId);
  1559. //Check if there is a subtasks board
  1560. if (board.subtasksDefaultBoardId) {
  1561. const subBoard = ReactiveCache.getBoard(board.subtasksDefaultBoardId);
  1562. //If there is, also add user to that board
  1563. if (subBoard) {
  1564. subBoard.addMember(user._id);
  1565. user.addInvite(subBoard._id);
  1566. }
  1567. }
  1568. try {
  1569. const fullName =
  1570. inviter.profile !== undefined &&
  1571. inviter.profile.fullname !== undefined
  1572. ? inviter.profile.fullname
  1573. : '';
  1574. const userFullName =
  1575. user.profile !== undefined && user.profile.fullname !== undefined
  1576. ? user.profile.fullname
  1577. : '';
  1578. const params = {
  1579. user:
  1580. userFullName != ''
  1581. ? userFullName + ' (' + user.username + ' )'
  1582. : user.username,
  1583. inviter:
  1584. fullName != ''
  1585. ? fullName + ' (' + inviter.username + ' )'
  1586. : inviter.username,
  1587. board: board.title,
  1588. url: board.absoluteUrl(),
  1589. };
  1590. const lang = user.getLanguage();
  1591. /*
  1592. if (process.env.MAIL_SERVICE !== '') {
  1593. let transporter = nodemailer.createTransport({
  1594. service: process.env.MAIL_SERVICE,
  1595. auth: {
  1596. user: process.env.MAIL_SERVICE_USER,
  1597. pass: process.env.MAIL_SERVICE_PASSWORD
  1598. },
  1599. })
  1600. let info = transporter.sendMail({
  1601. to: user.emails[0].address.toLowerCase(),
  1602. from: Accounts.emailTemplates.from,
  1603. subject: TAPi18n.__('email-invite-subject', params, lang),
  1604. text: TAPi18n.__('email-invite-text', params, lang),
  1605. })
  1606. } else {
  1607. Email.send({
  1608. to: user.emails[0].address.toLowerCase(),
  1609. from: Accounts.emailTemplates.from,
  1610. subject: TAPi18n.__('email-invite-subject', params, lang),
  1611. text: TAPi18n.__('email-invite-text', params, lang),
  1612. });
  1613. }
  1614. */
  1615. Email.send({
  1616. to: user.emails[0].address.toLowerCase(),
  1617. from: Accounts.emailTemplates.from,
  1618. subject: TAPi18n.__('email-invite-subject', params, lang),
  1619. text: TAPi18n.__('email-invite-text', params, lang),
  1620. });
  1621. } catch (e) {
  1622. throw new Meteor.Error('email-fail', e.message);
  1623. }
  1624. return {
  1625. username: user.username,
  1626. email: user.emails[0].address,
  1627. };
  1628. },
  1629. impersonate(userId) {
  1630. check(userId, String);
  1631. if (!ReactiveCache.getUser(userId))
  1632. throw new Meteor.Error(404, 'User not found');
  1633. if (!ReactiveCache.getCurrentUser().isAdmin)
  1634. throw new Meteor.Error(403, 'Permission denied');
  1635. ImpersonatedUsers.insert({
  1636. adminId: ReactiveCache.getCurrentUser()._id,
  1637. userId: userId,
  1638. reason: 'clickedImpersonate',
  1639. });
  1640. this.setUserId(userId);
  1641. },
  1642. isImpersonated(userId) {
  1643. check(userId, String);
  1644. const isImpersonated = ReactiveCache.getImpersonatedUser({ userId: userId });
  1645. return isImpersonated;
  1646. },
  1647. setUsersTeamsTeamDisplayName(teamId, teamDisplayName) {
  1648. check(teamId, String);
  1649. check(teamDisplayName, String);
  1650. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1651. ReactiveCache.getUsers({
  1652. teams: {
  1653. $elemMatch: { teamId: teamId },
  1654. },
  1655. }).forEach((user) => {
  1656. Users.update(
  1657. {
  1658. _id: user._id,
  1659. teams: {
  1660. $elemMatch: { teamId: teamId },
  1661. },
  1662. },
  1663. {
  1664. $set: {
  1665. 'teams.$.teamDisplayName': teamDisplayName,
  1666. },
  1667. },
  1668. );
  1669. });
  1670. }
  1671. },
  1672. setUsersOrgsOrgDisplayName(orgId, orgDisplayName) {
  1673. check(orgId, String);
  1674. check(orgDisplayName, String);
  1675. if (ReactiveCache.getCurrentUser()?.isAdmin) {
  1676. ReactiveCache.getUsers({
  1677. orgs: {
  1678. $elemMatch: { orgId: orgId },
  1679. },
  1680. }).forEach((user) => {
  1681. Users.update(
  1682. {
  1683. _id: user._id,
  1684. orgs: {
  1685. $elemMatch: { orgId: orgId },
  1686. },
  1687. },
  1688. {
  1689. $set: {
  1690. 'orgs.$.orgDisplayName': orgDisplayName,
  1691. },
  1692. },
  1693. );
  1694. });
  1695. }
  1696. },
  1697. });
  1698. Accounts.onCreateUser((options, user) => {
  1699. const userCount = ReactiveCache.getUsers({}, {}, true).count();
  1700. user.isAdmin = userCount === 0;
  1701. if (user.services.oidc) {
  1702. let email = user.services.oidc.email;
  1703. if (Array.isArray(email)) {
  1704. email = email.shift();
  1705. }
  1706. email = email.toLowerCase();
  1707. user.username = user.services.oidc.username;
  1708. user.emails = [
  1709. {
  1710. address: email,
  1711. verified: true,
  1712. },
  1713. ];
  1714. // Prevent Hyperlink Injection https://github.com/wekan/wekan/issues/5176
  1715. // Thanks to mc-marcy and xet7 !
  1716. if (user.username.includes('/') ||
  1717. email.includes('/')) {
  1718. return false;
  1719. }
  1720. const initials = user.services.oidc.fullname
  1721. .split(/\s+/)
  1722. .reduce((memo, word) => {
  1723. return memo + word[0];
  1724. }, '')
  1725. .toUpperCase();
  1726. user.profile = {
  1727. initials,
  1728. fullname: user.services.oidc.fullname,
  1729. boardView: 'board-view-swimlanes',
  1730. };
  1731. user.authenticationMethod = 'oauth2';
  1732. // see if any existing user has this email address or username, otherwise create new
  1733. const existingUser = ReactiveCache.getUser({
  1734. $or: [
  1735. {
  1736. 'emails.address': email,
  1737. },
  1738. {
  1739. username: user.username,
  1740. },
  1741. ],
  1742. });
  1743. if (!existingUser) return user;
  1744. // copy across new service info
  1745. const service = _.keys(user.services)[0];
  1746. existingUser.services[service] = user.services[service];
  1747. existingUser.emails = user.emails;
  1748. existingUser.username = user.username;
  1749. existingUser.profile = user.profile;
  1750. existingUser.authenticationMethod = user.authenticationMethod;
  1751. Meteor.users.remove({
  1752. _id: user._id,
  1753. });
  1754. Meteor.users.remove({
  1755. _id: existingUser._id,
  1756. }); // is going to be created again
  1757. return existingUser;
  1758. }
  1759. if (options.from === 'admin') {
  1760. user.createdThroughApi = true;
  1761. return user;
  1762. }
  1763. const disableRegistration = ReactiveCache.getCurrentSetting().disableRegistration;
  1764. // If this is the first Authentication by the ldap and self registration disabled
  1765. if (disableRegistration && options && options.ldap) {
  1766. user.authenticationMethod = 'ldap';
  1767. return user;
  1768. }
  1769. // If self registration enabled
  1770. if (!disableRegistration) {
  1771. return user;
  1772. }
  1773. if (!options || !options.profile) {
  1774. throw new Meteor.Error(
  1775. 'error-invitation-code-blank',
  1776. 'The invitation code is required',
  1777. );
  1778. }
  1779. const invitationCode = ReactiveCache.getInvitationCode({
  1780. code: options.profile.invitationcode,
  1781. email: options.email,
  1782. valid: true,
  1783. });
  1784. if (!invitationCode) {
  1785. throw new Meteor.Error(
  1786. 'error-invitation-code-not-exist',
  1787. // eslint-disable-next-line quotes
  1788. "The invitation code doesn't exist",
  1789. );
  1790. } else {
  1791. user.profile = {
  1792. icode: options.profile.invitationcode,
  1793. };
  1794. user.profile.boardView = 'board-view-swimlanes';
  1795. // Deletes the invitation code after the user was created successfully.
  1796. setTimeout(
  1797. Meteor.bindEnvironment(() => {
  1798. InvitationCodes.remove({
  1799. _id: invitationCode._id,
  1800. });
  1801. }),
  1802. 200,
  1803. );
  1804. return user;
  1805. }
  1806. });
  1807. }
  1808. const addCronJob = _.debounce(
  1809. Meteor.bindEnvironment(function notificationCleanupDebounced() {
  1810. // passed in the removeAge has to be a number standing for the number of days after a notification is read before we remove it
  1811. const envRemoveAge =
  1812. process.env.NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE;
  1813. // default notifications will be removed 2 days after they are read
  1814. const defaultRemoveAge = 2;
  1815. const removeAge = parseInt(envRemoveAge, 10) || defaultRemoveAge;
  1816. SyncedCron.add({
  1817. name: 'notification_cleanup',
  1818. schedule: (parser) => parser.text('every 1 days'),
  1819. job: () => {
  1820. for (const user of ReactiveCache.getUsers()) {
  1821. if (!user.profile || !user.profile.notifications) continue;
  1822. for (const notification of user.profile.notifications) {
  1823. if (notification.read) {
  1824. const removeDate = new Date(notification.read);
  1825. removeDate.setDate(removeDate.getDate() + removeAge);
  1826. if (removeDate <= new Date()) {
  1827. user.removeNotification(notification.activity);
  1828. }
  1829. }
  1830. }
  1831. }
  1832. },
  1833. });
  1834. SyncedCron.start();
  1835. }),
  1836. 500,
  1837. );
  1838. if (Meteor.isServer) {
  1839. // Let mongoDB ensure username unicity
  1840. Meteor.startup(() => {
  1841. allowedSortValues.forEach((value) => {
  1842. Lists._collection.createIndex(value);
  1843. });
  1844. Users._collection.createIndex({
  1845. modifiedAt: -1,
  1846. });
  1847. // Avatar URLs from CollectionFS to Meteor-Files, at users collection avatarUrl field:
  1848. Users.find({ "profile.avatarUrl": { $regex: "/cfs/files/avatars/" } }).forEach(function (doc) {
  1849. doc.profile.avatarUrl = doc.profile.avatarUrl.replace("/cfs/files/avatars/", "/cdn/storage/avatars/");
  1850. // Try to fix Users.save is not a fuction, by commenting it out:
  1851. //Users.save(doc);
  1852. });
  1853. /* TODO: Optionally, for additional complexity:
  1854. a) Support SubURLs with parthname from ROOT_URL
  1855. b) Remove beginning or avatar URL, replace it with pathname and new avatar URL
  1856. c) Does all avatar and attachment URLs need to be fixed every time when starting or restarting?
  1857. d) What if avatar URL is at some other server? In that case, links would point incorrectly to this instance, if ROOT_URL and path part is removed.
  1858. doc.profile.avatarUrl = process.env.ROOT_URL.pathname + doc.profile.avatarUrl.replace("/cfs/files/avatars/", "/cdn/storage/avatars/").substring(str.indexOf("/cdn/storage/avatars"));
  1859. */
  1860. /* Commented out extra index because of IndexOptionsConflict.
  1861. Users._collection.createIndex(
  1862. {
  1863. username: 1,
  1864. },
  1865. {
  1866. unique: true,
  1867. },
  1868. );
  1869. */
  1870. Meteor.defer(() => {
  1871. addCronJob();
  1872. });
  1873. });
  1874. // OLD WAY THIS CODE DID WORK: When user is last admin of board,
  1875. // if admin is removed, board is removed.
  1876. // NOW THIS IS COMMENTED OUT, because other board users still need to be able
  1877. // to use that board, and not have board deleted.
  1878. // Someone can be later changed to be admin of board, by making change to database.
  1879. // TODO: Add UI for changing someone as board admin.
  1880. //Users.before.remove((userId, doc) => {
  1881. // Boards
  1882. // .find({members: {$elemMatch: {userId: doc._id, isAdmin: true}}})
  1883. // .forEach((board) => {
  1884. // // If only one admin for the board
  1885. // if (board.members.filter((e) => e.isAdmin).length === 1) {
  1886. // Boards.remove(board._id);
  1887. // }
  1888. // });
  1889. //});
  1890. // Each board document contains the de-normalized number of users that have
  1891. // starred it. If the user star or unstar a board, we need to update this
  1892. // counter.
  1893. // We need to run this code on the server only, otherwise the incrementation
  1894. // will be done twice.
  1895. Users.after.update(function (userId, user, fieldNames) {
  1896. // The `starredBoards` list is hosted on the `profile` field. If this
  1897. // field hasn't been modificated we don't need to run this hook.
  1898. if (!_.contains(fieldNames, 'profile')) return;
  1899. // To calculate a diff of board starred ids, we get both the previous
  1900. // and the newly board ids list
  1901. function getStarredBoardsIds(doc) {
  1902. return doc.profile && doc.profile.starredBoards;
  1903. }
  1904. const oldIds = getStarredBoardsIds(this.previous);
  1905. const newIds = getStarredBoardsIds(user);
  1906. // The _.difference(a, b) method returns the values from a that are not in
  1907. // b. We use it to find deleted and newly inserted ids by using it in one
  1908. // direction and then in the other.
  1909. function incrementBoards(boardsIds, inc) {
  1910. boardsIds.forEach((boardId) => {
  1911. Boards.update(boardId, {
  1912. $inc: {
  1913. stars: inc,
  1914. },
  1915. });
  1916. });
  1917. }
  1918. incrementBoards(_.difference(oldIds, newIds), -1);
  1919. incrementBoards(_.difference(newIds, oldIds), +1);
  1920. });
  1921. // Override getUserId so that we can TODO get the current userId
  1922. const fakeUserId = new Meteor.EnvironmentVariable();
  1923. const getUserId = CollectionHooks.getUserId;
  1924. CollectionHooks.getUserId = () => {
  1925. return fakeUserId.get() || getUserId();
  1926. };
  1927. if (!isSandstorm) {
  1928. Users.after.insert((userId, doc) => {
  1929. const fakeUser = {
  1930. extendAutoValueContext: {
  1931. userId: doc._id,
  1932. },
  1933. };
  1934. fakeUserId.withValue(doc._id, () => {
  1935. /*
  1936. // Insert the Welcome Board
  1937. Boards.insert({
  1938. title: TAPi18n.__('welcome-board'),
  1939. permission: 'private',
  1940. }, fakeUser, (err, boardId) => {
  1941. Swimlanes.insert({
  1942. title: TAPi18n.__('welcome-swimlane'),
  1943. boardId,
  1944. sort: 1,
  1945. }, fakeUser);
  1946. ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
  1947. Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
  1948. });
  1949. });
  1950. */
  1951. // Insert Template Container
  1952. const Future = require('fibers/future');
  1953. const future1 = new Future();
  1954. const future2 = new Future();
  1955. const future3 = new Future();
  1956. Boards.insert(
  1957. {
  1958. title: TAPi18n.__('templates'),
  1959. permission: 'private',
  1960. type: 'template-container',
  1961. },
  1962. fakeUser,
  1963. (err, boardId) => {
  1964. // Insert the reference to our templates board
  1965. Users.update(fakeUserId.get(), {
  1966. $set: {
  1967. 'profile.templatesBoardId': boardId,
  1968. },
  1969. });
  1970. // Insert the card templates swimlane
  1971. Swimlanes.insert(
  1972. {
  1973. title: TAPi18n.__('card-templates-swimlane'),
  1974. boardId,
  1975. sort: 1,
  1976. type: 'template-container',
  1977. },
  1978. fakeUser,
  1979. (err, swimlaneId) => {
  1980. // Insert the reference to out card templates swimlane
  1981. Users.update(fakeUserId.get(), {
  1982. $set: {
  1983. 'profile.cardTemplatesSwimlaneId': swimlaneId,
  1984. },
  1985. });
  1986. future1.return();
  1987. },
  1988. );
  1989. // Insert the list templates swimlane
  1990. Swimlanes.insert(
  1991. {
  1992. title: TAPi18n.__('list-templates-swimlane'),
  1993. boardId,
  1994. sort: 2,
  1995. type: 'template-container',
  1996. },
  1997. fakeUser,
  1998. (err, swimlaneId) => {
  1999. // Insert the reference to out list templates swimlane
  2000. Users.update(fakeUserId.get(), {
  2001. $set: {
  2002. 'profile.listTemplatesSwimlaneId': swimlaneId,
  2003. },
  2004. });
  2005. future2.return();
  2006. },
  2007. );
  2008. // Insert the board templates swimlane
  2009. Swimlanes.insert(
  2010. {
  2011. title: TAPi18n.__('board-templates-swimlane'),
  2012. boardId,
  2013. sort: 3,
  2014. type: 'template-container',
  2015. },
  2016. fakeUser,
  2017. (err, swimlaneId) => {
  2018. // Insert the reference to out board templates swimlane
  2019. Users.update(fakeUserId.get(), {
  2020. $set: {
  2021. 'profile.boardTemplatesSwimlaneId': swimlaneId,
  2022. },
  2023. });
  2024. future3.return();
  2025. },
  2026. );
  2027. },
  2028. );
  2029. // HACK
  2030. future1.wait();
  2031. future2.wait();
  2032. future3.wait();
  2033. // End of Insert Template Container
  2034. });
  2035. });
  2036. }
  2037. Users.after.insert((userId, doc) => {
  2038. // HACK
  2039. doc = ReactiveCache.getUser(doc._id);
  2040. if (doc.createdThroughApi) {
  2041. // The admin user should be able to create a user despite disabling registration because
  2042. // it is two different things (registration and creation).
  2043. // So, when a new user is created via the api (only admin user can do that) one must avoid
  2044. // the disableRegistration check.
  2045. // Issue : https://github.com/wekan/wekan/issues/1232
  2046. // PR : https://github.com/wekan/wekan/pull/1251
  2047. Users.update(doc._id, {
  2048. $set: {
  2049. createdThroughApi: '',
  2050. },
  2051. });
  2052. return;
  2053. }
  2054. //invite user to corresponding boards
  2055. const disableRegistration = ReactiveCache.getCurrentSetting().disableRegistration;
  2056. // If ldap, bypass the inviation code if the self registration isn't allowed.
  2057. // TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type
  2058. if (doc.authenticationMethod !== 'ldap' && disableRegistration) {
  2059. let invitationCode = null;
  2060. if (doc.authenticationMethod.toLowerCase() == 'oauth2') {
  2061. // OIDC authentication mode
  2062. invitationCode = ReactiveCache.getInvitationCode({
  2063. email: doc.emails[0].address.toLowerCase(),
  2064. valid: true,
  2065. });
  2066. } else {
  2067. invitationCode = ReactiveCache.getInvitationCode({
  2068. code: doc.profile.icode,
  2069. valid: true,
  2070. });
  2071. }
  2072. if (!invitationCode) {
  2073. throw new Meteor.Error('error-invitation-code-not-exist');
  2074. } else {
  2075. invitationCode.boardsToBeInvited.forEach((boardId) => {
  2076. const board = ReactiveCache.getBoard(boardId);
  2077. board.addMember(doc._id);
  2078. });
  2079. if (!doc.profile) {
  2080. doc.profile = {};
  2081. }
  2082. doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
  2083. Users.update(doc._id, {
  2084. $set: {
  2085. profile: doc.profile,
  2086. },
  2087. });
  2088. InvitationCodes.update(invitationCode._id, {
  2089. $set: {
  2090. valid: false,
  2091. },
  2092. });
  2093. }
  2094. }
  2095. });
  2096. }
  2097. // USERS REST API
  2098. if (Meteor.isServer) {
  2099. // Middleware which checks that API is enabled.
  2100. JsonRoutes.Middleware.use(function (req, res, next) {
  2101. const api = req.url.startsWith('/api');
  2102. if ((api === true && process.env.WITH_API === 'true') || api === false) {
  2103. return next();
  2104. } else {
  2105. res.writeHead(301, {
  2106. Location: '/',
  2107. });
  2108. return res.end();
  2109. }
  2110. });
  2111. /**
  2112. * @operation get_current_user
  2113. *
  2114. * @summary returns the current user
  2115. * @return_type Users
  2116. */
  2117. JsonRoutes.add('GET', '/api/user', function (req, res) {
  2118. try {
  2119. Authentication.checkLoggedIn(req.userId);
  2120. const data = ReactiveCache.getUser({
  2121. _id: req.userId,
  2122. });
  2123. delete data.services;
  2124. // get all boards where the user is member of
  2125. let boards = ReactiveCache.getBoards(
  2126. {
  2127. type: 'board',
  2128. 'members.userId': req.userId,
  2129. },
  2130. {
  2131. fields: {
  2132. _id: 1,
  2133. members: 1,
  2134. },
  2135. },
  2136. );
  2137. boards = boards.map((b) => {
  2138. const u = b.members.find((m) => m.userId === req.userId);
  2139. delete u.userId;
  2140. u.boardId = b._id;
  2141. return u;
  2142. });
  2143. data.boards = boards;
  2144. JsonRoutes.sendResult(res, {
  2145. code: 200,
  2146. data,
  2147. });
  2148. } catch (error) {
  2149. JsonRoutes.sendResult(res, {
  2150. code: 200,
  2151. data: error,
  2152. });
  2153. }
  2154. });
  2155. /**
  2156. * @operation get_all_users
  2157. *
  2158. * @summary return all the users
  2159. *
  2160. * @description Only the admin user (the first user) can call the REST API.
  2161. * @return_type [{ _id: string,
  2162. * username: string}]
  2163. */
  2164. JsonRoutes.add('GET', '/api/users', function (req, res) {
  2165. try {
  2166. Authentication.checkUserId(req.userId);
  2167. JsonRoutes.sendResult(res, {
  2168. code: 200,
  2169. data: Meteor.users.find({}).map(function (doc) {
  2170. return {
  2171. _id: doc._id,
  2172. username: doc.username,
  2173. };
  2174. }),
  2175. });
  2176. } catch (error) {
  2177. JsonRoutes.sendResult(res, {
  2178. code: 200,
  2179. data: error,
  2180. });
  2181. }
  2182. });
  2183. /**
  2184. * @operation get_user
  2185. *
  2186. * @summary get a given user
  2187. *
  2188. * @description Only the admin user (the first user) can call the REST API.
  2189. *
  2190. * @param {string} userId the user ID or username
  2191. * @return_type Users
  2192. */
  2193. JsonRoutes.add('GET', '/api/users/:userId', function (req, res) {
  2194. try {
  2195. Authentication.checkUserId(req.userId);
  2196. let id = req.params.userId;
  2197. let user = ReactiveCache.getUser({
  2198. _id: id,
  2199. });
  2200. if (!user) {
  2201. user = ReactiveCache.getUser({
  2202. username: id,
  2203. });
  2204. id = user._id;
  2205. }
  2206. // get all boards where the user is member of
  2207. let boards = ReactiveCache.getBoards(
  2208. {
  2209. type: 'board',
  2210. 'members.userId': id,
  2211. },
  2212. {
  2213. fields: {
  2214. _id: 1,
  2215. members: 1,
  2216. },
  2217. },
  2218. );
  2219. boards = boards.map((b) => {
  2220. const u = b.members.find((m) => m.userId === id);
  2221. delete u.userId;
  2222. u.boardId = b._id;
  2223. return u;
  2224. });
  2225. user.boards = boards;
  2226. JsonRoutes.sendResult(res, {
  2227. code: 200,
  2228. data: user,
  2229. });
  2230. } catch (error) {
  2231. JsonRoutes.sendResult(res, {
  2232. code: 200,
  2233. data: error,
  2234. });
  2235. }
  2236. });
  2237. /**
  2238. * @operation edit_user
  2239. *
  2240. * @summary edit a given user
  2241. *
  2242. * @description Only the admin user (the first user) can call the REST API.
  2243. *
  2244. * Possible values for *action*:
  2245. * - `takeOwnership`: The admin takes the ownership of ALL boards of the user (archived and not archived) where the user is admin on.
  2246. * - `disableLogin`: Disable a user (the user is not allowed to login and his login tokens are purged)
  2247. * - `enableLogin`: Enable a user
  2248. *
  2249. * @param {string} userId the user ID
  2250. * @param {string} action the action
  2251. * @return_type {_id: string,
  2252. * title: string}
  2253. */
  2254. JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) {
  2255. try {
  2256. Authentication.checkUserId(req.userId);
  2257. const id = req.params.userId;
  2258. const action = req.body.action;
  2259. let data = ReactiveCache.getUser({
  2260. _id: id,
  2261. });
  2262. if (data !== undefined) {
  2263. if (action === 'takeOwnership') {
  2264. data = ReactiveCache.getBoards(
  2265. {
  2266. 'members.userId': id,
  2267. 'members.isAdmin': true,
  2268. },
  2269. {
  2270. sort: {
  2271. sort: 1 /* boards default sorting */,
  2272. },
  2273. },
  2274. ).map(function (board) {
  2275. if (board.hasMember(req.userId)) {
  2276. board.removeMember(req.userId);
  2277. }
  2278. board.changeOwnership(id, req.userId);
  2279. return {
  2280. _id: board._id,
  2281. title: board.title,
  2282. };
  2283. });
  2284. } else {
  2285. if (action === 'disableLogin' && id !== req.userId) {
  2286. Users.update(
  2287. {
  2288. _id: id,
  2289. },
  2290. {
  2291. $set: {
  2292. loginDisabled: true,
  2293. 'services.resume.loginTokens': '',
  2294. },
  2295. },
  2296. );
  2297. } else if (action === 'enableLogin') {
  2298. Users.update(
  2299. {
  2300. _id: id,
  2301. },
  2302. {
  2303. $set: {
  2304. loginDisabled: '',
  2305. },
  2306. },
  2307. );
  2308. }
  2309. data = ReactiveCache.getUser(id);
  2310. }
  2311. }
  2312. JsonRoutes.sendResult(res, {
  2313. code: 200,
  2314. data,
  2315. });
  2316. } catch (error) {
  2317. JsonRoutes.sendResult(res, {
  2318. code: 200,
  2319. data: error,
  2320. });
  2321. }
  2322. });
  2323. /**
  2324. * @operation add_board_member
  2325. * @tag Boards
  2326. *
  2327. * @summary Add New Board Member with Role
  2328. *
  2329. * @description Only the admin user (the first user) can call the REST API.
  2330. *
  2331. * **Note**: see [Boards.set_board_member_permission](#set_board_member_permission)
  2332. * to later change the permissions.
  2333. *
  2334. * @param {string} boardId the board ID
  2335. * @param {string} userId the user ID
  2336. * @param {string} action the action (needs to be `add`)
  2337. * @param {boolean} isAdmin is the user an admin of the board
  2338. * @param {boolean} isNoComments disable comments
  2339. * @param {boolean} isCommentOnly only enable comments
  2340. * @param {boolean} isWorker is the user a board worker
  2341. * @return_type {_id: string,
  2342. * title: string}
  2343. */
  2344. JsonRoutes.add(
  2345. 'POST',
  2346. '/api/boards/:boardId/members/:userId/add',
  2347. function (req, res) {
  2348. try {
  2349. Authentication.checkUserId(req.userId);
  2350. const userId = req.params.userId;
  2351. const boardId = req.params.boardId;
  2352. const action = req.body.action;
  2353. const { isAdmin, isNoComments, isCommentOnly, isWorker } = req.body;
  2354. let data = ReactiveCache.getUser(userId);
  2355. if (data !== undefined) {
  2356. if (action === 'add') {
  2357. data = ReactiveCache.getBoards({
  2358. _id: boardId,
  2359. }).map(function (board) {
  2360. if (!board.hasMember(userId)) {
  2361. board.addMember(userId);
  2362. function isTrue(data) {
  2363. return data.toLowerCase() === 'true';
  2364. }
  2365. board.setMemberPermission(
  2366. userId,
  2367. isTrue(isAdmin),
  2368. isTrue(isNoComments),
  2369. isTrue(isCommentOnly),
  2370. isTrue(isWorker),
  2371. userId,
  2372. );
  2373. }
  2374. return {
  2375. _id: board._id,
  2376. title: board.title,
  2377. };
  2378. });
  2379. }
  2380. }
  2381. JsonRoutes.sendResult(res, { code: 200, data });
  2382. } catch (error) {
  2383. JsonRoutes.sendResult(res, {
  2384. code: 200,
  2385. data: error,
  2386. });
  2387. }
  2388. },
  2389. );
  2390. /**
  2391. * @operation remove_board_member
  2392. * @tag Boards
  2393. *
  2394. * @summary Remove Member from Board
  2395. *
  2396. * @description Only the admin user (the first user) can call the REST API.
  2397. *
  2398. * @param {string} boardId the board ID
  2399. * @param {string} userId the user ID
  2400. * @param {string} action the action (needs to be `remove`)
  2401. * @return_type {_id: string,
  2402. * title: string}
  2403. */
  2404. JsonRoutes.add(
  2405. 'POST',
  2406. '/api/boards/:boardId/members/:userId/remove',
  2407. function (req, res) {
  2408. try {
  2409. Authentication.checkUserId(req.userId);
  2410. const userId = req.params.userId;
  2411. const boardId = req.params.boardId;
  2412. const action = req.body.action;
  2413. let data = ReactiveCache.getUser(userId);
  2414. if (data !== undefined) {
  2415. if (action === 'remove') {
  2416. data = ReactiveCache.getBoards({
  2417. _id: boardId,
  2418. }).map(function (board) {
  2419. if (board.hasMember(userId)) {
  2420. board.removeMember(userId);
  2421. }
  2422. return {
  2423. _id: board._id,
  2424. title: board.title,
  2425. };
  2426. });
  2427. }
  2428. }
  2429. JsonRoutes.sendResult(res, { code: 200, data });
  2430. } catch (error) {
  2431. JsonRoutes.sendResult(res, {
  2432. code: 200,
  2433. data: error,
  2434. });
  2435. }
  2436. },
  2437. );
  2438. /**
  2439. * @operation new_user
  2440. *
  2441. * @summary Create a new user
  2442. *
  2443. * @description Only the admin user (the first user) can call the REST API.
  2444. *
  2445. * @param {string} username the new username
  2446. * @param {string} email the email of the new user
  2447. * @param {string} password the password of the new user
  2448. * @return_type {_id: string}
  2449. */
  2450. JsonRoutes.add('POST', '/api/users/', function (req, res) {
  2451. try {
  2452. Authentication.checkUserId(req.userId);
  2453. const id = Accounts.createUser({
  2454. username: req.body.username,
  2455. email: req.body.email,
  2456. password: req.body.password,
  2457. from: 'admin',
  2458. });
  2459. JsonRoutes.sendResult(res, {
  2460. code: 200,
  2461. data: {
  2462. _id: id,
  2463. },
  2464. });
  2465. } catch (error) {
  2466. JsonRoutes.sendResult(res, {
  2467. code: 200,
  2468. data: error,
  2469. });
  2470. }
  2471. });
  2472. /**
  2473. * @operation delete_user
  2474. *
  2475. * @summary Delete a user
  2476. *
  2477. * @description Only the admin user (the first user) can call the REST API.
  2478. *
  2479. * @param {string} userId the ID of the user to delete
  2480. * @return_type {_id: string}
  2481. */
  2482. JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) {
  2483. try {
  2484. Authentication.checkUserId(req.userId);
  2485. const id = req.params.userId;
  2486. // Delete user is enabled, but is still has bug of leaving empty user avatars
  2487. // to boards: boards members, card members and assignees have
  2488. // empty users. So it would be better to delete user from all boards before
  2489. // deleting user.
  2490. // See:
  2491. // - wekan/client/components/settings/peopleBody.jade deleteButton
  2492. // - wekan/client/components/settings/peopleBody.js deleteButton
  2493. // - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
  2494. // that does now remove member from board, card members and assignees correctly,
  2495. // but that should be used to remove user from all boards similarly
  2496. // - wekan/models/users.js Delete is not enabled
  2497. Meteor.users.remove({ _id: id });
  2498. JsonRoutes.sendResult(res, {
  2499. code: 200,
  2500. data: {
  2501. _id: id,
  2502. },
  2503. });
  2504. } catch (error) {
  2505. JsonRoutes.sendResult(res, {
  2506. code: 200,
  2507. data: error,
  2508. });
  2509. }
  2510. });
  2511. /**
  2512. * @operation create_user_token
  2513. *
  2514. * @summary Create a user token
  2515. *
  2516. * @description Only the admin user (the first user) can call the REST API.
  2517. *
  2518. * @param {string} userId the ID of the user to create token for.
  2519. * @return_type {_id: string}
  2520. */
  2521. JsonRoutes.add('POST', '/api/createtoken/:userId', function (req, res) {
  2522. try {
  2523. Authentication.checkUserId(req.userId);
  2524. const id = req.params.userId;
  2525. const token = Accounts._generateStampedLoginToken();
  2526. Accounts._insertLoginToken(id, token);
  2527. JsonRoutes.sendResult(res, {
  2528. code: 200,
  2529. data: {
  2530. _id: id,
  2531. authToken: token.token,
  2532. },
  2533. });
  2534. } catch (error) {
  2535. JsonRoutes.sendResult(res, {
  2536. code: 200,
  2537. data: error,
  2538. });
  2539. }
  2540. });
  2541. /**
  2542. * @operation delete_user_token
  2543. *
  2544. * @summary Delete one or all user token.
  2545. *
  2546. * @description Only the admin user (the first user) can call the REST API.
  2547. *
  2548. * @param {string} userId the user ID
  2549. * @param {string} token the user hashedToken
  2550. * @return_type {message: string}
  2551. */
  2552. JsonRoutes.add('POST', '/api/deletetoken', function (req, res) {
  2553. try {
  2554. const { userId, token } = req.body;
  2555. Authentication.checkUserId(req.userId);
  2556. let data = {
  2557. message: 'Expected a userId to be set but received none.',
  2558. };
  2559. if (token && userId) {
  2560. Accounts.destroyToken(userId, token);
  2561. data.message = 'Delete token: [' + token + '] from user: ' + userId;
  2562. } else if (userId) {
  2563. check(userId, String);
  2564. Users.update(
  2565. {
  2566. _id: userId,
  2567. },
  2568. {
  2569. $set: {
  2570. 'services.resume.loginTokens': '',
  2571. },
  2572. },
  2573. );
  2574. data.message = 'Delete all token from user: ' + userId;
  2575. }
  2576. JsonRoutes.sendResult(res, {
  2577. code: 200,
  2578. data,
  2579. });
  2580. } catch (error) {
  2581. JsonRoutes.sendResult(res, {
  2582. code: 200,
  2583. data: error,
  2584. });
  2585. }
  2586. });
  2587. }
  2588. export default Users;