users.js 35 KB

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