users.js 33 KB

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