summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/accountSettings.js90
-rw-r--r--models/actions.js15
-rw-r--r--models/activities.js45
-rw-r--r--models/announcements.js74
-rw-r--r--models/attachments.js19
-rw-r--r--models/avatars.js10
-rw-r--r--models/boards.js953
-rw-r--r--models/cardComments.js280
-rw-r--r--models/cards.js25
-rw-r--r--models/checklistItems.js229
-rw-r--r--models/checklists.js313
-rw-r--r--models/customFields.js297
-rw-r--r--models/integrations.js336
-rw-r--r--models/invitationCodes.js87
-rw-r--r--models/lists.js361
-rw-r--r--models/rules.js86
-rw-r--r--models/settings.js262
-rw-r--r--models/swimlanes.js336
-rw-r--r--models/triggers.js16
-rw-r--r--models/unsavedEdits.js73
-rw-r--r--models/users.js889
m---------packages/wekan-iframe0
-rw-r--r--packages/wekan_accounts-oidc/.gitignore1
-rw-r--r--packages/wekan_accounts-oidc/LICENSE.txt14
-rw-r--r--packages/wekan_accounts-oidc/README.md75
-rw-r--r--packages/wekan_accounts-oidc/oidc.js22
-rw-r--r--packages/wekan_accounts-oidc/oidc_login_button.css3
-rw-r--r--packages/wekan_accounts-oidc/package.js19
-rw-r--r--packages/wekan_oidc/.gitignore1
-rw-r--r--packages/wekan_oidc/LICENSE.txt14
-rw-r--r--packages/wekan_oidc/README.md7
-rw-r--r--packages/wekan_oidc/oidc_client.js68
-rw-r--r--packages/wekan_oidc/oidc_configure.html6
-rw-r--r--packages/wekan_oidc/oidc_configure.js17
-rw-r--r--packages/wekan_oidc/oidc_server.js143
-rw-r--r--packages/wekan_oidc/package.js23
-rw-r--r--server/migrations.js684
37 files changed, 3723 insertions, 2170 deletions
diff --git a/models/accountSettings.js b/models/accountSettings.js
index 6dfbac5d..c4240f84 100644
--- a/models/accountSettings.js
+++ b/models/accountSettings.js
@@ -1,18 +1,44 @@
AccountSettings = new Mongo.Collection('accountSettings');
-AccountSettings.attachSchema(new SimpleSchema({
- _id: {
- type: String,
- },
- booleanValue: {
- type: Boolean,
- optional: true,
- },
- sort: {
- type: Number,
- decimal: true,
- },
-}));
+AccountSettings.attachSchema(
+ new SimpleSchema({
+ _id: {
+ type: String,
+ },
+ booleanValue: {
+ type: Boolean,
+ optional: true,
+ },
+ sort: {
+ type: Number,
+ decimal: true,
+ },
+ createdAt: {
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ })
+);
AccountSettings.allow({
update(userId) {
@@ -21,19 +47,33 @@ AccountSettings.allow({
},
});
+AccountSettings.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
if (Meteor.isServer) {
Meteor.startup(() => {
- AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, {
- $setOnInsert: {
- booleanValue: false,
- sort: 0,
- },
- });
- AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, {
- $setOnInsert: {
- booleanValue: false,
- sort: 1,
- },
- });
+ AccountSettings._collection._ensureIndex({ modifiedAt: -1 });
+ AccountSettings.upsert(
+ { _id: 'accounts-allowEmailChange' },
+ {
+ $setOnInsert: {
+ booleanValue: false,
+ sort: 0,
+ },
+ }
+ );
+ AccountSettings.upsert(
+ { _id: 'accounts-allowUserNameChange' },
+ {
+ $setOnInsert: {
+ booleanValue: false,
+ sort: 1,
+ },
+ }
+ );
});
}
+
+export default AccountSettings;
diff --git a/models/actions.js b/models/actions.js
index 0430b044..8ac764aa 100644
--- a/models/actions.js
+++ b/models/actions.js
@@ -1,3 +1,5 @@
+import { Meteor } from 'meteor/meteor';
+
Actions = new Mongo.Collection('actions');
Actions.allow({
@@ -17,3 +19,16 @@ Actions.helpers({
return this.desc;
},
});
+
+Actions.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
+if (Meteor.isServer) {
+ Meteor.startup(() => {
+ Actions._collection._ensureIndex({ modifiedAt: -1 });
+ });
+}
+
+export default Actions;
diff --git a/models/activities.js b/models/activities.js
index 908d4b14..0e158802 100644
--- a/models/activities.js
+++ b/models/activities.js
@@ -69,7 +69,11 @@ Activities.before.insert((userId, doc) => {
Activities.after.insert((userId, doc) => {
const activity = Activities._transform(doc);
RulesHelper.executeRules(activity);
+});
+Activities.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
@@ -78,11 +82,21 @@ if (Meteor.isServer) {
// are largely used in the App. See #524.
Meteor.startup(() => {
Activities._collection._ensureIndex({ createdAt: -1 });
+ Activities._collection._ensureIndex({ modifiedAt: -1 });
Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 });
- Activities._collection._ensureIndex({ commentId: 1 }, { partialFilterExpression: { commentId: { $exists: true } } });
- Activities._collection._ensureIndex({ attachmentId: 1 }, { partialFilterExpression: { attachmentId: { $exists: true } } });
- Activities._collection._ensureIndex({ customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } });
+ Activities._collection._ensureIndex(
+ { commentId: 1 },
+ { partialFilterExpression: { commentId: { $exists: true } } }
+ );
+ Activities._collection._ensureIndex(
+ { attachmentId: 1 },
+ { partialFilterExpression: { attachmentId: { $exists: true } } }
+ );
+ Activities._collection._ensureIndex(
+ { customFieldId: 1 },
+ { partialFilterExpression: { customFieldId: { $exists: true } } }
+ );
// Label activity did not work yet, unable to edit labels when tried this.
//Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 });
//Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
@@ -189,18 +203,35 @@ if (Meteor.isServer) {
// params.labelId = activity.labelId;
//}
if (board) {
- const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
- const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
- watchers = _.union(watchers, watchingUsers, _.intersection(participants, trackingUsers));
+ const watchingUsers = _.pluck(
+ _.where(board.watchers, { level: 'watching' }),
+ 'userId'
+ );
+ const trackingUsers = _.pluck(
+ _.where(board.watchers, { level: 'tracking' }),
+ 'userId'
+ );
+ watchers = _.union(
+ watchers,
+ watchingUsers,
+ _.intersection(participants, trackingUsers)
+ );
}
Notifications.getUsers(watchers).forEach((user) => {
Notifications.notify(user, title, description, params);
});
- const integrations = Integrations.find({ boardId: board._id, type: 'outgoing-webhooks', enabled: true, activities: { '$in': [description, 'all'] } }).fetch();
+ const integrations = Integrations.find({
+ boardId: board._id,
+ type: 'outgoing-webhooks',
+ enabled: true,
+ activities: { $in: [description, 'all'] },
+ }).fetch();
if (integrations.length > 0) {
Meteor.call('outgoingWebhooks', integrations, description, params);
}
});
}
+
+export default Activities;
diff --git a/models/announcements.js b/models/announcements.js
index 2cb1e1b7..f3a62244 100644
--- a/models/announcements.js
+++ b/models/announcements.js
@@ -1,23 +1,49 @@
Announcements = new Mongo.Collection('announcements');
-Announcements.attachSchema(new SimpleSchema({
- enabled: {
- type: Boolean,
- defaultValue: false,
- },
- title: {
- type: String,
- optional: true,
- },
- body: {
- type: String,
- optional: true,
- },
- sort: {
- type: Number,
- decimal: true,
- },
-}));
+Announcements.attachSchema(
+ new SimpleSchema({
+ enabled: {
+ type: Boolean,
+ defaultValue: false,
+ },
+ title: {
+ type: String,
+ optional: true,
+ },
+ body: {
+ type: String,
+ optional: true,
+ },
+ sort: {
+ type: Number,
+ decimal: true,
+ },
+ createdAt: {
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ })
+);
Announcements.allow({
update(userId) {
@@ -26,11 +52,19 @@ Announcements.allow({
},
});
+Announcements.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
if (Meteor.isServer) {
Meteor.startup(() => {
+ Announcements._collection._ensureIndex({ modifiedAt: -1 });
const announcements = Announcements.findOne({});
- if(!announcements){
- Announcements.insert({enabled: false, sort: 0});
+ if (!announcements) {
+ Announcements.insert({ enabled: false, sort: 0 });
}
});
}
+
+export default Announcements;
diff --git a/models/attachments.js b/models/attachments.js
index 71b30eee..893b5aca 100644
--- a/models/attachments.js
+++ b/models/attachments.js
@@ -1,6 +1,5 @@
Attachments = new FS.Collection('attachments', {
stores: [
-
// XXX Add a new store for cover thumbnails so we don't load big images in
// the general board view
new FS.Store.GridFS('attachments', {
@@ -25,7 +24,6 @@ Attachments = new FS.Collection('attachments', {
],
});
-
if (Meteor.isServer) {
Meteor.startup(() => {
Attachments.files._ensureIndex({ cardId: 1 });
@@ -78,13 +76,16 @@ if (Meteor.isServer) {
} else {
// Don't add activity about adding the attachment as the activity
// be imported and delete source field
- Attachments.update({
- _id: doc._id,
- }, {
- $unset: {
- source: '',
+ Attachments.update(
+ {
+ _id: doc._id,
},
- });
+ {
+ $unset: {
+ source: '',
+ },
+ }
+ );
}
});
@@ -107,3 +108,5 @@ if (Meteor.isServer) {
});
});
}
+
+export default Attachments;
diff --git a/models/avatars.js b/models/avatars.js
index 53924ffb..2fda031d 100644
--- a/models/avatars.js
+++ b/models/avatars.js
@@ -1,7 +1,5 @@
Avatars = new FS.Collection('avatars', {
- stores: [
- new FS.Store.GridFS('avatars'),
- ],
+ stores: [new FS.Store.GridFS('avatars')],
filter: {
maxSize: 72000,
allow: {
@@ -18,10 +16,14 @@ Avatars.allow({
insert: isOwner,
update: isOwner,
remove: isOwner,
- download() { return true; },
+ download() {
+ return true;
+ },
fetch: ['userId'],
});
Avatars.files.before.insert((userId, doc) => {
doc.userId = userId;
});
+
+export default Avatars;
diff --git a/models/boards.js b/models/boards.js
index 396d90fb..2792f80a 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -3,316 +3,347 @@ Boards = new Mongo.Collection('boards');
/**
* This is a Board.
*/
-Boards.attachSchema(new SimpleSchema({
- title: {
- /**
- * The title of the board
- */
- type: String,
- },
- slug: {
- /**
- * The title slugified.
- */
- type: String,
- autoValue() { // eslint-disable-line consistent-return
- // XXX We need to improve slug management. Only the id should be necessary
- // to identify a board in the code.
- // XXX If the board title is updated, the slug should also be updated.
- // In some cases (Chinese and Japanese for instance) the `getSlug` function
- // return an empty string. This is causes bugs in our application so we set
- // a default slug in this case.
- if (this.isInsert && !this.isSet) {
- let slug = 'board';
- const title = this.field('title');
- if (title.isSet) {
- slug = getSlug(title.value) || slug;
+Boards.attachSchema(
+ new SimpleSchema({
+ title: {
+ /**
+ * The title of the board
+ */
+ type: String,
+ },
+ slug: {
+ /**
+ * The title slugified.
+ */
+ type: String,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ // XXX We need to improve slug management. Only the id should be necessary
+ // to identify a board in the code.
+ // XXX If the board title is updated, the slug should also be updated.
+ // In some cases (Chinese and Japanese for instance) the `getSlug` function
+ // return an empty string. This is causes bugs in our application so we set
+ // a default slug in this case.
+ if (this.isInsert && !this.isSet) {
+ let slug = 'board';
+ const title = this.field('title');
+ if (title.isSet) {
+ slug = getSlug(title.value) || slug;
+ }
+ return slug;
}
- return slug;
- }
+ },
},
- },
- archived: {
- /**
- * Is the board archived?
- */
- type: Boolean,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return false;
- }
+ archived: {
+ /**
+ * Is the board archived?
+ */
+ type: Boolean,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return false;
+ }
+ },
},
- },
- createdAt: {
- /**
- * Creation time of the board
- */
- type: Date,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+ createdAt: {
+ /**
+ * Creation time of the board
+ */
+ type: Date,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
},
- },
- // XXX Inconsistent field naming
- modifiedAt: {
- /**
- * Last modification time of the board
- */
- type: Date,
- optional: true,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isUpdate) {
- return new Date();
- } else {
- this.unset();
- }
+ // XXX Inconsistent field naming
+ modifiedAt: {
+ /**
+ * Last modification time of the board
+ */
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
},
- },
- // De-normalized number of users that have starred this board
- stars: {
- /**
- * How many stars the board has
- */
- type: Number,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return 0;
- }
+ // De-normalized number of users that have starred this board
+ stars: {
+ /**
+ * How many stars the board has
+ */
+ type: Number,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return 0;
+ }
+ },
},
- },
- // De-normalized label system
- 'labels': {
- /**
- * List of labels attached to a board
- */
- type: [Object],
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
- const defaultLabelsColors = _.clone(colors).splice(0, 6);
- return defaultLabelsColors.map((color) => ({
- color,
- _id: Random.id(6),
- name: '',
- }));
- }
+ // De-normalized label system
+ labels: {
+ /**
+ * List of labels attached to a board
+ */
+ type: [Object],
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ const colors = Boards.simpleSchema()._schema['labels.$.color']
+ .allowedValues;
+ const defaultLabelsColors = _.clone(colors).splice(0, 6);
+ return defaultLabelsColors.map((color) => ({
+ color,
+ _id: Random.id(6),
+ name: '',
+ }));
+ }
+ },
},
- },
- 'labels.$._id': {
- /**
- * Unique id of a label
- */
- // We don't specify that this field must be unique in the board because that
- // will cause performance penalties and is not necessary since this field is
- // always set on the server.
- // XXX Actually if we create a new label, the `_id` is set on the client
- // without being overwritten by the server, could it be a problem?
- type: String,
- },
- 'labels.$.name': {
- /**
- * Name of a label
- */
- type: String,
- optional: true,
- },
- 'labels.$.color': {
- /**
- * color of a label.
- *
- * Can be amongst `green`, `yellow`, `orange`, `red`, `purple`,
- * `blue`, `sky`, `lime`, `pink`, `black`,
- * `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`,
- * `slateblue`, `magenta`, `gold`, `navy`, `gray`,
- * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`
- */
- type: String,
- allowedValues: [
- 'green', 'yellow', 'orange', 'red', 'purple',
- 'blue', 'sky', 'lime', 'pink', 'black',
- 'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen',
- 'slateblue', 'magenta', 'gold', 'navy', 'gray',
- 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
- ],
- },
- // XXX We might want to maintain more informations under the member sub-
- // documents like de-normalized meta-data (the date the member joined the
- // board, the number of contributions, etc.).
- 'members': {
- /**
- * List of members of a board
- */
- type: [Object],
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return [{
- userId: this.userId,
- isAdmin: true,
- isActive: true,
- isNoComments: false,
- isCommentOnly: false,
- }];
- }
+ 'labels.$._id': {
+ /**
+ * Unique id of a label
+ */
+ // We don't specify that this field must be unique in the board because that
+ // will cause performance penalties and is not necessary since this field is
+ // always set on the server.
+ // XXX Actually if we create a new label, the `_id` is set on the client
+ // without being overwritten by the server, could it be a problem?
+ type: String,
},
- },
- 'members.$.userId': {
- /**
- * The uniq ID of the member
- */
- type: String,
- },
- 'members.$.isAdmin': {
- /**
- * Is the member an admin of the board?
- */
- type: Boolean,
- },
- 'members.$.isActive': {
- /**
- * Is the member active?
- */
- type: Boolean,
- },
- 'members.$.isNoComments': {
- /**
- * Is the member not allowed to make comments
- */
- type: Boolean,
- optional: true,
- },
- 'members.$.isCommentOnly': {
- /**
- * Is the member only allowed to comment on the board
- */
- type: Boolean,
- optional: true,
- },
- permission: {
- /**
- * visibility of the board
- */
- type: String,
- allowedValues: ['public', 'private'],
- },
- color: {
- /**
- * The color of the board.
- */
- type: String,
- allowedValues: [
- 'belize',
- 'nephritis',
- 'pomegranate',
- 'pumpkin',
- 'wisteria',
- 'midnight',
- ],
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return Boards.simpleSchema()._schema.color.allowedValues[0];
- }
+ 'labels.$.name': {
+ /**
+ * Name of a label
+ */
+ type: String,
+ optional: true,
},
- },
- description: {
- /**
- * The description of the board
- */
- type: String,
- optional: true,
- },
- subtasksDefaultBoardId: {
- /**
- * The default board ID assigned to subtasks.
- */
- type: String,
- optional: true,
- defaultValue: null,
- },
- subtasksDefaultListId: {
- /**
- * The default List ID assigned to subtasks.
- */
- type: String,
- optional: true,
- defaultValue: null,
- },
- allowsSubtasks: {
- /**
- * Does the board allows subtasks?
- */
- type: Boolean,
- defaultValue: true,
- },
- presentParentTask: {
- /**
- * Controls how to present the parent task:
- *
- * - `prefix-with-full-path`: add a prefix with the full path
- * - `prefix-with-parent`: add a prefisx with the parent name
- * - `subtext-with-full-path`: add a subtext with the full path
- * - `subtext-with-parent`: add a subtext with the parent name
- * - `no-parent`: does not show the parent at all
- */
- type: String,
- allowedValues: [
- 'prefix-with-full-path',
- 'prefix-with-parent',
- 'subtext-with-full-path',
- 'subtext-with-parent',
- 'no-parent',
- ],
- optional: true,
- defaultValue: 'no-parent',
- },
- startAt: {
- /**
- * Starting date of the board.
- */
- type: Date,
- optional: true,
- },
- dueAt: {
- /**
- * Due date of the board.
- */
- type: Date,
- optional: true,
- },
- endAt: {
- /**
- * End date of the board.
- */
- type: Date,
- optional: true,
- },
- spentTime: {
- /**
- * Time spent in the board.
- */
- type: Number,
- decimal: true,
- optional: true,
- },
- isOvertime: {
- /**
- * Is the board overtimed?
- */
- type: Boolean,
- defaultValue: false,
- optional: true,
- },
- type: {
- /**
- * The type of board
- */
- type: String,
- defaultValue: 'board',
- },
-}));
-
+ 'labels.$.color': {
+ /**
+ * color of a label.
+ *
+ * Can be amongst `green`, `yellow`, `orange`, `red`, `purple`,
+ * `blue`, `sky`, `lime`, `pink`, `black`,
+ * `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`,
+ * `slateblue`, `magenta`, `gold`, `navy`, `gray`,
+ * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`
+ */
+ type: String,
+ allowedValues: [
+ 'green',
+ 'yellow',
+ 'orange',
+ 'red',
+ 'purple',
+ 'blue',
+ 'sky',
+ 'lime',
+ 'pink',
+ 'black',
+ 'silver',
+ 'peachpuff',
+ 'crimson',
+ 'plum',
+ 'darkgreen',
+ 'slateblue',
+ 'magenta',
+ 'gold',
+ 'navy',
+ 'gray',
+ 'saddlebrown',
+ 'paleturquoise',
+ 'mistyrose',
+ 'indigo',
+ ],
+ },
+ // XXX We might want to maintain more informations under the member sub-
+ // documents like de-normalized meta-data (the date the member joined the
+ // board, the number of contributions, etc.).
+ members: {
+ /**
+ * List of members of a board
+ */
+ type: [Object],
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return [
+ {
+ userId: this.userId,
+ isAdmin: true,
+ isActive: true,
+ isNoComments: false,
+ isCommentOnly: false,
+ },
+ ];
+ }
+ },
+ },
+ 'members.$.userId': {
+ /**
+ * The uniq ID of the member
+ */
+ type: String,
+ },
+ 'members.$.isAdmin': {
+ /**
+ * Is the member an admin of the board?
+ */
+ type: Boolean,
+ },
+ 'members.$.isActive': {
+ /**
+ * Is the member active?
+ */
+ type: Boolean,
+ },
+ 'members.$.isNoComments': {
+ /**
+ * Is the member not allowed to make comments
+ */
+ type: Boolean,
+ optional: true,
+ },
+ 'members.$.isCommentOnly': {
+ /**
+ * Is the member only allowed to comment on the board
+ */
+ type: Boolean,
+ optional: true,
+ },
+ permission: {
+ /**
+ * visibility of the board
+ */
+ type: String,
+ allowedValues: ['public', 'private'],
+ },
+ color: {
+ /**
+ * The color of the board.
+ */
+ type: String,
+ allowedValues: [
+ 'belize',
+ 'nephritis',
+ 'pomegranate',
+ 'pumpkin',
+ 'wisteria',
+ 'midnight',
+ ],
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return Boards.simpleSchema()._schema.color.allowedValues[0];
+ }
+ },
+ },
+ description: {
+ /**
+ * The description of the board
+ */
+ type: String,
+ optional: true,
+ },
+ subtasksDefaultBoardId: {
+ /**
+ * The default board ID assigned to subtasks.
+ */
+ type: String,
+ optional: true,
+ defaultValue: null,
+ },
+ subtasksDefaultListId: {
+ /**
+ * The default List ID assigned to subtasks.
+ */
+ type: String,
+ optional: true,
+ defaultValue: null,
+ },
+ allowsSubtasks: {
+ /**
+ * Does the board allows subtasks?
+ */
+ type: Boolean,
+ defaultValue: true,
+ },
+ presentParentTask: {
+ /**
+ * Controls how to present the parent task:
+ *
+ * - `prefix-with-full-path`: add a prefix with the full path
+ * - `prefix-with-parent`: add a prefisx with the parent name
+ * - `subtext-with-full-path`: add a subtext with the full path
+ * - `subtext-with-parent`: add a subtext with the parent name
+ * - `no-parent`: does not show the parent at all
+ */
+ type: String,
+ allowedValues: [
+ 'prefix-with-full-path',
+ 'prefix-with-parent',
+ 'subtext-with-full-path',
+ 'subtext-with-parent',
+ 'no-parent',
+ ],
+ optional: true,
+ defaultValue: 'no-parent',
+ },
+ startAt: {
+ /**
+ * Starting date of the board.
+ */
+ type: Date,
+ optional: true,
+ },
+ dueAt: {
+ /**
+ * Due date of the board.
+ */
+ type: Date,
+ optional: true,
+ },
+ endAt: {
+ /**
+ * End date of the board.
+ */
+ type: Date,
+ optional: true,
+ },
+ spentTime: {
+ /**
+ * Time spent in the board.
+ */
+ type: Number,
+ decimal: true,
+ optional: true,
+ },
+ isOvertime: {
+ /**
+ * Is the board overtimed?
+ */
+ type: Boolean,
+ defaultValue: false,
+ optional: true,
+ },
+ type: {
+ /**
+ * The type of board
+ */
+ type: String,
+ defaultValue: 'board',
+ },
+ })
+);
Boards.helpers({
copy() {
@@ -350,7 +381,9 @@ Boards.helpers({
*/
isActiveMember(userId) {
if (userId) {
- return this.members.find((member) => (member.userId === userId && member.isActive));
+ return this.members.find(
+ (member) => member.userId === userId && member.isActive
+ );
} else {
return false;
}
@@ -361,11 +394,17 @@ Boards.helpers({
},
cards() {
- return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } });
+ return Cards.find(
+ { boardId: this._id, archived: false },
+ { sort: { title: 1 } }
+ );
},
lists() {
- return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
+ return Lists.find(
+ { boardId: this._id, archived: false },
+ { sort: { sort: 1 } }
+ );
},
nullSortLists() {
@@ -377,18 +416,24 @@ Boards.helpers({
},
swimlanes() {
- return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
+ return Swimlanes.find(
+ { boardId: this._id, archived: false },
+ { sort: { sort: 1 } }
+ );
},
nextSwimlane(swimlane) {
- return Swimlanes.findOne({
- boardId: this._id,
- archived: false,
- sort: { $gte: swimlane.sort },
- _id: { $ne: swimlane._id },
- }, {
- sort: { sort: 1 },
- });
+ return Swimlanes.findOne(
+ {
+ boardId: this._id,
+ archived: false,
+ sort: { $gte: swimlane.sort },
+ _id: { $ne: swimlane._id },
+ },
+ {
+ sort: { sort: 1 },
+ }
+ );
},
nullSortSwimlanes() {
@@ -399,13 +444,21 @@ Boards.helpers({
});
},
- hasOvertimeCards(){
- const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
+ hasOvertimeCards() {
+ const card = Cards.findOne({
+ isOvertime: true,
+ boardId: this._id,
+ archived: false,
+ });
return card !== undefined;
},
- hasSpentTimeCards(){
- const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} );
+ hasSpentTimeCards() {
+ const card = Cards.findOne({
+ spentTime: { $gt: 0 },
+ boardId: this._id,
+ archived: false,
+ });
return card !== undefined;
},
@@ -429,7 +482,7 @@ Boards.helpers({
return _.findWhere(this.labels, { name, color });
},
- getLabelById(labelId){
+ getLabelById(labelId) {
return _.findWhere(this.labels, { _id: labelId });
},
@@ -446,15 +499,29 @@ Boards.helpers({
},
hasAdmin(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true });
+ return !!_.findWhere(this.members, {
+ userId: memberId,
+ isActive: true,
+ isAdmin: true,
+ });
},
hasNoComments(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true });
+ return !!_.findWhere(this.members, {
+ userId: memberId,
+ isActive: true,
+ isAdmin: false,
+ isNoComments: true,
+ });
},
hasCommentOnly(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true });
+ return !!_.findWhere(this.members, {
+ userId: memberId,
+ isActive: true,
+ isAdmin: false,
+ isCommentOnly: true,
+ });
},
absoluteUrl() {
@@ -466,7 +533,10 @@ Boards.helpers({
},
customFields() {
- return CustomFields.find({ boardIds: {$in: [this._id]} }, { sort: { name: 1 } });
+ return CustomFields.find(
+ { boardIds: { $in: [this._id] } },
+ { sort: { name: 1 } }
+ );
},
// XXX currently mutations return no value so we have an issue when using addLabel in import
@@ -489,10 +559,7 @@ Boards.helpers({
if (term) {
const regex = new RegExp(term, 'i');
- query.$or = [
- { title: regex },
- { description: regex },
- ];
+ query.$or = [{ title: regex }, { description: regex }];
}
return Cards.find(query, projection);
@@ -506,17 +573,14 @@ Boards.helpers({
query.type = 'template-swimlane';
query.archived = false;
} else {
- query.type = {$nin: ['template-swimlane']};
+ query.type = { $nin: ['template-swimlane'] };
}
const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) {
const regex = new RegExp(term, 'i');
- query.$or = [
- { title: regex },
- { description: regex },
- ];
+ query.$or = [{ title: regex }, { description: regex }];
}
return Swimlanes.find(query, projection);
@@ -530,17 +594,14 @@ Boards.helpers({
query.type = 'template-list';
query.archived = false;
} else {
- query.type = {$nin: ['template-list']};
+ query.type = { $nin: ['template-list'] };
}
const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) {
const regex = new RegExp(term, 'i');
- query.$or = [
- { title: regex },
- { description: regex },
- ];
+ query.$or = [{ title: regex }, { description: regex }];
}
return Lists.find(query, projection);
@@ -557,17 +618,14 @@ Boards.helpers({
query.type = 'template-card';
query.archived = false;
} else {
- query.type = {$nin: ['template-card']};
+ query.type = { $nin: ['template-card'] };
}
const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) {
const regex = new RegExp(term, 'i');
- query.$or = [
- { title: regex },
- { description: regex },
- ];
+ query.$or = [{ title: regex }, { description: regex }];
}
return Cards.find(query, projection);
@@ -575,22 +633,29 @@ Boards.helpers({
// A board alwasy has another board where it deposits subtasks of thasks
// that belong to itself.
getDefaultSubtasksBoardId() {
- if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) {
+ if (
+ this.subtasksDefaultBoardId === null ||
+ this.subtasksDefaultBoardId === undefined
+ ) {
this.subtasksDefaultBoardId = Boards.insert({
title: `^${this.title}^`,
permission: this.permission,
members: this.members,
color: this.color,
- description: TAPi18n.__('default-subtasks-board', {board: this.title}),
+ description: TAPi18n.__('default-subtasks-board', {
+ board: this.title,
+ }),
});
Swimlanes.insert({
title: TAPi18n.__('default'),
boardId: this.subtasksDefaultBoardId,
});
- Boards.update(this._id, {$set: {
- subtasksDefaultBoardId: this.subtasksDefaultBoardId,
- }});
+ Boards.update(this._id, {
+ $set: {
+ subtasksDefaultBoardId: this.subtasksDefaultBoardId,
+ },
+ });
}
return this.subtasksDefaultBoardId;
},
@@ -600,7 +665,10 @@ Boards.helpers({
},
getDefaultSubtasksListId() {
- if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) {
+ if (
+ this.subtasksDefaultListId === null ||
+ this.subtasksDefaultListId === undefined
+ ) {
this.subtasksDefaultListId = Lists.insert({
title: TAPi18n.__('queue'),
boardId: this._id,
@@ -615,13 +683,13 @@ Boards.helpers({
},
getDefaultSwimline() {
- let result = Swimlanes.findOne({boardId: this._id});
+ let result = Swimlanes.findOne({ boardId: this._id });
if (result === undefined) {
Swimlanes.insert({
title: TAPi18n.__('default'),
boardId: this._id,
});
- result = Swimlanes.findOne({boardId: this._id});
+ result = Swimlanes.findOne({ boardId: this._id });
}
return result;
},
@@ -633,19 +701,24 @@ Boards.helpers({
{
startAt: {
$lte: start,
- }, endAt: {
+ },
+ endAt: {
$gte: start,
},
- }, {
+ },
+ {
startAt: {
$lte: end,
- }, endAt: {
+ },
+ endAt: {
$gte: end,
},
- }, {
+ },
+ {
startAt: {
$gte: start,
- }, endAt: {
+ },
+ endAt: {
$lte: end,
},
},
@@ -662,7 +735,6 @@ Boards.helpers({
},
});
-
Boards.mutations({
archive() {
return { $set: { archived: true } };
@@ -753,7 +825,8 @@ Boards.mutations({
const memberIndex = this.memberIndex(memberId);
// we do not allow the only one admin to be removed
- const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1);
+ const allowRemove =
+ !this.members[memberIndex].isAdmin || this.activeAdmins().length > 1;
if (!allowRemove) {
return {
$set: {
@@ -770,7 +843,13 @@ Boards.mutations({
};
},
- setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly, currentUserId = Meteor.userId()) {
+ setMemberPermission(
+ memberId,
+ isAdmin,
+ isNoComments,
+ isCommentOnly,
+ currentUserId = Meteor.userId()
+ ) {
const memberIndex = this.memberIndex(memberId);
// do not allow change permission of self
if (memberId === currentUserId) {
@@ -804,12 +883,13 @@ Boards.mutations({
});
function boardRemover(userId, doc) {
- [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach((element) => {
- element.remove({ boardId: doc._id });
- });
+ [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach(
+ (element) => {
+ element.remove({ boardId: doc._id });
+ }
+ );
}
-
if (Meteor.isServer) {
Boards.allow({
insert: Meteor.userId,
@@ -830,25 +910,25 @@ if (Meteor.isServer) {
// We can't remove a member if it is the last administrator
Boards.deny({
update(userId, doc, fieldNames, modifier) {
- if (!_.contains(fieldNames, 'members'))
- return false;
+ if (!_.contains(fieldNames, 'members')) return false;
// We only care in case of a $pull operation, ie remove a member
- if (!_.isObject(modifier.$pull && modifier.$pull.members))
- return false;
+ if (!_.isObject(modifier.$pull && modifier.$pull.members)) return false;
// If there is more than one admin, it's ok to remove anyone
- const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }).length;
- if (nbAdmins > 1)
- return false;
+ const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true })
+ .length;
+ if (nbAdmins > 1) return false;
// If all the previous conditions were verified, we can't remove
// a user if it's an admin
const removedMemberId = modifier.$pull.members.userId;
- return Boolean(_.findWhere(doc.members, {
- userId: removedMemberId,
- isAdmin: true,
- }));
+ return Boolean(
+ _.findWhere(doc.members, {
+ userId: removedMemberId,
+ isAdmin: true,
+ })
+ );
},
fetch: ['members'],
});
@@ -882,16 +962,19 @@ if (Meteor.isServer) {
} else throw new Meteor.Error('error-board-doesNotExist');
},
});
-
}
if (Meteor.isServer) {
// Let MongoDB ensure that a member is not included twice in the same board
Meteor.startup(() => {
- Boards._collection._ensureIndex({
- _id: 1,
- 'members.userId': 1,
- }, { unique: true });
+ Boards._collection._ensureIndex({ modifiedAt: -1 });
+ Boards._collection._ensureIndex(
+ {
+ _id: 1,
+ 'members.userId': 1,
+ },
+ { unique: true }
+ );
Boards._collection._ensureIndex({ 'members.userId': 1 });
});
@@ -909,10 +992,12 @@ if (Meteor.isServer) {
// If the user remove one label from a board, we cant to remove reference of
// this label in any card of this board.
Boards.after.update((userId, doc, fieldNames, modifier) => {
- if (!_.contains(fieldNames, 'labels') ||
+ if (
+ !_.contains(fieldNames, 'labels') ||
!modifier.$pull ||
!modifier.$pull.labels ||
- !modifier.$pull.labels._id) {
+ !modifier.$pull.labels._id
+ ) {
return;
}
@@ -935,12 +1020,21 @@ if (Meteor.isServer) {
}
const parts = set.split('.');
- if (parts.length === 3 && parts[0] === 'members' && parts[2] === 'isActive') {
+ if (
+ parts.length === 3 &&
+ parts[0] === 'members' &&
+ parts[2] === 'isActive'
+ ) {
callback(doc.members[parts[1]].userId);
}
});
};
+ Boards.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ });
+
// Remove a member from all objects of the board before leaving the board
Boards.before.update((userId, doc, fieldNames, modifier) => {
if (!_.contains(fieldNames, 'members')) {
@@ -976,14 +1070,11 @@ if (Meteor.isServer) {
// Remove board from users starred list
if (!board.isPublic()) {
- Users.update(
- memberId,
- {
- $pull: {
- 'profile.starredBoards': boardId,
- },
- }
- );
+ Users.update(memberId, {
+ $pull: {
+ 'profile.starredBoards': boardId,
+ },
+ });
}
});
}
@@ -1044,29 +1135,34 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
title: string}]
*/
- JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) {
+ JsonRoutes.add('GET', '/api/users/:userId/boards', function(req, res) {
try {
Authentication.checkLoggedIn(req.userId);
const paramUserId = req.params.userId;
// A normal user should be able to see their own boards,
// admins can access boards of any user
- Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId);
+ Authentication.checkAdminOrCondition(
+ req.userId,
+ req.userId === paramUserId
+ );
- const data = Boards.find({
- archived: false,
- 'members.userId': paramUserId,
- }, {
- sort: ['title'],
- }).map(function(board) {
+ const data = Boards.find(
+ {
+ archived: false,
+ 'members.userId': paramUserId,
+ },
+ {
+ sort: ['title'],
+ }
+ ).map(function(board) {
return {
_id: board._id,
title: board.title,
};
});
- JsonRoutes.sendResult(res, {code: 200, data});
- }
- catch (error) {
+ JsonRoutes.sendResult(res, { code: 200, data });
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1081,20 +1177,19 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
title: string}]
*/
- JsonRoutes.add('GET', '/api/boards', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards', function(req, res) {
try {
Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Boards.find({ permission: 'public' }).map(function (doc) {
+ data: Boards.find({ permission: 'public' }).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
};
}),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1109,7 +1204,7 @@ if (Meteor.isServer) {
* @param {string} boardId the ID of the board to retrieve the data
* @return_type Boards
*/
- JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId', function(req, res) {
try {
const id = req.params.boardId;
Authentication.checkBoardAccess(req.userId, id);
@@ -1118,8 +1213,7 @@ if (Meteor.isServer) {
code: 200,
data: Boards.findOne({ _id: id }),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1152,7 +1246,7 @@ if (Meteor.isServer) {
* @return_type {_id: string,
defaultSwimlaneId: string}
*/
- JsonRoutes.add('POST', '/api/boards', function (req, res) {
+ JsonRoutes.add('POST', '/api/boards', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const id = Boards.insert({
@@ -1180,8 +1274,7 @@ if (Meteor.isServer) {
defaultSwimlaneId: swimlaneId,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1195,19 +1288,18 @@ if (Meteor.isServer) {
*
* @param {string} boardId the ID of the board
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId', function (req, res) {
+ JsonRoutes.add('DELETE', '/api/boards/:boardId', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const id = req.params.boardId;
Boards.remove({ _id: id });
JsonRoutes.sendResult(res, {
code: 200,
- data:{
+ data: {
_id: id,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1228,7 +1320,7 @@ if (Meteor.isServer) {
*
* @return_type string
*/
- JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function (req, res) {
+ JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function(req, res) {
Authentication.checkUserId(req.userId);
const id = req.params.boardId;
try {
@@ -1238,7 +1330,10 @@ if (Meteor.isServer) {
const name = req.body.label.name;
const labelId = Random.id(6);
if (!board.getLabel(name, color)) {
- Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } });
+ Boards.direct.update(
+ { _id: id },
+ { $push: { labels: { _id: labelId, name, color } } }
+ );
JsonRoutes.sendResult(res, {
code: 200,
data: labelId,
@@ -1249,8 +1344,7 @@ if (Meteor.isServer) {
});
}
}
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
data: error,
});
@@ -1268,29 +1362,36 @@ if (Meteor.isServer) {
* @param {boolean} isNoComments NoComments capability
* @param {boolean} isCommentOnly CommentsOnly capability
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function (req, res) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function(
+ req,
+ res
+ ) {
try {
const boardId = req.params.boardId;
const memberId = req.params.memberId;
- const {isAdmin, isNoComments, isCommentOnly} = req.body;
+ const { isAdmin, isNoComments, isCommentOnly } = req.body;
Authentication.checkBoardAccess(req.userId, boardId);
const board = Boards.findOne({ _id: boardId });
- function isTrue(data){
+ function isTrue(data) {
try {
return data.toLowerCase() === 'true';
- }
- catch (error) {
+ } catch (error) {
return data;
}
}
- const query = board.setMemberPermission(memberId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), req.userId);
+ const query = board.setMemberPermission(
+ memberId,
+ isTrue(isAdmin),
+ isTrue(isNoComments),
+ isTrue(isCommentOnly),
+ req.userId
+ );
JsonRoutes.sendResult(res, {
code: 200,
data: query,
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1298,3 +1399,5 @@ if (Meteor.isServer) {
}
});
}
+
+export default Boards;
diff --git a/models/cardComments.js b/models/cardComments.js
index a823066c..8f727aa0 100644
--- a/models/cardComments.js
+++ b/models/cardComments.js
@@ -3,55 +3,69 @@ CardComments = new Mongo.Collection('card_comments');
/**
* A comment on a card
*/
-CardComments.attachSchema(new SimpleSchema({
- boardId: {
- /**
- * the board ID
- */
- type: String,
- },
- cardId: {
- /**
- * the card ID
- */
- type: String,
- },
- // XXX Rename in `content`? `text` is a bit vague...
- text: {
- /**
- * the text of the comment
- */
- type: String,
- },
- // XXX We probably don't need this information here, since we already have it
- // in the associated comment creation activity
- createdAt: {
- /**
- * when was the comment created
- */
- type: Date,
- denyUpdate: false,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+CardComments.attachSchema(
+ new SimpleSchema({
+ boardId: {
+ /**
+ * the board ID
+ */
+ type: String,
},
- },
- // XXX Should probably be called `authorId`
- userId: {
- /**
- * the author ID of the comment
- */
- type: String,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return this.userId;
- }
+ cardId: {
+ /**
+ * the card ID
+ */
+ type: String,
},
- },
-}));
+ // XXX Rename in `content`? `text` is a bit vague...
+ text: {
+ /**
+ * the text of the comment
+ */
+ type: String,
+ },
+ createdAt: {
+ /**
+ * when was the comment created
+ */
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ // XXX Should probably be called `authorId`
+ userId: {
+ /**
+ * the author ID of the comment
+ */
+ type: String,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return this.userId;
+ }
+ },
+ },
+ })
+);
CardComments.allow({
insert(userId, doc) {
@@ -80,7 +94,7 @@ CardComments.helpers({
CardComments.hookOptions.after.update = { fetchPrevious: false };
-function commentCreation(userId, doc){
+function commentCreation(userId, doc) {
const card = Cards.findOne(doc.cardId);
Activities.insert({
userId,
@@ -93,10 +107,16 @@ function commentCreation(userId, doc){
});
}
+CardComments.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
if (Meteor.isServer) {
// Comments are often fetched within a card, so we create an index to make these
// queries more efficient.
Meteor.startup(() => {
+ CardComments._collection._ensureIndex({ modifiedAt: -1 });
CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 });
});
@@ -152,14 +172,20 @@ if (Meteor.isServer) {
* comment: string,
* authorId: string}]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function(
+ req,
+ res
+ ) {
try {
- Authentication.checkUserId( req.userId);
+ Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, {
code: 200,
- data: CardComments.find({ boardId: paramBoardId, cardId: paramCardId}).map(function (doc) {
+ data: CardComments.find({
+ boardId: paramBoardId,
+ cardId: paramCardId,
+ }).map(function(doc) {
return {
_id: doc._id,
comment: doc.text,
@@ -167,8 +193,7 @@ if (Meteor.isServer) {
};
}),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -185,24 +210,31 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to retrieve
* @return_type CardComments
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const paramCommentId = req.params.commentId;
- const paramCardId = req.params.cardId;
- JsonRoutes.sendResult(res, {
- code: 200,
- data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }),
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
+ JsonRoutes.add(
+ 'GET',
+ '/api/boards/:boardId/cards/:cardId/comments/:commentId',
+ function(req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const paramBoardId = req.params.boardId;
+ const paramCommentId = req.params.commentId;
+ const paramCardId = req.params.cardId;
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: CardComments.findOne({
+ _id: paramCommentId,
+ cardId: paramCardId,
+ boardId: paramBoardId,
+ }),
+ });
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
}
- });
+ );
/**
* @operation new_comment
@@ -214,35 +246,42 @@ if (Meteor.isServer) {
* @param {string} text the content of the comment
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const paramCardId = req.params.cardId;
- const id = CardComments.direct.insert({
- userId: req.body.authorId,
- text: req.body.comment,
- cardId: paramCardId,
- boardId: paramBoardId,
- });
+ JsonRoutes.add(
+ 'POST',
+ '/api/boards/:boardId/cards/:cardId/comments',
+ function(req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const paramBoardId = req.params.boardId;
+ const paramCardId = req.params.cardId;
+ const id = CardComments.direct.insert({
+ userId: req.body.authorId,
+ text: req.body.comment,
+ cardId: paramCardId,
+ boardId: paramBoardId,
+ });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
- const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId });
- commentCreation(req.body.authorId, cardComment);
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
+ const cardComment = CardComments.findOne({
+ _id: id,
+ cardId: paramCardId,
+ boardId: paramBoardId,
+ });
+ commentCreation(req.body.authorId, cardComment);
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
}
- });
+ );
/**
* @operation delete_comment
@@ -253,25 +292,34 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to delete
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const paramCommentId = req.params.commentId;
- const paramCardId = req.params.cardId;
- CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramCardId,
- },
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
+ JsonRoutes.add(
+ 'DELETE',
+ '/api/boards/:boardId/cards/:cardId/comments/:commentId',
+ function(req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const paramBoardId = req.params.boardId;
+ const paramCommentId = req.params.commentId;
+ const paramCardId = req.params.cardId;
+ CardComments.remove({
+ _id: paramCommentId,
+ cardId: paramCardId,
+ boardId: paramBoardId,
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramCardId,
+ },
+ });
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
}
- });
+ );
}
+
+export default CardComments;
diff --git a/models/cards.js b/models/cards.js
index fdb7deb3..b873c086 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -81,7 +81,8 @@ Cards.attachSchema(new SimpleSchema({
* creation date
*/
type: Date,
- autoValue() { // eslint-disable-line consistent-return
+ // eslint-disable-next-line consistent-return
+ autoValue() {
if (this.isInsert) {
return new Date();
} else {
@@ -89,6 +90,18 @@ Cards.attachSchema(new SimpleSchema({
}
},
},
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
customFields: {
/**
* list of custom fields
@@ -1539,7 +1552,8 @@ if (Meteor.isServer) {
// Cards are often fetched within a board, so we create an index to make these
// queries more efficient.
Meteor.startup(() => {
- Cards._collection._ensureIndex({boardId: 1, createdAt: -1});
+ Cards._collection._ensureIndex({ modifiedAt: -1 });
+ Cards._collection._ensureIndex({ boardId: 1, createdAt: -1 });
// https://github.com/wekan/wekan/issues/1863
// Swimlane added a new field in the cards collection of mongodb named parentId.
// When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection).
@@ -1581,6 +1595,11 @@ if (Meteor.isServer) {
cardCustomFields(userId, doc, fieldNames, modifier);
});
+ Cards.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ });
+
// Remove all activities associated with a card if we remove the card
// Remove also card_comments / checklists / attachments
Cards.before.remove((userId, doc) => {
@@ -1980,3 +1999,5 @@ if (Meteor.isServer) {
});
}
+
+export default Cards;
diff --git a/models/checklistItems.js b/models/checklistItems.js
index df56c475..d548e681 100644
--- a/models/checklistItems.js
+++ b/models/checklistItems.js
@@ -3,40 +3,66 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/**
* An item in a checklist
*/
-ChecklistItems.attachSchema(new SimpleSchema({
- title: {
- /**
- * the text of the item
- */
- type: String,
- },
- sort: {
- /**
- * the sorting field of the item
- */
- type: Number,
- decimal: true,
- },
- isFinished: {
- /**
- * Is the item checked?
- */
- type: Boolean,
- defaultValue: false,
- },
- checklistId: {
- /**
- * the checklist ID the item is attached to
- */
- type: String,
- },
- cardId: {
- /**
- * the card ID the item is attached to
- */
- type: String,
- },
-}));
+ChecklistItems.attachSchema(
+ new SimpleSchema({
+ title: {
+ /**
+ * the text of the item
+ */
+ type: String,
+ },
+ sort: {
+ /**
+ * the sorting field of the item
+ */
+ type: Number,
+ decimal: true,
+ },
+ isFinished: {
+ /**
+ * Is the item checked?
+ */
+ type: Boolean,
+ defaultValue: false,
+ },
+ checklistId: {
+ /**
+ * the checklist ID the item is attached to
+ */
+ type: String,
+ },
+ cardId: {
+ /**
+ * the card ID the item is attached to
+ */
+ type: String,
+ },
+ createdAt: {
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ })
+);
ChecklistItems.allow({
insert(userId, doc) {
@@ -62,10 +88,10 @@ ChecklistItems.mutations({
setTitle(title) {
return { $set: { title } };
},
- check(){
+ check() {
return { $set: { isFinished: true } };
},
- uncheck(){
+ uncheck() {
return { $set: { isFinished: false } };
},
toggleItem() {
@@ -79,7 +105,7 @@ ChecklistItems.mutations({
sort: sortIndex,
};
- return {$set: mutatedFields};
+ return { $set: mutatedFields };
},
});
@@ -106,13 +132,13 @@ function itemRemover(userId, doc) {
});
}
-function publishCheckActivity(userId, doc){
+function publishCheckActivity(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
let activityType;
- if(doc.isFinished){
+ if (doc.isFinished) {
activityType = 'checkedItem';
- }else{
+ } else {
activityType = 'uncheckedItem';
}
const act = {
@@ -122,19 +148,19 @@ function publishCheckActivity(userId, doc){
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
- checklistItemName:doc.title,
+ checklistItemName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
};
Activities.insert(act);
}
-function publishChekListCompleted(userId, doc){
+function publishChekListCompleted(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
const checklistId = doc.checklistId;
- const checkList = Checklists.findOne({_id:checklistId});
- if(checkList.isFinished()){
+ const checkList = Checklists.findOne({ _id: checklistId });
+ if (checkList.isFinished()) {
const act = {
userId,
activityType: 'completeChecklist',
@@ -149,11 +175,11 @@ function publishChekListCompleted(userId, doc){
}
}
-function publishChekListUncompleted(userId, doc){
+function publishChekListUncompleted(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
const checklistId = doc.checklistId;
- const checkList = Checklists.findOne({_id:checklistId});
+ const checkList = Checklists.findOne({ _id: checklistId });
// BUGS in IFTTT Rules: https://github.com/wekan/wekan/issues/1972
// Currently in checklist all are set as uncompleted/not checked,
// IFTTT Rule does not move card to other list.
@@ -167,7 +193,7 @@ function publishChekListUncompleted(userId, doc){
// find . | xargs grep 'count' -sl | grep -v .meteor | grep -v node_modules | grep -v .build
// Maybe something related here?
// wekan/client/components/rules/triggers/checklistTriggers.js
- if(checkList.isFinished()){
+ if (checkList.isFinished()) {
const act = {
userId,
activityType: 'uncompleteChecklist',
@@ -185,6 +211,7 @@ function publishChekListUncompleted(userId, doc){
// Activities
if (Meteor.isServer) {
Meteor.startup(() => {
+ ChecklistItems._collection._ensureIndex({ modifiedAt: -1 });
ChecklistItems._collection._ensureIndex({ checklistId: 1 });
ChecklistItems._collection._ensureIndex({ cardId: 1 });
});
@@ -198,6 +225,10 @@ if (Meteor.isServer) {
publishChekListUncompleted(userId, doc, fieldNames);
});
+ ChecklistItems.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ });
ChecklistItems.after.insert((userId, doc) => {
itemCreation(userId, doc);
@@ -214,7 +245,7 @@ if (Meteor.isServer) {
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
- checklistItemName:doc.title,
+ checklistItemName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
@@ -233,21 +264,25 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item
* @return_type ChecklistItems
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramItemId = req.params.itemId;
- const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
- if (checklistItem) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: checklistItem,
- });
- } else {
- JsonRoutes.sendResult(res, {
- code: 500,
- });
+ JsonRoutes.add(
+ 'GET',
+ '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramItemId = req.params.itemId;
+ const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
+ if (checklistItem) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: checklistItem,
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 500,
+ });
+ }
}
- });
+ );
/**
* @operation edit_checklist_item
@@ -262,25 +297,35 @@ if (Meteor.isServer) {
* @param {string} [title] the new text of the item
* @return_type {_id: string}
*/
- JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
- Authentication.checkUserId( req.userId);
+ JsonRoutes.add(
+ 'PUT',
+ '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
- const paramItemId = req.params.itemId;
+ const paramItemId = req.params.itemId;
- if (req.body.hasOwnProperty('isFinished')) {
- ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}});
- }
- if (req.body.hasOwnProperty('title')) {
- ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}});
- }
+ if (req.body.hasOwnProperty('isFinished')) {
+ ChecklistItems.direct.update(
+ { _id: paramItemId },
+ { $set: { isFinished: req.body.isFinished } }
+ );
+ }
+ if (req.body.hasOwnProperty('title')) {
+ ChecklistItems.direct.update(
+ { _id: paramItemId },
+ { $set: { title: req.body.title } }
+ );
+ }
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramItemId,
- },
- });
- });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramItemId,
+ },
+ });
+ }
+ );
/**
* @operation delete_checklist_item
@@ -295,15 +340,21 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item to be removed
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramItemId = req.params.itemId;
- ChecklistItems.direct.remove({ _id: paramItemId });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramItemId,
- },
- });
- });
+ JsonRoutes.add(
+ 'DELETE',
+ '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramItemId = req.params.itemId;
+ ChecklistItems.direct.remove({ _id: paramItemId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramItemId,
+ },
+ });
+ }
+ );
}
+
+export default ChecklistItems;
diff --git a/models/checklists.js b/models/checklists.js
index 653fed4d..6fd22702 100644
--- a/models/checklists.js
+++ b/models/checklists.js
@@ -3,49 +3,64 @@ Checklists = new Mongo.Collection('checklists');
/**
* A Checklist
*/
-Checklists.attachSchema(new SimpleSchema({
- cardId: {
- /**
- * The ID of the card the checklist is in
- */
- type: String,
- },
- title: {
- /**
- * the title of the checklist
- */
- type: String,
- defaultValue: 'Checklist',
- },
- finishedAt: {
- /**
- * When was the checklist finished
- */
- type: Date,
- optional: true,
- },
- createdAt: {
- /**
- * Creation date of the checklist
- */
- type: Date,
- denyUpdate: false,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+Checklists.attachSchema(
+ new SimpleSchema({
+ cardId: {
+ /**
+ * The ID of the card the checklist is in
+ */
+ type: String,
},
- },
- sort: {
- /**
- * sorting value of the checklist
- */
- type: Number,
- decimal: true,
- },
-}));
+ title: {
+ /**
+ * the title of the checklist
+ */
+ type: String,
+ defaultValue: 'Checklist',
+ },
+ finishedAt: {
+ /**
+ * When was the checklist finished
+ */
+ type: Date,
+ optional: true,
+ },
+ createdAt: {
+ /**
+ * Creation date of the checklist
+ */
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ sort: {
+ /**
+ * sorting value of the checklist
+ */
+ type: Number,
+ decimal: true,
+ },
+ })
+);
Checklists.helpers({
copy(newCardId) {
@@ -53,7 +68,7 @@ Checklists.helpers({
this._id = null;
this.cardId = newCardId;
const newChecklistId = Checklists.insert(this);
- ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => {
+ ChecklistItems.find({ checklistId: oldChecklistId }).forEach((item) => {
item._id = null;
item.checklistId = newChecklistId;
item.cardId = newCardId;
@@ -65,9 +80,12 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count();
},
items() {
- return ChecklistItems.find({
- checklistId: this._id,
- }, { sort: ['sort'] });
+ return ChecklistItems.find(
+ {
+ checklistId: this._id,
+ },
+ { sort: ['sort'] }
+ );
},
finishedCount() {
return ChecklistItems.find({
@@ -78,20 +96,20 @@ Checklists.helpers({
isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
},
- checkAllItems(){
- const checkItems = ChecklistItems.find({checklistId: this._id});
- checkItems.forEach(function(item){
+ checkAllItems() {
+ const checkItems = ChecklistItems.find({ checklistId: this._id });
+ checkItems.forEach(function(item) {
item.check();
});
},
- uncheckAllItems(){
- const checkItems = ChecklistItems.find({checklistId: this._id});
- checkItems.forEach(function(item){
+ uncheckAllItems() {
+ const checkItems = ChecklistItems.find({ checklistId: this._id });
+ checkItems.forEach(function(item) {
item.uncheck();
});
},
itemIndex(itemId) {
- const items = self.findOne({_id : this._id}).items;
+ const items = self.findOne({ _id: this._id }).items;
return _.pluck(items, '_id').indexOf(itemId);
},
});
@@ -124,6 +142,7 @@ Checklists.mutations({
if (Meteor.isServer) {
Meteor.startup(() => {
+ Checklists._collection._ensureIndex({ modifiedAt: -1 });
Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 });
});
@@ -135,12 +154,17 @@ if (Meteor.isServer) {
cardId: doc.cardId,
boardId: card.boardId,
checklistId: doc._id,
- checklistName:doc.title,
+ checklistName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
});
+ Checklists.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ });
+
Checklists.before.remove((userId, doc) => {
const activities = Activities.find({ checklistId: doc._id });
const card = Cards.findOne(doc.cardId);
@@ -155,7 +179,7 @@ if (Meteor.isServer) {
cardId: doc.cardId,
boardId: Cards.findOne(doc.cardId).boardId,
checklistId: doc._id,
- checklistName:doc.title,
+ checklistName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
@@ -172,26 +196,32 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramCardId = req.params.cardId;
- const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- });
- if (checklists) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: checklists,
- });
- } else {
- JsonRoutes.sendResult(res, {
- code: 500,
+ JsonRoutes.add(
+ 'GET',
+ '/api/boards/:boardId/cards/:cardId/checklists',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramCardId = req.params.cardId;
+ const checklists = Checklists.find({ cardId: paramCardId }).map(function(
+ doc
+ ) {
+ return {
+ _id: doc._id,
+ title: doc.title,
+ };
});
+ if (checklists) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: checklists,
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 500,
+ });
+ }
}
- });
+ );
/**
* @operation get_checklist
@@ -209,29 +239,38 @@ if (Meteor.isServer) {
* title: string,
* isFinished: boolean}]}
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramChecklistId = req.params.checklistId;
- const paramCardId = req.params.cardId;
- const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId });
- if (checklist) {
- checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- isFinished: doc.isFinished,
- };
- });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: checklist,
- });
- } else {
- JsonRoutes.sendResult(res, {
- code: 500,
+ JsonRoutes.add(
+ 'GET',
+ '/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramChecklistId = req.params.checklistId;
+ const paramCardId = req.params.cardId;
+ const checklist = Checklists.findOne({
+ _id: paramChecklistId,
+ cardId: paramCardId,
});
+ if (checklist) {
+ checklist.items = ChecklistItems.find({
+ checklistId: checklist._id,
+ }).map(function(doc) {
+ return {
+ _id: doc._id,
+ title: doc.title,
+ isFinished: doc.isFinished,
+ };
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: checklist,
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 500,
+ });
+ }
}
- });
+ );
/**
* @operation new_checklist
@@ -242,36 +281,40 @@ if (Meteor.isServer) {
* @param {string} title the title of the new checklist
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
- Authentication.checkUserId( req.userId);
+ JsonRoutes.add(
+ 'POST',
+ '/api/boards/:boardId/cards/:cardId/checklists',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
- const paramCardId = req.params.cardId;
- const id = Checklists.insert({
- title: req.body.title,
- cardId: paramCardId,
- sort: 0,
- });
- if (id) {
- req.body.items.forEach(function (item, idx) {
- ChecklistItems.insert({
- cardId: paramCardId,
- checklistId: id,
- title: item.title,
- sort: idx,
- });
- });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
- } else {
- JsonRoutes.sendResult(res, {
- code: 400,
+ const paramCardId = req.params.cardId;
+ const id = Checklists.insert({
+ title: req.body.title,
+ cardId: paramCardId,
+ sort: 0,
});
+ if (id) {
+ req.body.items.forEach(function(item, idx) {
+ ChecklistItems.insert({
+ cardId: paramCardId,
+ checklistId: id,
+ title: item.title,
+ sort: idx,
+ });
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 400,
+ });
+ }
}
- });
+ );
/**
* @operation delete_checklist
@@ -284,15 +327,21 @@ if (Meteor.isServer) {
* @param {string} checklistId the ID of the checklist to remove
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramChecklistId = req.params.checklistId;
- Checklists.remove({ _id: paramChecklistId });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramChecklistId,
- },
- });
- });
+ JsonRoutes.add(
+ 'DELETE',
+ '/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramChecklistId = req.params.checklistId;
+ Checklists.remove({ _id: paramChecklistId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramChecklistId,
+ },
+ });
+ }
+ );
}
+
+export default Checklists;
diff --git a/models/customFields.js b/models/customFields.js
index 83033cb4..8b51c0a3 100644
--- a/models/customFields.js
+++ b/models/customFields.js
@@ -3,74 +3,100 @@ CustomFields = new Mongo.Collection('customFields');
/**
* A custom field on a card in the board
*/
-CustomFields.attachSchema(new SimpleSchema({
- boardIds: {
- /**
- * the ID of the board
- */
- type: [String],
- },
- name: {
- /**
- * name of the custom field
- */
- type: String,
- },
- type: {
- /**
- * type of the custom field
- */
- type: String,
- allowedValues: ['text', 'number', 'date', 'dropdown'],
- },
- settings: {
- /**
- * settings of the custom field
- */
- type: Object,
- },
- 'settings.dropdownItems': {
- /**
- * list of drop down items objects
- */
- type: [Object],
- optional: true,
- },
- 'settings.dropdownItems.$': {
- type: new SimpleSchema({
- _id: {
- /**
- * ID of the drop down item
- */
- type: String,
+CustomFields.attachSchema(
+ new SimpleSchema({
+ boardIds: {
+ /**
+ * the ID of the board
+ */
+ type: [String],
+ },
+ name: {
+ /**
+ * name of the custom field
+ */
+ type: String,
+ },
+ type: {
+ /**
+ * type of the custom field
+ */
+ type: String,
+ allowedValues: ['text', 'number', 'date', 'dropdown'],
+ },
+ settings: {
+ /**
+ * settings of the custom field
+ */
+ type: Object,
+ },
+ 'settings.dropdownItems': {
+ /**
+ * list of drop down items objects
+ */
+ type: [Object],
+ optional: true,
+ },
+ 'settings.dropdownItems.$': {
+ type: new SimpleSchema({
+ _id: {
+ /**
+ * ID of the drop down item
+ */
+ type: String,
+ },
+ name: {
+ /**
+ * name of the drop down item
+ */
+ type: String,
+ },
+ }),
+ },
+ showOnCard: {
+ /**
+ * should we show on the cards this custom field
+ */
+ type: Boolean,
+ },
+ automaticallyOnCard: {
+ /**
+ * should the custom fields automatically be added on cards?
+ */
+ type: Boolean,
+ },
+ showLabelOnMiniCard: {
+ /**
+ * should the label of the custom field be shown on minicards?
+ */
+ type: Boolean,
+ },
+ createdAt: {
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
},
- name: {
- /**
- * name of the drop down item
- */
- type: String,
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
},
- }),
- },
- showOnCard: {
- /**
- * should we show on the cards this custom field
- */
- type: Boolean,
- },
- automaticallyOnCard: {
- /**
- * should the custom fields automatically be added on cards?
- */
- type: Boolean,
- },
- showLabelOnMiniCard: {
- /**
- * should the label of the custom field be shown on minicards?
- */
- type: Boolean,
- },
-}));
+ },
+ })
+);
CustomFields.mutations({
addBoard(boardId) {
@@ -88,19 +114,28 @@ CustomFields.mutations({
CustomFields.allow({
insert(userId, doc) {
- return allowIsAnyBoardMember(userId, Boards.find({
- _id: {$in: doc.boardIds},
- }).fetch());
+ return allowIsAnyBoardMember(
+ userId,
+ Boards.find({
+ _id: { $in: doc.boardIds },
+ }).fetch()
+ );
},
update(userId, doc) {
- return allowIsAnyBoardMember(userId, Boards.find({
- _id: {$in: doc.boardIds},
- }).fetch());
+ return allowIsAnyBoardMember(
+ userId,
+ Boards.find({
+ _id: { $in: doc.boardIds },
+ }).fetch()
+ );
},
remove(userId, doc) {
- return allowIsAnyBoardMember(userId, Boards.find({
- _id: {$in: doc.boardIds},
- }).fetch());
+ return allowIsAnyBoardMember(
+ userId,
+ Boards.find({
+ _id: { $in: doc.boardIds },
+ }).fetch()
+ );
},
fetch: ['userId', 'boardIds'],
});
@@ -108,7 +143,7 @@ CustomFields.allow({
// not sure if we need this?
//CustomFields.hookOptions.after.update = { fetchPrevious: false };
-function customFieldCreation(userId, doc){
+function customFieldCreation(userId, doc) {
Activities.insert({
userId,
activityType: 'createCustomField',
@@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){
if (Meteor.isServer) {
Meteor.startup(() => {
+ CustomFields._collection._ensureIndex({ modifiedAt: -1 });
CustomFields._collection._ensureIndex({ boardIds: 1 });
});
@@ -149,12 +185,17 @@ if (Meteor.isServer) {
customFieldCreation(userId, doc);
});
+ CustomFields.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ });
+
CustomFields.before.update((userId, doc, fieldNames, modifier) => {
if (_.contains(fieldNames, 'boardIds') && modifier.$pull) {
Cards.update(
- {boardId: modifier.$pull.boardIds, 'customFields._id': doc._id},
- {$pull: {'customFields': {'_id': doc._id}}},
- {multi: true}
+ { boardId: modifier.$pull.boardIds, 'customFields._id': doc._id },
+ { $pull: { customFields: { _id: doc._id } } },
+ { multi: true }
);
customFieldEdit(userId, doc);
Activities.remove({
@@ -180,9 +221,9 @@ if (Meteor.isServer) {
});
Cards.update(
- {boardId: {$in: doc.boardIds}, 'customFields._id': doc._id},
- {$pull: {'customFields': {'_id': doc._id}}},
- {multi: true}
+ { boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
+ { $pull: { customFields: { _id: doc._id } } },
+ { multi: true }
);
});
}
@@ -198,18 +239,23 @@ if (Meteor.isServer) {
* name: string,
* type: string}]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) {
- Authentication.checkUserId( req.userId);
+ JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function(
+ req,
+ res
+ ) {
+ Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, {
code: 200,
- data: CustomFields.find({ boardIds: {$in: [paramBoardId]} }).map(function (cf) {
- return {
- _id: cf._id,
- name: cf.name,
- type: cf.type,
- };
- }),
+ data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
+ function(cf) {
+ return {
+ _id: cf._id,
+ name: cf.name,
+ type: cf.type,
+ };
+ }
+ ),
});
});
@@ -221,15 +267,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field
* @return_type CustomFields
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const paramCustomFieldId = req.params.customFieldId;
- JsonRoutes.sendResult(res, {
- code: 200,
- data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }),
- });
- });
+ JsonRoutes.add(
+ 'GET',
+ '/api/boards/:boardId/custom-fields/:customFieldId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramBoardId = req.params.boardId;
+ const paramCustomFieldId = req.params.customFieldId;
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: CustomFields.findOne({
+ _id: paramCustomFieldId,
+ boardIds: { $in: [paramBoardId] },
+ }),
+ });
+ }
+ );
/**
* @operation new_custom_field
@@ -244,8 +297,11 @@ if (Meteor.isServer) {
* @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) {
- Authentication.checkUserId( req.userId);
+ JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function(
+ req,
+ res
+ ) {
+ Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const id = CustomFields.direct.insert({
name: req.body.name,
@@ -254,10 +310,13 @@ if (Meteor.isServer) {
showOnCard: req.body.showOnCard,
automaticallyOnCard: req.body.automaticallyOnCard,
showLabelOnMiniCard: req.body.showLabelOnMiniCard,
- boardIds: {$in: [paramBoardId]},
+ boardIds: { $in: [paramBoardId] },
});
- const customField = CustomFields.findOne({_id: id, boardIds: {$in: [paramBoardId]} });
+ const customField = CustomFields.findOne({
+ _id: id,
+ boardIds: { $in: [paramBoardId] },
+ });
customFieldCreation(req.body.authorId, customField);
JsonRoutes.sendResult(res, {
@@ -278,16 +337,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const id = req.params.customFieldId;
- CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
- });
+ JsonRoutes.add(
+ 'DELETE',
+ '/api/boards/:boardId/custom-fields/:customFieldId',
+ function(req, res) {
+ Authentication.checkUserId(req.userId);
+ const paramBoardId = req.params.boardId;
+ const id = req.params.customFieldId;
+ CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ }
+ );
}
+
+export default CustomFields;
diff --git a/models/integrations.js b/models/integrations.js
index 65a7af63..bb36d6e8 100644
--- a/models/integrations.js
+++ b/models/integrations.js
@@ -3,75 +3,96 @@ Integrations = new Mongo.Collection('integrations');
/**
* Integration with third-party applications
*/
-Integrations.attachSchema(new SimpleSchema({
- enabled: {
- /**
- * is the integration enabled?
- */
- type: Boolean,
- defaultValue: true,
- },
- title: {
- /**
- * name of the integration
- */
- type: String,
- optional: true,
- },
- type: {
- /**
- * type of the integratation (Default to 'outgoing-webhooks')
- */
- type: String,
- defaultValue: 'outgoing-webhooks',
- },
- activities: {
- /**
- * activities the integration gets triggered (list)
- */
- type: [String],
- defaultValue: ['all'],
- },
- url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex)
- /**
- * URL validation regex (https://mathiasbynens.be/demo/url-regex)
- */
- type: String,
- },
- token: {
- /**
- * token of the integration
- */
- type: String,
- optional: true,
- },
- boardId: {
- /**
- * Board ID of the integration
- */
- type: String,
- },
- createdAt: {
- /**
- * Creation date of the integration
- */
- type: Date,
- denyUpdate: false,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+Integrations.attachSchema(
+ new SimpleSchema({
+ enabled: {
+ /**
+ * is the integration enabled?
+ */
+ type: Boolean,
+ defaultValue: true,
},
- },
- userId: {
- /**
- * user ID who created the interation
- */
- type: String,
- },
-}));
+ title: {
+ /**
+ * name of the integration
+ */
+ type: String,
+ optional: true,
+ },
+ type: {
+ /**
+ * type of the integratation (Default to 'outgoing-webhooks')
+ */
+ type: String,
+ defaultValue: 'outgoing-webhooks',
+ },
+ activities: {
+ /**
+ * activities the integration gets triggered (list)
+ */
+ type: [String],
+ defaultValue: ['all'],
+ },
+ url: {
+ // URL validation regex (https://mathiasbynens.be/demo/url-regex)
+ /**
+ * URL validation regex (https://mathiasbynens.be/demo/url-regex)
+ */
+ type: String,
+ },
+ token: {
+ /**
+ * token of the integration
+ */
+ type: String,
+ optional: true,
+ },
+ boardId: {
+ /**
+ * Board ID of the integration
+ */
+ type: String,
+ },
+ createdAt: {
+ /**
+ * Creation date of the integration
+ */
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ userId: {
+ /**
+ * user ID who created the interation
+ */
+ type: String,
+ },
+ })
+);
+
+Integrations.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
Integrations.allow({
insert(userId, doc) {
@@ -89,6 +110,7 @@ Integrations.allow({
//INTEGRATIONS REST API
if (Meteor.isServer) {
Meteor.startup(() => {
+ Integrations._collection._ensureIndex({ modifiedAt: -1 });
Integrations._collection._ensureIndex({ boardId: 1 });
});
@@ -99,18 +121,23 @@ if (Meteor.isServer) {
* @param {string} boardId the board ID
* @return_type [Integrations]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
- const data = Integrations.find({ boardId: paramBoardId }, { fields: { token: 0 } }).map(function(doc) {
+ const data = Integrations.find(
+ { boardId: paramBoardId },
+ { fields: { token: 0 } }
+ ).map(function(doc) {
return doc;
});
- JsonRoutes.sendResult(res, {code: 200, data});
- }
- catch (error) {
+ JsonRoutes.sendResult(res, { code: 200, data });
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -126,7 +153,10 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID
* @return_type Integrations
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
@@ -134,10 +164,12 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, {
code: 200,
- data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }),
+ data: Integrations.findOne(
+ { _id: paramIntId, boardId: paramBoardId },
+ { fields: { token: 0 } }
+ ),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -153,7 +185,10 @@ if (Meteor.isServer) {
* @param {string} url the URL of the integration
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
@@ -170,8 +205,7 @@ if (Meteor.isServer) {
_id: id,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -192,7 +226,10 @@ if (Meteor.isServer) {
* @param {string} [activities] new list of activities of the integration
* @return_type {_id: string}
*/
- JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res) {
+ JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
@@ -200,28 +237,38 @@ if (Meteor.isServer) {
if (req.body.hasOwnProperty('enabled')) {
const newEnabled = req.body.enabled;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {enabled: newEnabled}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $set: { enabled: newEnabled } }
+ );
}
if (req.body.hasOwnProperty('title')) {
const newTitle = req.body.title;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {title: newTitle}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $set: { title: newTitle } }
+ );
}
if (req.body.hasOwnProperty('url')) {
const newUrl = req.body.url;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {url: newUrl}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $set: { url: newUrl } }
+ );
}
if (req.body.hasOwnProperty('token')) {
const newToken = req.body.token;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {token: newToken}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $set: { token: newToken } }
+ );
}
if (req.body.hasOwnProperty('activities')) {
const newActivities = req.body.activities;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {activities: newActivities}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $set: { activities: newActivities } }
+ );
}
JsonRoutes.sendResult(res, {
@@ -230,8 +277,7 @@ if (Meteor.isServer) {
_id: paramIntId,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -248,28 +294,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to remove from the integration
* @return_type Integrations
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) {
- try {
- const paramBoardId = req.params.boardId;
- const paramIntId = req.params.intId;
- const newActivities = req.body.activities;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
+ JsonRoutes.add(
+ 'DELETE',
+ '/api/boards/:boardId/integrations/:intId/activities',
+ function(req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ const paramIntId = req.params.intId;
+ const newActivities = req.body.activities;
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$pullAll: {activities: newActivities}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $pullAll: { activities: newActivities } }
+ );
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}),
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Integrations.findOne(
+ { _id: paramIntId, boardId: paramBoardId },
+ { fields: { _id: 1, activities: 1 } }
+ ),
+ });
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
}
- });
+ );
/**
* @operation new_integration_activities
@@ -280,28 +334,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to add to the integration
* @return_type Integrations
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) {
- try {
- const paramBoardId = req.params.boardId;
- const paramIntId = req.params.intId;
- const newActivities = req.body.activities;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
+ JsonRoutes.add(
+ 'POST',
+ '/api/boards/:boardId/integrations/:intId/activities',
+ function(req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ const paramIntId = req.params.intId;
+ const newActivities = req.body.activities;
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$addToSet: {activities: { $each: newActivities}}});
+ Integrations.direct.update(
+ { _id: paramIntId, boardId: paramBoardId },
+ { $addToSet: { activities: { $each: newActivities } } }
+ );
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}),
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Integrations.findOne(
+ { _id: paramIntId, boardId: paramBoardId },
+ { fields: { _id: 1, activities: 1 } }
+ ),
+ });
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
}
- });
+ );
/**
* @operation delete_integration
@@ -311,21 +373,23 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res) {
+ JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
- Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId});
+ Integrations.direct.remove({ _id: paramIntId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramIntId,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -333,3 +397,5 @@ if (Meteor.isServer) {
}
});
}
+
+export default Integrations;
diff --git a/models/invitationCodes.js b/models/invitationCodes.js
index 53163f06..5cdfc744 100644
--- a/models/invitationCodes.js
+++ b/models/invitationCodes.js
@@ -1,45 +1,78 @@
InvitationCodes = new Mongo.Collection('invitation_codes');
-InvitationCodes.attachSchema(new SimpleSchema({
- code: {
- type: String,
- },
- email: {
- type: String,
- unique: true,
- regEx: SimpleSchema.RegEx.Email,
- },
- createdAt: {
- type: Date,
- denyUpdate: false,
- },
- // always be the admin if only one admin
- authorId: {
- type: String,
- },
- boardsToBeInvited: {
- type: [String],
- optional: true,
- },
- valid: {
- type: Boolean,
- defaultValue: true,
- },
-}));
+InvitationCodes.attachSchema(
+ new SimpleSchema({
+ code: {
+ type: String,
+ },
+ email: {
+ type: String,
+ unique: true,
+ regEx: SimpleSchema.RegEx.Email,
+ },
+ createdAt: {
+ type: Date,
+ denyUpdate: false,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ // always be the admin if only one admin
+ authorId: {
+ type: String,
+ },
+ boardsToBeInvited: {
+ type: [String],
+ optional: true,
+ },
+ valid: {
+ type: Boolean,
+ defaultValue: true,
+ },
+ })
+);
InvitationCodes.helpers({
- author(){
+ author() {
return Users.findOne(this.authorId);
},
});
+InvitationCodes.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
// InvitationCodes.before.insert((userId, doc) => {
// doc.createdAt = new Date();
// doc.authorId = userId;
// });
if (Meteor.isServer) {
+ Meteor.startup(() => {
+ InvitationCodes._collection._ensureIndex({ modifiedAt: -1 });
+ });
Boards.deny({
fetch: ['members'],
});
}
+
+export default InvitationCodes;
diff --git a/models/lists.js b/models/lists.js
index 1a0910c2..6d77d7aa 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -3,125 +3,161 @@ Lists = new Mongo.Collection('lists');
/**
* A list (column) in the Wekan board.
*/
-Lists.attachSchema(new SimpleSchema({
- title: {
- /**
- * the title of the list
- */
- type: String,
- },
- archived: {
- /**
- * is the list archived
- */
- type: Boolean,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return false;
- }
+Lists.attachSchema(
+ new SimpleSchema({
+ title: {
+ /**
+ * the title of the list
+ */
+ type: String,
},
- },
- boardId: {
- /**
- * the board associated to this list
- */
- type: String,
- },
- swimlaneId: {
- /**
- * the swimlane associated to this list. Used for templates
- */
- type: String,
- defaultValue: '',
- },
- createdAt: {
- /**
- * creation date
- */
- type: Date,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+ archived: {
+ /**
+ * is the list archived
+ */
+ type: Boolean,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return false;
+ }
+ },
},
- },
- sort: {
- /**
- * is the list sorted
- */
- type: Number,
- decimal: true,
- // XXX We should probably provide a default
- optional: true,
- },
- updatedAt: {
- /**
- * last update of the list
- */
- type: Date,
- optional: true,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isUpdate) {
- return new Date();
- } else {
- this.unset();
- }
+ boardId: {
+ /**
+ * the board associated to this list
+ */
+ type: String,
},
- },
- wipLimit: {
- /**
- * WIP object, see below
- */
- type: Object,
- optional: true,
- },
- 'wipLimit.value': {
- /**
- * value of the WIP
- */
- type: Number,
- decimal: false,
- defaultValue: 1,
- },
- 'wipLimit.enabled': {
- /**
- * is the WIP enabled
- */
- type: Boolean,
- defaultValue: false,
- },
- 'wipLimit.soft': {
- /**
- * is the WIP a soft or hard requirement
- */
- type: Boolean,
- defaultValue: false,
- },
- color: {
- /**
- * the color of the list
- */
- type: String,
- optional: true,
- // silver is the default, so it is left out
- allowedValues: [
- 'white', 'green', 'yellow', 'orange', 'red', 'purple',
- 'blue', 'sky', 'lime', 'pink', 'black',
- 'peachpuff', 'crimson', 'plum', 'darkgreen',
- 'slateblue', 'magenta', 'gold', 'navy', 'gray',
- 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
- ],
- },
- type: {
- /**
- * The type of list
- */
- type: String,
- defaultValue: 'list',
- },
-}));
+ swimlaneId: {
+ /**
+ * the swimlane associated to this list. Used for templates
+ */
+ type: String,
+ defaultValue: '',
+ },
+ createdAt: {
+ /**
+ * creation date
+ */
+ type: Date,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ sort: {
+ /**
+ * is the list sorted
+ */
+ type: Number,
+ decimal: true,
+ // XXX We should probably provide a default
+ optional: true,
+ },
+ updatedAt: {
+ /**
+ * last update of the list
+ */
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isUpdate || this.isUpsert || this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ wipLimit: {
+ /**
+ * WIP object, see below
+ */
+ type: Object,
+ optional: true,
+ },
+ 'wipLimit.value': {
+ /**
+ * value of the WIP
+ */
+ type: Number,
+ decimal: false,
+ defaultValue: 1,
+ },
+ 'wipLimit.enabled': {
+ /**
+ * is the WIP enabled
+ */
+ type: Boolean,
+ defaultValue: false,
+ },
+ 'wipLimit.soft': {
+ /**
+ * is the WIP a soft or hard requirement
+ */
+ type: Boolean,
+ defaultValue: false,
+ },
+ color: {
+ /**
+ * the color of the list
+ */
+ type: String,
+ optional: true,
+ // silver is the default, so it is left out
+ allowedValues: [
+ 'white',
+ 'green',
+ 'yellow',
+ 'orange',
+ 'red',
+ 'purple',
+ 'blue',
+ 'sky',
+ 'lime',
+ 'pink',
+ 'black',
+ 'peachpuff',
+ 'crimson',
+ 'plum',
+ 'darkgreen',
+ 'slateblue',
+ 'magenta',
+ 'gold',
+ 'navy',
+ 'gray',
+ 'saddlebrown',
+ 'paleturquoise',
+ 'mistyrose',
+ 'indigo',
+ ],
+ },
+ type: {
+ /**
+ * The type of list
+ */
+ type: String,
+ defaultValue: 'list',
+ },
+ })
+);
Lists.allow({
insert(userId, doc) {
@@ -172,10 +208,8 @@ Lists.helpers({
listId: this._id,
archived: false,
};
- if (swimlaneId)
- selector.swimlaneId = swimlaneId;
- return Cards.find(Filter.mongoSelector(selector),
- { sort: ['sort'] });
+ if (swimlaneId) selector.swimlaneId = swimlaneId;
+ return Cards.find(Filter.mongoSelector(selector), { sort: ['sort'] });
},
cardsUnfiltered(swimlaneId) {
@@ -183,10 +217,8 @@ Lists.helpers({
listId: this._id,
archived: false,
};
- if (swimlaneId)
- selector.swimlaneId = swimlaneId;
- return Cards.find(selector,
- { sort: ['sort'] });
+ if (swimlaneId) selector.swimlaneId = swimlaneId;
+ return Cards.find(selector, { sort: ['sort'] });
},
allCards() {
@@ -197,11 +229,12 @@ Lists.helpers({
return Boards.findOne(this.boardId);
},
- getWipLimit(option){
+ getWipLimit(option) {
const list = Lists.findOne({ _id: this._id });
- if(!list.wipLimit) { // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
+ if (!list.wipLimit) {
+ // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
return 0;
- } else if(!option) {
+ } else if (!option) {
return list.wipLimit;
} else {
return list.wipLimit[option] ? list.wipLimit[option] : 0; // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
@@ -209,8 +242,7 @@ Lists.helpers({
},
colorClass() {
- if (this.color)
- return this.color;
+ if (this.color) return this.color;
return '';
},
@@ -219,7 +251,7 @@ Lists.helpers({
},
remove() {
- Lists.remove({ _id: this._id});
+ Lists.remove({ _id: this._id });
},
});
@@ -271,10 +303,10 @@ Lists.mutations({
});
Meteor.methods({
- applyWipLimit(listId, limit){
+ applyWipLimit(listId, limit) {
check(listId, String);
check(limit, Number);
- if(limit === 0){
+ if (limit === 0) {
limit = 1;
}
Lists.findOne({ _id: listId }).setWipLimit(limit);
@@ -283,7 +315,7 @@ Meteor.methods({
enableWipLimit(listId) {
check(listId, String);
const list = Lists.findOne({ _id: listId });
- if(list.getWipLimit('value') === 0){
+ if (list.getWipLimit('value') === 0) {
list.setWipLimit(1);
}
list.toggleWipLimit(!list.getWipLimit('enabled'));
@@ -300,6 +332,7 @@ Lists.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) {
Meteor.startup(() => {
+ Lists._collection._ensureIndex({ modifiedAt: -1 });
Lists._collection._ensureIndex({ boardId: 1 });
});
@@ -313,6 +346,11 @@ if (Meteor.isServer) {
});
});
+ Lists.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ });
+
Lists.before.remove((userId, doc) => {
const cards = Cards.find({ listId: doc._id });
if (cards) {
@@ -353,22 +391,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/lists', function(req, res) {
try {
const paramBoardId = req.params.boardId;
- Authentication.checkBoardAccess( req.userId, paramBoardId);
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- }),
+ data: Lists.find({ boardId: paramBoardId, archived: false }).map(
+ function(doc) {
+ return {
+ _id: doc._id,
+ title: doc.title,
+ };
+ }
+ ),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -384,17 +423,23 @@ if (Meteor.isServer) {
* @param {string} listId the List ID
* @return_type Lists
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
- Authentication.checkBoardAccess( req.userId, paramBoardId);
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }),
+ data: Lists.findOne({
+ _id: paramListId,
+ boardId: paramBoardId,
+ archived: false,
+ }),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -410,9 +455,9 @@ if (Meteor.isServer) {
* @param {string} title the title of the List
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/lists', function(req, res) {
try {
- Authentication.checkUserId( req.userId);
+ Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId);
const id = Lists.insert({
@@ -426,8 +471,7 @@ if (Meteor.isServer) {
_id: id,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -446,9 +490,12 @@ if (Meteor.isServer) {
* @param {string} listId the ID of the list to remove
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) {
+ JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function(
+ req,
+ res
+ ) {
try {
- Authentication.checkUserId( req.userId);
+ Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Lists.remove({ _id: paramListId, boardId: paramBoardId });
@@ -458,13 +505,13 @@ if (Meteor.isServer) {
_id: paramListId,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
-
}
+
+export default Lists;
diff --git a/models/rules.js b/models/rules.js
index 7d971980..76170b33 100644
--- a/models/rules.js
+++ b/models/rules.js
@@ -1,23 +1,51 @@
+import { Meteor } from 'meteor/meteor';
+
Rules = new Mongo.Collection('rules');
-Rules.attachSchema(new SimpleSchema({
- title: {
- type: String,
- optional: false,
- },
- triggerId: {
- type: String,
- optional: false,
- },
- actionId: {
- type: String,
- optional: false,
- },
- boardId: {
- type: String,
- optional: false,
- },
-}));
+Rules.attachSchema(
+ new SimpleSchema({
+ title: {
+ type: String,
+ optional: false,
+ },
+ triggerId: {
+ type: String,
+ optional: false,
+ },
+ actionId: {
+ type: String,
+ optional: false,
+ },
+ boardId: {
+ type: String,
+ optional: false,
+ },
+ createdAt: {
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ })
+);
Rules.mutations({
rename(description) {
@@ -26,15 +54,14 @@ Rules.mutations({
});
Rules.helpers({
- getAction(){
- return Actions.findOne({_id:this.actionId});
+ getAction() {
+ return Actions.findOne({ _id: this.actionId });
},
- getTrigger(){
- return Triggers.findOne({_id:this.triggerId});
+ getTrigger() {
+ return Triggers.findOne({ _id: this.triggerId });
},
});
-
Rules.allow({
insert(userId, doc) {
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
@@ -46,3 +73,16 @@ Rules.allow({
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
},
});
+
+Rules.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
+if (Meteor.isServer) {
+ Meteor.startup(() => {
+ Rules._collection._ensureIndex({ modifiedAt: -1 });
+ });
+}
+
+export default Rules;
diff --git a/models/settings.js b/models/settings.js
index e0f94fca..b97961eb 100644
--- a/models/settings.js
+++ b/models/settings.js
@@ -1,67 +1,85 @@
Settings = new Mongo.Collection('settings');
-Settings.attachSchema(new SimpleSchema({
- disableRegistration: {
- type: Boolean,
- },
- 'mailServer.username': {
- type: String,
- optional: true,
- },
- 'mailServer.password': {
- type: String,
- optional: true,
- },
- 'mailServer.host': {
- type: String,
- optional: true,
- },
- 'mailServer.port': {
- type: String,
- optional: true,
- },
- 'mailServer.enableTLS': {
- type: Boolean,
- optional: true,
- },
- 'mailServer.from': {
- type: String,
- optional: true,
- },
- productName: {
- type: String,
- optional: true,
- },
- customHTMLafterBodyStart: {
- type: String,
- optional: true,
- },
- customHTMLbeforeBodyEnd: {
- type: String,
- optional: true,
- },
- displayAuthenticationMethod: {
- type: Boolean,
- optional: true,
- },
- defaultAuthenticationMethod: {
- type: String,
- optional: false,
- },
- hideLogo: {
- type: Boolean,
- optional: true,
- },
- createdAt: {
- type: Date,
- denyUpdate: true,
- },
- modifiedAt: {
- type: Date,
- },
-}));
+Settings.attachSchema(
+ new SimpleSchema({
+ disableRegistration: {
+ type: Boolean,
+ },
+ 'mailServer.username': {
+ type: String,
+ optional: true,
+ },
+ 'mailServer.password': {
+ type: String,
+ optional: true,
+ },
+ 'mailServer.host': {
+ type: String,
+ optional: true,
+ },
+ 'mailServer.port': {
+ type: String,
+ optional: true,
+ },
+ 'mailServer.enableTLS': {
+ type: Boolean,
+ optional: true,
+ },
+ 'mailServer.from': {
+ type: String,
+ optional: true,
+ },
+ productName: {
+ type: String,
+ optional: true,
+ },
+ customHTMLafterBodyStart: {
+ type: String,
+ optional: true,
+ },
+ customHTMLbeforeBodyEnd: {
+ type: String,
+ optional: true,
+ },
+ displayAuthenticationMethod: {
+ type: Boolean,
+ optional: true,
+ },
+ defaultAuthenticationMethod: {
+ type: String,
+ optional: false,
+ },
+ hideLogo: {
+ type: Boolean,
+ optional: true,
+ },
+ createdAt: {
+ type: Date,
+ denyUpdate: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ })
+);
Settings.helpers({
- mailUrl () {
+ mailUrl() {
if (!this.mailServer.host) {
return null;
}
@@ -69,7 +87,9 @@ Settings.helpers({
if (!this.mailServer.username && !this.mailServer.password) {
return `${protocol}${this.mailServer.host}:${this.mailServer.port}/`;
}
- return `${protocol}${this.mailServer.username}:${encodeURIComponent(this.mailServer.password)}@${this.mailServer.host}:${this.mailServer.port}/`;
+ return `${protocol}${this.mailServer.username}:${encodeURIComponent(
+ this.mailServer.password
+ )}@${this.mailServer.host}:${this.mailServer.port}/`;
},
});
Settings.allow({
@@ -86,50 +106,75 @@ Settings.before.update((userId, doc, fieldNames, modifier) => {
if (Meteor.isServer) {
Meteor.startup(() => {
+ Settings._collection._ensureIndex({ modifiedAt: -1 });
const setting = Settings.findOne({});
- if(!setting){
+ if (!setting) {
const now = new Date();
- const domain = process.env.ROOT_URL.match(/\/\/(?:www\.)?(.*)?(?:\/)?/)[1];
+ const domain = process.env.ROOT_URL.match(
+ /\/\/(?:www\.)?(.*)?(?:\/)?/
+ )[1];
const from = `Boards Support <support@${domain}>`;
- const defaultSetting = {disableRegistration: false, mailServer: {
- username: '', password: '', host: '', port: '', enableTLS: false, from,
- }, createdAt: now, modifiedAt: now, displayAuthenticationMethod: true,
- defaultAuthenticationMethod: 'password'};
+ const defaultSetting = {
+ disableRegistration: false,
+ mailServer: {
+ username: '',
+ password: '',
+ host: '',
+ port: '',
+ enableTLS: false,
+ from,
+ },
+ createdAt: now,
+ modifiedAt: now,
+ displayAuthenticationMethod: true,
+ defaultAuthenticationMethod: 'password',
+ };
Settings.insert(defaultSetting);
}
const newSetting = Settings.findOne();
if (!process.env.MAIL_URL && newSetting.mailUrl())
process.env.MAIL_URL = newSetting.mailUrl();
- Accounts.emailTemplates.from = process.env.MAIL_FROM ? process.env.MAIL_FROM : newSetting.mailServer.from;
+ Accounts.emailTemplates.from = process.env.MAIL_FROM
+ ? process.env.MAIL_FROM
+ : newSetting.mailServer.from;
});
Settings.after.update((userId, doc, fieldNames) => {
// assign new values to mail-from & MAIL_URL in environment
if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) {
const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://';
if (!doc.mailServer.username && !doc.mailServer.password) {
- process.env.MAIL_URL = `${protocol}${doc.mailServer.host}:${doc.mailServer.port}/`;
+ process.env.MAIL_URL = `${protocol}${doc.mailServer.host}:${
+ doc.mailServer.port
+ }/`;
} else {
- process.env.MAIL_URL = `${protocol}${doc.mailServer.username}:${encodeURIComponent(doc.mailServer.password)}@${doc.mailServer.host}:${doc.mailServer.port}/`;
+ process.env.MAIL_URL = `${protocol}${
+ doc.mailServer.username
+ }:${encodeURIComponent(doc.mailServer.password)}@${
+ doc.mailServer.host
+ }:${doc.mailServer.port}/`;
}
Accounts.emailTemplates.from = doc.mailServer.from;
}
});
- function getRandomNum (min, max) {
+ function getRandomNum(min, max) {
const range = max - min;
const rand = Math.random();
- return (min + Math.round(rand * range));
+ return min + Math.round(rand * range);
}
- function getEnvVar(name){
+ function getEnvVar(name) {
const value = process.env[name];
- if (value){
+ if (value) {
return value;
}
- throw new Meteor.Error(['var-not-exist', `The environment variable ${name} does not exist`]);
+ throw new Meteor.Error([
+ 'var-not-exist',
+ `The environment variable ${name} does not exist`,
+ ]);
}
- function sendInvitationEmail (_id){
+ function sendInvitationEmail(_id) {
const icode = InvitationCodes.findOne(_id);
const author = Users.findOne(Meteor.userId());
try {
@@ -172,30 +217,47 @@ if (Meteor.isServer) {
check(boards, [String]);
const user = Users.findOne(Meteor.userId());
- if(!user.isAdmin){
+ if (!user.isAdmin) {
throw new Meteor.Error('not-allowed');
}
emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email)) {
// Checks if the email is already link to an account.
- const userExist = Users.findOne({email});
- if (userExist){
- throw new Meteor.Error('user-exist', `The user with the email ${email} has already an account.`);
+ const userExist = Users.findOne({ email });
+ if (userExist) {
+ throw new Meteor.Error(
+ 'user-exist',
+ `The user with the email ${email} has already an account.`
+ );
}
// Checks if the email is already link to an invitation.
- const invitation = InvitationCodes.findOne({email});
- if (invitation){
- InvitationCodes.update(invitation, {$set : {boardsToBeInvited: boards}});
+ const invitation = InvitationCodes.findOne({ email });
+ if (invitation) {
+ InvitationCodes.update(invitation, {
+ $set: { boardsToBeInvited: boards },
+ });
sendInvitationEmail(invitation._id);
- }else {
+ } else {
const code = getRandomNum(100000, 999999);
- InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){
- if (!err && _id) {
- sendInvitationEmail(_id);
- } else {
- throw new Meteor.Error('invitation-generated-fail', err.message);
+ InvitationCodes.insert(
+ {
+ code,
+ email,
+ boardsToBeInvited: boards,
+ createdAt: new Date(),
+ authorId: Meteor.userId(),
+ },
+ function(err, _id) {
+ if (!err && _id) {
+ sendInvitationEmail(_id);
+ } else {
+ throw new Meteor.Error(
+ 'invitation-generated-fail',
+ err.message
+ );
+ }
}
- });
+ );
}
}
});
@@ -215,11 +277,15 @@ if (Meteor.isServer) {
Email.send({
to: user.emails[0].address,
from: Accounts.emailTemplates.from,
- subject: TAPi18n.__('email-smtp-test-subject', {lng: lang}),
- text: TAPi18n.__('email-smtp-test-text', {lng: lang}),
+ subject: TAPi18n.__('email-smtp-test-subject', { lng: lang }),
+ text: TAPi18n.__('email-smtp-test-text', { lng: lang }),
});
- } catch ({message}) {
- throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message);
+ } catch ({ message }) {
+ throw new Meteor.Error(
+ 'email-fail',
+ `${TAPi18n.__('email-fail-text', { lng: lang })}: ${message}`,
+ message
+ );
}
return {
message: 'email-sent',
@@ -227,7 +293,7 @@ if (Meteor.isServer) {
};
},
- getCustomUI(){
+ getCustomUI() {
const setting = Settings.findOne({});
if (!setting.productName) {
return {
@@ -240,7 +306,7 @@ if (Meteor.isServer) {
}
},
- getMatomoConf(){
+ getMatomoConf() {
return {
address: getEnvVar('MATOMO_ADDRESS'),
siteId: getEnvVar('MATOMO_SITE_ID'),
@@ -275,3 +341,5 @@ if (Meteor.isServer) {
},
});
}
+
+export default Settings;
diff --git a/models/swimlanes.js b/models/swimlanes.js
index 9a53d116..82f73f79 100644
--- a/models/swimlanes.js
+++ b/models/swimlanes.js
@@ -3,89 +3,125 @@ Swimlanes = new Mongo.Collection('swimlanes');
/**
* A swimlane is an line in the kaban board.
*/
-Swimlanes.attachSchema(new SimpleSchema({
- title: {
- /**
- * the title of the swimlane
- */
- type: String,
- },
- archived: {
- /**
- * is the swimlane archived?
- */
- type: Boolean,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return false;
- }
+Swimlanes.attachSchema(
+ new SimpleSchema({
+ title: {
+ /**
+ * the title of the swimlane
+ */
+ type: String,
},
- },
- boardId: {
- /**
- * the ID of the board the swimlane is attached to
- */
- type: String,
- },
- createdAt: {
- /**
- * creation date of the swimlane
- */
- type: Date,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+ archived: {
+ /**
+ * is the swimlane archived?
+ */
+ type: Boolean,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return false;
+ }
+ },
},
- },
- sort: {
- /**
- * the sort value of the swimlane
- */
- type: Number,
- decimal: true,
- // XXX We should probably provide a default
- optional: true,
- },
- color: {
- /**
- * the color of the swimlane
- */
- type: String,
- optional: true,
- // silver is the default, so it is left out
- allowedValues: [
- 'white', 'green', 'yellow', 'orange', 'red', 'purple',
- 'blue', 'sky', 'lime', 'pink', 'black',
- 'peachpuff', 'crimson', 'plum', 'darkgreen',
- 'slateblue', 'magenta', 'gold', 'navy', 'gray',
- 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
- ],
- },
- updatedAt: {
- /**
- * when was the swimlane last edited
- */
- type: Date,
- optional: true,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isUpdate) {
- return new Date();
- } else {
- this.unset();
- }
+ boardId: {
+ /**
+ * the ID of the board the swimlane is attached to
+ */
+ type: String,
},
- },
- type: {
- /**
- * The type of swimlane
- */
- type: String,
- defaultValue: 'swimlane',
- },
-}));
+ createdAt: {
+ /**
+ * creation date of the swimlane
+ */
+ type: Date,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ sort: {
+ /**
+ * the sort value of the swimlane
+ */
+ type: Number,
+ decimal: true,
+ // XXX We should probably provide a default
+ optional: true,
+ },
+ color: {
+ /**
+ * the color of the swimlane
+ */
+ type: String,
+ optional: true,
+ // silver is the default, so it is left out
+ allowedValues: [
+ 'white',
+ 'green',
+ 'yellow',
+ 'orange',
+ 'red',
+ 'purple',
+ 'blue',
+ 'sky',
+ 'lime',
+ 'pink',
+ 'black',
+ 'peachpuff',
+ 'crimson',
+ 'plum',
+ 'darkgreen',
+ 'slateblue',
+ 'magenta',
+ 'gold',
+ 'navy',
+ 'gray',
+ 'saddlebrown',
+ 'paleturquoise',
+ 'mistyrose',
+ 'indigo',
+ ],
+ },
+ updatedAt: {
+ /**
+ * when was the swimlane last edited
+ */
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isUpdate || this.isUpsert || this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ type: {
+ /**
+ * The type of swimlane
+ */
+ type: String,
+ defaultValue: 'swimlane',
+ },
+ })
+);
Swimlanes.allow({
insert(userId, doc) {
@@ -109,7 +145,7 @@ Swimlanes.helpers({
const _id = Swimlanes.insert(this);
const query = {
- swimlaneId: {$in: [oldId, '']},
+ swimlaneId: { $in: [oldId, ''] },
archived: false,
};
if (oldBoardId) {
@@ -126,18 +162,24 @@ Swimlanes.helpers({
},
cards() {
- return Cards.find(Filter.mongoSelector({
- swimlaneId: this._id,
- archived: false,
- }), { sort: ['sort'] });
+ return Cards.find(
+ Filter.mongoSelector({
+ swimlaneId: this._id,
+ archived: false,
+ }),
+ { sort: ['sort'] }
+ );
},
lists() {
- return Lists.find({
- boardId: this.boardId,
- swimlaneId: {$in: [this._id, '']},
- archived: false,
- }, { sort: ['sort'] });
+ return Lists.find(
+ {
+ boardId: this.boardId,
+ swimlaneId: { $in: [this._id, ''] },
+ archived: false,
+ },
+ { sort: ['sort'] }
+ );
},
myLists() {
@@ -153,8 +195,7 @@ Swimlanes.helpers({
},
colorClass() {
- if (this.color)
- return this.color;
+ if (this.color) return this.color;
return '';
},
@@ -182,7 +223,7 @@ Swimlanes.helpers({
},
remove() {
- Swimlanes.remove({ _id: this._id});
+ Swimlanes.remove({ _id: this._id });
},
});
@@ -221,10 +262,16 @@ Swimlanes.mutations({
},
});
+Swimlanes.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
Swimlanes.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) {
Meteor.startup(() => {
+ Swimlanes._collection._ensureIndex({ modifiedAt: -1 });
Swimlanes._collection._ensureIndex({ boardId: 1 });
});
@@ -239,18 +286,21 @@ if (Meteor.isServer) {
});
Swimlanes.before.remove(function(userId, doc) {
- const lists = Lists.find({
- boardId: doc.boardId,
- swimlaneId: {$in: [doc._id, '']},
- archived: false,
- }, { sort: ['sort'] });
+ const lists = Lists.find(
+ {
+ boardId: doc.boardId,
+ swimlaneId: { $in: [doc._id, ''] },
+ archived: false,
+ },
+ { sort: ['sort'] }
+ );
if (lists.count() < 2) {
lists.forEach((list) => {
list.remove();
});
} else {
- Cards.remove({swimlaneId: doc._id});
+ Cards.remove({ swimlaneId: doc._id });
}
Activities.insert({
@@ -287,22 +337,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
try {
const paramBoardId = req.params.boardId;
- Authentication.checkBoardAccess( req.userId, paramBoardId);
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- }),
+ data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(
+ function(doc) {
+ return {
+ _id: doc._id,
+ title: doc.title,
+ };
+ }
+ ),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -319,17 +370,23 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane
* @return_type Swimlanes
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function(
+ req,
+ res
+ ) {
try {
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
- Authentication.checkBoardAccess( req.userId, paramBoardId);
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }),
+ data: Swimlanes.findOne({
+ _id: paramSwimlaneId,
+ boardId: paramBoardId,
+ archived: false,
+ }),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -346,9 +403,9 @@ if (Meteor.isServer) {
* @param {string} title the new title of the swimlane
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
try {
- Authentication.checkUserId( req.userId);
+ Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId);
const id = Swimlanes.insert({
@@ -362,8 +419,7 @@ if (Meteor.isServer) {
_id: id,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -382,25 +438,29 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
- try {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const paramSwimlaneId = req.params.swimlaneId;
- Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramSwimlaneId,
- },
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
+ JsonRoutes.add(
+ 'DELETE',
+ '/api/boards/:boardId/swimlanes/:swimlaneId',
+ function(req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const paramBoardId = req.params.boardId;
+ const paramSwimlaneId = req.params.swimlaneId;
+ Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramSwimlaneId,
+ },
+ });
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
}
- });
-
+ );
}
+
+export default Swimlanes;
diff --git a/models/triggers.js b/models/triggers.js
index 15982b6e..8f2448c4 100644
--- a/models/triggers.js
+++ b/models/triggers.js
@@ -1,3 +1,5 @@
+import { Meteor } from 'meteor/meteor';
+
Triggers = new Mongo.Collection('triggers');
Triggers.mutations({
@@ -23,7 +25,6 @@ Triggers.allow({
});
Triggers.helpers({
-
description() {
return this.desc;
},
@@ -56,3 +57,16 @@ Triggers.helpers({
return cardLabels;
},
});
+
+Triggers.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
+if (Meteor.isServer) {
+ Meteor.startup(() => {
+ Triggers._collection._ensureIndex({ modifiedAt: -1 });
+ });
+}
+
+export default Triggers;
diff --git a/models/unsavedEdits.js b/models/unsavedEdits.js
index d4f3616a..122b2cd2 100644
--- a/models/unsavedEdits.js
+++ b/models/unsavedEdits.js
@@ -2,31 +2,66 @@
// `UnsavedEdits` API on the client.
UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
-UnsavedEditCollection.attachSchema(new SimpleSchema({
- fieldName: {
- type: String,
- },
- docId: {
- type: String,
- },
- value: {
- type: String,
- },
- userId: {
- type: String,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return this.userId;
- }
+UnsavedEditCollection.attachSchema(
+ new SimpleSchema({
+ fieldName: {
+ type: String,
},
- },
-}));
+ docId: {
+ type: String,
+ },
+ value: {
+ type: String,
+ },
+ userId: {
+ type: String,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return this.userId;
+ }
+ },
+ },
+ createdAt: {
+ type: Date,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ })
+);
+
+UnsavedEditCollection.before.update(
+ (userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+ }
+);
if (Meteor.isServer) {
function isAuthor(userId, doc, fieldNames = []) {
return userId === doc.userId && fieldNames.indexOf('userId') === -1;
}
Meteor.startup(() => {
+ UnsavedEditCollection._collection._ensureIndex({ modifiedAt: -1 });
UnsavedEditCollection._collection._ensureIndex({ userId: 1 });
});
UnsavedEditCollection.allow({
@@ -36,3 +71,5 @@ if (Meteor.isServer) {
fetch: ['userId'],
});
}
+
+export default UnsavedEditCollection;
diff --git a/models/users.js b/models/users.js
index 5f949c80..306193aa 100644
--- a/models/users.js
+++ b/models/users.js
@@ -1,237 +1,254 @@
// Sandstorm context is detected using the METEOR_SETTINGS environment variable
// in the package definition.
-const isSandstorm = Meteor.settings && Meteor.settings.public &&
- Meteor.settings.public.sandstorm;
+const isSandstorm =
+ Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
Users = Meteor.users;
/**
* A User in wekan
*/
-Users.attachSchema(new SimpleSchema({
- username: {
- /**
- * the username of the user
- */
- type: String,
- optional: true,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- const name = this.field('profile.fullname');
- if (name.isSet) {
- return name.value.toLowerCase().replace(/\s/g, '');
+Users.attachSchema(
+ new SimpleSchema({
+ username: {
+ /**
+ * the username of the user
+ */
+ type: String,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ const name = this.field('profile.fullname');
+ if (name.isSet) {
+ return name.value.toLowerCase().replace(/\s/g, '');
+ }
}
- }
+ },
},
- },
- emails: {
- /**
- * the list of emails attached to a user
- */
- type: [Object],
- optional: true,
- },
- 'emails.$.address': {
- /**
- * The email address
- */
- type: String,
- regEx: SimpleSchema.RegEx.Email,
- },
- 'emails.$.verified': {
- /**
- * Has the email been verified
- */
- type: Boolean,
- },
- createdAt: {
- /**
- * creation date of the user
- */
- type: Date,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
+ emails: {
+ /**
+ * the list of emails attached to a user
+ */
+ type: [Object],
+ optional: true,
},
- },
- profile: {
- /**
- * profile settings
- */
- type: Object,
- optional: true,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return {
- boardView: 'board-view-lists',
- };
- }
+ 'emails.$.address': {
+ /**
+ * The email address
+ */
+ type: String,
+ regEx: SimpleSchema.RegEx.Email,
},
- },
- 'profile.avatarUrl': {
- /**
- * URL of the avatar of the user
- */
- type: String,
- optional: true,
- },
- 'profile.emailBuffer': {
- /**
- * list of email buffers of the user
- */
- type: [String],
- optional: true,
- },
- 'profile.fullname': {
- /**
- * full name of the user
- */
- type: String,
- optional: true,
- },
- 'profile.hiddenSystemMessages': {
- /**
- * does the user wants to hide system messages?
- */
- type: Boolean,
- optional: true,
- },
- 'profile.initials': {
- /**
- * initials of the user
- */
- type: String,
- optional: true,
- },
- 'profile.invitedBoards': {
- /**
- * board IDs the user has been invited to
- */
- type: [String],
- optional: true,
- },
- 'profile.language': {
- /**
- * language of the user
- */
- type: String,
- optional: true,
- },
- 'profile.notifications': {
- /**
- * enabled notifications for the user
- */
- type: [String],
- optional: true,
- },
- 'profile.showCardsCountAt': {
- /**
- * showCardCountAt field of the user
- */
- type: Number,
- optional: true,
- },
- 'profile.starredBoards': {
- /**
- * list of starred board IDs
- */
- type: [String],
- optional: true,
- },
- 'profile.icode': {
- /**
- * icode
- */
- type: String,
- optional: true,
- },
- 'profile.boardView': {
- /**
- * boardView field of the user
- */
- type: String,
- optional: true,
- allowedValues: [
- 'board-view-lists',
- 'board-view-swimlanes',
- 'board-view-cal',
- ],
- },
- 'profile.templatesBoardId': {
- /**
- * Reference to the templates board
- */
- type: String,
- defaultValue: '',
- },
- 'profile.cardTemplatesSwimlaneId': {
- /**
- * Reference to the card templates swimlane Id
- */
- type: String,
- defaultValue: '',
- },
- 'profile.listTemplatesSwimlaneId': {
- /**
- * Reference to the list templates swimlane Id
- */
- type: String,
- defaultValue: '',
- },
- 'profile.boardTemplatesSwimlaneId': {
- /**
- * Reference to the board templates swimlane Id
- */
- type: String,
- defaultValue: '',
- },
- services: {
- /**
- * services field of the user
- */
- type: Object,
- optional: true,
- blackbox: true,
- },
- heartbeat: {
- /**
- * last time the user has been seen
- */
- type: Date,
- optional: true,
- },
- isAdmin: {
- /**
- * is the user an admin of the board?
- */
- type: Boolean,
- optional: true,
- },
- createdThroughApi: {
- /**
- * was the user created through the API?
- */
- type: Boolean,
- optional: true,
- },
- loginDisabled: {
- /**
- * loginDisabled field of the user
- */
- type: Boolean,
- optional: true,
- },
- 'authenticationMethod': {
- /**
- * authentication method of the user
- */
- type: String,
- optional: false,
- defaultValue: 'password',
- },
-}));
+ 'emails.$.verified': {
+ /**
+ * Has the email been verified
+ */
+ type: Boolean,
+ },
+ createdAt: {
+ /**
+ * creation date of the user
+ */
+ type: Date,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ modifiedAt: {
+ type: Date,
+ denyUpdate: false,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert || this.isUpsert || this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ profile: {
+ /**
+ * profile settings
+ */
+ type: Object,
+ optional: true,
+ // eslint-disable-next-line consistent-return
+ autoValue() {
+ if (this.isInsert && !this.isSet) {
+ return {
+ boardView: 'board-view-lists',
+ };
+ }
+ },
+ },
+ 'profile.avatarUrl': {
+ /**
+ * URL of the avatar of the user
+ */
+ type: String,
+ optional: true,
+ },
+ 'profile.emailBuffer': {
+ /**
+ * list of email buffers of the user
+ */
+ type: [String],
+ optional: true,
+ },
+ 'profile.fullname': {
+ /**
+ * full name of the user
+ */
+ type: String,
+ optional: true,
+ },
+ 'profile.hiddenSystemMessages': {
+ /**
+ * does the user wants to hide system messages?
+ */
+ type: Boolean,
+ optional: true,
+ },
+ 'profile.initials': {
+ /**
+ * initials of the user
+ */
+ type: String,
+ optional: true,
+ },
+ 'profile.invitedBoards': {
+ /**
+ * board IDs the user has been invited to
+ */
+ type: [String],
+ optional: true,
+ },
+ 'profile.language': {
+ /**
+ * language of the user
+ */
+ type: String,
+ optional: true,
+ },
+ 'profile.notifications': {
+ /**
+ * enabled notifications for the user
+ */
+ type: [String],
+ optional: true,
+ },
+ 'profile.showCardsCountAt': {
+ /**
+ * showCardCountAt field of the user
+ */
+ type: Number,
+ optional: true,
+ },
+ 'profile.starredBoards': {
+ /**
+ * list of starred board IDs
+ */
+ type: [String],
+ optional: true,
+ },
+ 'profile.icode': {
+ /**
+ * icode
+ */
+ type: String,
+ optional: true,
+ },
+ 'profile.boardView': {
+ /**
+ * boardView field of the user
+ */
+ type: String,
+ optional: true,
+ allowedValues: [
+ 'board-view-lists',
+ 'board-view-swimlanes',
+ 'board-view-cal',
+ ],
+ },
+ 'profile.templatesBoardId': {
+ /**
+ * Reference to the templates board
+ */
+ type: String,
+ defaultValue: '',
+ },
+ 'profile.cardTemplatesSwimlaneId': {
+ /**
+ * Reference to the card templates swimlane Id
+ */
+ type: String,
+ defaultValue: '',
+ },
+ 'profile.listTemplatesSwimlaneId': {
+ /**
+ * Reference to the list templates swimlane Id
+ */
+ type: String,
+ defaultValue: '',
+ },
+ 'profile.boardTemplatesSwimlaneId': {
+ /**
+ * Reference to the board templates swimlane Id
+ */
+ type: String,
+ defaultValue: '',
+ },
+ services: {
+ /**
+ * services field of the user
+ */
+ type: Object,
+ optional: true,
+ blackbox: true,
+ },
+ heartbeat: {
+ /**
+ * last time the user has been seen
+ */
+ type: Date,
+ optional: true,
+ },
+ isAdmin: {
+ /**
+ * is the user an admin of the board?
+ */
+ type: Boolean,
+ optional: true,
+ },
+ createdThroughApi: {
+ /**
+ * was the user created through the API?
+ */
+ type: Boolean,
+ optional: true,
+ },
+ loginDisabled: {
+ /**
+ * loginDisabled field of the user
+ */
+ type: Boolean,
+ optional: true,
+ },
+ authenticationMethod: {
+ /**
+ * authentication method of the user
+ */
+ type: String,
+ optional: false,
+ defaultValue: 'password',
+ },
+ })
+);
Users.allow({
update(userId) {
@@ -240,7 +257,10 @@ Users.allow({
},
remove(userId, doc) {
const adminsNumber = Users.find({ isAdmin: true }).count();
- const { isAdmin } = Users.findOne({ _id: userId }, { fields: { 'isAdmin': 1 } });
+ const { isAdmin } = Users.findOne(
+ { _id: userId },
+ { fields: { isAdmin: 1 } }
+ );
// Prevents remove of the only one administrator
if (adminsNumber === 1 && isAdmin && userId === doc._id) {
@@ -270,7 +290,9 @@ if (Meteor.isClient) {
isNotNoComments() {
const board = Boards.findOne(Session.get('currentBoard'));
- return board && board.hasMember(this._id) && !board.hasNoComments(this._id);
+ return (
+ board && board.hasMember(this._id) && !board.hasNoComments(this._id)
+ );
},
isNoComments() {
@@ -280,7 +302,9 @@ if (Meteor.isClient) {
isNotCommentOnly() {
const board = Boards.findOne(Session.get('currentBoard'));
- return board && board.hasMember(this._id) && !board.hasCommentOnly(this._id);
+ return (
+ board && board.hasMember(this._id) && !board.hasCommentOnly(this._id)
+ );
},
isCommentOnly() {
@@ -301,32 +325,32 @@ Users.helpers({
},
starredBoards() {
- const {starredBoards = []} = this.profile || {};
- return Boards.find({archived: false, _id: {$in: starredBoards}});
+ const { starredBoards = [] } = this.profile || {};
+ return Boards.find({ archived: false, _id: { $in: starredBoards } });
},
hasStarred(boardId) {
- const {starredBoards = []} = this.profile || {};
+ const { starredBoards = [] } = this.profile || {};
return _.contains(starredBoards, boardId);
},
invitedBoards() {
- const {invitedBoards = []} = this.profile || {};
- return Boards.find({archived: false, _id: {$in: invitedBoards}});
+ const { invitedBoards = [] } = this.profile || {};
+ return Boards.find({ archived: false, _id: { $in: invitedBoards } });
},
isInvitedTo(boardId) {
- const {invitedBoards = []} = this.profile || {};
+ const { invitedBoards = [] } = this.profile || {};
return _.contains(invitedBoards, boardId);
},
hasTag(tag) {
- const {tags = []} = this.profile || {};
+ const { tags = [] } = this.profile || {};
return _.contains(tags, tag);
},
hasNotification(activityId) {
- const {notifications = []} = this.profile || {};
+ const { notifications = [] } = this.profile || {};
return _.contains(notifications, activityId);
},
@@ -336,20 +360,20 @@ Users.helpers({
},
getEmailBuffer() {
- const {emailBuffer = []} = this.profile || {};
+ const { emailBuffer = [] } = this.profile || {};
return emailBuffer;
},
getInitials() {
const profile = this.profile || {};
- if (profile.initials)
- return profile.initials;
-
+ if (profile.initials) return profile.initials;
else if (profile.fullname) {
- return profile.fullname.split(/\s+/).reduce((memo, word) => {
- return memo + word[0];
- }, '').toUpperCase();
-
+ return profile.fullname
+ .split(/\s+/)
+ .reduce((memo, word) => {
+ return memo + word[0];
+ }, '')
+ .toUpperCase();
} else {
return this.username[0].toUpperCase();
}
@@ -379,7 +403,7 @@ Users.helpers({
},
remove() {
- User.remove({ _id: this._id});
+ User.remove({ _id: this._id });
},
});
@@ -426,10 +450,8 @@ Users.mutations({
},
toggleTag(tag) {
- if (this.hasTag(tag))
- this.removeTag(tag);
- else
- this.addTag(tag);
+ if (this.hasTag(tag)) this.removeTag(tag);
+ else this.addTag(tag);
},
toggleSystem(value = false) {
@@ -473,16 +495,16 @@ Users.mutations({
},
setAvatarUrl(avatarUrl) {
- return {$set: {'profile.avatarUrl': avatarUrl}};
+ return { $set: { 'profile.avatarUrl': avatarUrl } };
},
setShowCardsCountAt(limit) {
- return {$set: {'profile.showCardsCountAt': limit}};
+ return { $set: { 'profile.showCardsCountAt': limit } };
},
setBoardView(view) {
return {
- $set : {
+ $set: {
'profile.boardView': view,
},
};
@@ -492,11 +514,11 @@ Users.mutations({
Meteor.methods({
setUsername(username, userId) {
check(username, String);
- const nUsersWithUsername = Users.find({username}).count();
+ const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else {
- Users.update(userId, {$set: {username}});
+ Users.update(userId, { $set: { username } });
}
},
toggleSystemMessages() {
@@ -509,16 +531,21 @@ Meteor.methods({
},
setEmail(email, userId) {
check(email, String);
- const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}});
+ const existingUser = Users.findOne(
+ { 'emails.address': email },
+ { fields: { _id: 1 } }
+ );
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
Users.update(userId, {
$set: {
- emails: [{
- address: email,
- verified: false,
- }],
+ emails: [
+ {
+ address: email,
+ verified: false,
+ },
+ ],
},
});
}
@@ -533,7 +560,7 @@ Meteor.methods({
setPassword(newPassword, userId) {
check(userId, String);
check(newPassword, String);
- if(Meteor.user().isAdmin){
+ if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword);
}
},
@@ -548,12 +575,13 @@ if (Meteor.isServer) {
const inviter = Meteor.user();
const board = Boards.findOne(boardId);
- const allowInvite = inviter &&
+ const allowInvite =
+ inviter &&
board &&
board.members &&
_.contains(_.pluck(board.members, 'userId'), inviter._id) &&
- _.where(board.members, {userId: inviter._id})[0].isActive &&
- _.where(board.members, {userId: inviter._id})[0].isAdmin;
+ _.where(board.members, { userId: inviter._id })[0].isActive &&
+ _.where(board.members, { userId: inviter._id })[0].isAdmin;
if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
this.unblock();
@@ -561,19 +589,21 @@ if (Meteor.isServer) {
const posAt = username.indexOf('@');
let user = null;
if (posAt >= 0) {
- user = Users.findOne({emails: {$elemMatch: {address: username}}});
+ user = Users.findOne({ emails: { $elemMatch: { address: username } } });
} else {
- user = Users.findOne(username) || Users.findOne({username});
+ user = Users.findOne(username) || Users.findOne({ username });
}
if (user) {
- if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
+ if (user._id === inviter._id)
+ throw new Meteor.Error('error-user-notAllowSelf');
} else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
- if (Settings.findOne().disableRegistration) throw new Meteor.Error('error-user-notCreated');
+ if (Settings.findOne().disableRegistration)
+ throw new Meteor.Error('error-user-notCreated');
// Set in lowercase email before creating account
const email = username.toLowerCase();
username = email.substring(0, posAt);
- const newUserId = Accounts.createUser({username, email});
+ const newUserId = Accounts.createUser({ username, email });
if (!newUserId) throw new Meteor.Error('error-user-notCreated');
// assume new user speak same language with inviter
if (inviter.profile && inviter.profile.language) {
@@ -607,7 +637,7 @@ if (Meteor.isServer) {
} catch (e) {
throw new Meteor.Error('email-fail', e.message);
}
- return {username: user.username, email: user.emails[0].address};
+ return { username: user.username, email: user.emails[0].address };
},
});
Accounts.onCreateUser((options, user) => {
@@ -621,14 +651,22 @@ if (Meteor.isServer) {
const email = user.services.oidc.email.toLowerCase();
user.username = user.services.oidc.username;
user.emails = [{ address: email, verified: true }];
- const initials = user.services.oidc.fullname.match(/\b[a-zA-Z]/g).join('').toUpperCase();
- user.profile = { initials, fullname: user.services.oidc.fullname, boardView: 'board-view-lists' };
+ const initials = user.services.oidc.fullname
+ .match(/\b[a-zA-Z]/g)
+ .join('')
+ .toUpperCase();
+ user.profile = {
+ initials,
+ fullname: user.services.oidc.fullname,
+ boardView: 'board-view-lists',
+ };
user.authenticationMethod = 'oauth2';
// see if any existing user has this email address or username, otherwise create new
- const existingUser = Meteor.users.findOne({$or: [{'emails.address': email}, {'username':user.username}]});
- if (!existingUser)
- return user;
+ const existingUser = Meteor.users.findOne({
+ $or: [{ 'emails.address': email }, { username: user.username }],
+ });
+ if (!existingUser) return user;
// copy across new service info
const service = _.keys(user.services)[0];
@@ -638,7 +676,7 @@ if (Meteor.isServer) {
existingUser.profile = user.profile;
existingUser.authenticationMethod = user.authenticationMethod;
- Meteor.users.remove({_id: existingUser._id}); // remove existing record
+ Meteor.users.remove({ _id: existingUser._id }); // remove existing record
return existingUser;
}
@@ -660,7 +698,10 @@ if (Meteor.isServer) {
}
if (!options || !options.profile) {
- throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required');
+ throw new Meteor.Error(
+ 'error-invitation-code-blank',
+ 'The invitation code is required'
+ );
}
const invitationCode = InvitationCodes.findOne({
code: options.profile.invitationcode,
@@ -668,26 +709,41 @@ if (Meteor.isServer) {
valid: true,
});
if (!invitationCode) {
- throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist');
+ throw new Meteor.Error(
+ 'error-invitation-code-not-exist',
+ 'The invitation code doesn\'t exist'
+ );
} else {
- user.profile = {icode: options.profile.invitationcode};
+ user.profile = { icode: options.profile.invitationcode };
user.profile.boardView = 'board-view-lists';
// Deletes the invitation code after the user was created successfully.
- setTimeout(Meteor.bindEnvironment(() => {
- InvitationCodes.remove({'_id': invitationCode._id});
- }), 200);
+ setTimeout(
+ Meteor.bindEnvironment(() => {
+ InvitationCodes.remove({ _id: invitationCode._id });
+ }),
+ 200
+ );
return user;
}
});
}
+Users.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = Date.now();
+});
+
if (Meteor.isServer) {
// Let mongoDB ensure username unicity
Meteor.startup(() => {
- Users._collection._ensureIndex({
- username: 1,
- }, {unique: true});
+ Users._collection._ensureIndex({ modifiedAt: -1 });
+ Users._collection._ensureIndex(
+ {
+ username: 1,
+ },
+ { unique: true }
+ );
});
// OLD WAY THIS CODE DID WORK: When user is last admin of board,
@@ -712,11 +768,10 @@ if (Meteor.isServer) {
// counter.
// We need to run this code on the server only, otherwise the incrementation
// will be done twice.
- Users.after.update(function (userId, user, fieldNames) {
+ Users.after.update(function(userId, user, fieldNames) {
// The `starredBoards` list is hosted on the `profile` field. If this
// field hasn't been modificated we don't need to run this hook.
- if (!_.contains(fieldNames, 'profile'))
- return;
+ if (!_.contains(fieldNames, 'profile')) return;
// To calculate a diff of board starred ids, we get both the previous
// and the newly board ids list
@@ -732,7 +787,7 @@ if (Meteor.isServer) {
// direction and then in the other.
function incrementBoards(boardsIds, inc) {
boardsIds.forEach((boardId) => {
- Boards.update(boardId, {$inc: {stars: inc}});
+ Boards.update(boardId, { $inc: { stars: inc } });
});
}
@@ -754,7 +809,7 @@ if (Meteor.isServer) {
};
fakeUserId.withValue(doc._id, () => {
- /*
+ /*
// Insert the Welcome Board
Boards.insert({
title: TAPi18n.__('welcome-board'),
@@ -773,57 +828,76 @@ if (Meteor.isServer) {
});
*/
- Boards.insert({
- title: TAPi18n.__('templates'),
- permission: 'private',
- type: 'template-container',
- }, fakeUser, (err, boardId) => {
-
- // Insert the reference to our templates board
- Users.update(fakeUserId.get(), {$set: {'profile.templatesBoardId': boardId}});
-
- // Insert the card templates swimlane
- Swimlanes.insert({
- title: TAPi18n.__('card-templates-swimlane'),
- boardId,
- sort: 1,
- type: 'template-container',
- }, fakeUser, (err, swimlaneId) => {
-
- // Insert the reference to out card templates swimlane
- Users.update(fakeUserId.get(), {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}});
- });
-
- // Insert the list templates swimlane
- Swimlanes.insert({
- title: TAPi18n.__('list-templates-swimlane'),
- boardId,
- sort: 2,
- type: 'template-container',
- }, fakeUser, (err, swimlaneId) => {
-
- // Insert the reference to out list templates swimlane
- Users.update(fakeUserId.get(), {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}});
- });
-
- // Insert the board templates swimlane
- Swimlanes.insert({
- title: TAPi18n.__('board-templates-swimlane'),
- boardId,
- sort: 3,
+ Boards.insert(
+ {
+ title: TAPi18n.__('templates'),
+ permission: 'private',
type: 'template-container',
- }, fakeUser, (err, swimlaneId) => {
-
- // Insert the reference to out board templates swimlane
- Users.update(fakeUserId.get(), {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}});
- });
- });
+ },
+ fakeUser,
+ (err, boardId) => {
+ // Insert the reference to our templates board
+ Users.update(fakeUserId.get(), {
+ $set: { 'profile.templatesBoardId': boardId },
+ });
+
+ // Insert the card templates swimlane
+ Swimlanes.insert(
+ {
+ title: TAPi18n.__('card-templates-swimlane'),
+ boardId,
+ sort: 1,
+ type: 'template-container',
+ },
+ fakeUser,
+ (err, swimlaneId) => {
+ // Insert the reference to out card templates swimlane
+ Users.update(fakeUserId.get(), {
+ $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
+ });
+ }
+ );
+
+ // Insert the list templates swimlane
+ Swimlanes.insert(
+ {
+ title: TAPi18n.__('list-templates-swimlane'),
+ boardId,
+ sort: 2,
+ type: 'template-container',
+ },
+ fakeUser,
+ (err, swimlaneId) => {
+ // Insert the reference to out list templates swimlane
+ Users.update(fakeUserId.get(), {
+ $set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
+ });
+ }
+ );
+
+ // Insert the board templates swimlane
+ Swimlanes.insert(
+ {
+ title: TAPi18n.__('board-templates-swimlane'),
+ boardId,
+ sort: 3,
+ type: 'template-container',
+ },
+ fakeUser,
+ (err, swimlaneId) => {
+ // Insert the reference to out board templates swimlane
+ Users.update(fakeUserId.get(), {
+ $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
+ });
+ }
+ );
+ }
+ );
});
});
}
Users.after.insert((userId, doc) => {
-
if (doc.createdThroughApi) {
// The admin user should be able to create a user despite disabling registration because
// it is two different things (registration and creation).
@@ -831,7 +905,7 @@ if (Meteor.isServer) {
// the disableRegistration check.
// Issue : https://github.com/wekan/wekan/issues/1232
// PR : https://github.com/wekan/wekan/pull/1251
- Users.update(doc._id, {$set: {createdThroughApi: ''}});
+ Users.update(doc._id, { $set: { createdThroughApi: '' } });
return;
}
@@ -840,7 +914,10 @@ if (Meteor.isServer) {
// If ldap, bypass the inviation code if the self registration isn't allowed.
// TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type
if (doc.authenticationMethod !== 'ldap' && disableRegistration) {
- const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true});
+ const invitationCode = InvitationCodes.findOne({
+ code: doc.profile.icode,
+ valid: true,
+ });
if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist');
} else {
@@ -852,8 +929,8 @@ if (Meteor.isServer) {
doc.profile = {};
}
doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
- Users.update(doc._id, {$set: {profile: doc.profile}});
- InvitationCodes.update(invitationCode._id, {$set: {valid: false}});
+ Users.update(doc._id, { $set: { profile: doc.profile } });
+ InvitationCodes.update(invitationCode._id, { $set: { valid: false } });
}
}
});
@@ -862,13 +939,12 @@ if (Meteor.isServer) {
// USERS REST API
if (Meteor.isServer) {
// Middleware which checks that API is enabled.
- JsonRoutes.Middleware.use(function (req, res, next) {
+ JsonRoutes.Middleware.use(function(req, res, next) {
const api = req.url.search('api');
- if (api === 1 && process.env.WITH_API === 'true' || api === -1){
+ if ((api === 1 && process.env.WITH_API === 'true') || api === -1) {
return next();
- }
- else {
- res.writeHead(301, {Location: '/'});
+ } else {
+ res.writeHead(301, { Location: '/' });
return res.end();
}
});
@@ -882,14 +958,13 @@ if (Meteor.isServer) {
JsonRoutes.add('GET', '/api/user', function(req, res) {
try {
Authentication.checkLoggedIn(req.userId);
- const data = Meteor.users.findOne({ _id: req.userId});
+ const data = Meteor.users.findOne({ _id: req.userId });
delete data.services;
JsonRoutes.sendResult(res, {
code: 200,
data,
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -906,17 +981,16 @@ if (Meteor.isServer) {
* @return_type [{ _id: string,
* username: string}]
*/
- JsonRoutes.add('GET', '/api/users', function (req, res) {
+ JsonRoutes.add('GET', '/api/users', function(req, res) {
try {
Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Meteor.users.find({}).map(function (doc) {
+ data: Meteor.users.find({}).map(function(doc) {
return { _id: doc._id, username: doc.username };
}),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -934,7 +1008,7 @@ if (Meteor.isServer) {
* @param {string} userId the user ID
* @return_type Users
*/
- JsonRoutes.add('GET', '/api/users/:userId', function (req, res) {
+ JsonRoutes.add('GET', '/api/users/:userId', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const id = req.params.userId;
@@ -942,8 +1016,7 @@ if (Meteor.isServer) {
code: 200,
data: Meteor.users.findOne({ _id: id }),
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -968,7 +1041,7 @@ if (Meteor.isServer) {
* @return_type {_id: string,
* title: string}
*/
- JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) {
+ JsonRoutes.add('PUT', '/api/users/:userId', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const id = req.params.userId;
@@ -990,8 +1063,16 @@ if (Meteor.isServer) {
};
});
} else {
- if ((action === 'disableLogin') && (id !== req.userId)) {
- Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } });
+ if (action === 'disableLogin' && id !== req.userId) {
+ Users.update(
+ { _id: id },
+ {
+ $set: {
+ loginDisabled: true,
+ 'services.resume.loginTokens': '',
+ },
+ }
+ );
} else if (action === 'enableLogin') {
Users.update({ _id: id }, { $set: { loginDisabled: '' } });
}
@@ -1002,8 +1083,7 @@ if (Meteor.isServer) {
code: 200,
data,
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1030,13 +1110,16 @@ if (Meteor.isServer) {
* @return_type {_id: string,
* title: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function (req, res) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function(
+ req,
+ res
+ ) {
try {
Authentication.checkUserId(req.userId);
const userId = req.params.userId;
const boardId = req.params.boardId;
const action = req.body.action;
- const {isAdmin, isNoComments, isCommentOnly} = req.body;
+ const { isAdmin, isNoComments, isCommentOnly } = req.body;
let data = Meteor.users.findOne({ _id: userId });
if (data !== undefined) {
if (action === 'add') {
@@ -1045,10 +1128,16 @@ if (Meteor.isServer) {
}).map(function(board) {
if (!board.hasMember(userId)) {
board.addMember(userId);
- function isTrue(data){
+ function isTrue(data) {
return data.toLowerCase() === 'true';
}
- board.setMemberPermission(userId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), userId);
+ board.setMemberPermission(
+ userId,
+ isTrue(isAdmin),
+ isTrue(isNoComments),
+ isTrue(isCommentOnly),
+ userId
+ );
}
return {
_id: board._id,
@@ -1061,8 +1150,7 @@ if (Meteor.isServer) {
code: 200,
data: query,
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1084,40 +1172,43 @@ if (Meteor.isServer) {
* @return_type {_id: string,
* title: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/remove', function (req, res) {
- try {
- Authentication.checkUserId(req.userId);
- const userId = req.params.userId;
- const boardId = req.params.boardId;
- const action = req.body.action;
- let data = Meteor.users.findOne({ _id: userId });
- if (data !== undefined) {
- if (action === 'remove') {
- data = Boards.find({
- _id: boardId,
- }).map(function(board) {
- if (board.hasMember(userId)) {
- board.removeMember(userId);
- }
- return {
- _id: board._id,
- title: board.title,
- };
- });
+ JsonRoutes.add(
+ 'POST',
+ '/api/boards/:boardId/members/:userId/remove',
+ function(req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const userId = req.params.userId;
+ const boardId = req.params.boardId;
+ const action = req.body.action;
+ let data = Meteor.users.findOne({ _id: userId });
+ if (data !== undefined) {
+ if (action === 'remove') {
+ data = Boards.find({
+ _id: boardId,
+ }).map(function(board) {
+ if (board.hasMember(userId)) {
+ board.removeMember(userId);
+ }
+ return {
+ _id: board._id,
+ title: board.title,
+ };
+ });
+ }
}
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: query,
+ });
+ } catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
}
- JsonRoutes.sendResult(res, {
- code: 200,
- data: query,
- });
}
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
- });
+ );
/**
* @operation new_user
@@ -1131,7 +1222,7 @@ if (Meteor.isServer) {
* @param {string} password the password of the new user
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/users/', function (req, res) {
+ JsonRoutes.add('POST', '/api/users/', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const id = Accounts.createUser({
@@ -1146,8 +1237,7 @@ if (Meteor.isServer) {
_id: id,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1165,7 +1255,7 @@ if (Meteor.isServer) {
* @param {string} userId the ID of the user to delete
* @return_type {_id: string}
*/
- JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) {
+ JsonRoutes.add('DELETE', '/api/users/:userId', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const id = req.params.userId;
@@ -1176,8 +1266,7 @@ if (Meteor.isServer) {
_id: id,
},
});
- }
- catch (error) {
+ } catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@@ -1185,3 +1274,5 @@ if (Meteor.isServer) {
}
});
}
+
+export default Users;
diff --git a/packages/wekan-iframe b/packages/wekan-iframe
new file mode 160000
+Subproject e105dcc9c3424beee0ff0a9db9ca543a6d4b7f8
diff --git a/packages/wekan_accounts-oidc/.gitignore b/packages/wekan_accounts-oidc/.gitignore
new file mode 100644
index 00000000..5379d4c3
--- /dev/null
+++ b/packages/wekan_accounts-oidc/.gitignore
@@ -0,0 +1 @@
+.versions
diff --git a/packages/wekan_accounts-oidc/LICENSE.txt b/packages/wekan_accounts-oidc/LICENSE.txt
new file mode 100644
index 00000000..c7be3264
--- /dev/null
+++ b/packages/wekan_accounts-oidc/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright (C) 2016 SWITCH
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
diff --git a/packages/wekan_accounts-oidc/README.md b/packages/wekan_accounts-oidc/README.md
new file mode 100644
index 00000000..ce0b5738
--- /dev/null
+++ b/packages/wekan_accounts-oidc/README.md
@@ -0,0 +1,75 @@
+# salleman:accounts-oidc package
+
+A Meteor login service for OpenID Connect (OIDC).
+
+## Installation
+
+ meteor add salleman:accounts-oidc
+
+## Usage
+
+`Meteor.loginWithOidc(options, callback)`
+* `options` - object containing options, see below (optional)
+* `callback` - callback function (optional)
+
+#### Example
+
+```js
+Template.myTemplateName.events({
+ 'click #login-button': function() {
+ Meteor.loginWithOidc();
+ }
+);
+```
+
+
+## Options
+
+These options override service configuration stored in the database.
+
+* `loginStyle`: `redirect` or `popup`
+* `redirectUrl`: Where to redirect after successful login. Only used if `loginStyle` is set to `redirect`
+
+## Manual Configuration Setup
+
+You can manually configure this package by upserting the service configuration on startup. First, add the `service-configuration` package:
+
+ meteor add service-configuration
+
+### Service Configuration
+
+The following service configuration are available:
+
+* `clientId`: OIDC client identifier
+* `secret`: OIDC client shared secret
+* `serverUrl`: URL of the OIDC server. e.g. `https://openid.example.org:8443`
+* `authorizationEndpoint`: Endpoint of the OIDC authorization service, e.g. `/oidc/authorize`
+* `tokenEndpoint`: Endpoint of the OIDC token service, e.g. `/oidc/token`
+* `userinfoEndpoint`: Endpoint of the OIDC userinfo service, e.g. `/oidc/userinfo`
+* `idTokenWhitelistFields`: A list of fields from IDToken to be added to Meteor.user().services.oidc object
+
+### Project Configuration
+
+Then in your project:
+
+```js
+if (Meteor.isServer) {
+ Meteor.startup(function () {
+ ServiceConfiguration.configurations.upsert(
+ { service: 'oidc' },
+ {
+ $set: {
+ loginStyle: 'redirect',
+ clientId: 'my-client-id-registered-with-the-oidc-server',
+ secret: 'my-client-shared-secret',
+ serverUrl: 'https://openid.example.org',
+ authorizationEndpoint: '/oidc/authorize',
+ tokenEndpoint: '/oidc/token',
+ userinfoEndpoint: '/oidc/userinfo',
+ idTokenWhitelistFields: []
+ }
+ }
+ );
+ });
+}
+```
diff --git a/packages/wekan_accounts-oidc/oidc.js b/packages/wekan_accounts-oidc/oidc.js
new file mode 100644
index 00000000..75cd89ae
--- /dev/null
+++ b/packages/wekan_accounts-oidc/oidc.js
@@ -0,0 +1,22 @@
+Accounts.oauth.registerService('oidc');
+
+if (Meteor.isClient) {
+ Meteor.loginWithOidc = function(options, callback) {
+ // support a callback without options
+ if (! callback && typeof options === "function") {
+ callback = options;
+ options = null;
+ }
+
+ var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
+ Oidc.requestCredential(options, credentialRequestCompleteCallback);
+ };
+} else {
+ Accounts.addAutopublishFields({
+ // not sure whether the OIDC api can be used from the browser,
+ // thus not sure if we should be sending access tokens; but we do it
+ // for all other oauth2 providers, and it may come in handy.
+ forLoggedInUser: ['services.oidc'],
+ forOtherUsers: ['services.oidc.id']
+ });
+}
diff --git a/packages/wekan_accounts-oidc/oidc_login_button.css b/packages/wekan_accounts-oidc/oidc_login_button.css
new file mode 100644
index 00000000..da42120b
--- /dev/null
+++ b/packages/wekan_accounts-oidc/oidc_login_button.css
@@ -0,0 +1,3 @@
+#login-buttons-image-oidc {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACQUlEQVQ4T5WTy2taQRTGv4kSXw0IoYIihCjFmhhfhUqW9a+o0I2LInTRRbtw05V2I9KQuuimi24KXQqChIhQQcQgGGNz0YpvMCG1yL1tGqvBZsKMIIXcjQcOcznnfL+ZOecOEUVx4/Ly8mQ6neqxhKlUKmltbc1Nut2uqJ/bEnJAkiTmEhEEgVqtViiVyjuAP70j/Pj6Htbglzu52WyGdrsNUq1Wqc1mk939+9sHPP7wTVM232g0QMrlMrXb7bIFndgcbAk3ZPP1eh2kVCrRra2tRcFoNEK1WoXf78fg3Rxsfl3H3t4e3G43dnd3wWrMZjNqtRpIsVhcAFKpFPL5PBfF43H8TDj49/2XAvb393F2dgaNRgNKKaLR6ByQz+epw+HAwcEBisUijEYjgsEg1Go1pA9ODtC/+MZFDCKKIo9FIhEIggCSy+Xozs4OYrEY2ChDoRAIIVww/ujhxdrnFTSbTX6Cfr+Pi4sLhMNhnJ6egmSzWepyuZBIJGAwGBAIBLiY2ezTI74qg2UoFIqFr6ys4OrqiveKHB4eckAmk8FgMMD29jZ8Ph8XKj4/5uu/ZyXZKXBAOp2mHo+H/0isD6zDOp0Om5ubsAuvcA+/8ffpkSygUqmApFIp6vV6+b2ZsNfrodVqYTgcwqXtwul04pfhiSzg+PgYJJlMUovFwhvIbHV1lTs70c3NDSaTCa6vr+8A2FvodDr8CmwuepPJtIDIbvdfkInPz89ZRCKFQmFjNBqdjMfjpZ6jVquV1tfX3bcYegI7CyIWlgAAAABJRU5ErkJggg==');
+}
diff --git a/packages/wekan_accounts-oidc/package.js b/packages/wekan_accounts-oidc/package.js
new file mode 100644
index 00000000..251fb265
--- /dev/null
+++ b/packages/wekan_accounts-oidc/package.js
@@ -0,0 +1,19 @@
+Package.describe({
+ summary: "OpenID Connect (OIDC) for Meteor accounts",
+ version: "1.0.10",
+ name: "wekan-accounts-oidc",
+ git: "https://github.com/wekan/meteor-accounts-oidc.git",
+
+});
+
+Package.onUse(function(api) {
+ api.use('accounts-base@1.2.0', ['client', 'server']);
+ // Export Accounts (etc) to packages using this one.
+ api.imply('accounts-base', ['client', 'server']);
+ api.use('accounts-oauth@1.1.0', ['client', 'server']);
+ api.use('wekan-oidc@1.0.10', ['client', 'server']);
+
+ api.addFiles('oidc_login_button.css', 'client');
+
+ api.addFiles('oidc.js');
+});
diff --git a/packages/wekan_oidc/.gitignore b/packages/wekan_oidc/.gitignore
new file mode 100644
index 00000000..5379d4c3
--- /dev/null
+++ b/packages/wekan_oidc/.gitignore
@@ -0,0 +1 @@
+.versions
diff --git a/packages/wekan_oidc/LICENSE.txt b/packages/wekan_oidc/LICENSE.txt
new file mode 100644
index 00000000..c7be3264
--- /dev/null
+++ b/packages/wekan_oidc/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright (C) 2016 SWITCH
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
diff --git a/packages/wekan_oidc/README.md b/packages/wekan_oidc/README.md
new file mode 100644
index 00000000..8948971c
--- /dev/null
+++ b/packages/wekan_oidc/README.md
@@ -0,0 +1,7 @@
+# salleman:oidc package
+
+A Meteor implementation of OpenID Connect Login flow
+
+## Usage and Documentation
+
+Look at the `salleman:accounts-oidc` package for the documentation about using OpenID Connect with Meteor.
diff --git a/packages/wekan_oidc/oidc_client.js b/packages/wekan_oidc/oidc_client.js
new file mode 100644
index 00000000..744bd841
--- /dev/null
+++ b/packages/wekan_oidc/oidc_client.js
@@ -0,0 +1,68 @@
+Oidc = {};
+
+// Request OpenID Connect credentials for the user
+// @param options {optional}
+// @param credentialRequestCompleteCallback {Function} Callback function to call on
+// completion. Takes one argument, credentialToken on success, or Error on
+// error.
+Oidc.requestCredential = function (options, credentialRequestCompleteCallback) {
+ // support both (options, callback) and (callback).
+ if (!credentialRequestCompleteCallback && typeof options === 'function') {
+ credentialRequestCompleteCallback = options;
+ options = {};
+ }
+
+ var config = ServiceConfiguration.configurations.findOne({service: 'oidc'});
+ if (!config) {
+ credentialRequestCompleteCallback && credentialRequestCompleteCallback(
+ new ServiceConfiguration.ConfigError('Service oidc not configured.'));
+ return;
+ }
+
+ var credentialToken = Random.secret();
+ var loginStyle = OAuth._loginStyle('oidc', config, options);
+ var scope = config.requestPermissions || ['openid', 'profile', 'email'];
+
+ // options
+ options = options || {};
+ options.client_id = config.clientId;
+ options.response_type = options.response_type || 'code';
+ options.redirect_uri = OAuth._redirectUri('oidc', config);
+ options.state = OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl);
+ options.scope = scope.join(' ');
+
+ if (config.loginStyle && config.loginStyle == 'popup') {
+ options.display = 'popup';
+ }
+
+ var loginUrl = config.serverUrl + config.authorizationEndpoint;
+ // check if the loginUrl already contains a "?"
+ var first = loginUrl.indexOf('?') === -1;
+ for (var k in options) {
+ if (first) {
+ loginUrl += '?';
+ first = false;
+ }
+ else {
+ loginUrl += '&'
+ }
+ loginUrl += encodeURIComponent(k) + '=' + encodeURIComponent(options[k]);
+ }
+
+ //console.log('XXX: loginURL: ' + loginUrl)
+
+ options.popupOptions = options.popupOptions || {};
+ var popupOptions = {
+ width: options.popupOptions.width || 320,
+ height: options.popupOptions.height || 450
+ };
+
+ OAuth.launchLogin({
+ loginService: 'oidc',
+ loginStyle: loginStyle,
+ loginUrl: loginUrl,
+ credentialRequestCompleteCallback: credentialRequestCompleteCallback,
+ credentialToken: credentialToken,
+ popupOptions: popupOptions,
+ });
+};
diff --git a/packages/wekan_oidc/oidc_configure.html b/packages/wekan_oidc/oidc_configure.html
new file mode 100644
index 00000000..49282fc1
--- /dev/null
+++ b/packages/wekan_oidc/oidc_configure.html
@@ -0,0 +1,6 @@
+<template name="configureLoginServiceDialogForOidc">
+ <p>
+ You'll need to create an OpenID Connect client configuration with your provider.
+ Set App Callbacks URLs to: <span class="url">{{siteUrl}}_oauth/oidc</span>
+ </p>
+</template>
diff --git a/packages/wekan_oidc/oidc_configure.js b/packages/wekan_oidc/oidc_configure.js
new file mode 100644
index 00000000..5eedaa04
--- /dev/null
+++ b/packages/wekan_oidc/oidc_configure.js
@@ -0,0 +1,17 @@
+Template.configureLoginServiceDialogForOidc.helpers({
+ siteUrl: function () {
+ return Meteor.absoluteUrl();
+ }
+});
+
+Template.configureLoginServiceDialogForOidc.fields = function () {
+ return [
+ { property: 'clientId', label: 'Client ID'},
+ { property: 'secret', label: 'Client Secret'},
+ { property: 'serverUrl', label: 'OIDC Server URL'},
+ { property: 'authorizationEndpoint', label: 'Authorization Endpoint'},
+ { property: 'tokenEndpoint', label: 'Token Endpoint'},
+ { property: 'userinfoEndpoint', label: 'Userinfo Endpoint'},
+ { property: 'idTokenWhitelistFields', label: 'Id Token Fields'}
+ ];
+};
diff --git a/packages/wekan_oidc/oidc_server.js b/packages/wekan_oidc/oidc_server.js
new file mode 100644
index 00000000..fb948c52
--- /dev/null
+++ b/packages/wekan_oidc/oidc_server.js
@@ -0,0 +1,143 @@
+Oidc = {};
+
+OAuth.registerService('oidc', 2, null, function (query) {
+
+ var debug = process.env.DEBUG || false;
+ var token = getToken(query);
+ if (debug) console.log('XXX: register token:', token);
+
+ var accessToken = token.access_token || token.id_token;
+ var expiresAt = (+new Date) + (1000 * parseInt(token.expires_in, 10));
+
+ var userinfo = getUserInfo(accessToken);
+ if (debug) console.log('XXX: userinfo:', userinfo);
+
+ var serviceData = {};
+ serviceData.id = userinfo[process.env.OAUTH2_ID_MAP] || userinfo[id];
+ serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP] || userinfo[uid];
+ serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP] || userinfo[displayName];
+ serviceData.accessToken = accessToken;
+ serviceData.expiresAt = expiresAt;
+ serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP] || userinfo[email];
+
+ if (accessToken) {
+ var tokenContent = getTokenContent(accessToken);
+ var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields);
+ _.extend(serviceData, fields);
+ }
+
+ if (token.refresh_token)
+ serviceData.refreshToken = token.refresh_token;
+ if (debug) console.log('XXX: serviceData:', serviceData);
+
+ var profile = {};
+ profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP] || userinfo[displayName];
+ profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP] || userinfo[email];
+ if (debug) console.log('XXX: profile:', profile);
+
+ return {
+ serviceData: serviceData,
+ options: { profile: profile }
+ };
+});
+
+var userAgent = "Meteor";
+if (Meteor.release) {
+ userAgent += "/" + Meteor.release;
+}
+
+var getToken = function (query) {
+ var debug = process.env.DEBUG || false;
+ var config = getConfiguration();
+ var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
+ var response;
+
+ try {
+ response = HTTP.post(
+ serverTokenEndpoint,
+ {
+ headers: {
+ Accept: 'application/json',
+ "User-Agent": userAgent
+ },
+ params: {
+ code: query.code,
+ client_id: config.clientId,
+ client_secret: OAuth.openSecret(config.secret),
+ redirect_uri: OAuth._redirectUri('oidc', config),
+ grant_type: 'authorization_code',
+ state: query.state
+ }
+ }
+ );
+ } catch (err) {
+ throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
+ { response: err.response });
+ }
+ if (response.data.error) {
+ // if the http response was a json object with an error attribute
+ throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
+ } else {
+ if (debug) console.log('XXX: getToken response: ', response.data);
+ return response.data;
+ }
+};
+
+var getUserInfo = function (accessToken) {
+ var debug = process.env.DEBUG || false;
+ var config = getConfiguration();
+ // Some userinfo endpoints use a different base URL than the authorization or token endpoints.
+ // This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
+ if (config.userinfoEndpoint.includes("https://")) {
+ var serverUserinfoEndpoint = config.userinfoEndpoint;
+ } else {
+ var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
+ }
+ var response;
+ try {
+ response = HTTP.get(
+ serverUserinfoEndpoint,
+ {
+ headers: {
+ "User-Agent": userAgent,
+ "Authorization": "Bearer " + accessToken
+ }
+ }
+ );
+ } catch (err) {
+ throw _.extend(new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + err.message),
+ {response: err.response});
+ }
+ if (debug) console.log('XXX: getUserInfo response: ', response.data);
+ return response.data;
+};
+
+var getConfiguration = function () {
+ var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
+ if (!config) {
+ throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
+ }
+ return config;
+};
+
+var getTokenContent = function (token) {
+ var content = null;
+ if (token) {
+ try {
+ var parts = token.split('.');
+ var header = JSON.parse(new Buffer(parts[0], 'base64').toString());
+ content = JSON.parse(new Buffer(parts[1], 'base64').toString());
+ var signature = new Buffer(parts[2], 'base64');
+ var signed = parts[0] + '.' + parts[1];
+ } catch (err) {
+ this.content = {
+ exp: 0
+ };
+ }
+ }
+ return content;
+}
+
+Oidc.retrieveCredential = function (credentialToken, credentialSecret) {
+ return OAuth.retrieveCredential(credentialToken, credentialSecret);
+};
diff --git a/packages/wekan_oidc/package.js b/packages/wekan_oidc/package.js
new file mode 100644
index 00000000..faf4a68d
--- /dev/null
+++ b/packages/wekan_oidc/package.js
@@ -0,0 +1,23 @@
+Package.describe({
+ summary: "OpenID Connect (OIDC) flow for Meteor",
+ version: "1.0.12",
+ name: "wekan-oidc",
+ git: "https://github.com/wekan/meteor-accounts-oidc.git",
+});
+
+Package.onUse(function(api) {
+ api.use('oauth2@1.1.0', ['client', 'server']);
+ api.use('oauth@1.1.0', ['client', 'server']);
+ api.use('http@1.1.0', ['server']);
+ api.use('underscore@1.0.0', 'client');
+ api.use('templating@1.1.0', 'client');
+ api.use('random@1.0.0', 'client');
+ api.use('service-configuration@1.0.0', ['client', 'server']);
+
+ api.export('Oidc');
+
+ api.addFiles(['oidc_configure.html', 'oidc_configure.js'], 'client');
+
+ api.addFiles('oidc_server.js', 'server');
+ api.addFiles('oidc_client.js', 'client');
+});
diff --git a/server/migrations.js b/server/migrations.js
index 09852495..eefda9c8 100644
--- a/server/migrations.js
+++ b/server/migrations.js
@@ -1,3 +1,23 @@
+import AccountSettings from '../models/accountSettings';
+import Actions from '../models/actions';
+import Activities from '../models/activities';
+import Announcements from '../models/announcements';
+import Boards from '../models/boards';
+import CardComments from '../models/cardComments';
+import Cards from '../models/cards';
+import ChecklistItems from '../models/checklistItems';
+import Checklists from '../models/checklists';
+import CustomFields from '../models/customFields';
+import Integrations from '../models/integrations';
+import InvitationCodes from '../models/invitationCodes';
+import Lists from '../models/lists';
+import Rules from '../models/rules';
+import Settings from '../models/settings';
+import Swimlanes from '../models/swimlanes';
+import Triggers from '../models/triggers';
+import UnsavedEdits from '../models/unsavedEdits';
+import Users from '../models/users';
+
// Anytime you change the schema of one of the collection in a non-backward
// compatible way you have to write a migration in this file using the following
// API:
@@ -28,18 +48,22 @@ const noValidateMulti = { ...noValidate, multi: true };
Migrations.add('board-background-color', () => {
const defaultColor = '#16A085';
- Boards.update({
- background: {
- $exists: false,
- },
- }, {
- $set: {
+ Boards.update(
+ {
background: {
- type: 'color',
- color: defaultColor,
+ $exists: false,
},
},
- }, noValidateMulti);
+ {
+ $set: {
+ background: {
+ type: 'color',
+ color: defaultColor,
+ },
+ },
+ },
+ noValidateMulti
+ );
});
Migrations.add('lowercase-board-permission', () => {
@@ -57,24 +81,28 @@ Migrations.add('change-attachments-type-for-non-images', () => {
const newTypeForNonImage = 'application/octet-stream';
Attachments.find().forEach((file) => {
if (!file.isImage()) {
- Attachments.update(file._id, {
- $set: {
- 'original.type': newTypeForNonImage,
- 'copies.attachments.type': newTypeForNonImage,
+ Attachments.update(
+ file._id,
+ {
+ $set: {
+ 'original.type': newTypeForNonImage,
+ 'copies.attachments.type': newTypeForNonImage,
+ },
},
- }, noValidate);
+ noValidate
+ );
}
});
});
Migrations.add('card-covers', () => {
Cards.find().forEach((card) => {
- const cover = Attachments.findOne({ cardId: card._id, cover: true });
+ const cover = Attachments.findOne({ cardId: card._id, cover: true });
if (cover) {
- Cards.update(card._id, {$set: {coverId: cover._id}}, noValidate);
+ Cards.update(card._id, { $set: { coverId: cover._id } }, noValidate);
}
});
- Attachments.update({}, {$unset: {cover: ''}}, noValidateMulti);
+ Attachments.update({}, { $unset: { cover: '' } }, noValidateMulti);
});
Migrations.add('use-css-class-for-boards-colors', () => {
@@ -89,26 +117,31 @@ Migrations.add('use-css-class-for-boards-colors', () => {
Boards.find().forEach((board) => {
const oldBoardColor = board.background.color;
const newBoardColor = associationTable[oldBoardColor];
- Boards.update(board._id, {
- $set: { color: newBoardColor },
- $unset: { background: '' },
- }, noValidate);
+ Boards.update(
+ board._id,
+ {
+ $set: { color: newBoardColor },
+ $unset: { background: '' },
+ },
+ noValidate
+ );
});
});
Migrations.add('denormalize-star-number-per-board', () => {
Boards.find().forEach((board) => {
- const nStars = Users.find({'profile.starredBoards': board._id}).count();
- Boards.update(board._id, {$set: {stars: nStars}}, noValidate);
+ const nStars = Users.find({ 'profile.starredBoards': board._id }).count();
+ Boards.update(board._id, { $set: { stars: nStars } }, noValidate);
});
});
// We want to keep a trace of former members so we can efficiently publish their
// infos in the general board publication.
Migrations.add('add-member-isactive-field', () => {
- Boards.find({}, {fields: {members: 1}}).forEach((board) => {
+ Boards.find({}, { fields: { members: 1 } }).forEach((board) => {
const allUsersWithSomeActivity = _.chain(
- Activities.find({ boardId: board._id }, { fields:{ userId:1 }}).fetch())
+ Activities.find({ boardId: board._id }, { fields: { userId: 1 } }).fetch()
+ )
.pluck('userId')
.uniq()
.value();
@@ -127,7 +160,7 @@ Migrations.add('add-member-isactive-field', () => {
isActive: false,
});
});
- Boards.update(board._id, {$set: {members: newMemberSet}}, noValidate);
+ Boards.update(board._id, { $set: { members: newMemberSet } }, noValidate);
});
});
@@ -184,7 +217,7 @@ Migrations.add('add-checklist-items', () => {
// Create new items
_.sortBy(checklist.items, 'sort').forEach((item, index) => {
ChecklistItems.direct.insert({
- title: (item.title ? item.title : 'Checklist'),
+ title: item.title ? item.title : 'Checklist',
sort: index,
isFinished: item.isFinished,
checklistId: checklist._id,
@@ -193,8 +226,9 @@ Migrations.add('add-checklist-items', () => {
});
// Delete old ones
- Checklists.direct.update({ _id: checklist._id },
- { $unset: { items : 1 } },
+ Checklists.direct.update(
+ { _id: checklist._id },
+ { $unset: { items: 1 } },
noValidate
);
});
@@ -217,324 +251,512 @@ Migrations.add('add-card-types', () => {
Cards.find().forEach((card) => {
Cards.direct.update(
{ _id: card._id },
- { $set: {
- type: 'cardType-card',
- linkedId: null } },
+ {
+ $set: {
+ type: 'cardType-card',
+ linkedId: null,
+ },
+ },
noValidate
);
});
});
Migrations.add('add-custom-fields-to-cards', () => {
- Cards.update({
- customFields: {
- $exists: false,
+ Cards.update(
+ {
+ customFields: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- customFields:[],
+ {
+ $set: {
+ customFields: [],
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-requester-field', () => {
- Cards.update({
- requestedBy: {
- $exists: false,
+ Cards.update(
+ {
+ requestedBy: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- requestedBy:'',
+ {
+ $set: {
+ requestedBy: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-assigner-field', () => {
- Cards.update({
- assignedBy: {
- $exists: false,
+ Cards.update(
+ {
+ assignedBy: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- assignedBy:'',
+ {
+ $set: {
+ assignedBy: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-parent-field-to-cards', () => {
- Cards.update({
- parentId: {
- $exists: false,
+ Cards.update(
+ {
+ parentId: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- parentId:'',
+ {
+ $set: {
+ parentId: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-subtasks-boards', () => {
- Boards.update({
- subtasksDefaultBoardId: {
- $exists: false,
+ Boards.update(
+ {
+ subtasksDefaultBoardId: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- subtasksDefaultBoardId: null,
- subtasksDefaultListId: null,
+ {
+ $set: {
+ subtasksDefaultBoardId: null,
+ subtasksDefaultListId: null,
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-subtasks-sort', () => {
- Boards.update({
- subtaskSort: {
- $exists: false,
+ Boards.update(
+ {
+ subtaskSort: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- subtaskSort: -1,
+ {
+ $set: {
+ subtaskSort: -1,
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-subtasks-allowed', () => {
- Boards.update({
- allowsSubtasks: {
- $exists: false,
+ Boards.update(
+ {
+ allowsSubtasks: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- allowsSubtasks: true,
+ {
+ $set: {
+ allowsSubtasks: true,
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-subtasks-allowed', () => {
- Boards.update({
- presentParentTask: {
- $exists: false,
+ Boards.update(
+ {
+ presentParentTask: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- presentParentTask: 'no-parent',
+ {
+ $set: {
+ presentParentTask: 'no-parent',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-authenticationMethod', () => {
- Users.update({
- 'authenticationMethod': {
- $exists: false,
+ Users.update(
+ {
+ authenticationMethod: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- 'authenticationMethod': 'password',
+ {
+ $set: {
+ authenticationMethod: 'password',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('remove-tag', () => {
- Users.update({
- }, {
- $unset: {
- 'profile.tags':1,
+ Users.update(
+ {},
+ {
+ $unset: {
+ 'profile.tags': 1,
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('remove-customFields-references-broken', () => {
- Cards.update({'customFields.$value': null},
- { $pull: {
- customFields: {value: null},
+ Cards.update(
+ { 'customFields.$value': null },
+ {
+ $pull: {
+ customFields: { value: null },
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-product-name', () => {
- Settings.update({
- productName: {
- $exists: false,
+ Settings.update(
+ {
+ productName: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- productName:'',
+ {
+ $set: {
+ productName: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-hide-logo', () => {
- Settings.update({
- hideLogo: {
- $exists: false,
+ Settings.update(
+ {
+ hideLogo: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- hideLogo: false,
+ {
+ $set: {
+ hideLogo: false,
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-custom-html-after-body-start', () => {
- Settings.update({
- customHTMLafterBodyStart: {
- $exists: false,
+ Settings.update(
+ {
+ customHTMLafterBodyStart: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- customHTMLafterBodyStart:'',
+ {
+ $set: {
+ customHTMLafterBodyStart: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-custom-html-before-body-end', () => {
- Settings.update({
- customHTMLbeforeBodyEnd: {
- $exists: false,
+ Settings.update(
+ {
+ customHTMLbeforeBodyEnd: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- customHTMLbeforeBodyEnd:'',
+ {
+ $set: {
+ customHTMLbeforeBodyEnd: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-displayAuthenticationMethod', () => {
- Settings.update({
- displayAuthenticationMethod: {
- $exists: false,
+ Settings.update(
+ {
+ displayAuthenticationMethod: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- displayAuthenticationMethod: true,
+ {
+ $set: {
+ displayAuthenticationMethod: true,
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-defaultAuthenticationMethod', () => {
- Settings.update({
- defaultAuthenticationMethod: {
- $exists: false,
+ Settings.update(
+ {
+ defaultAuthenticationMethod: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- defaultAuthenticationMethod: 'password',
+ {
+ $set: {
+ defaultAuthenticationMethod: 'password',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
Migrations.add('add-templates', () => {
- Boards.update({
- type: {
- $exists: false,
- },
- }, {
- $set: {
- type: 'board',
+ Boards.update(
+ {
+ type: {
+ $exists: false,
+ },
},
- }, noValidateMulti);
- Swimlanes.update({
- type: {
- $exists: false,
+ {
+ $set: {
+ type: 'board',
+ },
},
- }, {
- $set: {
- type: 'swimlane',
+ noValidateMulti
+ );
+ Swimlanes.update(
+ {
+ type: {
+ $exists: false,
+ },
},
- }, noValidateMulti);
- Lists.update({
- type: {
- $exists: false,
+ {
+ $set: {
+ type: 'swimlane',
+ },
},
- swimlaneId: {
- $exists: false,
+ noValidateMulti
+ );
+ Lists.update(
+ {
+ type: {
+ $exists: false,
+ },
+ swimlaneId: {
+ $exists: false,
+ },
},
- }, {
- $set: {
- type: 'list',
- swimlaneId: '',
+ {
+ $set: {
+ type: 'list',
+ swimlaneId: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
Users.find({
'profile.templatesBoardId': {
$exists: false,
},
}).forEach((user) => {
// Create board and swimlanes
- Boards.insert({
- title: TAPi18n.__('templates'),
- permission: 'private',
- type: 'template-container',
- members: [
- {
- userId: user._id,
- isAdmin: true,
- isActive: true,
- isNoComments: false,
- isCommentOnly: false,
- },
- ],
- }, (err, boardId) => {
-
- // Insert the reference to our templates board
- Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}});
-
- // Insert the card templates swimlane
- Swimlanes.insert({
- title: TAPi18n.__('card-templates-swimlane'),
- boardId,
- sort: 1,
- type: 'template-container',
- }, (err, swimlaneId) => {
-
- // Insert the reference to out card templates swimlane
- Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}});
- });
-
- // Insert the list templates swimlane
- Swimlanes.insert({
- title: TAPi18n.__('list-templates-swimlane'),
- boardId,
- sort: 2,
+ Boards.insert(
+ {
+ title: TAPi18n.__('templates'),
+ permission: 'private',
type: 'template-container',
- }, (err, swimlaneId) => {
-
- // Insert the reference to out list templates swimlane
- Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}});
- });
+ members: [
+ {
+ userId: user._id,
+ isAdmin: true,
+ isActive: true,
+ isNoComments: false,
+ isCommentOnly: false,
+ },
+ ],
+ },
+ (err, boardId) => {
+ // Insert the reference to our templates board
+ Users.update(user._id, {
+ $set: { 'profile.templatesBoardId': boardId },
+ });
+
+ // Insert the card templates swimlane
+ Swimlanes.insert(
+ {
+ title: TAPi18n.__('card-templates-swimlane'),
+ boardId,
+ sort: 1,
+ type: 'template-container',
+ },
+ (err, swimlaneId) => {
+ // Insert the reference to out card templates swimlane
+ Users.update(user._id, {
+ $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
+ });
+ }
+ );
- // Insert the board templates swimlane
- Swimlanes.insert({
- title: TAPi18n.__('board-templates-swimlane'),
- boardId,
- sort: 3,
- type: 'template-container',
- }, (err, swimlaneId) => {
+ // Insert the list templates swimlane
+ Swimlanes.insert(
+ {
+ title: TAPi18n.__('list-templates-swimlane'),
+ boardId,
+ sort: 2,
+ type: 'template-container',
+ },
+ (err, swimlaneId) => {
+ // Insert the reference to out list templates swimlane
+ Users.update(user._id, {
+ $set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
+ });
+ }
+ );
- // Insert the reference to out board templates swimlane
- Users.update(user._id, {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}});
- });
- });
+ // Insert the board templates swimlane
+ Swimlanes.insert(
+ {
+ title: TAPi18n.__('board-templates-swimlane'),
+ boardId,
+ sort: 3,
+ type: 'template-container',
+ },
+ (err, swimlaneId) => {
+ // Insert the reference to out board templates swimlane
+ Users.update(user._id, {
+ $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
+ });
+ }
+ );
+ }
+ );
});
});
Migrations.add('fix-circular-reference_', () => {
Cards.find().forEach((card) => {
if (card.parentId === card._id) {
- Cards.update(card._id, {$set: {parentId: ''}}, noValidateMulti);
+ Cards.update(card._id, { $set: { parentId: '' } }, noValidateMulti);
}
});
});
Migrations.add('mutate-boardIds-in-customfields', () => {
CustomFields.find().forEach((cf) => {
- CustomFields.update(cf, {
- $set: {
- boardIds: [cf.boardId],
- },
- $unset: {
- boardId: '',
+ CustomFields.update(
+ cf,
+ {
+ $set: {
+ boardIds: [cf.boardId],
+ },
+ $unset: {
+ boardId: '',
+ },
},
- }, noValidateMulti);
+ noValidateMulti
+ );
});
});
+
+const firstBatchOfDbsToAddCreatedAndUpdated = [
+ AccountSettings,
+ Actions,
+ Activities,
+ Announcements,
+ Boards,
+ CardComments,
+ Cards,
+ ChecklistItems,
+ Checklists,
+ CustomFields,
+ Integrations,
+ InvitationCodes,
+ Lists,
+ Rules,
+ Settings,
+ Swimlanes,
+ Triggers,
+ UnsavedEdits,
+];
+
+firstBatchOfDbsToAddCreatedAndUpdated.forEach((db) => {
+ db.before.insert((userId, doc) => {
+ doc.createdAt = Date.now();
+ doc.updatedAt = doc.createdAt;
+ });
+
+ db.before.update((userId, doc, fieldNames, modifier, options) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.updatedAt = new Date();
+ });
+});
+
+const modifiedAtTables = [
+ AccountSettings,
+ Actions,
+ Activities,
+ Announcements,
+ Boards,
+ CardComments,
+ Cards,
+ ChecklistItems,
+ Checklists,
+ CustomFields,
+ Integrations,
+ InvitationCodes,
+ Lists,
+ Rules,
+ Settings,
+ Swimlanes,
+ Triggers,
+ UnsavedEdits,
+ Users,
+];
+
+Migrations.add('add-missing-created-and-modified', () => {
+ Promise.all(
+ modifiedAtTables.map((db) =>
+ db
+ .rawCollection()
+ .update(
+ { modifiedAt: { $exists: false } },
+ { $set: { modifiedAt: new Date() } },
+ { multi: true }
+ )
+ .then(() =>
+ db
+ .rawCollection()
+ .update(
+ { createdAt: { $exists: false } },
+ { $set: { createdAt: new Date() } },
+ { multi: true }
+ )
+ )
+ )
+ )
+ .then(() => {
+ // eslint-disable-next-line no-console
+ console.info('Successfully added createdAt and updatedAt to all tables');
+ })
+ .catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ });
+});