users.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. // Sandstorm context is detected using the METEOR_SETTINGS environment variable
  2. // in the package definition.
  3. const isSandstorm = Meteor.settings && Meteor.settings.public &&
  4. Meteor.settings.public.sandstorm;
  5. Users = Meteor.users;
  6. Users.attachSchema(new SimpleSchema({
  7. username: {
  8. type: String,
  9. optional: true,
  10. autoValue() { // eslint-disable-line consistent-return
  11. if (this.isInsert && !this.isSet) {
  12. const name = this.field('profile.fullname');
  13. if (name.isSet) {
  14. return name.value.toLowerCase().replace(/\s/g, '');
  15. }
  16. }
  17. },
  18. },
  19. emails: {
  20. type: [Object],
  21. optional: true,
  22. },
  23. 'emails.$.address': {
  24. type: String,
  25. regEx: SimpleSchema.RegEx.Email,
  26. },
  27. 'emails.$.verified': {
  28. type: Boolean,
  29. },
  30. createdAt: {
  31. type: Date,
  32. autoValue() { // eslint-disable-line consistent-return
  33. if (this.isInsert) {
  34. return new Date();
  35. } else {
  36. this.unset();
  37. }
  38. },
  39. },
  40. profile: {
  41. type: Object,
  42. optional: true,
  43. autoValue() { // eslint-disable-line consistent-return
  44. if (this.isInsert && !this.isSet) {
  45. return {
  46. boardView: 'board-view-lists',
  47. };
  48. }
  49. },
  50. },
  51. 'profile.avatarUrl': {
  52. type: String,
  53. optional: true,
  54. },
  55. 'profile.emailBuffer': {
  56. type: [String],
  57. optional: true,
  58. },
  59. 'profile.fullname': {
  60. type: String,
  61. optional: true,
  62. },
  63. 'profile.hiddenSystemMessages': {
  64. type: Boolean,
  65. optional: true,
  66. },
  67. 'profile.initials': {
  68. type: String,
  69. optional: true,
  70. },
  71. 'profile.invitedBoards': {
  72. type: [String],
  73. optional: true,
  74. },
  75. 'profile.language': {
  76. type: String,
  77. optional: true,
  78. },
  79. 'profile.notifications': {
  80. type: [String],
  81. optional: true,
  82. },
  83. 'profile.showCardsCountAt': {
  84. type: Number,
  85. optional: true,
  86. },
  87. 'profile.starredBoards': {
  88. type: [String],
  89. optional: true,
  90. },
  91. 'profile.tags': {
  92. type: [String],
  93. optional: true,
  94. },
  95. 'profile.icode': {
  96. type: String,
  97. optional: true,
  98. },
  99. 'profile.boardView': {
  100. type: String,
  101. optional: true,
  102. },
  103. services: {
  104. type: Object,
  105. optional: true,
  106. blackbox: true,
  107. },
  108. heartbeat: {
  109. type: Date,
  110. optional: true,
  111. },
  112. isAdmin: {
  113. type: Boolean,
  114. optional: true,
  115. },
  116. createdThroughApi: {
  117. type: Boolean,
  118. optional: true,
  119. },
  120. loginDisabled: {
  121. type: Boolean,
  122. optional: true,
  123. },
  124. }));
  125. Users.allow({
  126. update(userId) {
  127. const user = Users.findOne(userId);
  128. return user && Meteor.user().isAdmin;
  129. },
  130. });
  131. // Search a user in the complete server database by its name or username. This
  132. // is used for instance to add a new user to a board.
  133. const searchInFields = ['username', 'profile.fullname'];
  134. Users.initEasySearch(searchInFields, {
  135. use: 'mongo-db',
  136. returnFields: [...searchInFields, 'profile.avatarUrl'],
  137. });
  138. if (Meteor.isClient) {
  139. Users.helpers({
  140. isBoardMember() {
  141. const board = Boards.findOne(Session.get('currentBoard'));
  142. return board && board.hasMember(this._id);
  143. },
  144. isNotCommentOnly() {
  145. const board = Boards.findOne(Session.get('currentBoard'));
  146. return board && board.hasMember(this._id) && !board.hasCommentOnly(this._id);
  147. },
  148. isCommentOnly() {
  149. const board = Boards.findOne(Session.get('currentBoard'));
  150. return board && board.hasCommentOnly(this._id);
  151. },
  152. isBoardAdmin() {
  153. const board = Boards.findOne(Session.get('currentBoard'));
  154. return board && board.hasAdmin(this._id);
  155. },
  156. });
  157. }
  158. Users.helpers({
  159. boards() {
  160. return Boards.find({ 'members.userId': this._id });
  161. },
  162. starredBoards() {
  163. const {starredBoards = []} = this.profile;
  164. return Boards.find({archived: false, _id: {$in: starredBoards}});
  165. },
  166. hasStarred(boardId) {
  167. const {starredBoards = []} = this.profile;
  168. return _.contains(starredBoards, boardId);
  169. },
  170. invitedBoards() {
  171. const {invitedBoards = []} = this.profile;
  172. return Boards.find({archived: false, _id: {$in: invitedBoards}});
  173. },
  174. isInvitedTo(boardId) {
  175. const {invitedBoards = []} = this.profile;
  176. return _.contains(invitedBoards, boardId);
  177. },
  178. hasTag(tag) {
  179. const {tags = []} = this.profile;
  180. return _.contains(tags, tag);
  181. },
  182. hasNotification(activityId) {
  183. const {notifications = []} = this.profile;
  184. return _.contains(notifications, activityId);
  185. },
  186. hasHiddenSystemMessages() {
  187. const profile = this.profile || {};
  188. return profile.hiddenSystemMessages || false;
  189. },
  190. getEmailBuffer() {
  191. const {emailBuffer = []} = this.profile;
  192. return emailBuffer;
  193. },
  194. getInitials() {
  195. const profile = this.profile || {};
  196. if (profile.initials)
  197. return profile.initials;
  198. else if (profile.fullname) {
  199. return profile.fullname.split(/\s+/).reduce((memo, word) => {
  200. return memo + word[0];
  201. }, '').toUpperCase();
  202. } else {
  203. return this.username[0].toUpperCase();
  204. }
  205. },
  206. getLimitToShowCardsCount() {
  207. const profile = this.profile || {};
  208. return profile.showCardsCountAt;
  209. },
  210. getName() {
  211. const profile = this.profile || {};
  212. return profile.fullname || this.username;
  213. },
  214. getLanguage() {
  215. const profile = this.profile || {};
  216. return profile.language || 'en';
  217. },
  218. });
  219. Users.mutations({
  220. toggleBoardStar(boardId) {
  221. const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
  222. return {
  223. [queryKind]: {
  224. 'profile.starredBoards': boardId,
  225. },
  226. };
  227. },
  228. addInvite(boardId) {
  229. return {
  230. $addToSet: {
  231. 'profile.invitedBoards': boardId,
  232. },
  233. };
  234. },
  235. removeInvite(boardId) {
  236. return {
  237. $pull: {
  238. 'profile.invitedBoards': boardId,
  239. },
  240. };
  241. },
  242. addTag(tag) {
  243. return {
  244. $addToSet: {
  245. 'profile.tags': tag,
  246. },
  247. };
  248. },
  249. removeTag(tag) {
  250. return {
  251. $pull: {
  252. 'profile.tags': tag,
  253. },
  254. };
  255. },
  256. toggleTag(tag) {
  257. if (this.hasTag(tag))
  258. this.removeTag(tag);
  259. else
  260. this.addTag(tag);
  261. },
  262. toggleSystem(value = false) {
  263. return {
  264. $set: {
  265. 'profile.hiddenSystemMessages': !value,
  266. },
  267. };
  268. },
  269. addNotification(activityId) {
  270. return {
  271. $addToSet: {
  272. 'profile.notifications': activityId,
  273. },
  274. };
  275. },
  276. removeNotification(activityId) {
  277. return {
  278. $pull: {
  279. 'profile.notifications': activityId,
  280. },
  281. };
  282. },
  283. addEmailBuffer(text) {
  284. return {
  285. $addToSet: {
  286. 'profile.emailBuffer': text,
  287. },
  288. };
  289. },
  290. clearEmailBuffer() {
  291. return {
  292. $set: {
  293. 'profile.emailBuffer': [],
  294. },
  295. };
  296. },
  297. setAvatarUrl(avatarUrl) {
  298. return {$set: {'profile.avatarUrl': avatarUrl}};
  299. },
  300. setShowCardsCountAt(limit) {
  301. return {$set: {'profile.showCardsCountAt': limit}};
  302. },
  303. setBoardView(view) {
  304. return {
  305. $set : {
  306. 'profile.boardView': view,
  307. },
  308. };
  309. },
  310. });
  311. Meteor.methods({
  312. setUsername(username, userId) {
  313. check(username, String);
  314. const nUsersWithUsername = Users.find({username}).count();
  315. if (nUsersWithUsername > 0) {
  316. throw new Meteor.Error('username-already-taken');
  317. } else {
  318. Users.update(userId, {$set: {username}});
  319. }
  320. },
  321. toggleSystemMessages() {
  322. const user = Meteor.user();
  323. user.toggleSystem(user.hasHiddenSystemMessages());
  324. },
  325. changeLimitToShowCardsCount(limit) {
  326. check(limit, Number);
  327. Meteor.user().setShowCardsCountAt(limit);
  328. },
  329. setEmail(email, userId) {
  330. check(email, String);
  331. const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}});
  332. if (existingUser) {
  333. throw new Meteor.Error('email-already-taken');
  334. } else {
  335. Users.update(userId, {
  336. $set: {
  337. emails: [{
  338. address: email,
  339. verified: false,
  340. }],
  341. },
  342. });
  343. }
  344. },
  345. setUsernameAndEmail(username, email, userId) {
  346. check(username, String);
  347. check(email, String);
  348. check(userId, String);
  349. Meteor.call('setUsername', username, userId);
  350. Meteor.call('setEmail', email, userId);
  351. },
  352. setPassword(newPassword, userId) {
  353. check(userId, String);
  354. check(newPassword, String);
  355. if(Meteor.user().isAdmin){
  356. Accounts.setPassword(userId, newPassword);
  357. }
  358. },
  359. });
  360. if (Meteor.isServer) {
  361. Meteor.methods({
  362. // we accept userId, username, email
  363. inviteUserToBoard(username, boardId) {
  364. check(username, String);
  365. check(boardId, String);
  366. const inviter = Meteor.user();
  367. const board = Boards.findOne(boardId);
  368. const allowInvite = inviter &&
  369. board &&
  370. board.members &&
  371. _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
  372. _.where(board.members, {userId: inviter._id})[0].isActive &&
  373. _.where(board.members, {userId: inviter._id})[0].isAdmin;
  374. if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
  375. this.unblock();
  376. const posAt = username.indexOf('@');
  377. let user = null;
  378. if (posAt >= 0) {
  379. user = Users.findOne({emails: {$elemMatch: {address: username}}});
  380. } else {
  381. user = Users.findOne(username) || Users.findOne({username});
  382. }
  383. if (user) {
  384. if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
  385. } else {
  386. if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
  387. if (Settings.findOne().disableRegistration) throw new Meteor.Error('error-user-notCreated');
  388. // Set in lowercase email before creating account
  389. const email = username.toLowerCase();
  390. username = email.substring(0, posAt);
  391. const newUserId = Accounts.createUser({username, email});
  392. if (!newUserId) throw new Meteor.Error('error-user-notCreated');
  393. // assume new user speak same language with inviter
  394. if (inviter.profile && inviter.profile.language) {
  395. Users.update(newUserId, {
  396. $set: {
  397. 'profile.language': inviter.profile.language,
  398. },
  399. });
  400. }
  401. Accounts.sendEnrollmentEmail(newUserId);
  402. user = Users.findOne(newUserId);
  403. }
  404. board.addMember(user._id);
  405. user.addInvite(boardId);
  406. try {
  407. const params = {
  408. user: user.username,
  409. inviter: inviter.username,
  410. board: board.title,
  411. url: board.absoluteUrl(),
  412. };
  413. const lang = user.getLanguage();
  414. Email.send({
  415. to: user.emails[0].address.toLowerCase(),
  416. from: Accounts.emailTemplates.from,
  417. subject: TAPi18n.__('email-invite-subject', params, lang),
  418. text: TAPi18n.__('email-invite-text', params, lang),
  419. });
  420. } catch (e) {
  421. throw new Meteor.Error('email-fail', e.message);
  422. }
  423. return {username: user.username, email: user.emails[0].address};
  424. },
  425. });
  426. Accounts.onCreateUser((options, user) => {
  427. const userCount = Users.find().count();
  428. if (!isSandstorm && userCount === 0) {
  429. user.isAdmin = true;
  430. return user;
  431. }
  432. if (options.from === 'admin') {
  433. user.createdThroughApi = true;
  434. return user;
  435. }
  436. const disableRegistration = Settings.findOne().disableRegistration;
  437. if (!disableRegistration) {
  438. return user;
  439. }
  440. if (!options || !options.profile) {
  441. throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required');
  442. }
  443. const invitationCode = InvitationCodes.findOne({
  444. code: options.profile.invitationcode,
  445. email: options.email,
  446. valid: true,
  447. });
  448. if (!invitationCode) {
  449. throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist');
  450. } else {
  451. user.profile = {icode: options.profile.invitationcode};
  452. }
  453. return user;
  454. });
  455. }
  456. if (Meteor.isServer) {
  457. // Let mongoDB ensure username unicity
  458. Meteor.startup(() => {
  459. Users._collection._ensureIndex({
  460. username: 1,
  461. }, {unique: true});
  462. });
  463. // Each board document contains the de-normalized number of users that have
  464. // starred it. If the user star or unstar a board, we need to update this
  465. // counter.
  466. // We need to run this code on the server only, otherwise the incrementation
  467. // will be done twice.
  468. Users.after.update(function (userId, user, fieldNames) {
  469. // The `starredBoards` list is hosted on the `profile` field. If this
  470. // field hasn't been modificated we don't need to run this hook.
  471. if (!_.contains(fieldNames, 'profile'))
  472. return;
  473. // To calculate a diff of board starred ids, we get both the previous
  474. // and the newly board ids list
  475. function getStarredBoardsIds(doc) {
  476. return doc.profile && doc.profile.starredBoards;
  477. }
  478. const oldIds = getStarredBoardsIds(this.previous);
  479. const newIds = getStarredBoardsIds(user);
  480. // The _.difference(a, b) method returns the values from a that are not in
  481. // b. We use it to find deleted and newly inserted ids by using it in one
  482. // direction and then in the other.
  483. function incrementBoards(boardsIds, inc) {
  484. boardsIds.forEach((boardId) => {
  485. Boards.update(boardId, {$inc: {stars: inc}});
  486. });
  487. }
  488. incrementBoards(_.difference(oldIds, newIds), -1);
  489. incrementBoards(_.difference(newIds, oldIds), +1);
  490. });
  491. const fakeUserId = new Meteor.EnvironmentVariable();
  492. const getUserId = CollectionHooks.getUserId;
  493. CollectionHooks.getUserId = () => {
  494. return fakeUserId.get() || getUserId();
  495. };
  496. if (!isSandstorm) {
  497. Users.after.insert((userId, doc) => {
  498. const fakeUser = {
  499. extendAutoValueContext: {
  500. userId: doc._id,
  501. },
  502. };
  503. fakeUserId.withValue(doc._id, () => {
  504. // Insert the Welcome Board
  505. Boards.insert({
  506. title: TAPi18n.__('welcome-board'),
  507. permission: 'private',
  508. }, fakeUser, (err, boardId) => {
  509. Swimlanes.insert({
  510. title: TAPi18n.__('welcome-swimlane'),
  511. boardId,
  512. sort: 1,
  513. }, fakeUser);
  514. ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
  515. Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
  516. });
  517. });
  518. });
  519. });
  520. }
  521. Users.after.insert((userId, doc) => {
  522. if (doc.createdThroughApi) {
  523. // The admin user should be able to create a user despite disabling registration because
  524. // it is two different things (registration and creation).
  525. // So, when a new user is created via the api (only admin user can do that) one must avoid
  526. // the disableRegistration check.
  527. // Issue : https://github.com/wekan/wekan/issues/1232
  528. // PR : https://github.com/wekan/wekan/pull/1251
  529. Users.update(doc._id, {$set: {createdThroughApi: ''}});
  530. return;
  531. }
  532. //invite user to corresponding boards
  533. const disableRegistration = Settings.findOne().disableRegistration;
  534. if (disableRegistration) {
  535. const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true});
  536. if (!invitationCode) {
  537. throw new Meteor.Error('error-invitation-code-not-exist');
  538. } else {
  539. invitationCode.boardsToBeInvited.forEach((boardId) => {
  540. const board = Boards.findOne(boardId);
  541. board.addMember(doc._id);
  542. });
  543. if (!doc.profile) {
  544. doc.profile = {};
  545. }
  546. doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
  547. Users.update(doc._id, {$set: {profile: doc.profile}});
  548. InvitationCodes.update(invitationCode._id, {$set: {valid: false}});
  549. }
  550. }
  551. });
  552. }
  553. // USERS REST API
  554. if (Meteor.isServer) {
  555. JsonRoutes.add('GET', '/api/user', function(req, res) {
  556. try {
  557. Authentication.checkLoggedIn(req.userId);
  558. const data = Meteor.users.findOne({ _id: req.userId});
  559. delete data.services;
  560. JsonRoutes.sendResult(res, {
  561. code: 200,
  562. data,
  563. });
  564. }
  565. catch (error) {
  566. JsonRoutes.sendResult(res, {
  567. code: 200,
  568. data: error,
  569. });
  570. }
  571. });
  572. JsonRoutes.add('GET', '/api/users', function (req, res) {
  573. try {
  574. Authentication.checkUserId(req.userId);
  575. JsonRoutes.sendResult(res, {
  576. code: 200,
  577. data: Meteor.users.find({}).map(function (doc) {
  578. return { _id: doc._id, username: doc.username };
  579. }),
  580. });
  581. }
  582. catch (error) {
  583. JsonRoutes.sendResult(res, {
  584. code: 200,
  585. data: error,
  586. });
  587. }
  588. });
  589. JsonRoutes.add('GET', '/api/users/:id', function (req, res) {
  590. try {
  591. Authentication.checkUserId(req.userId);
  592. const id = req.params.id;
  593. JsonRoutes.sendResult(res, {
  594. code: 200,
  595. data: Meteor.users.findOne({ _id: id }),
  596. });
  597. }
  598. catch (error) {
  599. JsonRoutes.sendResult(res, {
  600. code: 200,
  601. data: error,
  602. });
  603. }
  604. });
  605. JsonRoutes.add('PUT', '/api/users/:id', function (req, res) {
  606. try {
  607. Authentication.checkUserId(req.userId);
  608. const id = req.params.id;
  609. const action = req.body.action;
  610. let data = Meteor.users.findOne({ _id: id });
  611. if (data !== undefined) {
  612. if (action === 'takeOwnership') {
  613. data = Boards.find({
  614. 'members.userId': id,
  615. 'members.isAdmin': true,
  616. }).map(function(board) {
  617. if (board.hasMember(req.userId)) {
  618. board.removeMember(req.userId);
  619. }
  620. board.changeOwnership(id, req.userId);
  621. return {
  622. _id: board._id,
  623. title: board.title,
  624. };
  625. });
  626. } else {
  627. if ((action === 'disableLogin') && (id !== req.userId)) {
  628. Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } });
  629. } else if (action === 'enableLogin') {
  630. Users.update({ _id: id }, { $set: { loginDisabled: '' } });
  631. }
  632. data = Meteor.users.findOne({ _id: id });
  633. }
  634. }
  635. JsonRoutes.sendResult(res, {
  636. code: 200,
  637. data,
  638. });
  639. }
  640. catch (error) {
  641. JsonRoutes.sendResult(res, {
  642. code: 200,
  643. data: error,
  644. });
  645. }
  646. });
  647. JsonRoutes.add('POST', '/api/users/', function (req, res) {
  648. try {
  649. Authentication.checkUserId(req.userId);
  650. const id = Accounts.createUser({
  651. username: req.body.username,
  652. email: req.body.email,
  653. password: req.body.password,
  654. from: 'admin',
  655. });
  656. JsonRoutes.sendResult(res, {
  657. code: 200,
  658. data: {
  659. _id: id,
  660. },
  661. });
  662. }
  663. catch (error) {
  664. JsonRoutes.sendResult(res, {
  665. code: 200,
  666. data: error,
  667. });
  668. }
  669. });
  670. JsonRoutes.add('DELETE', '/api/users/:id', function (req, res) {
  671. try {
  672. Authentication.checkUserId(req.userId);
  673. const id = req.params.id;
  674. Meteor.users.remove({ _id: id });
  675. JsonRoutes.sendResult(res, {
  676. code: 200,
  677. data: {
  678. _id: id,
  679. },
  680. });
  681. }
  682. catch (error) {
  683. JsonRoutes.sendResult(res, {
  684. code: 200,
  685. data: error,
  686. });
  687. }
  688. });
  689. }