users.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473
  1. // Sandstorm context is detected using the METEOR_SETTINGS environment variable
  2. // in the package definition.
  3. const isSandstorm =
  4. Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
  5. Users = Meteor.users;
  6. const allowedSortValues = [
  7. '-modifiedAt',
  8. 'modifiedAt',
  9. '-title',
  10. 'title',
  11. '-sort',
  12. 'sort',
  13. ];
  14. const defaultSortBy = allowedSortValues[0];
  15. /**
  16. * A User in wekan
  17. */
  18. Users.attachSchema(
  19. new SimpleSchema({
  20. username: {
  21. /**
  22. * the username of the user
  23. */
  24. type: String,
  25. optional: true,
  26. // eslint-disable-next-line consistent-return
  27. autoValue() {
  28. if (this.isInsert && !this.isSet) {
  29. const name = this.field('profile.fullname');
  30. if (name.isSet) {
  31. return name.value.toLowerCase().replace(/\s/g, '');
  32. }
  33. }
  34. },
  35. },
  36. emails: {
  37. /**
  38. * the list of emails attached to a user
  39. */
  40. type: [Object],
  41. optional: true,
  42. },
  43. 'emails.$.address': {
  44. /**
  45. * The email address
  46. */
  47. type: String,
  48. regEx: SimpleSchema.RegEx.Email,
  49. },
  50. 'emails.$.verified': {
  51. /**
  52. * Has the email been verified
  53. */
  54. type: Boolean,
  55. },
  56. createdAt: {
  57. /**
  58. * creation date of the user
  59. */
  60. type: Date,
  61. // eslint-disable-next-line consistent-return
  62. autoValue() {
  63. if (this.isInsert) {
  64. return new Date();
  65. } else if (this.isUpsert) {
  66. return { $setOnInsert: new Date() };
  67. } else {
  68. this.unset();
  69. }
  70. },
  71. },
  72. modifiedAt: {
  73. type: Date,
  74. denyUpdate: false,
  75. // eslint-disable-next-line consistent-return
  76. autoValue() {
  77. if (this.isInsert || this.isUpsert || this.isUpdate) {
  78. return new Date();
  79. } else {
  80. this.unset();
  81. }
  82. },
  83. },
  84. profile: {
  85. /**
  86. * profile settings
  87. */
  88. type: Object,
  89. optional: true,
  90. // eslint-disable-next-line consistent-return
  91. autoValue() {
  92. if (this.isInsert && !this.isSet) {
  93. return {
  94. boardView: 'board-view-lists',
  95. };
  96. }
  97. },
  98. },
  99. 'profile.avatarUrl': {
  100. /**
  101. * URL of the avatar of the user
  102. */
  103. type: String,
  104. optional: true,
  105. },
  106. 'profile.emailBuffer': {
  107. /**
  108. * list of email buffers of the user
  109. */
  110. type: [String],
  111. optional: true,
  112. },
  113. 'profile.fullname': {
  114. /**
  115. * full name of the user
  116. */
  117. type: String,
  118. optional: true,
  119. },
  120. 'profile.showDesktopDragHandles': {
  121. /**
  122. * does the user want to hide system messages?
  123. */
  124. type: Boolean,
  125. optional: true,
  126. },
  127. 'profile.hiddenSystemMessages': {
  128. /**
  129. * does the user want to hide system messages?
  130. */
  131. type: Boolean,
  132. optional: true,
  133. },
  134. 'profile.hiddenMinicardLabelText': {
  135. /**
  136. * does the user want to hide minicard label texts?
  137. */
  138. type: Boolean,
  139. optional: true,
  140. },
  141. 'profile.initials': {
  142. /**
  143. * initials of the user
  144. */
  145. type: String,
  146. optional: true,
  147. },
  148. 'profile.invitedBoards': {
  149. /**
  150. * board IDs the user has been invited to
  151. */
  152. type: [String],
  153. optional: true,
  154. },
  155. 'profile.language': {
  156. /**
  157. * language of the user
  158. */
  159. type: String,
  160. optional: true,
  161. },
  162. 'profile.notifications': {
  163. /**
  164. * enabled notifications for the user
  165. */
  166. type: [Object],
  167. optional: true,
  168. },
  169. 'profile.notifications.$.activity': {
  170. /**
  171. * The id of the activity this notification references
  172. */
  173. type: String,
  174. },
  175. 'profile.notifications.$.read': {
  176. /**
  177. * the date on which this notification was read
  178. */
  179. type: Date,
  180. optional: true,
  181. },
  182. 'profile.showCardsCountAt': {
  183. /**
  184. * showCardCountAt field of the user
  185. */
  186. type: Number,
  187. optional: true,
  188. },
  189. 'profile.starredBoards': {
  190. /**
  191. * list of starred board IDs
  192. */
  193. type: [String],
  194. optional: true,
  195. },
  196. 'profile.icode': {
  197. /**
  198. * icode
  199. */
  200. type: String,
  201. optional: true,
  202. },
  203. 'profile.boardView': {
  204. /**
  205. * boardView field of the user
  206. */
  207. type: String,
  208. optional: true,
  209. allowedValues: [
  210. 'board-view-lists',
  211. 'board-view-swimlanes',
  212. 'board-view-cal',
  213. ],
  214. },
  215. 'profile.listSortBy': {
  216. /**
  217. * default sort list for user
  218. */
  219. type: String,
  220. optional: true,
  221. defaultValue: defaultSortBy,
  222. allowedValues: allowedSortValues,
  223. },
  224. 'profile.templatesBoardId': {
  225. /**
  226. * Reference to the templates board
  227. */
  228. type: String,
  229. defaultValue: '',
  230. },
  231. 'profile.cardTemplatesSwimlaneId': {
  232. /**
  233. * Reference to the card templates swimlane Id
  234. */
  235. type: String,
  236. defaultValue: '',
  237. },
  238. 'profile.listTemplatesSwimlaneId': {
  239. /**
  240. * Reference to the list templates swimlane Id
  241. */
  242. type: String,
  243. defaultValue: '',
  244. },
  245. 'profile.boardTemplatesSwimlaneId': {
  246. /**
  247. * Reference to the board templates swimlane Id
  248. */
  249. type: String,
  250. defaultValue: '',
  251. },
  252. services: {
  253. /**
  254. * services field of the user
  255. */
  256. type: Object,
  257. optional: true,
  258. blackbox: true,
  259. },
  260. heartbeat: {
  261. /**
  262. * last time the user has been seen
  263. */
  264. type: Date,
  265. optional: true,
  266. },
  267. isAdmin: {
  268. /**
  269. * is the user an admin of the board?
  270. */
  271. type: Boolean,
  272. optional: true,
  273. },
  274. createdThroughApi: {
  275. /**
  276. * was the user created through the API?
  277. */
  278. type: Boolean,
  279. optional: true,
  280. },
  281. loginDisabled: {
  282. /**
  283. * loginDisabled field of the user
  284. */
  285. type: Boolean,
  286. optional: true,
  287. },
  288. authenticationMethod: {
  289. /**
  290. * authentication method of the user
  291. */
  292. type: String,
  293. optional: false,
  294. defaultValue: 'password',
  295. },
  296. }),
  297. );
  298. Users.allow({
  299. update(userId, doc) {
  300. const user = Users.findOne({ _id: userId });
  301. if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
  302. return true;
  303. if (!user) {
  304. return false;
  305. }
  306. return doc._id === userId;
  307. },
  308. remove(userId, doc) {
  309. const adminsNumber = Users.find({ isAdmin: true }).count();
  310. const { isAdmin } = Users.findOne(
  311. { _id: userId },
  312. { fields: { isAdmin: 1 } },
  313. );
  314. // Prevents remove of the only one administrator
  315. if (adminsNumber === 1 && isAdmin && userId === doc._id) {
  316. return false;
  317. }
  318. // If it's the user or an admin
  319. return userId === doc._id || isAdmin;
  320. },
  321. fetch: [],
  322. });
  323. // Search a user in the complete server database by its name or username. This
  324. // is used for instance to add a new user to a board.
  325. const searchInFields = ['username', 'profile.fullname'];
  326. Users.initEasySearch(searchInFields, {
  327. use: 'mongo-db',
  328. returnFields: [...searchInFields, 'profile.avatarUrl'],
  329. });
  330. if (Meteor.isClient) {
  331. Users.helpers({
  332. isBoardMember() {
  333. const board = Boards.findOne(Session.get('currentBoard'));
  334. return board && board.hasMember(this._id);
  335. },
  336. isNotNoComments() {
  337. const board = Boards.findOne(Session.get('currentBoard'));
  338. return (
  339. board && board.hasMember(this._id) && !board.hasNoComments(this._id)
  340. );
  341. },
  342. isNoComments() {
  343. const board = Boards.findOne(Session.get('currentBoard'));
  344. return board && board.hasNoComments(this._id);
  345. },
  346. isNotCommentOnly() {
  347. const board = Boards.findOne(Session.get('currentBoard'));
  348. return (
  349. board && board.hasMember(this._id) && !board.hasCommentOnly(this._id)
  350. );
  351. },
  352. isCommentOnly() {
  353. const board = Boards.findOne(Session.get('currentBoard'));
  354. return board && board.hasCommentOnly(this._id);
  355. },
  356. isNotWorker() {
  357. const board = Boards.findOne(Session.get('currentBoard'));
  358. return board && board.hasMember(this._id) && !board.hasWorker(this._id);
  359. },
  360. isWorker() {
  361. const board = Boards.findOne(Session.get('currentBoard'));
  362. return board && board.hasWorker(this._id);
  363. },
  364. isBoardAdmin() {
  365. const board = Boards.findOne(Session.get('currentBoard'));
  366. return board && board.hasAdmin(this._id);
  367. },
  368. });
  369. }
  370. Users.helpers({
  371. boards() {
  372. return Boards.find({ 'members.userId': this._id });
  373. },
  374. starredBoards() {
  375. const { starredBoards = [] } = this.profile || {};
  376. return Boards.find({ archived: false, _id: { $in: starredBoards } });
  377. },
  378. hasStarred(boardId) {
  379. const { starredBoards = [] } = this.profile || {};
  380. return _.contains(starredBoards, boardId);
  381. },
  382. invitedBoards() {
  383. const { invitedBoards = [] } = this.profile || {};
  384. return Boards.find({ archived: false, _id: { $in: invitedBoards } });
  385. },
  386. isInvitedTo(boardId) {
  387. const { invitedBoards = [] } = this.profile || {};
  388. return _.contains(invitedBoards, boardId);
  389. },
  390. _getListSortBy() {
  391. const profile = this.profile || {};
  392. const sortBy = profile.listSortBy || defaultSortBy;
  393. const keyPattern = /^(-{0,1})(.*$)/;
  394. const ret = [];
  395. if (keyPattern.exec(sortBy)) {
  396. ret[0] = RegExp.$2;
  397. ret[1] = RegExp.$1 ? -1 : 1;
  398. }
  399. return ret;
  400. },
  401. hasSortBy() {
  402. // if use doesn't have dragHandle, then we can let user to choose sort list by different order
  403. return !this.hasShowDesktopDragHandles();
  404. },
  405. getListSortBy() {
  406. return this._getListSortBy()[0];
  407. },
  408. getListSortTypes() {
  409. return allowedSortValues;
  410. },
  411. getListSortByDirection() {
  412. return this._getListSortBy()[1];
  413. },
  414. hasTag(tag) {
  415. const { tags = [] } = this.profile || {};
  416. return _.contains(tags, tag);
  417. },
  418. hasNotification(activityId) {
  419. const { notifications = [] } = this.profile || {};
  420. return _.contains(notifications, activityId);
  421. },
  422. notifications() {
  423. const { notifications = [] } = this.profile || {};
  424. for (const index in notifications) {
  425. if (!notifications.hasOwnProperty(index)) continue;
  426. const notification = notifications[index];
  427. // this preserves their db sort order for editing
  428. notification.dbIndex = index;
  429. notification.activity = Activities.findOne(notification.activity);
  430. }
  431. // this sorts them newest to oldest to match Trello's behavior
  432. notifications.reverse();
  433. return notifications;
  434. },
  435. hasShowDesktopDragHandles() {
  436. const profile = this.profile || {};
  437. return profile.showDesktopDragHandles || false;
  438. },
  439. hasHiddenSystemMessages() {
  440. const profile = this.profile || {};
  441. return profile.hiddenSystemMessages || false;
  442. },
  443. hasHiddenMinicardLabelText() {
  444. const profile = this.profile || {};
  445. return profile.hiddenMinicardLabelText || false;
  446. },
  447. getEmailBuffer() {
  448. const { emailBuffer = [] } = this.profile || {};
  449. return emailBuffer;
  450. },
  451. getInitials() {
  452. const profile = this.profile || {};
  453. if (profile.initials) return profile.initials;
  454. else if (profile.fullname) {
  455. return profile.fullname
  456. .split(/\s+/)
  457. .reduce((memo, word) => {
  458. return memo + word[0];
  459. }, '')
  460. .toUpperCase();
  461. } else {
  462. return this.username[0].toUpperCase();
  463. }
  464. },
  465. getLimitToShowCardsCount() {
  466. const profile = this.profile || {};
  467. return profile.showCardsCountAt;
  468. },
  469. getName() {
  470. const profile = this.profile || {};
  471. return profile.fullname || this.username;
  472. },
  473. getLanguage() {
  474. const profile = this.profile || {};
  475. return profile.language || 'en';
  476. },
  477. getTemplatesBoardId() {
  478. return (this.profile || {}).templatesBoardId;
  479. },
  480. getTemplatesBoardSlug() {
  481. return (Boards.findOne((this.profile || {}).templatesBoardId) || {}).slug;
  482. },
  483. remove() {
  484. User.remove({ _id: this._id });
  485. },
  486. });
  487. Users.mutations({
  488. toggleBoardStar(boardId) {
  489. const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
  490. return {
  491. [queryKind]: {
  492. 'profile.starredBoards': boardId,
  493. },
  494. };
  495. },
  496. addInvite(boardId) {
  497. return {
  498. $addToSet: {
  499. 'profile.invitedBoards': boardId,
  500. },
  501. };
  502. },
  503. removeInvite(boardId) {
  504. return {
  505. $pull: {
  506. 'profile.invitedBoards': boardId,
  507. },
  508. };
  509. },
  510. addTag(tag) {
  511. return {
  512. $addToSet: {
  513. 'profile.tags': tag,
  514. },
  515. };
  516. },
  517. removeTag(tag) {
  518. return {
  519. $pull: {
  520. 'profile.tags': tag,
  521. },
  522. };
  523. },
  524. toggleTag(tag) {
  525. if (this.hasTag(tag)) this.removeTag(tag);
  526. else this.addTag(tag);
  527. },
  528. setListSortBy(value) {
  529. return {
  530. $set: {
  531. 'profile.listSortBy': value,
  532. },
  533. };
  534. },
  535. toggleDesktopHandles(value = false) {
  536. return {
  537. $set: {
  538. 'profile.showDesktopDragHandles': !value,
  539. },
  540. };
  541. },
  542. toggleSystem(value = false) {
  543. return {
  544. $set: {
  545. 'profile.hiddenSystemMessages': !value,
  546. },
  547. };
  548. },
  549. toggleLabelText(value = false) {
  550. return {
  551. $set: {
  552. 'profile.hiddenMinicardLabelText': !value,
  553. },
  554. };
  555. },
  556. addNotification(activityId) {
  557. return {
  558. $addToSet: {
  559. 'profile.notifications': { activity: activityId },
  560. },
  561. };
  562. },
  563. removeNotification(activityId) {
  564. return {
  565. $pull: {
  566. 'profile.notifications': { activity: activityId },
  567. },
  568. };
  569. },
  570. addEmailBuffer(text) {
  571. return {
  572. $addToSet: {
  573. 'profile.emailBuffer': text,
  574. },
  575. };
  576. },
  577. clearEmailBuffer() {
  578. return {
  579. $set: {
  580. 'profile.emailBuffer': [],
  581. },
  582. };
  583. },
  584. setAvatarUrl(avatarUrl) {
  585. return { $set: { 'profile.avatarUrl': avatarUrl } };
  586. },
  587. setShowCardsCountAt(limit) {
  588. return { $set: { 'profile.showCardsCountAt': limit } };
  589. },
  590. setBoardView(view) {
  591. return {
  592. $set: {
  593. 'profile.boardView': view,
  594. },
  595. };
  596. },
  597. });
  598. Meteor.methods({
  599. setListSortBy(value) {
  600. check(value, String);
  601. Meteor.user().setListSortBy(value);
  602. },
  603. toggleDesktopDragHandles() {
  604. const user = Meteor.user();
  605. user.toggleDesktopHandles(user.hasShowDesktopDragHandles());
  606. },
  607. toggleSystemMessages() {
  608. const user = Meteor.user();
  609. user.toggleSystem(user.hasHiddenSystemMessages());
  610. },
  611. toggleMinicardLabelText() {
  612. const user = Meteor.user();
  613. user.toggleLabelText(user.hasHiddenMinicardLabelText());
  614. },
  615. changeLimitToShowCardsCount(limit) {
  616. check(limit, Number);
  617. Meteor.user().setShowCardsCountAt(limit);
  618. },
  619. });
  620. if (Meteor.isServer) {
  621. Meteor.methods({
  622. setCreateUser(fullname, username, password, isAdmin, isActive, email) {
  623. if (Meteor.user() && Meteor.user().isAdmin) {
  624. check(fullname, String);
  625. check(username, String);
  626. check(password, String);
  627. check(isAdmin, String);
  628. check(isActive, String);
  629. check(email, String);
  630. const nUsersWithUsername = Users.find({ username }).count();
  631. const nUsersWithEmail = Users.find({ email }).count();
  632. if (nUsersWithUsername > 0) {
  633. throw new Meteor.Error('username-already-taken');
  634. } else if (nUsersWithEmail > 0) {
  635. throw new Meteor.Error('email-already-taken');
  636. } else {
  637. Accounts.createUser({
  638. fullname,
  639. username,
  640. password,
  641. isAdmin,
  642. isActive,
  643. email: email.toLowerCase(),
  644. from: 'admin',
  645. });
  646. }
  647. }
  648. },
  649. setUsername(username, userId) {
  650. if (Meteor.user() && Meteor.user().isAdmin) {
  651. check(username, String);
  652. check(userId, String);
  653. const nUsersWithUsername = Users.find({ username }).count();
  654. if (nUsersWithUsername > 0) {
  655. throw new Meteor.Error('username-already-taken');
  656. } else {
  657. Users.update(userId, { $set: { username } });
  658. }
  659. }
  660. },
  661. setEmail(email, userId) {
  662. if (Meteor.user() && Meteor.user().isAdmin) {
  663. if (Array.isArray(email)) {
  664. email = email.shift();
  665. }
  666. check(email, String);
  667. const existingUser = Users.findOne(
  668. { 'emails.address': email },
  669. { fields: { _id: 1 } },
  670. );
  671. if (existingUser) {
  672. throw new Meteor.Error('email-already-taken');
  673. } else {
  674. Users.update(userId, {
  675. $set: {
  676. emails: [
  677. {
  678. address: email,
  679. verified: false,
  680. },
  681. ],
  682. },
  683. });
  684. }
  685. }
  686. },
  687. setUsernameAndEmail(username, email, userId) {
  688. if (Meteor.user() && Meteor.user().isAdmin) {
  689. check(username, String);
  690. if (Array.isArray(email)) {
  691. email = email.shift();
  692. }
  693. check(email, String);
  694. check(userId, String);
  695. Meteor.call('setUsername', username, userId);
  696. Meteor.call('setEmail', email, userId);
  697. }
  698. },
  699. setPassword(newPassword, userId) {
  700. if (Meteor.user() && Meteor.user().isAdmin) {
  701. check(userId, String);
  702. check(newPassword, String);
  703. if (Meteor.user().isAdmin) {
  704. Accounts.setPassword(userId, newPassword);
  705. }
  706. }
  707. },
  708. // we accept userId, username, email
  709. inviteUserToBoard(username, boardId) {
  710. check(username, String);
  711. check(boardId, String);
  712. const inviter = Meteor.user();
  713. const board = Boards.findOne(boardId);
  714. const allowInvite =
  715. inviter &&
  716. board &&
  717. board.members &&
  718. _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
  719. _.where(board.members, { userId: inviter._id })[0].isActive;
  720. // GitHub issue 2060
  721. //_.where(board.members, { userId: inviter._id })[0].isAdmin;
  722. if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
  723. this.unblock();
  724. const posAt = username.indexOf('@');
  725. let user = null;
  726. if (posAt >= 0) {
  727. user = Users.findOne({ emails: { $elemMatch: { address: username } } });
  728. } else {
  729. user = Users.findOne(username) || Users.findOne({ username });
  730. }
  731. if (user) {
  732. if (user._id === inviter._id)
  733. throw new Meteor.Error('error-user-notAllowSelf');
  734. } else {
  735. if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
  736. if (Settings.findOne({ disableRegistration: true })) {
  737. throw new Meteor.Error('error-user-notCreated');
  738. }
  739. // Set in lowercase email before creating account
  740. const email = username.toLowerCase();
  741. username = email.substring(0, posAt);
  742. const newUserId = Accounts.createUser({ username, email });
  743. if (!newUserId) throw new Meteor.Error('error-user-notCreated');
  744. // assume new user speak same language with inviter
  745. if (inviter.profile && inviter.profile.language) {
  746. Users.update(newUserId, {
  747. $set: {
  748. 'profile.language': inviter.profile.language,
  749. },
  750. });
  751. }
  752. Accounts.sendEnrollmentEmail(newUserId);
  753. user = Users.findOne(newUserId);
  754. }
  755. board.addMember(user._id);
  756. user.addInvite(boardId);
  757. try {
  758. const params = {
  759. user: user.username,
  760. inviter: inviter.username,
  761. board: board.title,
  762. url: board.absoluteUrl(),
  763. };
  764. const lang = user.getLanguage();
  765. Email.send({
  766. to: user.emails[0].address.toLowerCase(),
  767. from: Accounts.emailTemplates.from,
  768. subject: TAPi18n.__('email-invite-subject', params, lang),
  769. text: TAPi18n.__('email-invite-text', params, lang),
  770. });
  771. } catch (e) {
  772. throw new Meteor.Error('email-fail', e.message);
  773. }
  774. return { username: user.username, email: user.emails[0].address };
  775. },
  776. });
  777. Accounts.onCreateUser((options, user) => {
  778. const userCount = Users.find().count();
  779. if (userCount === 0) {
  780. user.isAdmin = true;
  781. return user;
  782. }
  783. if (user.services.oidc) {
  784. let email = user.services.oidc.email;
  785. if (Array.isArray(email)) {
  786. email = email.shift();
  787. }
  788. email = email.toLowerCase();
  789. user.username = user.services.oidc.username;
  790. user.emails = [{ address: email, verified: true }];
  791. const initials = user.services.oidc.fullname
  792. .match(/\b[a-zA-Z]/g)
  793. .join('')
  794. .toUpperCase();
  795. user.profile = {
  796. initials,
  797. fullname: user.services.oidc.fullname,
  798. boardView: 'board-view-lists',
  799. };
  800. user.authenticationMethod = 'oauth2';
  801. // see if any existing user has this email address or username, otherwise create new
  802. const existingUser = Meteor.users.findOne({
  803. $or: [{ 'emails.address': email }, { username: user.username }],
  804. });
  805. if (!existingUser) return user;
  806. // copy across new service info
  807. const service = _.keys(user.services)[0];
  808. existingUser.services[service] = user.services[service];
  809. existingUser.emails = user.emails;
  810. existingUser.username = user.username;
  811. existingUser.profile = user.profile;
  812. existingUser.authenticationMethod = user.authenticationMethod;
  813. Meteor.users.remove({ _id: existingUser._id }); // remove existing record
  814. return existingUser;
  815. }
  816. if (options.from === 'admin') {
  817. user.createdThroughApi = true;
  818. return user;
  819. }
  820. const disableRegistration = Settings.findOne().disableRegistration;
  821. // If this is the first Authentication by the ldap and self registration disabled
  822. if (disableRegistration && options && options.ldap) {
  823. user.authenticationMethod = 'ldap';
  824. return user;
  825. }
  826. // If self registration enabled
  827. if (!disableRegistration) {
  828. return user;
  829. }
  830. if (!options || !options.profile) {
  831. throw new Meteor.Error(
  832. 'error-invitation-code-blank',
  833. 'The invitation code is required',
  834. );
  835. }
  836. const invitationCode = InvitationCodes.findOne({
  837. code: options.profile.invitationcode,
  838. email: options.email,
  839. valid: true,
  840. });
  841. if (!invitationCode) {
  842. throw new Meteor.Error(
  843. 'error-invitation-code-not-exist',
  844. // eslint-disable-next-line quotes
  845. "The invitation code doesn't exist",
  846. );
  847. } else {
  848. user.profile = { icode: options.profile.invitationcode };
  849. user.profile.boardView = 'board-view-lists';
  850. // Deletes the invitation code after the user was created successfully.
  851. setTimeout(
  852. Meteor.bindEnvironment(() => {
  853. InvitationCodes.remove({ _id: invitationCode._id });
  854. }),
  855. 200,
  856. );
  857. return user;
  858. }
  859. });
  860. }
  861. if (Meteor.isServer) {
  862. // Let mongoDB ensure username unicity
  863. Meteor.startup(() => {
  864. allowedSortValues.forEach(value => {
  865. Lists._collection._ensureIndex(value);
  866. });
  867. Users._collection._ensureIndex({ modifiedAt: -1 });
  868. Users._collection._ensureIndex(
  869. {
  870. username: 1,
  871. },
  872. { unique: true },
  873. );
  874. });
  875. // OLD WAY THIS CODE DID WORK: When user is last admin of board,
  876. // if admin is removed, board is removed.
  877. // NOW THIS IS COMMENTED OUT, because other board users still need to be able
  878. // to use that board, and not have board deleted.
  879. // Someone can be later changed to be admin of board, by making change to database.
  880. // TODO: Add UI for changing someone as board admin.
  881. //Users.before.remove((userId, doc) => {
  882. // Boards
  883. // .find({members: {$elemMatch: {userId: doc._id, isAdmin: true}}})
  884. // .forEach((board) => {
  885. // // If only one admin for the board
  886. // if (board.members.filter((e) => e.isAdmin).length === 1) {
  887. // Boards.remove(board._id);
  888. // }
  889. // });
  890. //});
  891. // Each board document contains the de-normalized number of users that have
  892. // starred it. If the user star or unstar a board, we need to update this
  893. // counter.
  894. // We need to run this code on the server only, otherwise the incrementation
  895. // will be done twice.
  896. Users.after.update(function(userId, user, fieldNames) {
  897. // The `starredBoards` list is hosted on the `profile` field. If this
  898. // field hasn't been modificated we don't need to run this hook.
  899. if (!_.contains(fieldNames, 'profile')) return;
  900. // To calculate a diff of board starred ids, we get both the previous
  901. // and the newly board ids list
  902. function getStarredBoardsIds(doc) {
  903. return doc.profile && doc.profile.starredBoards;
  904. }
  905. const oldIds = getStarredBoardsIds(this.previous);
  906. const newIds = getStarredBoardsIds(user);
  907. // The _.difference(a, b) method returns the values from a that are not in
  908. // b. We use it to find deleted and newly inserted ids by using it in one
  909. // direction and then in the other.
  910. function incrementBoards(boardsIds, inc) {
  911. boardsIds.forEach(boardId => {
  912. Boards.update(boardId, { $inc: { stars: inc } });
  913. });
  914. }
  915. incrementBoards(_.difference(oldIds, newIds), -1);
  916. incrementBoards(_.difference(newIds, oldIds), +1);
  917. });
  918. const fakeUserId = new Meteor.EnvironmentVariable();
  919. const getUserId = CollectionHooks.getUserId;
  920. CollectionHooks.getUserId = () => {
  921. return fakeUserId.get() || getUserId();
  922. };
  923. if (!isSandstorm) {
  924. Users.after.insert((userId, doc) => {
  925. const fakeUser = {
  926. extendAutoValueContext: {
  927. userId: doc._id,
  928. },
  929. };
  930. fakeUserId.withValue(doc._id, () => {
  931. /*
  932. // Insert the Welcome Board
  933. Boards.insert({
  934. title: TAPi18n.__('welcome-board'),
  935. permission: 'private',
  936. }, fakeUser, (err, boardId) => {
  937. Swimlanes.insert({
  938. title: TAPi18n.__('welcome-swimlane'),
  939. boardId,
  940. sort: 1,
  941. }, fakeUser);
  942. ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
  943. Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
  944. });
  945. });
  946. */
  947. Boards.insert(
  948. {
  949. title: TAPi18n.__('templates'),
  950. permission: 'private',
  951. type: 'template-container',
  952. },
  953. fakeUser,
  954. (err, boardId) => {
  955. // Insert the reference to our templates board
  956. Users.update(fakeUserId.get(), {
  957. $set: { 'profile.templatesBoardId': boardId },
  958. });
  959. // Insert the card templates swimlane
  960. Swimlanes.insert(
  961. {
  962. title: TAPi18n.__('card-templates-swimlane'),
  963. boardId,
  964. sort: 1,
  965. type: 'template-container',
  966. },
  967. fakeUser,
  968. (err, swimlaneId) => {
  969. // Insert the reference to out card templates swimlane
  970. Users.update(fakeUserId.get(), {
  971. $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
  972. });
  973. },
  974. );
  975. // Insert the list templates swimlane
  976. Swimlanes.insert(
  977. {
  978. title: TAPi18n.__('list-templates-swimlane'),
  979. boardId,
  980. sort: 2,
  981. type: 'template-container',
  982. },
  983. fakeUser,
  984. (err, swimlaneId) => {
  985. // Insert the reference to out list templates swimlane
  986. Users.update(fakeUserId.get(), {
  987. $set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
  988. });
  989. },
  990. );
  991. // Insert the board templates swimlane
  992. Swimlanes.insert(
  993. {
  994. title: TAPi18n.__('board-templates-swimlane'),
  995. boardId,
  996. sort: 3,
  997. type: 'template-container',
  998. },
  999. fakeUser,
  1000. (err, swimlaneId) => {
  1001. // Insert the reference to out board templates swimlane
  1002. Users.update(fakeUserId.get(), {
  1003. $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
  1004. });
  1005. },
  1006. );
  1007. },
  1008. );
  1009. });
  1010. });
  1011. }
  1012. Users.after.insert((userId, doc) => {
  1013. if (doc.createdThroughApi) {
  1014. // The admin user should be able to create a user despite disabling registration because
  1015. // it is two different things (registration and creation).
  1016. // So, when a new user is created via the api (only admin user can do that) one must avoid
  1017. // the disableRegistration check.
  1018. // Issue : https://github.com/wekan/wekan/issues/1232
  1019. // PR : https://github.com/wekan/wekan/pull/1251
  1020. Users.update(doc._id, { $set: { createdThroughApi: '' } });
  1021. return;
  1022. }
  1023. //invite user to corresponding boards
  1024. const disableRegistration = Settings.findOne().disableRegistration;
  1025. // If ldap, bypass the inviation code if the self registration isn't allowed.
  1026. // TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type
  1027. if (doc.authenticationMethod !== 'ldap' && disableRegistration) {
  1028. const invitationCode = InvitationCodes.findOne({
  1029. code: doc.profile.icode,
  1030. valid: true,
  1031. });
  1032. if (!invitationCode) {
  1033. throw new Meteor.Error('error-invitation-code-not-exist');
  1034. } else {
  1035. invitationCode.boardsToBeInvited.forEach(boardId => {
  1036. const board = Boards.findOne(boardId);
  1037. board.addMember(doc._id);
  1038. });
  1039. if (!doc.profile) {
  1040. doc.profile = {};
  1041. }
  1042. doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
  1043. Users.update(doc._id, { $set: { profile: doc.profile } });
  1044. InvitationCodes.update(invitationCode._id, { $set: { valid: false } });
  1045. }
  1046. }
  1047. });
  1048. }
  1049. // USERS REST API
  1050. if (Meteor.isServer) {
  1051. // Middleware which checks that API is enabled.
  1052. JsonRoutes.Middleware.use(function(req, res, next) {
  1053. const api = req.url.startsWith('/api');
  1054. if ((api === true && process.env.WITH_API === 'true') || api === false) {
  1055. return next();
  1056. } else {
  1057. res.writeHead(301, { Location: '/' });
  1058. return res.end();
  1059. }
  1060. });
  1061. /**
  1062. * @operation get_current_user
  1063. *
  1064. * @summary returns the current user
  1065. * @return_type Users
  1066. */
  1067. JsonRoutes.add('GET', '/api/user', function(req, res) {
  1068. try {
  1069. Authentication.checkLoggedIn(req.userId);
  1070. const data = Meteor.users.findOne({ _id: req.userId });
  1071. delete data.services;
  1072. JsonRoutes.sendResult(res, {
  1073. code: 200,
  1074. data,
  1075. });
  1076. } catch (error) {
  1077. JsonRoutes.sendResult(res, {
  1078. code: 200,
  1079. data: error,
  1080. });
  1081. }
  1082. });
  1083. /**
  1084. * @operation get_all_users
  1085. *
  1086. * @summary return all the users
  1087. *
  1088. * @description Only the admin user (the first user) can call the REST API.
  1089. * @return_type [{ _id: string,
  1090. * username: string}]
  1091. */
  1092. JsonRoutes.add('GET', '/api/users', function(req, res) {
  1093. try {
  1094. Authentication.checkUserId(req.userId);
  1095. JsonRoutes.sendResult(res, {
  1096. code: 200,
  1097. data: Meteor.users.find({}).map(function(doc) {
  1098. return { _id: doc._id, username: doc.username };
  1099. }),
  1100. });
  1101. } catch (error) {
  1102. JsonRoutes.sendResult(res, {
  1103. code: 200,
  1104. data: error,
  1105. });
  1106. }
  1107. });
  1108. /**
  1109. * @operation get_user
  1110. *
  1111. * @summary get a given user
  1112. *
  1113. * @description Only the admin user (the first user) can call the REST API.
  1114. *
  1115. * @param {string} userId the user ID
  1116. * @return_type Users
  1117. */
  1118. JsonRoutes.add('GET', '/api/users/:userId', function(req, res) {
  1119. try {
  1120. Authentication.checkUserId(req.userId);
  1121. const id = req.params.userId;
  1122. JsonRoutes.sendResult(res, {
  1123. code: 200,
  1124. data: Meteor.users.findOne({ _id: id }),
  1125. });
  1126. } catch (error) {
  1127. JsonRoutes.sendResult(res, {
  1128. code: 200,
  1129. data: error,
  1130. });
  1131. }
  1132. });
  1133. /**
  1134. * @operation edit_user
  1135. *
  1136. * @summary edit a given user
  1137. *
  1138. * @description Only the admin user (the first user) can call the REST API.
  1139. *
  1140. * Possible values for *action*:
  1141. * - `takeOwnership`: The admin takes the ownership of ALL boards of the user (archived and not archived) where the user is admin on.
  1142. * - `disableLogin`: Disable a user (the user is not allowed to login and his login tokens are purged)
  1143. * - `enableLogin`: Enable a user
  1144. *
  1145. * @param {string} userId the user ID
  1146. * @param {string} action the action
  1147. * @return_type {_id: string,
  1148. * title: string}
  1149. */
  1150. JsonRoutes.add('PUT', '/api/users/:userId', function(req, res) {
  1151. try {
  1152. Authentication.checkUserId(req.userId);
  1153. const id = req.params.userId;
  1154. const action = req.body.action;
  1155. let data = Meteor.users.findOne({ _id: id });
  1156. if (data !== undefined) {
  1157. if (action === 'takeOwnership') {
  1158. data = Boards.find({
  1159. 'members.userId': id,
  1160. 'members.isAdmin': true,
  1161. }).map(function(board) {
  1162. if (board.hasMember(req.userId)) {
  1163. board.removeMember(req.userId);
  1164. }
  1165. board.changeOwnership(id, req.userId);
  1166. return {
  1167. _id: board._id,
  1168. title: board.title,
  1169. };
  1170. });
  1171. } else {
  1172. if (action === 'disableLogin' && id !== req.userId) {
  1173. Users.update(
  1174. { _id: id },
  1175. {
  1176. $set: {
  1177. loginDisabled: true,
  1178. 'services.resume.loginTokens': '',
  1179. },
  1180. },
  1181. );
  1182. } else if (action === 'enableLogin') {
  1183. Users.update({ _id: id }, { $set: { loginDisabled: '' } });
  1184. }
  1185. data = Meteor.users.findOne({ _id: id });
  1186. }
  1187. }
  1188. JsonRoutes.sendResult(res, {
  1189. code: 200,
  1190. data,
  1191. });
  1192. } catch (error) {
  1193. JsonRoutes.sendResult(res, {
  1194. code: 200,
  1195. data: error,
  1196. });
  1197. }
  1198. });
  1199. /**
  1200. * @operation add_board_member
  1201. * @tag Boards
  1202. *
  1203. * @summary Add New Board Member with Role
  1204. *
  1205. * @description Only the admin user (the first user) can call the REST API.
  1206. *
  1207. * **Note**: see [Boards.set_board_member_permission](#set_board_member_permission)
  1208. * to later change the permissions.
  1209. *
  1210. * @param {string} boardId the board ID
  1211. * @param {string} userId the user ID
  1212. * @param {boolean} isAdmin is the user an admin of the board
  1213. * @param {boolean} isNoComments disable comments
  1214. * @param {boolean} isCommentOnly only enable comments
  1215. * @return_type {_id: string,
  1216. * title: string}
  1217. */
  1218. JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function(
  1219. req,
  1220. res,
  1221. ) {
  1222. try {
  1223. Authentication.checkUserId(req.userId);
  1224. const userId = req.params.userId;
  1225. const boardId = req.params.boardId;
  1226. const action = req.body.action;
  1227. const { isAdmin, isNoComments, isCommentOnly } = req.body;
  1228. let data = Meteor.users.findOne({ _id: userId });
  1229. if (data !== undefined) {
  1230. if (action === 'add') {
  1231. data = Boards.find({
  1232. _id: boardId,
  1233. }).map(function(board) {
  1234. if (!board.hasMember(userId)) {
  1235. board.addMember(userId);
  1236. function isTrue(data) {
  1237. return data.toLowerCase() === 'true';
  1238. }
  1239. board.setMemberPermission(
  1240. userId,
  1241. isTrue(isAdmin),
  1242. isTrue(isNoComments),
  1243. isTrue(isCommentOnly),
  1244. userId,
  1245. );
  1246. }
  1247. return {
  1248. _id: board._id,
  1249. title: board.title,
  1250. };
  1251. });
  1252. }
  1253. }
  1254. JsonRoutes.sendResult(res, {
  1255. code: 200,
  1256. data: query,
  1257. });
  1258. } catch (error) {
  1259. JsonRoutes.sendResult(res, {
  1260. code: 200,
  1261. data: error,
  1262. });
  1263. }
  1264. });
  1265. /**
  1266. * @operation remove_board_member
  1267. * @tag Boards
  1268. *
  1269. * @summary Remove Member from Board
  1270. *
  1271. * @description Only the admin user (the first user) can call the REST API.
  1272. *
  1273. * @param {string} boardId the board ID
  1274. * @param {string} userId the user ID
  1275. * @param {string} action the action (needs to be `remove`)
  1276. * @return_type {_id: string,
  1277. * title: string}
  1278. */
  1279. JsonRoutes.add(
  1280. 'POST',
  1281. '/api/boards/:boardId/members/:userId/remove',
  1282. function(req, res) {
  1283. try {
  1284. Authentication.checkUserId(req.userId);
  1285. const userId = req.params.userId;
  1286. const boardId = req.params.boardId;
  1287. const action = req.body.action;
  1288. let data = Meteor.users.findOne({ _id: userId });
  1289. if (data !== undefined) {
  1290. if (action === 'remove') {
  1291. data = Boards.find({
  1292. _id: boardId,
  1293. }).map(function(board) {
  1294. if (board.hasMember(userId)) {
  1295. board.removeMember(userId);
  1296. }
  1297. return {
  1298. _id: board._id,
  1299. title: board.title,
  1300. };
  1301. });
  1302. }
  1303. }
  1304. JsonRoutes.sendResult(res, {
  1305. code: 200,
  1306. data: query,
  1307. });
  1308. } catch (error) {
  1309. JsonRoutes.sendResult(res, {
  1310. code: 200,
  1311. data: error,
  1312. });
  1313. }
  1314. },
  1315. );
  1316. /**
  1317. * @operation new_user
  1318. *
  1319. * @summary Create a new user
  1320. *
  1321. * @description Only the admin user (the first user) can call the REST API.
  1322. *
  1323. * @param {string} username the new username
  1324. * @param {string} email the email of the new user
  1325. * @param {string} password the password of the new user
  1326. * @return_type {_id: string}
  1327. */
  1328. JsonRoutes.add('POST', '/api/users/', function(req, res) {
  1329. try {
  1330. Authentication.checkUserId(req.userId);
  1331. const id = Accounts.createUser({
  1332. username: req.body.username,
  1333. email: req.body.email,
  1334. password: req.body.password,
  1335. from: 'admin',
  1336. });
  1337. JsonRoutes.sendResult(res, {
  1338. code: 200,
  1339. data: {
  1340. _id: id,
  1341. },
  1342. });
  1343. } catch (error) {
  1344. JsonRoutes.sendResult(res, {
  1345. code: 200,
  1346. data: error,
  1347. });
  1348. }
  1349. });
  1350. /**
  1351. * @operation delete_user
  1352. *
  1353. * @summary Delete a user
  1354. *
  1355. * @description Only the admin user (the first user) can call the REST API.
  1356. *
  1357. * @param {string} userId the ID of the user to delete
  1358. * @return_type {_id: string}
  1359. */
  1360. JsonRoutes.add('DELETE', '/api/users/:userId', function(req, res) {
  1361. try {
  1362. Authentication.checkUserId(req.userId);
  1363. const id = req.params.userId;
  1364. Meteor.users.remove({ _id: id });
  1365. JsonRoutes.sendResult(res, {
  1366. code: 200,
  1367. data: {
  1368. _id: id,
  1369. },
  1370. });
  1371. } catch (error) {
  1372. JsonRoutes.sendResult(res, {
  1373. code: 200,
  1374. data: error,
  1375. });
  1376. }
  1377. });
  1378. }
  1379. export default Users;