Add createdAt and modifiedAt to all collections

This commit is contained in:
Justin Reynolds 2019-06-26 17:47:27 -05:00
parent fb728baf0c
commit c60e80d25b
37 changed files with 3722 additions and 2168 deletions

View file

@ -1,6 +1,7 @@
AccountSettings = new Mongo.Collection('accountSettings'); AccountSettings = new Mongo.Collection('accountSettings');
AccountSettings.attachSchema(new SimpleSchema({ AccountSettings.attachSchema(
new SimpleSchema({
_id: { _id: {
type: String, type: String,
}, },
@ -12,7 +13,32 @@ AccountSettings.attachSchema(new SimpleSchema({
type: Number, type: Number,
decimal: true, 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({ AccountSettings.allow({
update(userId) { 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) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, { AccountSettings._collection._ensureIndex({ modifiedAt: -1 });
AccountSettings.upsert(
{ _id: 'accounts-allowEmailChange' },
{
$setOnInsert: { $setOnInsert: {
booleanValue: false, booleanValue: false,
sort: 0, sort: 0,
}, },
}); }
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, { );
AccountSettings.upsert(
{ _id: 'accounts-allowUserNameChange' },
{
$setOnInsert: { $setOnInsert: {
booleanValue: false, booleanValue: false,
sort: 1, sort: 1,
}, },
}); }
);
}); });
} }
export default AccountSettings;

View file

@ -1,3 +1,5 @@
import { Meteor } from 'meteor/meteor';
Actions = new Mongo.Collection('actions'); Actions = new Mongo.Collection('actions');
Actions.allow({ Actions.allow({
@ -17,3 +19,16 @@ Actions.helpers({
return this.desc; 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;

View file

@ -69,7 +69,11 @@ Activities.before.insert((userId, doc) => {
Activities.after.insert((userId, doc) => { Activities.after.insert((userId, doc) => {
const activity = Activities._transform(doc); const activity = Activities._transform(doc);
RulesHelper.executeRules(activity); RulesHelper.executeRules(activity);
});
Activities.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
@ -78,11 +82,21 @@ if (Meteor.isServer) {
// are largely used in the App. See #524. // are largely used in the App. See #524.
Meteor.startup(() => { Meteor.startup(() => {
Activities._collection._ensureIndex({ createdAt: -1 }); Activities._collection._ensureIndex({ createdAt: -1 });
Activities._collection._ensureIndex({ modifiedAt: -1 });
Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 }); Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 }); Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ commentId: 1 }, { partialFilterExpression: { commentId: { $exists: true } } }); Activities._collection._ensureIndex(
Activities._collection._ensureIndex({ attachmentId: 1 }, { partialFilterExpression: { attachmentId: { $exists: true } } }); { commentId: 1 },
Activities._collection._ensureIndex({ customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } }); { 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. // 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 }, { "indexKey": -1 });
//Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } }); //Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
@ -189,18 +203,35 @@ if (Meteor.isServer) {
// params.labelId = activity.labelId; // params.labelId = activity.labelId;
//} //}
if (board) { if (board) {
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId'); const watchingUsers = _.pluck(
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId'); _.where(board.watchers, { level: 'watching' }),
watchers = _.union(watchers, watchingUsers, _.intersection(participants, trackingUsers)); 'userId'
);
const trackingUsers = _.pluck(
_.where(board.watchers, { level: 'tracking' }),
'userId'
);
watchers = _.union(
watchers,
watchingUsers,
_.intersection(participants, trackingUsers)
);
} }
Notifications.getUsers(watchers).forEach((user) => { Notifications.getUsers(watchers).forEach((user) => {
Notifications.notify(user, title, description, params); 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) { if (integrations.length > 0) {
Meteor.call('outgoingWebhooks', integrations, description, params); Meteor.call('outgoingWebhooks', integrations, description, params);
} }
}); });
} }
export default Activities;

View file

@ -1,6 +1,7 @@
Announcements = new Mongo.Collection('announcements'); Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({ Announcements.attachSchema(
new SimpleSchema({
enabled: { enabled: {
type: Boolean, type: Boolean,
defaultValue: false, defaultValue: false,
@ -17,7 +18,32 @@ Announcements.attachSchema(new SimpleSchema({
type: Number, type: Number,
decimal: true, 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({ Announcements.allow({
update(userId) { 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) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Announcements._collection._ensureIndex({ modifiedAt: -1 });
const announcements = Announcements.findOne({}); const announcements = Announcements.findOne({});
if(!announcements){ if (!announcements) {
Announcements.insert({enabled: false, sort: 0}); Announcements.insert({ enabled: false, sort: 0 });
} }
}); });
} }
export default Announcements;

View file

@ -1,6 +1,5 @@
Attachments = new FS.Collection('attachments', { Attachments = new FS.Collection('attachments', {
stores: [ stores: [
// XXX Add a new store for cover thumbnails so we don't load big images in // XXX Add a new store for cover thumbnails so we don't load big images in
// the general board view // the general board view
new FS.Store.GridFS('attachments', { new FS.Store.GridFS('attachments', {
@ -25,7 +24,6 @@ Attachments = new FS.Collection('attachments', {
], ],
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Attachments.files._ensureIndex({ cardId: 1 }); Attachments.files._ensureIndex({ cardId: 1 });
@ -78,13 +76,16 @@ if (Meteor.isServer) {
} else { } else {
// Don't add activity about adding the attachment as the activity // Don't add activity about adding the attachment as the activity
// be imported and delete source field // be imported and delete source field
Attachments.update({ Attachments.update(
{
_id: doc._id, _id: doc._id,
}, { },
{
$unset: { $unset: {
source: '', source: '',
}, },
}); }
);
} }
}); });
@ -107,3 +108,5 @@ if (Meteor.isServer) {
}); });
}); });
} }
export default Attachments;

View file

@ -1,7 +1,5 @@
Avatars = new FS.Collection('avatars', { Avatars = new FS.Collection('avatars', {
stores: [ stores: [new FS.Store.GridFS('avatars')],
new FS.Store.GridFS('avatars'),
],
filter: { filter: {
maxSize: 72000, maxSize: 72000,
allow: { allow: {
@ -18,10 +16,14 @@ Avatars.allow({
insert: isOwner, insert: isOwner,
update: isOwner, update: isOwner,
remove: isOwner, remove: isOwner,
download() { return true; }, download() {
return true;
},
fetch: ['userId'], fetch: ['userId'],
}); });
Avatars.files.before.insert((userId, doc) => { Avatars.files.before.insert((userId, doc) => {
doc.userId = userId; doc.userId = userId;
}); });
export default Avatars;

View file

@ -3,7 +3,8 @@ Boards = new Mongo.Collection('boards');
/** /**
* This is a Board. * This is a Board.
*/ */
Boards.attachSchema(new SimpleSchema({ Boards.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* The title of the board * The title of the board
@ -15,7 +16,8 @@ Boards.attachSchema(new SimpleSchema({
* The title slugified. * The title slugified.
*/ */
type: String, type: String,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
// XXX We need to improve slug management. Only the id should be necessary // XXX We need to improve slug management. Only the id should be necessary
// to identify a board in the code. // to identify a board in the code.
// XXX If the board title is updated, the slug should also be updated. // XXX If the board title is updated, the slug should also be updated.
@ -37,7 +39,8 @@ Boards.attachSchema(new SimpleSchema({
* Is the board archived? * Is the board archived?
*/ */
type: Boolean, type: Boolean,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return false; return false;
} }
@ -48,7 +51,8 @@ Boards.attachSchema(new SimpleSchema({
* Creation time of the board * Creation time of the board
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -63,8 +67,9 @@ Boards.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
if (this.isUpdate) { autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date(); return new Date();
} else { } else {
this.unset(); this.unset();
@ -77,21 +82,24 @@ Boards.attachSchema(new SimpleSchema({
* How many stars the board has * How many stars the board has
*/ */
type: Number, type: Number,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return 0; return 0;
} }
}, },
}, },
// De-normalized label system // De-normalized label system
'labels': { labels: {
/** /**
* List of labels attached to a board * List of labels attached to a board
*/ */
type: [Object], type: [Object],
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; const colors = Boards.simpleSchema()._schema['labels.$.color']
.allowedValues;
const defaultLabelsColors = _.clone(colors).splice(0, 6); const defaultLabelsColors = _.clone(colors).splice(0, 6);
return defaultLabelsColors.map((color) => ({ return defaultLabelsColors.map((color) => ({
color, color,
@ -131,30 +139,52 @@ Boards.attachSchema(new SimpleSchema({
*/ */
type: String, type: String,
allowedValues: [ allowedValues: [
'green', 'yellow', 'orange', 'red', 'purple', 'green',
'blue', 'sky', 'lime', 'pink', 'black', 'yellow',
'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen', 'orange',
'slateblue', 'magenta', 'gold', 'navy', 'gray', 'red',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', '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- // XXX We might want to maintain more informations under the member sub-
// documents like de-normalized meta-data (the date the member joined the // documents like de-normalized meta-data (the date the member joined the
// board, the number of contributions, etc.). // board, the number of contributions, etc.).
'members': { members: {
/** /**
* List of members of a board * List of members of a board
*/ */
type: [Object], type: [Object],
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return [{ return [
{
userId: this.userId, userId: this.userId,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
isNoComments: false, isNoComments: false,
isCommentOnly: false, isCommentOnly: false,
}]; },
];
} }
}, },
}, },
@ -210,7 +240,8 @@ Boards.attachSchema(new SimpleSchema({
'wisteria', 'wisteria',
'midnight', 'midnight',
], ],
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return Boards.simpleSchema()._schema.color.allowedValues[0]; return Boards.simpleSchema()._schema.color.allowedValues[0];
} }
@ -311,8 +342,8 @@ Boards.attachSchema(new SimpleSchema({
type: String, type: String,
defaultValue: 'board', defaultValue: 'board',
}, },
})); })
);
Boards.helpers({ Boards.helpers({
copy() { copy() {
@ -350,7 +381,9 @@ Boards.helpers({
*/ */
isActiveMember(userId) { isActiveMember(userId) {
if (userId) { if (userId) {
return this.members.find((member) => (member.userId === userId && member.isActive)); return this.members.find(
(member) => member.userId === userId && member.isActive
);
} else { } else {
return false; return false;
} }
@ -361,11 +394,17 @@ Boards.helpers({
}, },
cards() { cards() {
return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } }); return Cards.find(
{ boardId: this._id, archived: false },
{ sort: { title: 1 } }
);
}, },
lists() { lists() {
return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); return Lists.find(
{ boardId: this._id, archived: false },
{ sort: { sort: 1 } }
);
}, },
nullSortLists() { nullSortLists() {
@ -377,18 +416,24 @@ Boards.helpers({
}, },
swimlanes() { 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) { nextSwimlane(swimlane) {
return Swimlanes.findOne({ return Swimlanes.findOne(
{
boardId: this._id, boardId: this._id,
archived: false, archived: false,
sort: { $gte: swimlane.sort }, sort: { $gte: swimlane.sort },
_id: { $ne: swimlane._id }, _id: { $ne: swimlane._id },
}, { },
{
sort: { sort: 1 }, sort: { sort: 1 },
}); }
);
}, },
nullSortSwimlanes() { nullSortSwimlanes() {
@ -399,13 +444,21 @@ Boards.helpers({
}); });
}, },
hasOvertimeCards(){ hasOvertimeCards() {
const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); const card = Cards.findOne({
isOvertime: true,
boardId: this._id,
archived: false,
});
return card !== undefined; return card !== undefined;
}, },
hasSpentTimeCards(){ hasSpentTimeCards() {
const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} ); const card = Cards.findOne({
spentTime: { $gt: 0 },
boardId: this._id,
archived: false,
});
return card !== undefined; return card !== undefined;
}, },
@ -429,7 +482,7 @@ Boards.helpers({
return _.findWhere(this.labels, { name, color }); return _.findWhere(this.labels, { name, color });
}, },
getLabelById(labelId){ getLabelById(labelId) {
return _.findWhere(this.labels, { _id: labelId }); return _.findWhere(this.labels, { _id: labelId });
}, },
@ -446,15 +499,29 @@ Boards.helpers({
}, },
hasAdmin(memberId) { hasAdmin(memberId) {
return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true }); return !!_.findWhere(this.members, {
userId: memberId,
isActive: true,
isAdmin: true,
});
}, },
hasNoComments(memberId) { 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) { 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() { absoluteUrl() {
@ -466,7 +533,10 @@ Boards.helpers({
}, },
customFields() { 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 // XXX currently mutations return no value so we have an issue when using addLabel in import
@ -489,10 +559,7 @@ Boards.helpers({
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Cards.find(query, projection); return Cards.find(query, projection);
@ -506,17 +573,14 @@ Boards.helpers({
query.type = 'template-swimlane'; query.type = 'template-swimlane';
query.archived = false; query.archived = false;
} else { } else {
query.type = {$nin: ['template-swimlane']}; query.type = { $nin: ['template-swimlane'] };
} }
const projection = { limit: 10, sort: { createdAt: -1 } }; const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Swimlanes.find(query, projection); return Swimlanes.find(query, projection);
@ -530,17 +594,14 @@ Boards.helpers({
query.type = 'template-list'; query.type = 'template-list';
query.archived = false; query.archived = false;
} else { } else {
query.type = {$nin: ['template-list']}; query.type = { $nin: ['template-list'] };
} }
const projection = { limit: 10, sort: { createdAt: -1 } }; const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Lists.find(query, projection); return Lists.find(query, projection);
@ -557,17 +618,14 @@ Boards.helpers({
query.type = 'template-card'; query.type = 'template-card';
query.archived = false; query.archived = false;
} else { } else {
query.type = {$nin: ['template-card']}; query.type = { $nin: ['template-card'] };
} }
const projection = { limit: 10, sort: { createdAt: -1 } }; const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Cards.find(query, projection); return Cards.find(query, projection);
@ -575,22 +633,29 @@ Boards.helpers({
// A board alwasy has another board where it deposits subtasks of thasks // A board alwasy has another board where it deposits subtasks of thasks
// that belong to itself. // that belong to itself.
getDefaultSubtasksBoardId() { getDefaultSubtasksBoardId() {
if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) { if (
this.subtasksDefaultBoardId === null ||
this.subtasksDefaultBoardId === undefined
) {
this.subtasksDefaultBoardId = Boards.insert({ this.subtasksDefaultBoardId = Boards.insert({
title: `^${this.title}^`, title: `^${this.title}^`,
permission: this.permission, permission: this.permission,
members: this.members, members: this.members,
color: this.color, color: this.color,
description: TAPi18n.__('default-subtasks-board', {board: this.title}), description: TAPi18n.__('default-subtasks-board', {
board: this.title,
}),
}); });
Swimlanes.insert({ Swimlanes.insert({
title: TAPi18n.__('default'), title: TAPi18n.__('default'),
boardId: this.subtasksDefaultBoardId, boardId: this.subtasksDefaultBoardId,
}); });
Boards.update(this._id, {$set: { Boards.update(this._id, {
$set: {
subtasksDefaultBoardId: this.subtasksDefaultBoardId, subtasksDefaultBoardId: this.subtasksDefaultBoardId,
}}); },
});
} }
return this.subtasksDefaultBoardId; return this.subtasksDefaultBoardId;
}, },
@ -600,7 +665,10 @@ Boards.helpers({
}, },
getDefaultSubtasksListId() { getDefaultSubtasksListId() {
if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) { if (
this.subtasksDefaultListId === null ||
this.subtasksDefaultListId === undefined
) {
this.subtasksDefaultListId = Lists.insert({ this.subtasksDefaultListId = Lists.insert({
title: TAPi18n.__('queue'), title: TAPi18n.__('queue'),
boardId: this._id, boardId: this._id,
@ -615,13 +683,13 @@ Boards.helpers({
}, },
getDefaultSwimline() { getDefaultSwimline() {
let result = Swimlanes.findOne({boardId: this._id}); let result = Swimlanes.findOne({ boardId: this._id });
if (result === undefined) { if (result === undefined) {
Swimlanes.insert({ Swimlanes.insert({
title: TAPi18n.__('default'), title: TAPi18n.__('default'),
boardId: this._id, boardId: this._id,
}); });
result = Swimlanes.findOne({boardId: this._id}); result = Swimlanes.findOne({ boardId: this._id });
} }
return result; return result;
}, },
@ -633,19 +701,24 @@ Boards.helpers({
{ {
startAt: { startAt: {
$lte: start, $lte: start,
}, endAt: { },
endAt: {
$gte: start, $gte: start,
}, },
}, { },
{
startAt: { startAt: {
$lte: end, $lte: end,
}, endAt: { },
endAt: {
$gte: end, $gte: end,
}, },
}, { },
{
startAt: { startAt: {
$gte: start, $gte: start,
}, endAt: { },
endAt: {
$lte: end, $lte: end,
}, },
}, },
@ -662,7 +735,6 @@ Boards.helpers({
}, },
}); });
Boards.mutations({ Boards.mutations({
archive() { archive() {
return { $set: { archived: true } }; return { $set: { archived: true } };
@ -753,7 +825,8 @@ Boards.mutations({
const memberIndex = this.memberIndex(memberId); const memberIndex = this.memberIndex(memberId);
// we do not allow the only one admin to be removed // 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) { if (!allowRemove) {
return { return {
$set: { $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); const memberIndex = this.memberIndex(memberId);
// do not allow change permission of self // do not allow change permission of self
if (memberId === currentUserId) { if (memberId === currentUserId) {
@ -804,12 +883,13 @@ Boards.mutations({
}); });
function boardRemover(userId, doc) { function boardRemover(userId, doc) {
[Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach((element) => { [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach(
(element) => {
element.remove({ boardId: doc._id }); element.remove({ boardId: doc._id });
}); }
);
} }
if (Meteor.isServer) { if (Meteor.isServer) {
Boards.allow({ Boards.allow({
insert: Meteor.userId, insert: Meteor.userId,
@ -830,25 +910,25 @@ if (Meteor.isServer) {
// We can't remove a member if it is the last administrator // We can't remove a member if it is the last administrator
Boards.deny({ Boards.deny({
update(userId, doc, fieldNames, modifier) { update(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'members')) if (!_.contains(fieldNames, 'members')) return false;
return false;
// We only care in case of a $pull operation, ie remove a member // We only care in case of a $pull operation, ie remove a member
if (!_.isObject(modifier.$pull && modifier.$pull.members)) if (!_.isObject(modifier.$pull && modifier.$pull.members)) return false;
return false;
// If there is more than one admin, it's ok to remove anyone // If there is more than one admin, it's ok to remove anyone
const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }).length; const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true })
if (nbAdmins > 1) .length;
return false; if (nbAdmins > 1) return false;
// If all the previous conditions were verified, we can't remove // If all the previous conditions were verified, we can't remove
// a user if it's an admin // a user if it's an admin
const removedMemberId = modifier.$pull.members.userId; const removedMemberId = modifier.$pull.members.userId;
return Boolean(_.findWhere(doc.members, { return Boolean(
_.findWhere(doc.members, {
userId: removedMemberId, userId: removedMemberId,
isAdmin: true, isAdmin: true,
})); })
);
}, },
fetch: ['members'], fetch: ['members'],
}); });
@ -882,16 +962,19 @@ if (Meteor.isServer) {
} else throw new Meteor.Error('error-board-doesNotExist'); } else throw new Meteor.Error('error-board-doesNotExist');
}, },
}); });
} }
if (Meteor.isServer) { if (Meteor.isServer) {
// Let MongoDB ensure that a member is not included twice in the same board // Let MongoDB ensure that a member is not included twice in the same board
Meteor.startup(() => { Meteor.startup(() => {
Boards._collection._ensureIndex({ Boards._collection._ensureIndex({ modifiedAt: -1 });
Boards._collection._ensureIndex(
{
_id: 1, _id: 1,
'members.userId': 1, 'members.userId': 1,
}, { unique: true }); },
{ unique: true }
);
Boards._collection._ensureIndex({ 'members.userId': 1 }); 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 // If the user remove one label from a board, we cant to remove reference of
// this label in any card of this board. // this label in any card of this board.
Boards.after.update((userId, doc, fieldNames, modifier) => { Boards.after.update((userId, doc, fieldNames, modifier) => {
if (!_.contains(fieldNames, 'labels') || if (
!_.contains(fieldNames, 'labels') ||
!modifier.$pull || !modifier.$pull ||
!modifier.$pull.labels || !modifier.$pull.labels ||
!modifier.$pull.labels._id) { !modifier.$pull.labels._id
) {
return; return;
} }
@ -935,12 +1020,21 @@ if (Meteor.isServer) {
} }
const parts = set.split('.'); 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); 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 // Remove a member from all objects of the board before leaving the board
Boards.before.update((userId, doc, fieldNames, modifier) => { Boards.before.update((userId, doc, fieldNames, modifier) => {
if (!_.contains(fieldNames, 'members')) { if (!_.contains(fieldNames, 'members')) {
@ -976,14 +1070,11 @@ if (Meteor.isServer) {
// Remove board from users starred list // Remove board from users starred list
if (!board.isPublic()) { if (!board.isPublic()) {
Users.update( Users.update(memberId, {
memberId,
{
$pull: { $pull: {
'profile.starredBoards': boardId, 'profile.starredBoards': boardId,
}, },
} });
);
} }
}); });
} }
@ -1044,29 +1135,34 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
title: string}] title: string}]
*/ */
JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) { JsonRoutes.add('GET', '/api/users/:userId/boards', function(req, res) {
try { try {
Authentication.checkLoggedIn(req.userId); Authentication.checkLoggedIn(req.userId);
const paramUserId = req.params.userId; const paramUserId = req.params.userId;
// A normal user should be able to see their own boards, // A normal user should be able to see their own boards,
// admins can access boards of any user // 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({ const data = Boards.find(
{
archived: false, archived: false,
'members.userId': paramUserId, 'members.userId': paramUserId,
}, { },
{
sort: ['title'], sort: ['title'],
}).map(function(board) { }
).map(function(board) {
return { return {
_id: board._id, _id: board._id,
title: board.title, title: board.title,
}; };
}); });
JsonRoutes.sendResult(res, {code: 200, data}); JsonRoutes.sendResult(res, { code: 200, data });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1081,20 +1177,19 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
title: string}] title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards', function (req, res) { JsonRoutes.add('GET', '/api/boards', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Boards.find({ permission: 'public' }).map(function (doc) { data: Boards.find({ permission: 'public' }).map(function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
}; };
}), }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1109,7 +1204,7 @@ if (Meteor.isServer) {
* @param {string} boardId the ID of the board to retrieve the data * @param {string} boardId the ID of the board to retrieve the data
* @return_type Boards * @return_type Boards
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId', function(req, res) {
try { try {
const id = req.params.boardId; const id = req.params.boardId;
Authentication.checkBoardAccess(req.userId, id); Authentication.checkBoardAccess(req.userId, id);
@ -1118,8 +1213,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: Boards.findOne({ _id: id }), data: Boards.findOne({ _id: id }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1152,7 +1246,7 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
defaultSwimlaneId: string} defaultSwimlaneId: string}
*/ */
JsonRoutes.add('POST', '/api/boards', function (req, res) { JsonRoutes.add('POST', '/api/boards', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = Boards.insert({ const id = Boards.insert({
@ -1180,8 +1274,7 @@ if (Meteor.isServer) {
defaultSwimlaneId: swimlaneId, defaultSwimlaneId: swimlaneId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1195,19 +1288,18 @@ if (Meteor.isServer) {
* *
* @param {string} boardId the ID of the board * @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 { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.boardId; const id = req.params.boardId;
Boards.remove({ _id: id }); Boards.remove({ _id: id });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data:{ data: {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1228,7 +1320,7 @@ if (Meteor.isServer) {
* *
* @return_type string * @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); Authentication.checkUserId(req.userId);
const id = req.params.boardId; const id = req.params.boardId;
try { try {
@ -1238,7 +1330,10 @@ if (Meteor.isServer) {
const name = req.body.label.name; const name = req.body.label.name;
const labelId = Random.id(6); const labelId = Random.id(6);
if (!board.getLabel(name, color)) { 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, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: labelId, data: labelId,
@ -1249,8 +1344,7 @@ if (Meteor.isServer) {
}); });
} }
} }
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
data: error, data: error,
}); });
@ -1268,29 +1362,36 @@ if (Meteor.isServer) {
* @param {boolean} isNoComments NoComments capability * @param {boolean} isNoComments NoComments capability
* @param {boolean} isCommentOnly CommentsOnly 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 { try {
const boardId = req.params.boardId; const boardId = req.params.boardId;
const memberId = req.params.memberId; const memberId = req.params.memberId;
const {isAdmin, isNoComments, isCommentOnly} = req.body; const { isAdmin, isNoComments, isCommentOnly } = req.body;
Authentication.checkBoardAccess(req.userId, boardId); Authentication.checkBoardAccess(req.userId, boardId);
const board = Boards.findOne({ _id: boardId }); const board = Boards.findOne({ _id: boardId });
function isTrue(data){ function isTrue(data) {
try { try {
return data.toLowerCase() === 'true'; return data.toLowerCase() === 'true';
} } catch (error) {
catch (error) {
return data; 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, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: query, data: query,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1298,3 +1399,5 @@ if (Meteor.isServer) {
} }
}); });
} }
export default Boards;

View file

@ -3,7 +3,8 @@ CardComments = new Mongo.Collection('card_comments');
/** /**
* A comment on a card * A comment on a card
*/ */
CardComments.attachSchema(new SimpleSchema({ CardComments.attachSchema(
new SimpleSchema({
boardId: { boardId: {
/** /**
* the board ID * the board ID
@ -23,15 +24,14 @@ CardComments.attachSchema(new SimpleSchema({
*/ */
type: String, type: String,
}, },
// XXX We probably don't need this information here, since we already have it
// in the associated comment creation activity
createdAt: { createdAt: {
/** /**
* when was the comment created * when was the comment created
*/ */
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -39,19 +39,33 @@ CardComments.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();
}
},
},
// XXX Should probably be called `authorId` // XXX Should probably be called `authorId`
userId: { userId: {
/** /**
* the author ID of the comment * the author ID of the comment
*/ */
type: String, type: String,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return this.userId; return this.userId;
} }
}, },
}, },
})); })
);
CardComments.allow({ CardComments.allow({
insert(userId, doc) { insert(userId, doc) {
@ -80,7 +94,7 @@ CardComments.helpers({
CardComments.hookOptions.after.update = { fetchPrevious: false }; CardComments.hookOptions.after.update = { fetchPrevious: false };
function commentCreation(userId, doc){ function commentCreation(userId, doc) {
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
Activities.insert({ Activities.insert({
userId, 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) { if (Meteor.isServer) {
// Comments are often fetched within a card, so we create an index to make these // Comments are often fetched within a card, so we create an index to make these
// queries more efficient. // queries more efficient.
Meteor.startup(() => { Meteor.startup(() => {
CardComments._collection._ensureIndex({ modifiedAt: -1 });
CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 }); CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 });
}); });
@ -152,14 +172,20 @@ if (Meteor.isServer) {
* comment: string, * comment: string,
* authorId: 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 { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CardComments.find({ boardId: paramBoardId, cardId: paramCardId}).map(function (doc) { data: CardComments.find({
boardId: paramBoardId,
cardId: paramCardId,
}).map(function(doc) {
return { return {
_id: doc._id, _id: doc._id,
comment: doc.text, comment: doc.text,
@ -167,8 +193,7 @@ if (Meteor.isServer) {
}; };
}), }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -185,24 +210,31 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to retrieve * @param {string} commentId the ID of the comment to retrieve
* @return_type CardComments * @return_type CardComments
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId; const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }), data: CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
}),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation new_comment * @operation new_comment
@ -214,9 +246,12 @@ if (Meteor.isServer) {
* @param {string} text the content of the comment * @param {string} text the content of the comment
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/comments',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const id = CardComments.direct.insert({ const id = CardComments.direct.insert({
@ -233,16 +268,20 @@ if (Meteor.isServer) {
}, },
}); });
const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId }); const cardComment = CardComments.findOne({
_id: id,
cardId: paramCardId,
boardId: paramBoardId,
});
commentCreation(req.body.authorId, cardComment); commentCreation(req.body.authorId, cardComment);
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation delete_comment * @operation delete_comment
@ -253,25 +292,34 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to delete * @param {string} commentId the ID of the comment to delete
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId; const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }); CardComments.remove({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: paramCardId, _id: paramCardId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
} }
export default CardComments;

View file

@ -81,7 +81,8 @@ Cards.attachSchema(new SimpleSchema({
* creation date * creation date
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } 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: { customFields: {
/** /**
* list of custom fields * 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 // Cards are often fetched within a board, so we create an index to make these
// queries more efficient. // queries more efficient.
Meteor.startup(() => { 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 // https://github.com/wekan/wekan/issues/1863
// Swimlane added a new field in the cards collection of mongodb named parentId. // 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). // 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); 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 all activities associated with a card if we remove the card
// Remove also card_comments / checklists / attachments // Remove also card_comments / checklists / attachments
Cards.before.remove((userId, doc) => { Cards.before.remove((userId, doc) => {
@ -1980,3 +1999,5 @@ if (Meteor.isServer) {
}); });
} }
export default Cards;

View file

@ -3,7 +3,8 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/** /**
* An item in a checklist * An item in a checklist
*/ */
ChecklistItems.attachSchema(new SimpleSchema({ ChecklistItems.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* the text of the item * the text of the item
@ -36,7 +37,32 @@ ChecklistItems.attachSchema(new SimpleSchema({
*/ */
type: String, 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({ ChecklistItems.allow({
insert(userId, doc) { insert(userId, doc) {
@ -62,10 +88,10 @@ ChecklistItems.mutations({
setTitle(title) { setTitle(title) {
return { $set: { title } }; return { $set: { title } };
}, },
check(){ check() {
return { $set: { isFinished: true } }; return { $set: { isFinished: true } };
}, },
uncheck(){ uncheck() {
return { $set: { isFinished: false } }; return { $set: { isFinished: false } };
}, },
toggleItem() { toggleItem() {
@ -79,7 +105,7 @@ ChecklistItems.mutations({
sort: sortIndex, 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 card = Cards.findOne(doc.cardId);
const boardId = card.boardId; const boardId = card.boardId;
let activityType; let activityType;
if(doc.isFinished){ if (doc.isFinished) {
activityType = 'checkedItem'; activityType = 'checkedItem';
}else{ } else {
activityType = 'uncheckedItem'; activityType = 'uncheckedItem';
} }
const act = { const act = {
@ -122,19 +148,19 @@ function publishCheckActivity(userId, doc){
boardId, boardId,
checklistId: doc.checklistId, checklistId: doc.checklistId,
checklistItemId: doc._id, checklistItemId: doc._id,
checklistItemName:doc.title, checklistItemName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}; };
Activities.insert(act); Activities.insert(act);
} }
function publishChekListCompleted(userId, doc){ function publishChekListCompleted(userId, doc) {
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
const boardId = card.boardId; const boardId = card.boardId;
const checklistId = doc.checklistId; const checklistId = doc.checklistId;
const checkList = Checklists.findOne({_id:checklistId}); const checkList = Checklists.findOne({ _id: checklistId });
if(checkList.isFinished()){ if (checkList.isFinished()) {
const act = { const act = {
userId, userId,
activityType: 'completeChecklist', 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 card = Cards.findOne(doc.cardId);
const boardId = card.boardId; const boardId = card.boardId;
const checklistId = doc.checklistId; 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 // BUGS in IFTTT Rules: https://github.com/wekan/wekan/issues/1972
// Currently in checklist all are set as uncompleted/not checked, // Currently in checklist all are set as uncompleted/not checked,
// IFTTT Rule does not move card to other list. // 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 // find . | xargs grep 'count' -sl | grep -v .meteor | grep -v node_modules | grep -v .build
// Maybe something related here? // Maybe something related here?
// wekan/client/components/rules/triggers/checklistTriggers.js // wekan/client/components/rules/triggers/checklistTriggers.js
if(checkList.isFinished()){ if (checkList.isFinished()) {
const act = { const act = {
userId, userId,
activityType: 'uncompleteChecklist', activityType: 'uncompleteChecklist',
@ -185,6 +211,7 @@ function publishChekListUncompleted(userId, doc){
// Activities // Activities
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
ChecklistItems._collection._ensureIndex({ modifiedAt: -1 });
ChecklistItems._collection._ensureIndex({ checklistId: 1 }); ChecklistItems._collection._ensureIndex({ checklistId: 1 });
ChecklistItems._collection._ensureIndex({ cardId: 1 }); ChecklistItems._collection._ensureIndex({ cardId: 1 });
}); });
@ -198,6 +225,10 @@ if (Meteor.isServer) {
publishChekListUncompleted(userId, doc, fieldNames); 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) => { ChecklistItems.after.insert((userId, doc) => {
itemCreation(userId, doc); itemCreation(userId, doc);
@ -214,7 +245,7 @@ if (Meteor.isServer) {
boardId, boardId,
checklistId: doc.checklistId, checklistId: doc.checklistId,
checklistItemId: doc._id, checklistItemId: doc._id,
checklistItemName:doc.title, checklistItemName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}); });
@ -233,8 +264,11 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item * @param {string} itemId the ID of the item
* @return_type ChecklistItems * @return_type ChecklistItems
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/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;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId }); const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) { if (checklistItem) {
@ -247,7 +281,8 @@ if (Meteor.isServer) {
code: 500, code: 500,
}); });
} }
}); }
);
/** /**
* @operation edit_checklist_item * @operation edit_checklist_item
@ -262,16 +297,25 @@ if (Meteor.isServer) {
* @param {string} [title] the new text of the item * @param {string} [title] the new text of the item
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); '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')) { if (req.body.hasOwnProperty('isFinished')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}}); ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { isFinished: req.body.isFinished } }
);
} }
if (req.body.hasOwnProperty('title')) { if (req.body.hasOwnProperty('title')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}}); ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { title: req.body.title } }
);
} }
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -280,7 +324,8 @@ if (Meteor.isServer) {
_id: paramItemId, _id: paramItemId,
}, },
}); });
}); }
);
/** /**
* @operation delete_checklist_item * @operation delete_checklist_item
@ -295,8 +340,11 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item to be removed * @param {string} itemId the ID of the item to be removed
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'DELETE',
'/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;
ChecklistItems.direct.remove({ _id: paramItemId }); ChecklistItems.direct.remove({ _id: paramItemId });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -305,5 +353,8 @@ if (Meteor.isServer) {
_id: paramItemId, _id: paramItemId,
}, },
}); });
}); }
);
} }
export default ChecklistItems;

View file

@ -3,7 +3,8 @@ Checklists = new Mongo.Collection('checklists');
/** /**
* A Checklist * A Checklist
*/ */
Checklists.attachSchema(new SimpleSchema({ Checklists.attachSchema(
new SimpleSchema({
cardId: { cardId: {
/** /**
* The ID of the card the checklist is in * The ID of the card the checklist is in
@ -30,7 +31,8 @@ Checklists.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -38,6 +40,18 @@ Checklists.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();
}
},
},
sort: { sort: {
/** /**
* sorting value of the checklist * sorting value of the checklist
@ -45,7 +59,8 @@ Checklists.attachSchema(new SimpleSchema({
type: Number, type: Number,
decimal: true, decimal: true,
}, },
})); })
);
Checklists.helpers({ Checklists.helpers({
copy(newCardId) { copy(newCardId) {
@ -53,7 +68,7 @@ Checklists.helpers({
this._id = null; this._id = null;
this.cardId = newCardId; this.cardId = newCardId;
const newChecklistId = Checklists.insert(this); const newChecklistId = Checklists.insert(this);
ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => { ChecklistItems.find({ checklistId: oldChecklistId }).forEach((item) => {
item._id = null; item._id = null;
item.checklistId = newChecklistId; item.checklistId = newChecklistId;
item.cardId = newCardId; item.cardId = newCardId;
@ -65,9 +80,12 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count(); return ChecklistItems.find({ checklistId: this._id }).count();
}, },
items() { items() {
return ChecklistItems.find({ return ChecklistItems.find(
{
checklistId: this._id, checklistId: this._id,
}, { sort: ['sort'] }); },
{ sort: ['sort'] }
);
}, },
finishedCount() { finishedCount() {
return ChecklistItems.find({ return ChecklistItems.find({
@ -78,20 +96,20 @@ Checklists.helpers({
isFinished() { isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount(); return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
}, },
checkAllItems(){ checkAllItems() {
const checkItems = ChecklistItems.find({checklistId: this._id}); const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item){ checkItems.forEach(function(item) {
item.check(); item.check();
}); });
}, },
uncheckAllItems(){ uncheckAllItems() {
const checkItems = ChecklistItems.find({checklistId: this._id}); const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item){ checkItems.forEach(function(item) {
item.uncheck(); item.uncheck();
}); });
}, },
itemIndex(itemId) { itemIndex(itemId) {
const items = self.findOne({_id : this._id}).items; const items = self.findOne({ _id: this._id }).items;
return _.pluck(items, '_id').indexOf(itemId); return _.pluck(items, '_id').indexOf(itemId);
}, },
}); });
@ -124,6 +142,7 @@ Checklists.mutations({
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Checklists._collection._ensureIndex({ modifiedAt: -1 });
Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 }); Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 });
}); });
@ -135,12 +154,17 @@ if (Meteor.isServer) {
cardId: doc.cardId, cardId: doc.cardId,
boardId: card.boardId, boardId: card.boardId,
checklistId: doc._id, checklistId: doc._id,
checklistName:doc.title, checklistName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, 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) => { Checklists.before.remove((userId, doc) => {
const activities = Activities.find({ checklistId: doc._id }); const activities = Activities.find({ checklistId: doc._id });
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
@ -155,7 +179,7 @@ if (Meteor.isServer) {
cardId: doc.cardId, cardId: doc.cardId,
boardId: Cards.findOne(doc.cardId).boardId, boardId: Cards.findOne(doc.cardId).boardId,
checklistId: doc._id, checklistId: doc._id,
checklistName:doc.title, checklistName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}); });
@ -172,10 +196,15 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
* title: string}] * title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) { const checklists = Checklists.find({ cardId: paramCardId }).map(function(
doc
) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
@ -191,7 +220,8 @@ if (Meteor.isServer) {
code: 500, code: 500,
}); });
} }
}); }
);
/** /**
* @operation get_checklist * @operation get_checklist
@ -209,13 +239,21 @@ if (Meteor.isServer) {
* title: string, * title: string,
* isFinished: boolean}]} * isFinished: boolean}]}
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId; const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }); const checklist = Checklists.findOne({
_id: paramChecklistId,
cardId: paramCardId,
});
if (checklist) { if (checklist) {
checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) { checklist.items = ChecklistItems.find({
checklistId: checklist._id,
}).map(function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
@ -231,7 +269,8 @@ if (Meteor.isServer) {
code: 500, code: 500,
}); });
} }
}); }
);
/** /**
* @operation new_checklist * @operation new_checklist
@ -242,8 +281,11 @@ if (Meteor.isServer) {
* @param {string} title the title of the new checklist * @param {string} title the title of the new checklist
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'POST',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const id = Checklists.insert({ const id = Checklists.insert({
@ -252,7 +294,7 @@ if (Meteor.isServer) {
sort: 0, sort: 0,
}); });
if (id) { if (id) {
req.body.items.forEach(function (item, idx) { req.body.items.forEach(function(item, idx) {
ChecklistItems.insert({ ChecklistItems.insert({
cardId: paramCardId, cardId: paramCardId,
checklistId: id, checklistId: id,
@ -271,7 +313,8 @@ if (Meteor.isServer) {
code: 400, code: 400,
}); });
} }
}); }
);
/** /**
* @operation delete_checklist * @operation delete_checklist
@ -284,8 +327,11 @@ if (Meteor.isServer) {
* @param {string} checklistId the ID of the checklist to remove * @param {string} checklistId the ID of the checklist to remove
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId; const paramChecklistId = req.params.checklistId;
Checklists.remove({ _id: paramChecklistId }); Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -294,5 +340,8 @@ if (Meteor.isServer) {
_id: paramChecklistId, _id: paramChecklistId,
}, },
}); });
}); }
);
} }
export default Checklists;

View file

@ -3,7 +3,8 @@ CustomFields = new Mongo.Collection('customFields');
/** /**
* A custom field on a card in the board * A custom field on a card in the board
*/ */
CustomFields.attachSchema(new SimpleSchema({ CustomFields.attachSchema(
new SimpleSchema({
boardIds: { boardIds: {
/** /**
* the ID of the board * the ID of the board
@ -70,7 +71,32 @@ CustomFields.attachSchema(new SimpleSchema({
*/ */
type: Boolean, type: Boolean,
}, },
})); 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();
}
},
},
})
);
CustomFields.mutations({ CustomFields.mutations({
addBoard(boardId) { addBoard(boardId) {
@ -88,19 +114,28 @@ CustomFields.mutations({
CustomFields.allow({ CustomFields.allow({
insert(userId, doc) { insert(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({ return allowIsAnyBoardMember(
_id: {$in: doc.boardIds}, userId,
}).fetch()); Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
}, },
update(userId, doc) { update(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({ return allowIsAnyBoardMember(
_id: {$in: doc.boardIds}, userId,
}).fetch()); Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
}, },
remove(userId, doc) { remove(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({ return allowIsAnyBoardMember(
_id: {$in: doc.boardIds}, userId,
}).fetch()); Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
}, },
fetch: ['userId', 'boardIds'], fetch: ['userId', 'boardIds'],
}); });
@ -108,7 +143,7 @@ CustomFields.allow({
// not sure if we need this? // not sure if we need this?
//CustomFields.hookOptions.after.update = { fetchPrevious: false }; //CustomFields.hookOptions.after.update = { fetchPrevious: false };
function customFieldCreation(userId, doc){ function customFieldCreation(userId, doc) {
Activities.insert({ Activities.insert({
userId, userId,
activityType: 'createCustomField', activityType: 'createCustomField',
@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
CustomFields._collection._ensureIndex({ modifiedAt: -1 });
CustomFields._collection._ensureIndex({ boardIds: 1 }); CustomFields._collection._ensureIndex({ boardIds: 1 });
}); });
@ -149,12 +185,17 @@ if (Meteor.isServer) {
customFieldCreation(userId, doc); 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) => { CustomFields.before.update((userId, doc, fieldNames, modifier) => {
if (_.contains(fieldNames, 'boardIds') && modifier.$pull) { if (_.contains(fieldNames, 'boardIds') && modifier.$pull) {
Cards.update( Cards.update(
{boardId: modifier.$pull.boardIds, 'customFields._id': doc._id}, { boardId: modifier.$pull.boardIds, 'customFields._id': doc._id },
{$pull: {'customFields': {'_id': doc._id}}}, { $pull: { customFields: { _id: doc._id } } },
{multi: true} { multi: true }
); );
customFieldEdit(userId, doc); customFieldEdit(userId, doc);
Activities.remove({ Activities.remove({
@ -180,9 +221,9 @@ if (Meteor.isServer) {
}); });
Cards.update( Cards.update(
{boardId: {$in: doc.boardIds}, 'customFields._id': doc._id}, { boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
{$pull: {'customFields': {'_id': doc._id}}}, { $pull: { customFields: { _id: doc._id } } },
{multi: true} { multi: true }
); );
}); });
} }
@ -198,18 +239,23 @@ if (Meteor.isServer) {
* name: string, * name: string,
* type: string}] * type: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function(
Authentication.checkUserId( req.userId); req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CustomFields.find({ boardIds: {$in: [paramBoardId]} }).map(function (cf) { data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
function(cf) {
return { return {
_id: cf._id, _id: cf._id,
name: cf.name, name: cf.name,
type: cf.type, type: cf.type,
}; };
}), }
),
}); });
}); });
@ -221,15 +267,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field * @param {string} customFieldId the ID of the custom field
* @return_type CustomFields * @return_type CustomFields
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId; const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }), data: CustomFields.findOne({
}); _id: paramCustomFieldId,
boardIds: { $in: [paramBoardId] },
}),
}); });
}
);
/** /**
* @operation new_custom_field * @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? * @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function(
Authentication.checkUserId( req.userId); req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const id = CustomFields.direct.insert({ const id = CustomFields.direct.insert({
name: req.body.name, name: req.body.name,
@ -254,10 +310,13 @@ if (Meteor.isServer) {
showOnCard: req.body.showOnCard, showOnCard: req.body.showOnCard,
automaticallyOnCard: req.body.automaticallyOnCard, automaticallyOnCard: req.body.automaticallyOnCard,
showLabelOnMiniCard: req.body.showLabelOnMiniCard, 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); customFieldCreation(req.body.authorId, customField);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -278,16 +337,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field * @param {string} customFieldId the ID of the custom field
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'DELETE',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const id = req.params.customFieldId; const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} }); CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: id, _id: id,
}, },
}); });
}); }
);
} }
export default CustomFields;

View file

@ -3,7 +3,8 @@ Integrations = new Mongo.Collection('integrations');
/** /**
* Integration with third-party applications * Integration with third-party applications
*/ */
Integrations.attachSchema(new SimpleSchema({ Integrations.attachSchema(
new SimpleSchema({
enabled: { enabled: {
/** /**
* is the integration enabled? * is the integration enabled?
@ -32,7 +33,8 @@ Integrations.attachSchema(new SimpleSchema({
type: [String], type: [String],
defaultValue: ['all'], defaultValue: ['all'],
}, },
url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex) url: {
// URL validation regex (https://mathiasbynens.be/demo/url-regex)
/** /**
* URL validation regex (https://mathiasbynens.be/demo/url-regex) * URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/ */
@ -57,7 +59,8 @@ Integrations.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -65,13 +68,31 @@ Integrations.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();
}
},
},
userId: { userId: {
/** /**
* user ID who created the interation * user ID who created the interation
*/ */
type: String, type: String,
}, },
})); })
);
Integrations.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Integrations.allow({ Integrations.allow({
insert(userId, doc) { insert(userId, doc) {
@ -89,6 +110,7 @@ Integrations.allow({
//INTEGRATIONS REST API //INTEGRATIONS REST API
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Integrations._collection._ensureIndex({ modifiedAt: -1 });
Integrations._collection._ensureIndex({ boardId: 1 }); Integrations._collection._ensureIndex({ boardId: 1 });
}); });
@ -99,18 +121,23 @@ if (Meteor.isServer) {
* @param {string} boardId the board ID * @param {string} boardId the board ID
* @return_type [Integrations] * @return_type [Integrations]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId); 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; return doc;
}); });
JsonRoutes.sendResult(res, {code: 200, data}); JsonRoutes.sendResult(res, { code: 200, data });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -126,7 +153,10 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID * @param {string} intId the integration ID
* @return_type Integrations * @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 { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
@ -134,10 +164,12 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, 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, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -153,7 +185,10 @@ if (Meteor.isServer) {
* @param {string} url the URL of the integration * @param {string} url the URL of the integration
* @return_type {_id: string} * @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 { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
@ -170,8 +205,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -192,7 +226,10 @@ if (Meteor.isServer) {
* @param {string} [activities] new list of activities of the integration * @param {string} [activities] new list of activities of the integration
* @return_type {_id: string} * @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 { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
@ -200,28 +237,38 @@ if (Meteor.isServer) {
if (req.body.hasOwnProperty('enabled')) { if (req.body.hasOwnProperty('enabled')) {
const newEnabled = req.body.enabled; const newEnabled = req.body.enabled;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {enabled: newEnabled}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { enabled: newEnabled } }
);
} }
if (req.body.hasOwnProperty('title')) { if (req.body.hasOwnProperty('title')) {
const newTitle = req.body.title; const newTitle = req.body.title;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {title: newTitle}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { title: newTitle } }
);
} }
if (req.body.hasOwnProperty('url')) { if (req.body.hasOwnProperty('url')) {
const newUrl = req.body.url; const newUrl = req.body.url;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {url: newUrl}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { url: newUrl } }
);
} }
if (req.body.hasOwnProperty('token')) { if (req.body.hasOwnProperty('token')) {
const newToken = req.body.token; const newToken = req.body.token;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {token: newToken}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { token: newToken } }
);
} }
if (req.body.hasOwnProperty('activities')) { if (req.body.hasOwnProperty('activities')) {
const newActivities = req.body.activities; const newActivities = req.body.activities;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {activities: newActivities}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { activities: newActivities } }
);
} }
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -230,8 +277,7 @@ if (Meteor.isServer) {
_id: paramIntId, _id: paramIntId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -248,28 +294,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to remove from the integration * @param {string} newActivities the activities to remove from the integration
* @return_type Integrations * @return_type Integrations
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) { JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/integrations/:intId/activities',
function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
const newActivities = req.body.activities; const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$pullAll: {activities: newActivities}}); { _id: paramIntId, boardId: paramBoardId },
{ $pullAll: { activities: newActivities } }
);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { _id: 1, activities: 1 } }
),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation new_integration_activities * @operation new_integration_activities
@ -280,28 +334,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to add to the integration * @param {string} newActivities the activities to add to the integration
* @return_type Integrations * @return_type Integrations
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) { JsonRoutes.add(
'POST',
'/api/boards/:boardId/integrations/:intId/activities',
function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
const newActivities = req.body.activities; const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$addToSet: {activities: { $each: newActivities}}}); { _id: paramIntId, boardId: paramBoardId },
{ $addToSet: { activities: { $each: newActivities } } }
);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { _id: 1, activities: 1 } }
),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation delete_integration * @operation delete_integration
@ -311,21 +373,23 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID * @param {string} intId the integration ID
* @return_type {_id: string} * @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 { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId}); Integrations.direct.remove({ _id: paramIntId, boardId: paramBoardId });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: paramIntId, _id: paramIntId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -333,3 +397,5 @@ if (Meteor.isServer) {
} }
}); });
} }
export default Integrations;

View file

@ -1,6 +1,7 @@
InvitationCodes = new Mongo.Collection('invitation_codes'); InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({ InvitationCodes.attachSchema(
new SimpleSchema({
code: { code: {
type: String, type: String,
}, },
@ -12,6 +13,27 @@ InvitationCodes.attachSchema(new SimpleSchema({
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: false, 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 // always be the admin if only one admin
authorId: { authorId: {
@ -25,21 +47,32 @@ InvitationCodes.attachSchema(new SimpleSchema({
type: Boolean, type: Boolean,
defaultValue: true, defaultValue: true,
}, },
})); })
);
InvitationCodes.helpers({ InvitationCodes.helpers({
author(){ author() {
return Users.findOne(this.authorId); 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) => { // InvitationCodes.before.insert((userId, doc) => {
// doc.createdAt = new Date(); // doc.createdAt = new Date();
// doc.authorId = userId; // doc.authorId = userId;
// }); // });
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => {
InvitationCodes._collection._ensureIndex({ modifiedAt: -1 });
});
Boards.deny({ Boards.deny({
fetch: ['members'], fetch: ['members'],
}); });
} }
export default InvitationCodes;

View file

@ -3,7 +3,8 @@ Lists = new Mongo.Collection('lists');
/** /**
* A list (column) in the Wekan board. * A list (column) in the Wekan board.
*/ */
Lists.attachSchema(new SimpleSchema({ Lists.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* the title of the list * the title of the list
@ -15,7 +16,8 @@ Lists.attachSchema(new SimpleSchema({
* is the list archived * is the list archived
*/ */
type: Boolean, type: Boolean,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return false; return false;
} }
@ -39,7 +41,8 @@ Lists.attachSchema(new SimpleSchema({
* creation date * creation date
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -62,8 +65,21 @@ Lists.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
if (this.isUpdate) { 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(); return new Date();
} else { } else {
this.unset(); this.unset();
@ -107,11 +123,30 @@ Lists.attachSchema(new SimpleSchema({
optional: true, optional: true,
// silver is the default, so it is left out // silver is the default, so it is left out
allowedValues: [ allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple', 'white',
'blue', 'sky', 'lime', 'pink', 'black', 'green',
'peachpuff', 'crimson', 'plum', 'darkgreen', 'yellow',
'slateblue', 'magenta', 'gold', 'navy', 'gray', 'orange',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', 'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
], ],
}, },
type: { type: {
@ -121,7 +156,8 @@ Lists.attachSchema(new SimpleSchema({
type: String, type: String,
defaultValue: 'list', defaultValue: 'list',
}, },
})); })
);
Lists.allow({ Lists.allow({
insert(userId, doc) { insert(userId, doc) {
@ -172,10 +208,8 @@ Lists.helpers({
listId: this._id, listId: this._id,
archived: false, archived: false,
}; };
if (swimlaneId) if (swimlaneId) selector.swimlaneId = swimlaneId;
selector.swimlaneId = swimlaneId; return Cards.find(Filter.mongoSelector(selector), { sort: ['sort'] });
return Cards.find(Filter.mongoSelector(selector),
{ sort: ['sort'] });
}, },
cardsUnfiltered(swimlaneId) { cardsUnfiltered(swimlaneId) {
@ -183,10 +217,8 @@ Lists.helpers({
listId: this._id, listId: this._id,
archived: false, archived: false,
}; };
if (swimlaneId) if (swimlaneId) selector.swimlaneId = swimlaneId;
selector.swimlaneId = swimlaneId; return Cards.find(selector, { sort: ['sort'] });
return Cards.find(selector,
{ sort: ['sort'] });
}, },
allCards() { allCards() {
@ -197,11 +229,12 @@ Lists.helpers({
return Boards.findOne(this.boardId); return Boards.findOne(this.boardId);
}, },
getWipLimit(option){ getWipLimit(option) {
const list = Lists.findOne({ _id: this._id }); 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; return 0;
} else if(!option) { } else if (!option) {
return list.wipLimit; return list.wipLimit;
} else { } 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 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() { colorClass() {
if (this.color) if (this.color) return this.color;
return this.color;
return ''; return '';
}, },
@ -219,7 +251,7 @@ Lists.helpers({
}, },
remove() { remove() {
Lists.remove({ _id: this._id}); Lists.remove({ _id: this._id });
}, },
}); });
@ -271,10 +303,10 @@ Lists.mutations({
}); });
Meteor.methods({ Meteor.methods({
applyWipLimit(listId, limit){ applyWipLimit(listId, limit) {
check(listId, String); check(listId, String);
check(limit, Number); check(limit, Number);
if(limit === 0){ if (limit === 0) {
limit = 1; limit = 1;
} }
Lists.findOne({ _id: listId }).setWipLimit(limit); Lists.findOne({ _id: listId }).setWipLimit(limit);
@ -283,7 +315,7 @@ Meteor.methods({
enableWipLimit(listId) { enableWipLimit(listId) {
check(listId, String); check(listId, String);
const list = Lists.findOne({ _id: listId }); const list = Lists.findOne({ _id: listId });
if(list.getWipLimit('value') === 0){ if (list.getWipLimit('value') === 0) {
list.setWipLimit(1); list.setWipLimit(1);
} }
list.toggleWipLimit(!list.getWipLimit('enabled')); list.toggleWipLimit(!list.getWipLimit('enabled'));
@ -300,6 +332,7 @@ Lists.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Lists._collection._ensureIndex({ modifiedAt: -1 });
Lists._collection._ensureIndex({ boardId: 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) => { Lists.before.remove((userId, doc) => {
const cards = Cards.find({ listId: doc._id }); const cards = Cards.find({ listId: doc._id });
if (cards) { if (cards) {
@ -353,22 +391,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
* title: string}] * title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/lists', function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) { data: Lists.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
}; };
}),
});
} }
catch (error) { ),
});
} catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -384,17 +423,23 @@ if (Meteor.isServer) {
* @param {string} listId the List ID * @param {string} listId the List ID
* @return_type Lists * @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 { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramListId = req.params.listId; const paramListId = req.params.listId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, 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, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -410,9 +455,9 @@ if (Meteor.isServer) {
* @param {string} title the title of the List * @param {string} title the title of the List
* @return_type {_id: string} * @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 { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId); const board = Boards.findOne(paramBoardId);
const id = Lists.insert({ const id = Lists.insert({
@ -426,8 +471,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -446,9 +490,12 @@ if (Meteor.isServer) {
* @param {string} listId the ID of the list to remove * @param {string} listId the ID of the list to remove
* @return_type {_id: string} * @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 { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramListId = req.params.listId; const paramListId = req.params.listId;
Lists.remove({ _id: paramListId, boardId: paramBoardId }); Lists.remove({ _id: paramListId, boardId: paramBoardId });
@ -458,13 +505,13 @@ if (Meteor.isServer) {
_id: paramListId, _id: paramListId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); });
} }
export default Lists;

View file

@ -1,6 +1,9 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules'); Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({ Rules.attachSchema(
new SimpleSchema({
title: { title: {
type: String, type: String,
optional: false, optional: false,
@ -17,7 +20,32 @@ Rules.attachSchema(new SimpleSchema({
type: String, type: String,
optional: false, 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({ Rules.mutations({
rename(description) { rename(description) {
@ -26,15 +54,14 @@ Rules.mutations({
}); });
Rules.helpers({ Rules.helpers({
getAction(){ getAction() {
return Actions.findOne({_id:this.actionId}); return Actions.findOne({ _id: this.actionId });
}, },
getTrigger(){ getTrigger() {
return Triggers.findOne({_id:this.triggerId}); return Triggers.findOne({ _id: this.triggerId });
}, },
}); });
Rules.allow({ Rules.allow({
insert(userId, doc) { insert(userId, doc) {
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
@ -46,3 +73,16 @@ Rules.allow({
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); 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;

View file

@ -1,6 +1,7 @@
Settings = new Mongo.Collection('settings'); Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({ Settings.attachSchema(
new SimpleSchema({
disableRegistration: { disableRegistration: {
type: Boolean, type: Boolean,
}, },
@ -55,13 +56,30 @@ Settings.attachSchema(new SimpleSchema({
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: true, denyUpdate: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
}, },
modifiedAt: { modifiedAt: {
type: Date, type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
})); },
})
);
Settings.helpers({ Settings.helpers({
mailUrl () { mailUrl() {
if (!this.mailServer.host) { if (!this.mailServer.host) {
return null; return null;
} }
@ -69,7 +87,9 @@ Settings.helpers({
if (!this.mailServer.username && !this.mailServer.password) { if (!this.mailServer.username && !this.mailServer.password) {
return `${protocol}${this.mailServer.host}:${this.mailServer.port}/`; 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({ Settings.allow({
@ -86,50 +106,75 @@ Settings.before.update((userId, doc, fieldNames, modifier) => {
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Settings._collection._ensureIndex({ modifiedAt: -1 });
const setting = Settings.findOne({}); const setting = Settings.findOne({});
if(!setting){ if (!setting) {
const now = new Date(); 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 from = `Boards Support <support@${domain}>`;
const defaultSetting = {disableRegistration: false, mailServer: { const defaultSetting = {
username: '', password: '', host: '', port: '', enableTLS: false, from, disableRegistration: false,
}, createdAt: now, modifiedAt: now, displayAuthenticationMethod: true, mailServer: {
defaultAuthenticationMethod: 'password'}; username: '',
password: '',
host: '',
port: '',
enableTLS: false,
from,
},
createdAt: now,
modifiedAt: now,
displayAuthenticationMethod: true,
defaultAuthenticationMethod: 'password',
};
Settings.insert(defaultSetting); Settings.insert(defaultSetting);
} }
const newSetting = Settings.findOne(); const newSetting = Settings.findOne();
if (!process.env.MAIL_URL && newSetting.mailUrl()) if (!process.env.MAIL_URL && newSetting.mailUrl())
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) => { Settings.after.update((userId, doc, fieldNames) => {
// assign new values to mail-from & MAIL_URL in environment // assign new values to mail-from & MAIL_URL in environment
if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) { if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) {
const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://'; const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://';
if (!doc.mailServer.username && !doc.mailServer.password) { 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 { } 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; Accounts.emailTemplates.from = doc.mailServer.from;
} }
}); });
function getRandomNum (min, max) { function getRandomNum(min, max) {
const range = max - min; const range = max - min;
const rand = Math.random(); 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]; const value = process.env[name];
if (value){ if (value) {
return 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 icode = InvitationCodes.findOne(_id);
const author = Users.findOne(Meteor.userId()); const author = Users.findOne(Meteor.userId());
try { try {
@ -172,30 +217,47 @@ if (Meteor.isServer) {
check(boards, [String]); check(boards, [String]);
const user = Users.findOne(Meteor.userId()); const user = Users.findOne(Meteor.userId());
if(!user.isAdmin){ if (!user.isAdmin) {
throw new Meteor.Error('not-allowed'); throw new Meteor.Error('not-allowed');
} }
emails.forEach((email) => { emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email)) { if (email && SimpleSchema.RegEx.Email.test(email)) {
// Checks if the email is already link to an account. // Checks if the email is already link to an account.
const userExist = Users.findOne({email}); const userExist = Users.findOne({ email });
if (userExist){ if (userExist) {
throw new Meteor.Error('user-exist', `The user with the email ${email} has already an account.`); 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. // Checks if the email is already link to an invitation.
const invitation = InvitationCodes.findOne({email}); const invitation = InvitationCodes.findOne({ email });
if (invitation){ if (invitation) {
InvitationCodes.update(invitation, {$set : {boardsToBeInvited: boards}}); InvitationCodes.update(invitation, {
$set: { boardsToBeInvited: boards },
});
sendInvitationEmail(invitation._id); sendInvitationEmail(invitation._id);
}else { } else {
const code = getRandomNum(100000, 999999); const code = getRandomNum(100000, 999999);
InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){ InvitationCodes.insert(
{
code,
email,
boardsToBeInvited: boards,
createdAt: new Date(),
authorId: Meteor.userId(),
},
function(err, _id) {
if (!err && _id) { if (!err && _id) {
sendInvitationEmail(_id); sendInvitationEmail(_id);
} else { } else {
throw new Meteor.Error('invitation-generated-fail', err.message); throw new Meteor.Error(
'invitation-generated-fail',
err.message
);
} }
}); }
);
} }
} }
}); });
@ -215,11 +277,15 @@ if (Meteor.isServer) {
Email.send({ Email.send({
to: user.emails[0].address, to: user.emails[0].address,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-smtp-test-subject', {lng: lang}), subject: TAPi18n.__('email-smtp-test-subject', { lng: lang }),
text: TAPi18n.__('email-smtp-test-text', {lng: lang}), text: TAPi18n.__('email-smtp-test-text', { lng: lang }),
}); });
} catch ({message}) { } catch ({ message }) {
throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message); throw new Meteor.Error(
'email-fail',
`${TAPi18n.__('email-fail-text', { lng: lang })}: ${message}`,
message
);
} }
return { return {
message: 'email-sent', message: 'email-sent',
@ -227,7 +293,7 @@ if (Meteor.isServer) {
}; };
}, },
getCustomUI(){ getCustomUI() {
const setting = Settings.findOne({}); const setting = Settings.findOne({});
if (!setting.productName) { if (!setting.productName) {
return { return {
@ -240,7 +306,7 @@ if (Meteor.isServer) {
} }
}, },
getMatomoConf(){ getMatomoConf() {
return { return {
address: getEnvVar('MATOMO_ADDRESS'), address: getEnvVar('MATOMO_ADDRESS'),
siteId: getEnvVar('MATOMO_SITE_ID'), siteId: getEnvVar('MATOMO_SITE_ID'),
@ -275,3 +341,5 @@ if (Meteor.isServer) {
}, },
}); });
} }
export default Settings;

View file

@ -3,7 +3,8 @@ Swimlanes = new Mongo.Collection('swimlanes');
/** /**
* A swimlane is an line in the kaban board. * A swimlane is an line in the kaban board.
*/ */
Swimlanes.attachSchema(new SimpleSchema({ Swimlanes.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* the title of the swimlane * the title of the swimlane
@ -15,7 +16,8 @@ Swimlanes.attachSchema(new SimpleSchema({
* is the swimlane archived? * is the swimlane archived?
*/ */
type: Boolean, type: Boolean,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return false; return false;
} }
@ -32,7 +34,8 @@ Swimlanes.attachSchema(new SimpleSchema({
* creation date of the swimlane * creation date of the swimlane
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -57,11 +60,30 @@ Swimlanes.attachSchema(new SimpleSchema({
optional: true, optional: true,
// silver is the default, so it is left out // silver is the default, so it is left out
allowedValues: [ allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple', 'white',
'blue', 'sky', 'lime', 'pink', 'black', 'green',
'peachpuff', 'crimson', 'plum', 'darkgreen', 'yellow',
'slateblue', 'magenta', 'gold', 'navy', 'gray', 'orange',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', 'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
], ],
}, },
updatedAt: { updatedAt: {
@ -70,8 +92,21 @@ Swimlanes.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
if (this.isUpdate) { 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(); return new Date();
} else { } else {
this.unset(); this.unset();
@ -85,7 +120,8 @@ Swimlanes.attachSchema(new SimpleSchema({
type: String, type: String,
defaultValue: 'swimlane', defaultValue: 'swimlane',
}, },
})); })
);
Swimlanes.allow({ Swimlanes.allow({
insert(userId, doc) { insert(userId, doc) {
@ -109,7 +145,7 @@ Swimlanes.helpers({
const _id = Swimlanes.insert(this); const _id = Swimlanes.insert(this);
const query = { const query = {
swimlaneId: {$in: [oldId, '']}, swimlaneId: { $in: [oldId, ''] },
archived: false, archived: false,
}; };
if (oldBoardId) { if (oldBoardId) {
@ -126,18 +162,24 @@ Swimlanes.helpers({
}, },
cards() { cards() {
return Cards.find(Filter.mongoSelector({ return Cards.find(
Filter.mongoSelector({
swimlaneId: this._id, swimlaneId: this._id,
archived: false, archived: false,
}), { sort: ['sort'] }); }),
{ sort: ['sort'] }
);
}, },
lists() { lists() {
return Lists.find({ return Lists.find(
{
boardId: this.boardId, boardId: this.boardId,
swimlaneId: {$in: [this._id, '']}, swimlaneId: { $in: [this._id, ''] },
archived: false, archived: false,
}, { sort: ['sort'] }); },
{ sort: ['sort'] }
);
}, },
myLists() { myLists() {
@ -153,8 +195,7 @@ Swimlanes.helpers({
}, },
colorClass() { colorClass() {
if (this.color) if (this.color) return this.color;
return this.color;
return ''; return '';
}, },
@ -182,7 +223,7 @@ Swimlanes.helpers({
}, },
remove() { 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 }; Swimlanes.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Swimlanes._collection._ensureIndex({ modifiedAt: -1 });
Swimlanes._collection._ensureIndex({ boardId: 1 }); Swimlanes._collection._ensureIndex({ boardId: 1 });
}); });
@ -239,18 +286,21 @@ if (Meteor.isServer) {
}); });
Swimlanes.before.remove(function(userId, doc) { Swimlanes.before.remove(function(userId, doc) {
const lists = Lists.find({ const lists = Lists.find(
{
boardId: doc.boardId, boardId: doc.boardId,
swimlaneId: {$in: [doc._id, '']}, swimlaneId: { $in: [doc._id, ''] },
archived: false, archived: false,
}, { sort: ['sort'] }); },
{ sort: ['sort'] }
);
if (lists.count() < 2) { if (lists.count() < 2) {
lists.forEach((list) => { lists.forEach((list) => {
list.remove(); list.remove();
}); });
} else { } else {
Cards.remove({swimlaneId: doc._id}); Cards.remove({ swimlaneId: doc._id });
} }
Activities.insert({ Activities.insert({
@ -287,22 +337,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
* title: string}] * title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) { data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
}; };
}),
});
} }
catch (error) { ),
});
} catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -319,17 +370,23 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane * @param {string} swimlaneId the ID of the swimlane
* @return_type Swimlanes * @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 { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId; const paramSwimlaneId = req.params.swimlaneId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, 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, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -346,9 +403,9 @@ if (Meteor.isServer) {
* @param {string} title the new title of the swimlane * @param {string} title the new title of the swimlane
* @return_type {_id: string} * @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 { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId); const board = Boards.findOne(paramBoardId);
const id = Swimlanes.insert({ const id = Swimlanes.insert({
@ -362,8 +419,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -382,9 +438,12 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane * @param {string} swimlaneId the ID of the swimlane
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) { JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/swimlanes/:swimlaneId',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId; const paramSwimlaneId = req.params.swimlaneId;
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId }); Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
@ -394,13 +453,14 @@ if (Meteor.isServer) {
_id: paramSwimlaneId, _id: paramSwimlaneId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
} }
export default Swimlanes;

View file

@ -1,3 +1,5 @@
import { Meteor } from 'meteor/meteor';
Triggers = new Mongo.Collection('triggers'); Triggers = new Mongo.Collection('triggers');
Triggers.mutations({ Triggers.mutations({
@ -23,7 +25,6 @@ Triggers.allow({
}); });
Triggers.helpers({ Triggers.helpers({
description() { description() {
return this.desc; return this.desc;
}, },
@ -56,3 +57,16 @@ Triggers.helpers({
return cardLabels; 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;

View file

@ -2,7 +2,8 @@
// `UnsavedEdits` API on the client. // `UnsavedEdits` API on the client.
UnsavedEditCollection = new Mongo.Collection('unsaved-edits'); UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
UnsavedEditCollection.attachSchema(new SimpleSchema({ UnsavedEditCollection.attachSchema(
new SimpleSchema({
fieldName: { fieldName: {
type: String, type: String,
}, },
@ -14,19 +15,53 @@ UnsavedEditCollection.attachSchema(new SimpleSchema({
}, },
userId: { userId: {
type: String, type: String,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return this.userId; 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) { if (Meteor.isServer) {
function isAuthor(userId, doc, fieldNames = []) { function isAuthor(userId, doc, fieldNames = []) {
return userId === doc.userId && fieldNames.indexOf('userId') === -1; return userId === doc.userId && fieldNames.indexOf('userId') === -1;
} }
Meteor.startup(() => { Meteor.startup(() => {
UnsavedEditCollection._collection._ensureIndex({ modifiedAt: -1 });
UnsavedEditCollection._collection._ensureIndex({ userId: 1 }); UnsavedEditCollection._collection._ensureIndex({ userId: 1 });
}); });
UnsavedEditCollection.allow({ UnsavedEditCollection.allow({
@ -36,3 +71,5 @@ if (Meteor.isServer) {
fetch: ['userId'], fetch: ['userId'],
}); });
} }
export default UnsavedEditCollection;

View file

@ -1,20 +1,22 @@
// Sandstorm context is detected using the METEOR_SETTINGS environment variable // Sandstorm context is detected using the METEOR_SETTINGS environment variable
// in the package definition. // in the package definition.
const isSandstorm = Meteor.settings && Meteor.settings.public && const isSandstorm =
Meteor.settings.public.sandstorm; Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
Users = Meteor.users; Users = Meteor.users;
/** /**
* A User in wekan * A User in wekan
*/ */
Users.attachSchema(new SimpleSchema({ Users.attachSchema(
new SimpleSchema({
username: { username: {
/** /**
* the username of the user * the username of the user
*/ */
type: String, type: String,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
const name = this.field('profile.fullname'); const name = this.field('profile.fullname');
if (name.isSet) { if (name.isSet) {
@ -48,7 +50,8 @@ Users.attachSchema(new SimpleSchema({
* creation date of the user * creation date of the user
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -56,13 +59,26 @@ Users.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();
}
},
},
profile: { profile: {
/** /**
* profile settings * profile settings
*/ */
type: Object, type: Object,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return { return {
boardView: 'board-view-lists', boardView: 'board-view-lists',
@ -223,7 +239,7 @@ Users.attachSchema(new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
'authenticationMethod': { authenticationMethod: {
/** /**
* authentication method of the user * authentication method of the user
*/ */
@ -231,7 +247,8 @@ Users.attachSchema(new SimpleSchema({
optional: false, optional: false,
defaultValue: 'password', defaultValue: 'password',
}, },
})); })
);
Users.allow({ Users.allow({
update(userId) { update(userId) {
@ -240,7 +257,10 @@ Users.allow({
}, },
remove(userId, doc) { remove(userId, doc) {
const adminsNumber = Users.find({ isAdmin: true }).count(); 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 // Prevents remove of the only one administrator
if (adminsNumber === 1 && isAdmin && userId === doc._id) { if (adminsNumber === 1 && isAdmin && userId === doc._id) {
@ -270,7 +290,9 @@ if (Meteor.isClient) {
isNotNoComments() { isNotNoComments() {
const board = Boards.findOne(Session.get('currentBoard')); 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() { isNoComments() {
@ -280,7 +302,9 @@ if (Meteor.isClient) {
isNotCommentOnly() { isNotCommentOnly() {
const board = Boards.findOne(Session.get('currentBoard')); 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() { isCommentOnly() {
@ -301,32 +325,32 @@ Users.helpers({
}, },
starredBoards() { starredBoards() {
const {starredBoards = []} = this.profile || {}; const { starredBoards = [] } = this.profile || {};
return Boards.find({archived: false, _id: {$in: starredBoards}}); return Boards.find({ archived: false, _id: { $in: starredBoards } });
}, },
hasStarred(boardId) { hasStarred(boardId) {
const {starredBoards = []} = this.profile || {}; const { starredBoards = [] } = this.profile || {};
return _.contains(starredBoards, boardId); return _.contains(starredBoards, boardId);
}, },
invitedBoards() { invitedBoards() {
const {invitedBoards = []} = this.profile || {}; const { invitedBoards = [] } = this.profile || {};
return Boards.find({archived: false, _id: {$in: invitedBoards}}); return Boards.find({ archived: false, _id: { $in: invitedBoards } });
}, },
isInvitedTo(boardId) { isInvitedTo(boardId) {
const {invitedBoards = []} = this.profile || {}; const { invitedBoards = [] } = this.profile || {};
return _.contains(invitedBoards, boardId); return _.contains(invitedBoards, boardId);
}, },
hasTag(tag) { hasTag(tag) {
const {tags = []} = this.profile || {}; const { tags = [] } = this.profile || {};
return _.contains(tags, tag); return _.contains(tags, tag);
}, },
hasNotification(activityId) { hasNotification(activityId) {
const {notifications = []} = this.profile || {}; const { notifications = [] } = this.profile || {};
return _.contains(notifications, activityId); return _.contains(notifications, activityId);
}, },
@ -336,20 +360,20 @@ Users.helpers({
}, },
getEmailBuffer() { getEmailBuffer() {
const {emailBuffer = []} = this.profile || {}; const { emailBuffer = [] } = this.profile || {};
return emailBuffer; return emailBuffer;
}, },
getInitials() { getInitials() {
const profile = this.profile || {}; const profile = this.profile || {};
if (profile.initials) if (profile.initials) return profile.initials;
return profile.initials;
else if (profile.fullname) { else if (profile.fullname) {
return profile.fullname.split(/\s+/).reduce((memo, word) => { return profile.fullname
.split(/\s+/)
.reduce((memo, word) => {
return memo + word[0]; return memo + word[0];
}, '').toUpperCase(); }, '')
.toUpperCase();
} else { } else {
return this.username[0].toUpperCase(); return this.username[0].toUpperCase();
} }
@ -379,7 +403,7 @@ Users.helpers({
}, },
remove() { remove() {
User.remove({ _id: this._id}); User.remove({ _id: this._id });
}, },
}); });
@ -426,10 +450,8 @@ Users.mutations({
}, },
toggleTag(tag) { toggleTag(tag) {
if (this.hasTag(tag)) if (this.hasTag(tag)) this.removeTag(tag);
this.removeTag(tag); else this.addTag(tag);
else
this.addTag(tag);
}, },
toggleSystem(value = false) { toggleSystem(value = false) {
@ -473,16 +495,16 @@ Users.mutations({
}, },
setAvatarUrl(avatarUrl) { setAvatarUrl(avatarUrl) {
return {$set: {'profile.avatarUrl': avatarUrl}}; return { $set: { 'profile.avatarUrl': avatarUrl } };
}, },
setShowCardsCountAt(limit) { setShowCardsCountAt(limit) {
return {$set: {'profile.showCardsCountAt': limit}}; return { $set: { 'profile.showCardsCountAt': limit } };
}, },
setBoardView(view) { setBoardView(view) {
return { return {
$set : { $set: {
'profile.boardView': view, 'profile.boardView': view,
}, },
}; };
@ -492,11 +514,11 @@ Users.mutations({
Meteor.methods({ Meteor.methods({
setUsername(username, userId) { setUsername(username, userId) {
check(username, String); check(username, String);
const nUsersWithUsername = Users.find({username}).count(); const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) { if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken'); throw new Meteor.Error('username-already-taken');
} else { } else {
Users.update(userId, {$set: {username}}); Users.update(userId, { $set: { username } });
} }
}, },
toggleSystemMessages() { toggleSystemMessages() {
@ -509,16 +531,21 @@ Meteor.methods({
}, },
setEmail(email, userId) { setEmail(email, userId) {
check(email, String); 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) { if (existingUser) {
throw new Meteor.Error('email-already-taken'); throw new Meteor.Error('email-already-taken');
} else { } else {
Users.update(userId, { Users.update(userId, {
$set: { $set: {
emails: [{ emails: [
{
address: email, address: email,
verified: false, verified: false,
}], },
],
}, },
}); });
} }
@ -533,7 +560,7 @@ Meteor.methods({
setPassword(newPassword, userId) { setPassword(newPassword, userId) {
check(userId, String); check(userId, String);
check(newPassword, String); check(newPassword, String);
if(Meteor.user().isAdmin){ if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword); Accounts.setPassword(userId, newPassword);
} }
}, },
@ -548,12 +575,13 @@ if (Meteor.isServer) {
const inviter = Meteor.user(); const inviter = Meteor.user();
const board = Boards.findOne(boardId); const board = Boards.findOne(boardId);
const allowInvite = inviter && const allowInvite =
inviter &&
board && board &&
board.members && board.members &&
_.contains(_.pluck(board.members, 'userId'), inviter._id) && _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
_.where(board.members, {userId: inviter._id})[0].isActive && _.where(board.members, { userId: inviter._id })[0].isActive &&
_.where(board.members, {userId: inviter._id})[0].isAdmin; _.where(board.members, { userId: inviter._id })[0].isAdmin;
if (!allowInvite) throw new Meteor.Error('error-board-notAMember'); if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
this.unblock(); this.unblock();
@ -561,19 +589,21 @@ if (Meteor.isServer) {
const posAt = username.indexOf('@'); const posAt = username.indexOf('@');
let user = null; let user = null;
if (posAt >= 0) { if (posAt >= 0) {
user = Users.findOne({emails: {$elemMatch: {address: username}}}); user = Users.findOne({ emails: { $elemMatch: { address: username } } });
} else { } else {
user = Users.findOne(username) || Users.findOne({username}); user = Users.findOne(username) || Users.findOne({ username });
} }
if (user) { 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 { } else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist'); 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 // Set in lowercase email before creating account
const email = username.toLowerCase(); const email = username.toLowerCase();
username = email.substring(0, posAt); 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'); if (!newUserId) throw new Meteor.Error('error-user-notCreated');
// assume new user speak same language with inviter // assume new user speak same language with inviter
if (inviter.profile && inviter.profile.language) { if (inviter.profile && inviter.profile.language) {
@ -607,7 +637,7 @@ if (Meteor.isServer) {
} catch (e) { } catch (e) {
throw new Meteor.Error('email-fail', e.message); 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) => { Accounts.onCreateUser((options, user) => {
@ -621,14 +651,22 @@ if (Meteor.isServer) {
const email = user.services.oidc.email.toLowerCase(); const email = user.services.oidc.email.toLowerCase();
user.username = user.services.oidc.username; user.username = user.services.oidc.username;
user.emails = [{ address: email, verified: true }]; user.emails = [{ address: email, verified: true }];
const initials = user.services.oidc.fullname.match(/\b[a-zA-Z]/g).join('').toUpperCase(); const initials = user.services.oidc.fullname
user.profile = { initials, fullname: user.services.oidc.fullname, boardView: 'board-view-lists' }; .match(/\b[a-zA-Z]/g)
.join('')
.toUpperCase();
user.profile = {
initials,
fullname: user.services.oidc.fullname,
boardView: 'board-view-lists',
};
user.authenticationMethod = 'oauth2'; user.authenticationMethod = 'oauth2';
// see if any existing user has this email address or username, otherwise create new // 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}]}); const existingUser = Meteor.users.findOne({
if (!existingUser) $or: [{ 'emails.address': email }, { username: user.username }],
return user; });
if (!existingUser) return user;
// copy across new service info // copy across new service info
const service = _.keys(user.services)[0]; const service = _.keys(user.services)[0];
@ -638,7 +676,7 @@ if (Meteor.isServer) {
existingUser.profile = user.profile; existingUser.profile = user.profile;
existingUser.authenticationMethod = user.authenticationMethod; existingUser.authenticationMethod = user.authenticationMethod;
Meteor.users.remove({_id: existingUser._id}); // remove existing record Meteor.users.remove({ _id: existingUser._id }); // remove existing record
return existingUser; return existingUser;
} }
@ -660,7 +698,10 @@ if (Meteor.isServer) {
} }
if (!options || !options.profile) { 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({ const invitationCode = InvitationCodes.findOne({
code: options.profile.invitationcode, code: options.profile.invitationcode,
@ -668,26 +709,41 @@ if (Meteor.isServer) {
valid: true, valid: true,
}); });
if (!invitationCode) { 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 { } else {
user.profile = {icode: options.profile.invitationcode}; user.profile = { icode: options.profile.invitationcode };
user.profile.boardView = 'board-view-lists'; user.profile.boardView = 'board-view-lists';
// Deletes the invitation code after the user was created successfully. // Deletes the invitation code after the user was created successfully.
setTimeout(Meteor.bindEnvironment(() => { setTimeout(
InvitationCodes.remove({'_id': invitationCode._id}); Meteor.bindEnvironment(() => {
}), 200); InvitationCodes.remove({ _id: invitationCode._id });
}),
200
);
return user; return user;
} }
}); });
} }
Users.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) { if (Meteor.isServer) {
// Let mongoDB ensure username unicity // Let mongoDB ensure username unicity
Meteor.startup(() => { Meteor.startup(() => {
Users._collection._ensureIndex({ Users._collection._ensureIndex({ modifiedAt: -1 });
Users._collection._ensureIndex(
{
username: 1, username: 1,
}, {unique: true}); },
{ unique: true }
);
}); });
// OLD WAY THIS CODE DID WORK: When user is last admin of board, // OLD WAY THIS CODE DID WORK: When user is last admin of board,
@ -712,11 +768,10 @@ if (Meteor.isServer) {
// counter. // counter.
// We need to run this code on the server only, otherwise the incrementation // We need to run this code on the server only, otherwise the incrementation
// will be done twice. // 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 // The `starredBoards` list is hosted on the `profile` field. If this
// field hasn't been modificated we don't need to run this hook. // field hasn't been modificated we don't need to run this hook.
if (!_.contains(fieldNames, 'profile')) if (!_.contains(fieldNames, 'profile')) return;
return;
// To calculate a diff of board starred ids, we get both the previous // To calculate a diff of board starred ids, we get both the previous
// and the newly board ids list // and the newly board ids list
@ -732,7 +787,7 @@ if (Meteor.isServer) {
// direction and then in the other. // direction and then in the other.
function incrementBoards(boardsIds, inc) { function incrementBoards(boardsIds, inc) {
boardsIds.forEach((boardId) => { boardsIds.forEach((boardId) => {
Boards.update(boardId, {$inc: {stars: inc}}); Boards.update(boardId, { $inc: { stars: inc } });
}); });
} }
@ -773,57 +828,76 @@ if (Meteor.isServer) {
}); });
*/ */
Boards.insert({ Boards.insert(
{
title: TAPi18n.__('templates'), title: TAPi18n.__('templates'),
permission: 'private', permission: 'private',
type: 'template-container', type: 'template-container',
}, fakeUser, (err, boardId) => { },
fakeUser,
(err, boardId) => {
// Insert the reference to our templates board // Insert the reference to our templates board
Users.update(fakeUserId.get(), {$set: {'profile.templatesBoardId': boardId}}); Users.update(fakeUserId.get(), {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the card templates swimlane // Insert the card templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('card-templates-swimlane'), title: TAPi18n.__('card-templates-swimlane'),
boardId, boardId,
sort: 1, sort: 1,
type: 'template-container', type: 'template-container',
}, fakeUser, (err, swimlaneId) => { },
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane // Insert the reference to out card templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); Users.update(fakeUserId.get(), {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the list templates swimlane // Insert the list templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'), title: TAPi18n.__('list-templates-swimlane'),
boardId, boardId,
sort: 2, sort: 2,
type: 'template-container', type: 'template-container',
}, fakeUser, (err, swimlaneId) => { },
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane // Insert the reference to out list templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); Users.update(fakeUserId.get(), {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the board templates swimlane // Insert the board templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'), title: TAPi18n.__('board-templates-swimlane'),
boardId, boardId,
sort: 3, sort: 3,
type: 'template-container', type: 'template-container',
}, fakeUser, (err, swimlaneId) => { },
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane // Insert the reference to out board templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); Users.update(fakeUserId.get(), {
}); $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
}
);
}); });
}); });
} }
Users.after.insert((userId, doc) => { Users.after.insert((userId, doc) => {
if (doc.createdThroughApi) { if (doc.createdThroughApi) {
// The admin user should be able to create a user despite disabling registration because // The admin user should be able to create a user despite disabling registration because
// it is two different things (registration and creation). // it is two different things (registration and creation).
@ -831,7 +905,7 @@ if (Meteor.isServer) {
// the disableRegistration check. // the disableRegistration check.
// Issue : https://github.com/wekan/wekan/issues/1232 // Issue : https://github.com/wekan/wekan/issues/1232
// PR : https://github.com/wekan/wekan/pull/1251 // PR : https://github.com/wekan/wekan/pull/1251
Users.update(doc._id, {$set: {createdThroughApi: ''}}); Users.update(doc._id, { $set: { createdThroughApi: '' } });
return; return;
} }
@ -840,7 +914,10 @@ if (Meteor.isServer) {
// If ldap, bypass the inviation code if the self registration isn't allowed. // 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 // 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) { 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) { if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist'); throw new Meteor.Error('error-invitation-code-not-exist');
} else { } else {
@ -852,8 +929,8 @@ if (Meteor.isServer) {
doc.profile = {}; doc.profile = {};
} }
doc.profile.invitedBoards = invitationCode.boardsToBeInvited; doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
Users.update(doc._id, {$set: {profile: doc.profile}}); Users.update(doc._id, { $set: { profile: doc.profile } });
InvitationCodes.update(invitationCode._id, {$set: {valid: false}}); InvitationCodes.update(invitationCode._id, { $set: { valid: false } });
} }
} }
}); });
@ -862,13 +939,12 @@ if (Meteor.isServer) {
// USERS REST API // USERS REST API
if (Meteor.isServer) { if (Meteor.isServer) {
// Middleware which checks that API is enabled. // 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'); 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(); return next();
} } else {
else { res.writeHead(301, { Location: '/' });
res.writeHead(301, {Location: '/'});
return res.end(); return res.end();
} }
}); });
@ -882,14 +958,13 @@ if (Meteor.isServer) {
JsonRoutes.add('GET', '/api/user', function(req, res) { JsonRoutes.add('GET', '/api/user', function(req, res) {
try { try {
Authentication.checkLoggedIn(req.userId); Authentication.checkLoggedIn(req.userId);
const data = Meteor.users.findOne({ _id: req.userId}); const data = Meteor.users.findOne({ _id: req.userId });
delete data.services; delete data.services;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data, data,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -906,17 +981,16 @@ if (Meteor.isServer) {
* @return_type [{ _id: string, * @return_type [{ _id: string,
* username: string}] * username: string}]
*/ */
JsonRoutes.add('GET', '/api/users', function (req, res) { JsonRoutes.add('GET', '/api/users', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Meteor.users.find({}).map(function (doc) { data: Meteor.users.find({}).map(function(doc) {
return { _id: doc._id, username: doc.username }; return { _id: doc._id, username: doc.username };
}), }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -934,7 +1008,7 @@ if (Meteor.isServer) {
* @param {string} userId the user ID * @param {string} userId the user ID
* @return_type Users * @return_type Users
*/ */
JsonRoutes.add('GET', '/api/users/:userId', function (req, res) { JsonRoutes.add('GET', '/api/users/:userId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.userId; const id = req.params.userId;
@ -942,8 +1016,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: Meteor.users.findOne({ _id: id }), data: Meteor.users.findOne({ _id: id }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -968,7 +1041,7 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
* title: string} * title: string}
*/ */
JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) { JsonRoutes.add('PUT', '/api/users/:userId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.userId; const id = req.params.userId;
@ -990,8 +1063,16 @@ if (Meteor.isServer) {
}; };
}); });
} else { } else {
if ((action === 'disableLogin') && (id !== req.userId)) { if (action === 'disableLogin' && id !== req.userId) {
Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } }); Users.update(
{ _id: id },
{
$set: {
loginDisabled: true,
'services.resume.loginTokens': '',
},
}
);
} else if (action === 'enableLogin') { } else if (action === 'enableLogin') {
Users.update({ _id: id }, { $set: { loginDisabled: '' } }); Users.update({ _id: id }, { $set: { loginDisabled: '' } });
} }
@ -1002,8 +1083,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data, data,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1030,13 +1110,16 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
* title: 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 { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const userId = req.params.userId; const userId = req.params.userId;
const boardId = req.params.boardId; const boardId = req.params.boardId;
const action = req.body.action; const action = req.body.action;
const {isAdmin, isNoComments, isCommentOnly} = req.body; const { isAdmin, isNoComments, isCommentOnly } = req.body;
let data = Meteor.users.findOne({ _id: userId }); let data = Meteor.users.findOne({ _id: userId });
if (data !== undefined) { if (data !== undefined) {
if (action === 'add') { if (action === 'add') {
@ -1045,10 +1128,16 @@ if (Meteor.isServer) {
}).map(function(board) { }).map(function(board) {
if (!board.hasMember(userId)) { if (!board.hasMember(userId)) {
board.addMember(userId); board.addMember(userId);
function isTrue(data){ function isTrue(data) {
return data.toLowerCase() === 'true'; 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 { return {
_id: board._id, _id: board._id,
@ -1061,8 +1150,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: query, data: query,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1084,7 +1172,10 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
* title: string} * title: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/remove', function (req, res) { JsonRoutes.add(
'POST',
'/api/boards/:boardId/members/:userId/remove',
function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const userId = req.params.userId; const userId = req.params.userId;
@ -1110,14 +1201,14 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: query, data: query,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation new_user * @operation new_user
@ -1131,7 +1222,7 @@ if (Meteor.isServer) {
* @param {string} password the password of the new user * @param {string} password the password of the new user
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/users/', function (req, res) { JsonRoutes.add('POST', '/api/users/', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = Accounts.createUser({ const id = Accounts.createUser({
@ -1146,8 +1237,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1165,7 +1255,7 @@ if (Meteor.isServer) {
* @param {string} userId the ID of the user to delete * @param {string} userId the ID of the user to delete
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) { JsonRoutes.add('DELETE', '/api/users/:userId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.userId; const id = req.params.userId;
@ -1176,8 +1266,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1185,3 +1274,5 @@ if (Meteor.isServer) {
} }
}); });
} }
export default Users;

1
packages/wekan-iframe Submodule

@ -0,0 +1 @@
Subproject commit e105dcc9c3424beee0ff0a9db9ca543a6d4b7f85

View file

@ -0,0 +1 @@
.versions

View file

@ -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.

View file

@ -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: []
}
}
);
});
}
```

View file

@ -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']
});
}

View file

@ -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==');
}

View file

@ -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');
});

1
packages/wekan_oidc/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.versions

View file

@ -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.

View file

@ -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.

View file

@ -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,
});
};

View file

@ -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>

View file

@ -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'}
];
};

View file

@ -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);
};

View file

@ -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');
});

View file

@ -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 // 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 // compatible way you have to write a migration in this file using the following
// API: // API:
@ -28,18 +48,22 @@ const noValidateMulti = { ...noValidate, multi: true };
Migrations.add('board-background-color', () => { Migrations.add('board-background-color', () => {
const defaultColor = '#16A085'; const defaultColor = '#16A085';
Boards.update({ Boards.update(
{
background: { background: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
background: { background: {
type: 'color', type: 'color',
color: defaultColor, color: defaultColor,
}, },
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('lowercase-board-permission', () => { Migrations.add('lowercase-board-permission', () => {
@ -57,12 +81,16 @@ Migrations.add('change-attachments-type-for-non-images', () => {
const newTypeForNonImage = 'application/octet-stream'; const newTypeForNonImage = 'application/octet-stream';
Attachments.find().forEach((file) => { Attachments.find().forEach((file) => {
if (!file.isImage()) { if (!file.isImage()) {
Attachments.update(file._id, { Attachments.update(
file._id,
{
$set: { $set: {
'original.type': newTypeForNonImage, 'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage, 'copies.attachments.type': newTypeForNonImage,
}, },
}, noValidate); },
noValidate
);
} }
}); });
}); });
@ -71,10 +99,10 @@ Migrations.add('card-covers', () => {
Cards.find().forEach((card) => { Cards.find().forEach((card) => {
const cover = Attachments.findOne({ cardId: card._id, cover: true }); const cover = Attachments.findOne({ cardId: card._id, cover: true });
if (cover) { 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', () => { Migrations.add('use-css-class-for-boards-colors', () => {
@ -89,26 +117,31 @@ Migrations.add('use-css-class-for-boards-colors', () => {
Boards.find().forEach((board) => { Boards.find().forEach((board) => {
const oldBoardColor = board.background.color; const oldBoardColor = board.background.color;
const newBoardColor = associationTable[oldBoardColor]; const newBoardColor = associationTable[oldBoardColor];
Boards.update(board._id, { Boards.update(
board._id,
{
$set: { color: newBoardColor }, $set: { color: newBoardColor },
$unset: { background: '' }, $unset: { background: '' },
}, noValidate); },
noValidate
);
}); });
}); });
Migrations.add('denormalize-star-number-per-board', () => { Migrations.add('denormalize-star-number-per-board', () => {
Boards.find().forEach((board) => { Boards.find().forEach((board) => {
const nStars = Users.find({'profile.starredBoards': board._id}).count(); const nStars = Users.find({ 'profile.starredBoards': board._id }).count();
Boards.update(board._id, {$set: {stars: nStars}}, noValidate); Boards.update(board._id, { $set: { stars: nStars } }, noValidate);
}); });
}); });
// We want to keep a trace of former members so we can efficiently publish their // We want to keep a trace of former members so we can efficiently publish their
// infos in the general board publication. // infos in the general board publication.
Migrations.add('add-member-isactive-field', () => { Migrations.add('add-member-isactive-field', () => {
Boards.find({}, {fields: {members: 1}}).forEach((board) => { Boards.find({}, { fields: { members: 1 } }).forEach((board) => {
const allUsersWithSomeActivity = _.chain( const allUsersWithSomeActivity = _.chain(
Activities.find({ boardId: board._id }, { fields:{ userId:1 }}).fetch()) Activities.find({ boardId: board._id }, { fields: { userId: 1 } }).fetch()
)
.pluck('userId') .pluck('userId')
.uniq() .uniq()
.value(); .value();
@ -127,7 +160,7 @@ Migrations.add('add-member-isactive-field', () => {
isActive: false, 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 // Create new items
_.sortBy(checklist.items, 'sort').forEach((item, index) => { _.sortBy(checklist.items, 'sort').forEach((item, index) => {
ChecklistItems.direct.insert({ ChecklistItems.direct.insert({
title: (item.title ? item.title : 'Checklist'), title: item.title ? item.title : 'Checklist',
sort: index, sort: index,
isFinished: item.isFinished, isFinished: item.isFinished,
checklistId: checklist._id, checklistId: checklist._id,
@ -193,8 +226,9 @@ Migrations.add('add-checklist-items', () => {
}); });
// Delete old ones // Delete old ones
Checklists.direct.update({ _id: checklist._id }, Checklists.direct.update(
{ $unset: { items : 1 } }, { _id: checklist._id },
{ $unset: { items: 1 } },
noValidate noValidate
); );
}); });
@ -217,251 +251,334 @@ Migrations.add('add-card-types', () => {
Cards.find().forEach((card) => { Cards.find().forEach((card) => {
Cards.direct.update( Cards.direct.update(
{ _id: card._id }, { _id: card._id },
{ $set: { {
$set: {
type: 'cardType-card', type: 'cardType-card',
linkedId: null } }, linkedId: null,
},
},
noValidate noValidate
); );
}); });
}); });
Migrations.add('add-custom-fields-to-cards', () => { Migrations.add('add-custom-fields-to-cards', () => {
Cards.update({ Cards.update(
{
customFields: { customFields: {
$exists: false, $exists: false,
}, },
}, {
$set: {
customFields:[],
}, },
}, noValidateMulti); {
$set: {
customFields: [],
},
},
noValidateMulti
);
}); });
Migrations.add('add-requester-field', () => { Migrations.add('add-requester-field', () => {
Cards.update({ Cards.update(
{
requestedBy: { requestedBy: {
$exists: false, $exists: false,
}, },
}, {
$set: {
requestedBy:'',
}, },
}, noValidateMulti); {
$set: {
requestedBy: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-assigner-field', () => { Migrations.add('add-assigner-field', () => {
Cards.update({ Cards.update(
{
assignedBy: { assignedBy: {
$exists: false, $exists: false,
}, },
}, {
$set: {
assignedBy:'',
}, },
}, noValidateMulti); {
$set: {
assignedBy: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-parent-field-to-cards', () => { Migrations.add('add-parent-field-to-cards', () => {
Cards.update({ Cards.update(
{
parentId: { parentId: {
$exists: false, $exists: false,
}, },
}, {
$set: {
parentId:'',
}, },
}, noValidateMulti); {
$set: {
parentId: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-subtasks-boards', () => { Migrations.add('add-subtasks-boards', () => {
Boards.update({ Boards.update(
{
subtasksDefaultBoardId: { subtasksDefaultBoardId: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
subtasksDefaultBoardId: null, subtasksDefaultBoardId: null,
subtasksDefaultListId: null, subtasksDefaultListId: null,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-subtasks-sort', () => { Migrations.add('add-subtasks-sort', () => {
Boards.update({ Boards.update(
{
subtaskSort: { subtaskSort: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
subtaskSort: -1, subtaskSort: -1,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-subtasks-allowed', () => { Migrations.add('add-subtasks-allowed', () => {
Boards.update({ Boards.update(
{
allowsSubtasks: { allowsSubtasks: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
allowsSubtasks: true, allowsSubtasks: true,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-subtasks-allowed', () => { Migrations.add('add-subtasks-allowed', () => {
Boards.update({ Boards.update(
{
presentParentTask: { presentParentTask: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
presentParentTask: 'no-parent', presentParentTask: 'no-parent',
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-authenticationMethod', () => { Migrations.add('add-authenticationMethod', () => {
Users.update({ Users.update(
'authenticationMethod': { {
authenticationMethod: {
$exists: false, $exists: false,
}, },
}, {
$set: {
'authenticationMethod': 'password',
}, },
}, noValidateMulti); {
$set: {
authenticationMethod: 'password',
},
},
noValidateMulti
);
}); });
Migrations.add('remove-tag', () => { Migrations.add('remove-tag', () => {
Users.update({ Users.update(
}, { {},
{
$unset: { $unset: {
'profile.tags':1, 'profile.tags': 1,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('remove-customFields-references-broken', () => { Migrations.add('remove-customFields-references-broken', () => {
Cards.update({'customFields.$value': null}, Cards.update(
{ $pull: { { 'customFields.$value': null },
customFields: {value: null}, {
$pull: {
customFields: { value: null },
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-product-name', () => { Migrations.add('add-product-name', () => {
Settings.update({ Settings.update(
{
productName: { productName: {
$exists: false, $exists: false,
}, },
}, {
$set: {
productName:'',
}, },
}, noValidateMulti); {
$set: {
productName: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-hide-logo', () => { Migrations.add('add-hide-logo', () => {
Settings.update({ Settings.update(
{
hideLogo: { hideLogo: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
hideLogo: false, hideLogo: false,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-custom-html-after-body-start', () => { Migrations.add('add-custom-html-after-body-start', () => {
Settings.update({ Settings.update(
{
customHTMLafterBodyStart: { customHTMLafterBodyStart: {
$exists: false, $exists: false,
}, },
}, {
$set: {
customHTMLafterBodyStart:'',
}, },
}, noValidateMulti); {
$set: {
customHTMLafterBodyStart: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-custom-html-before-body-end', () => { Migrations.add('add-custom-html-before-body-end', () => {
Settings.update({ Settings.update(
{
customHTMLbeforeBodyEnd: { customHTMLbeforeBodyEnd: {
$exists: false, $exists: false,
}, },
}, {
$set: {
customHTMLbeforeBodyEnd:'',
}, },
}, noValidateMulti); {
$set: {
customHTMLbeforeBodyEnd: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-displayAuthenticationMethod', () => { Migrations.add('add-displayAuthenticationMethod', () => {
Settings.update({ Settings.update(
{
displayAuthenticationMethod: { displayAuthenticationMethod: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
displayAuthenticationMethod: true, displayAuthenticationMethod: true,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-defaultAuthenticationMethod', () => { Migrations.add('add-defaultAuthenticationMethod', () => {
Settings.update({ Settings.update(
{
defaultAuthenticationMethod: { defaultAuthenticationMethod: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
defaultAuthenticationMethod: 'password', defaultAuthenticationMethod: 'password',
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-templates', () => { Migrations.add('add-templates', () => {
Boards.update({ Boards.update(
{
type: { type: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
type: 'board', type: 'board',
}, },
}, noValidateMulti); },
Swimlanes.update({ noValidateMulti
);
Swimlanes.update(
{
type: { type: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
type: 'swimlane', type: 'swimlane',
}, },
}, noValidateMulti); },
Lists.update({ noValidateMulti
);
Lists.update(
{
type: { type: {
$exists: false, $exists: false,
}, },
swimlaneId: { swimlaneId: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
type: 'list', type: 'list',
swimlaneId: '', swimlaneId: '',
}, },
}, noValidateMulti); },
noValidateMulti
);
Users.find({ Users.find({
'profile.templatesBoardId': { 'profile.templatesBoardId': {
$exists: false, $exists: false,
}, },
}).forEach((user) => { }).forEach((user) => {
// Create board and swimlanes // Create board and swimlanes
Boards.insert({ Boards.insert(
{
title: TAPi18n.__('templates'), title: TAPi18n.__('templates'),
permission: 'private', permission: 'private',
type: 'template-container', type: 'template-container',
@ -474,67 +591,172 @@ Migrations.add('add-templates', () => {
isCommentOnly: false, isCommentOnly: false,
}, },
], ],
}, (err, boardId) => { },
(err, boardId) => {
// Insert the reference to our templates board // Insert the reference to our templates board
Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}}); Users.update(user._id, {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the card templates swimlane // Insert the card templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('card-templates-swimlane'), title: TAPi18n.__('card-templates-swimlane'),
boardId, boardId,
sort: 1, sort: 1,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { },
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane // Insert the reference to out card templates swimlane
Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); Users.update(user._id, {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the list templates swimlane // Insert the list templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'), title: TAPi18n.__('list-templates-swimlane'),
boardId, boardId,
sort: 2, sort: 2,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { },
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane // Insert the reference to out list templates swimlane
Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); Users.update(user._id, {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the board templates swimlane // Insert the board templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'), title: TAPi18n.__('board-templates-swimlane'),
boardId, boardId,
sort: 3, sort: 3,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { },
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane // Insert the reference to out board templates swimlane
Users.update(user._id, {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); Users.update(user._id, {
}); $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
}
);
}); });
}); });
Migrations.add('fix-circular-reference_', () => { Migrations.add('fix-circular-reference_', () => {
Cards.find().forEach((card) => { Cards.find().forEach((card) => {
if (card.parentId === card._id) { 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', () => { Migrations.add('mutate-boardIds-in-customfields', () => {
CustomFields.find().forEach((cf) => { CustomFields.find().forEach((cf) => {
CustomFields.update(cf, { CustomFields.update(
cf,
{
$set: { $set: {
boardIds: [cf.boardId], boardIds: [cf.boardId],
}, },
$unset: { $unset: {
boardId: '', 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);
}); });
}); });