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,18 +1,44 @@
AccountSettings = new Mongo.Collection('accountSettings'); AccountSettings = new Mongo.Collection('accountSettings');
AccountSettings.attachSchema(new SimpleSchema({ AccountSettings.attachSchema(
_id: { new SimpleSchema({
type: String, _id: {
}, type: String,
booleanValue: { },
type: Boolean, booleanValue: {
optional: true, type: Boolean,
}, optional: true,
sort: { },
type: Number, sort: {
decimal: true, type: Number,
}, decimal: true,
})); },
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
AccountSettings.allow({ 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 });
$setOnInsert: { AccountSettings.upsert(
booleanValue: false, { _id: 'accounts-allowEmailChange' },
sort: 0, {
}, $setOnInsert: {
}); booleanValue: false,
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, { sort: 0,
$setOnInsert: { },
booleanValue: false, }
sort: 1, );
}, AccountSettings.upsert(
}); { _id: 'accounts-allowUserNameChange' },
{
$setOnInsert: {
booleanValue: false,
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,23 +1,49 @@
Announcements = new Mongo.Collection('announcements'); Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({ Announcements.attachSchema(
enabled: { new SimpleSchema({
type: Boolean, enabled: {
defaultValue: false, type: Boolean,
}, defaultValue: false,
title: { },
type: String, title: {
optional: true, type: String,
}, optional: true,
body: { },
type: String, body: {
optional: true, type: String,
}, optional: true,
sort: { },
type: Number, sort: {
decimal: true, type: Number,
}, decimal: true,
})); },
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Announcements.allow({ 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: {
source: '',
}, },
}); {
$unset: {
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;

File diff suppressed because it is too large Load diff

View file

@ -3,55 +3,69 @@ CardComments = new Mongo.Collection('card_comments');
/** /**
* A comment on a card * A comment on a card
*/ */
CardComments.attachSchema(new SimpleSchema({ CardComments.attachSchema(
boardId: { new SimpleSchema({
/** boardId: {
* the board ID /**
*/ * the board ID
type: String, */
}, type: String,
cardId: {
/**
* the card ID
*/
type: String,
},
// XXX Rename in `content`? `text` is a bit vague...
text: {
/**
* the text of the comment
*/
type: String,
},
// XXX We probably don't need this information here, since we already have it
// in the associated comment creation activity
createdAt: {
/**
* when was the comment created
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, cardId: {
// XXX Should probably be called `authorId` /**
userId: { * the card ID
/** */
* the author ID of the comment type: String,
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
}, },
}, // XXX Rename in `content`? `text` is a bit vague...
})); text: {
/**
* the text of the comment
*/
type: String,
},
createdAt: {
/**
* when was the comment created
*/
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
// XXX Should probably be called `authorId`
userId: {
/**
* the author ID of the comment
*/
type: String,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return this.userId;
}
},
},
})
);
CardComments.allow({ 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(
try { 'GET',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/cards/:cardId/comments/:commentId',
const paramBoardId = req.params.boardId; function(req, res) {
const paramCommentId = req.params.commentId; try {
const paramCardId = req.params.cardId; Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramBoardId = req.params.boardId;
code: 200, const paramCommentId = req.params.commentId;
data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }), const paramCardId = req.params.cardId;
}); JsonRoutes.sendResult(res, {
code: 200,
data: CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
}),
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
/** /**
* @operation new_comment * @operation new_comment
@ -214,35 +246,42 @@ 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(
try { 'POST',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/cards/:cardId/comments',
const paramBoardId = req.params.boardId; function(req, res) {
const paramCardId = req.params.cardId; try {
const id = CardComments.direct.insert({ Authentication.checkUserId(req.userId);
userId: req.body.authorId, const paramBoardId = req.params.boardId;
text: req.body.comment, const paramCardId = req.params.cardId;
cardId: paramCardId, const id = CardComments.direct.insert({
boardId: paramBoardId, userId: req.body.authorId,
}); text: req.body.comment,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: id,
},
});
const cardComment = CardComments.findOne({
_id: id, _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) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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(
try { 'DELETE',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/cards/:cardId/comments/:commentId',
const paramBoardId = req.params.boardId; function(req, res) {
const paramCommentId = req.params.commentId; try {
const paramCardId = req.params.cardId; Authentication.checkUserId(req.userId);
CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }); const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, { const paramCommentId = req.params.commentId;
code: 200, const paramCardId = req.params.cardId;
data: { CardComments.remove({
_id: paramCardId, _id: paramCommentId,
}, cardId: paramCardId,
}); boardId: paramBoardId,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
},
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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,40 +3,66 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/** /**
* An item in a checklist * An item in a checklist
*/ */
ChecklistItems.attachSchema(new SimpleSchema({ ChecklistItems.attachSchema(
title: { new SimpleSchema({
/** title: {
* the text of the item /**
*/ * the text of the item
type: String, */
}, type: String,
sort: { },
/** sort: {
* the sorting field of the item /**
*/ * the sorting field of the item
type: Number, */
decimal: true, type: Number,
}, decimal: true,
isFinished: { },
/** isFinished: {
* Is the item checked? /**
*/ * Is the item checked?
type: Boolean, */
defaultValue: false, type: Boolean,
}, defaultValue: false,
checklistId: { },
/** checklistId: {
* the checklist ID the item is attached to /**
*/ * the checklist ID the item is attached to
type: String, */
}, type: String,
cardId: { },
/** cardId: {
* the card ID the item is attached to /**
*/ * the card ID the item is attached to
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,21 +264,25 @@ 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',
const paramItemId = req.params.itemId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
const checklistItem = ChecklistItems.findOne({ _id: paramItemId }); function(req, res) {
if (checklistItem) { Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramItemId = req.params.itemId;
code: 200, const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
data: checklistItem, if (checklistItem) {
}); JsonRoutes.sendResult(res, {
} else { code: 200,
JsonRoutes.sendResult(res, { data: checklistItem,
code: 500, });
}); } else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
} }
}); );
/** /**
* @operation edit_checklist_item * @operation edit_checklist_item
@ -262,25 +297,35 @@ 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')) {
ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { title: req.body.title } }
);
}
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
} }
if (req.body.hasOwnProperty('title')) { );
ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}});
}
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
});
/** /**
* @operation delete_checklist_item * @operation delete_checklist_item
@ -295,15 +340,21 @@ 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',
const paramItemId = req.params.itemId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
ChecklistItems.direct.remove({ _id: paramItemId }); function(req, res) {
JsonRoutes.sendResult(res, { Authentication.checkUserId(req.userId);
code: 200, const paramItemId = req.params.itemId;
data: { ChecklistItems.direct.remove({ _id: paramItemId });
_id: paramItemId, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
}); _id: paramItemId,
},
});
}
);
} }
export default ChecklistItems;

View file

@ -3,49 +3,64 @@ Checklists = new Mongo.Collection('checklists');
/** /**
* A Checklist * A Checklist
*/ */
Checklists.attachSchema(new SimpleSchema({ Checklists.attachSchema(
cardId: { new SimpleSchema({
/** cardId: {
* The ID of the card the checklist is in /**
*/ * The ID of the card the checklist is in
type: String, */
}, type: String,
title: {
/**
* the title of the checklist
*/
type: String,
defaultValue: 'Checklist',
},
finishedAt: {
/**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, title: {
sort: { /**
/** * the title of the checklist
* sorting value of the checklist */
*/ type: String,
type: Number, defaultValue: 'Checklist',
decimal: true, },
}, finishedAt: {
})); /**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* sorting value of the checklist
*/
type: Number,
decimal: true,
},
})
);
Checklists.helpers({ 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, {
}, { sort: ['sort'] }); checklistId: this._id,
},
{ 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,26 +196,32 @@ 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',
const paramCardId = req.params.cardId; '/api/boards/:boardId/cards/:cardId/checklists',
const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) { function(req, res) {
return { Authentication.checkUserId(req.userId);
_id: doc._id, const paramCardId = req.params.cardId;
title: doc.title, const checklists = Checklists.find({ cardId: paramCardId }).map(function(
}; doc
}); ) {
if (checklists) { return {
JsonRoutes.sendResult(res, { _id: doc._id,
code: 200, title: doc.title,
data: checklists, };
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
}); });
if (checklists) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklists,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
} }
}); );
/** /**
* @operation get_checklist * @operation get_checklist
@ -209,29 +239,38 @@ 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',
const paramChecklistId = req.params.checklistId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
const paramCardId = req.params.cardId; function(req, res) {
const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }); Authentication.checkUserId(req.userId);
if (checklist) { const paramChecklistId = req.params.checklistId;
checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) { const paramCardId = req.params.cardId;
return { const checklist = Checklists.findOne({
_id: doc._id, _id: paramChecklistId,
title: doc.title, cardId: paramCardId,
isFinished: doc.isFinished,
};
});
JsonRoutes.sendResult(res, {
code: 200,
data: checklist,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
}); });
if (checklist) {
checklist.items = ChecklistItems.find({
checklistId: checklist._id,
}).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
isFinished: doc.isFinished,
};
});
JsonRoutes.sendResult(res, {
code: 200,
data: checklist,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
} }
}); );
/** /**
* @operation new_checklist * @operation new_checklist
@ -242,36 +281,40 @@ 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({
title: req.body.title, title: req.body.title,
cardId: paramCardId, cardId: paramCardId,
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,
title: item.title, title: item.title,
sort: idx, sort: idx,
});
}); });
}); JsonRoutes.sendResult(res, {
JsonRoutes.sendResult(res, { code: 200,
code: 200, data: {
data: { _id: id,
_id: id, },
}, });
}); } else {
} else { JsonRoutes.sendResult(res, {
JsonRoutes.sendResult(res, { code: 400,
code: 400, });
}); }
} }
}); );
/** /**
* @operation delete_checklist * @operation delete_checklist
@ -284,15 +327,21 @@ 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',
const paramChecklistId = req.params.checklistId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
Checklists.remove({ _id: paramChecklistId }); function(req, res) {
JsonRoutes.sendResult(res, { Authentication.checkUserId(req.userId);
code: 200, const paramChecklistId = req.params.checklistId;
data: { Checklists.remove({ _id: paramChecklistId });
_id: paramChecklistId, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
}); _id: paramChecklistId,
},
});
}
);
} }
export default Checklists;

View file

@ -3,74 +3,100 @@ 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(
boardIds: { new SimpleSchema({
/** boardIds: {
* the ID of the board /**
*/ * the ID of the board
type: [String], */
}, type: [String],
name: { },
/** name: {
* name of the custom field /**
*/ * name of the custom field
type: String, */
}, type: String,
type: { },
/** type: {
* type of the custom field /**
*/ * type of the custom field
type: String, */
allowedValues: ['text', 'number', 'date', 'dropdown'], type: String,
}, allowedValues: ['text', 'number', 'date', 'dropdown'],
settings: { },
/** settings: {
* settings of the custom field /**
*/ * settings of the custom field
type: Object, */
}, type: Object,
'settings.dropdownItems': { },
/** 'settings.dropdownItems': {
* list of drop down items objects /**
*/ * list of drop down items objects
type: [Object], */
optional: true, type: [Object],
}, optional: true,
'settings.dropdownItems.$': { },
type: new SimpleSchema({ 'settings.dropdownItems.$': {
_id: { type: new SimpleSchema({
/** _id: {
* ID of the drop down item /**
*/ * ID of the drop down item
type: String, */
type: String,
},
name: {
/**
* name of the drop down item
*/
type: String,
},
}),
},
showOnCard: {
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
name: { },
/** modifiedAt: {
* name of the drop down item type: Date,
*/ denyUpdate: false,
type: String, // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
}), },
}, })
showOnCard: { );
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
}));
CustomFields.mutations({ 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(
return { function(cf) {
_id: cf._id, return {
name: cf.name, _id: cf._id,
type: cf.type, name: cf.name,
}; 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',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/custom-fields/:customFieldId',
const paramCustomFieldId = req.params.customFieldId; function(req, res) {
JsonRoutes.sendResult(res, { Authentication.checkUserId(req.userId);
code: 200, const paramBoardId = req.params.boardId;
data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }), const paramCustomFieldId = req.params.customFieldId;
}); JsonRoutes.sendResult(res, {
}); code: 200,
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',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/custom-fields/:customFieldId',
const id = req.params.customFieldId; function(req, res) {
CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} }); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramBoardId = req.params.boardId;
code: 200, const id = req.params.customFieldId;
data: { CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
_id: id, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
}); _id: id,
},
});
}
);
} }
export default CustomFields;

View file

@ -3,75 +3,96 @@ Integrations = new Mongo.Collection('integrations');
/** /**
* Integration with third-party applications * Integration with third-party applications
*/ */
Integrations.attachSchema(new SimpleSchema({ Integrations.attachSchema(
enabled: { new SimpleSchema({
/** enabled: {
* is the integration enabled? /**
*/ * is the integration enabled?
type: Boolean, */
defaultValue: true, type: Boolean,
}, defaultValue: true,
title: {
/**
* name of the integration
*/
type: String,
optional: true,
},
type: {
/**
* type of the integratation (Default to 'outgoing-webhooks')
*/
type: String,
defaultValue: 'outgoing-webhooks',
},
activities: {
/**
* activities the integration gets triggered (list)
*/
type: [String],
defaultValue: ['all'],
},
url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex)
/**
* URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/
type: String,
},
token: {
/**
* token of the integration
*/
type: String,
optional: true,
},
boardId: {
/**
* Board ID of the integration
*/
type: String,
},
createdAt: {
/**
* Creation date of the integration
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, title: {
userId: { /**
/** * name of the integration
* user ID who created the interation */
*/ type: String,
type: String, optional: true,
}, },
})); type: {
/**
* type of the integratation (Default to 'outgoing-webhooks')
*/
type: String,
defaultValue: 'outgoing-webhooks',
},
activities: {
/**
* activities the integration gets triggered (list)
*/
type: [String],
defaultValue: ['all'],
},
url: {
// URL validation regex (https://mathiasbynens.be/demo/url-regex)
/**
* URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/
type: String,
},
token: {
/**
* token of the integration
*/
type: String,
optional: true,
},
boardId: {
/**
* Board ID of the integration
*/
type: String,
},
createdAt: {
/**
* Creation date of the integration
*/
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
userId: {
/**
* user ID who created the interation
*/
type: String,
},
})
);
Integrations.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Integrations.allow({ 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(
try { 'DELETE',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/integrations/:intId/activities',
const paramIntId = req.params.intId; function(req, res) {
const newActivities = req.body.activities; try {
Authentication.checkBoardAccess(req.userId, paramBoardId); const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, 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) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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(
try { 'POST',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/integrations/:intId/activities',
const paramIntId = req.params.intId; function(req, res) {
const newActivities = req.body.activities; try {
Authentication.checkBoardAccess(req.userId, paramBoardId); const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, 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) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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,45 +1,78 @@
InvitationCodes = new Mongo.Collection('invitation_codes'); InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({ InvitationCodes.attachSchema(
code: { new SimpleSchema({
type: String, code: {
}, type: String,
email: { },
type: String, email: {
unique: true, type: String,
regEx: SimpleSchema.RegEx.Email, unique: true,
}, regEx: SimpleSchema.RegEx.Email,
createdAt: { },
type: Date, createdAt: {
denyUpdate: false, type: Date,
}, denyUpdate: false,
// always be the admin if only one admin optional: true,
authorId: { // eslint-disable-next-line consistent-return
type: String, autoValue() {
}, if (this.isInsert) {
boardsToBeInvited: { return new Date();
type: [String], } else {
optional: true, this.unset();
}, }
valid: { },
type: Boolean, },
defaultValue: true, modifiedAt: {
}, type: Date,
})); denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
// always be the admin if only one admin
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
defaultValue: true,
},
})
);
InvitationCodes.helpers({ 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,125 +3,161 @@ 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(
title: { new SimpleSchema({
/** title: {
* the title of the list /**
*/ * the title of the list
type: String, */
}, type: String,
archived: {
/**
* is the list archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
}, },
}, archived: {
boardId: { /**
/** * is the list archived
* the board associated to this list */
*/ type: Boolean,
type: String, // eslint-disable-next-line consistent-return
}, autoValue() {
swimlaneId: { if (this.isInsert && !this.isSet) {
/** return false;
* the swimlane associated to this list. Used for templates }
*/ },
type: String,
defaultValue: '',
},
createdAt: {
/**
* creation date
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, boardId: {
sort: { /**
/** * the board associated to this list
* is the list sorted */
*/ type: String,
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* last update of the list
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
}, swimlaneId: {
wipLimit: { /**
/** * the swimlane associated to this list. Used for templates
* WIP object, see below */
*/ type: String,
type: Object, defaultValue: '',
optional: true, },
}, createdAt: {
'wipLimit.value': { /**
/** * creation date
* value of the WIP */
*/ type: Date,
type: Number, // eslint-disable-next-line consistent-return
decimal: false, autoValue() {
defaultValue: 1, if (this.isInsert) {
}, return new Date();
'wipLimit.enabled': { } else {
/** this.unset();
* is the WIP enabled }
*/ },
type: Boolean, },
defaultValue: false, sort: {
}, /**
'wipLimit.soft': { * is the list sorted
/** */
* is the WIP a soft or hard requirement type: Number,
*/ decimal: true,
type: Boolean, // XXX We should probably provide a default
defaultValue: false, optional: true,
}, },
color: { updatedAt: {
/** /**
* the color of the list * last update of the list
*/ */
type: String, type: Date,
optional: true, optional: true,
// silver is the default, so it is left out // eslint-disable-next-line consistent-return
allowedValues: [ autoValue() {
'white', 'green', 'yellow', 'orange', 'red', 'purple', if (this.isUpdate || this.isUpsert || this.isInsert) {
'blue', 'sky', 'lime', 'pink', 'black', return new Date();
'peachpuff', 'crimson', 'plum', 'darkgreen', } else {
'slateblue', 'magenta', 'gold', 'navy', 'gray', this.unset();
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', }
], },
}, },
type: { modifiedAt: {
/** type: Date,
* The type of list denyUpdate: false,
*/ // eslint-disable-next-line consistent-return
type: String, autoValue() {
defaultValue: 'list', if (this.isInsert || this.isUpsert || this.isUpdate) {
}, return new Date();
})); } else {
this.unset();
}
},
},
wipLimit: {
/**
* WIP object, see below
*/
type: Object,
optional: true,
},
'wipLimit.value': {
/**
* value of the WIP
*/
type: Number,
decimal: false,
defaultValue: 1,
},
'wipLimit.enabled': {
/**
* is the WIP enabled
*/
type: Boolean,
defaultValue: false,
},
'wipLimit.soft': {
/**
* is the WIP a soft or hard requirement
*/
type: Boolean,
defaultValue: false,
},
color: {
/**
* the color of the list
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
},
type: {
/**
* The type of list
*/
type: String,
defaultValue: 'list',
},
})
);
Lists.allow({ 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(
return { function(doc) {
_id: doc._id, return {
title: doc.title, _id: doc._id,
}; 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,23 +1,51 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules'); Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({ Rules.attachSchema(
title: { new SimpleSchema({
type: String, title: {
optional: false, type: String,
}, optional: false,
triggerId: { },
type: String, triggerId: {
optional: false, type: String,
}, optional: false,
actionId: { },
type: String, actionId: {
optional: false, type: String,
}, optional: false,
boardId: { },
type: String, boardId: {
optional: false, type: String,
}, optional: false,
})); },
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Rules.mutations({ 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,67 +1,85 @@
Settings = new Mongo.Collection('settings'); Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({ Settings.attachSchema(
disableRegistration: { new SimpleSchema({
type: Boolean, disableRegistration: {
}, type: Boolean,
'mailServer.username': { },
type: String, 'mailServer.username': {
optional: true, type: String,
}, optional: true,
'mailServer.password': { },
type: String, 'mailServer.password': {
optional: true, type: String,
}, optional: true,
'mailServer.host': { },
type: String, 'mailServer.host': {
optional: true, type: String,
}, optional: true,
'mailServer.port': { },
type: String, 'mailServer.port': {
optional: true, type: String,
}, optional: true,
'mailServer.enableTLS': { },
type: Boolean, 'mailServer.enableTLS': {
optional: true, type: Boolean,
}, optional: true,
'mailServer.from': { },
type: String, 'mailServer.from': {
optional: true, type: String,
}, optional: true,
productName: { },
type: String, productName: {
optional: true, type: String,
}, optional: true,
customHTMLafterBodyStart: { },
type: String, customHTMLafterBodyStart: {
optional: true, type: String,
}, optional: true,
customHTMLbeforeBodyEnd: { },
type: String, customHTMLbeforeBodyEnd: {
optional: true, type: String,
}, optional: true,
displayAuthenticationMethod: { },
type: Boolean, displayAuthenticationMethod: {
optional: true, type: Boolean,
}, optional: true,
defaultAuthenticationMethod: { },
type: String, defaultAuthenticationMethod: {
optional: false, type: String,
}, optional: false,
hideLogo: { },
type: Boolean, hideLogo: {
optional: true, type: Boolean,
}, optional: true,
createdAt: { },
type: Date, createdAt: {
denyUpdate: true, type: Date,
}, denyUpdate: true,
modifiedAt: { // eslint-disable-next-line consistent-return
type: Date, autoValue() {
}, if (this.isInsert) {
})); return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Settings.helpers({ 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, {
sendInvitationEmail(invitation._id); $set: { boardsToBeInvited: boards },
}else {
const code = getRandomNum(100000, 999999);
InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){
if (!err && _id) {
sendInvitationEmail(_id);
} else {
throw new Meteor.Error('invitation-generated-fail', err.message);
}
}); });
sendInvitationEmail(invitation._id);
} else {
const code = getRandomNum(100000, 999999);
InvitationCodes.insert(
{
code,
email,
boardsToBeInvited: boards,
createdAt: new Date(),
authorId: Meteor.userId(),
},
function(err, _id) {
if (!err && _id) {
sendInvitationEmail(_id);
} else {
throw new Meteor.Error(
'invitation-generated-fail',
err.message
);
}
}
);
} }
} }
}); });
@ -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,89 +3,125 @@ 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(
title: { new SimpleSchema({
/** title: {
* the title of the swimlane /**
*/ * the title of the swimlane
type: String, */
}, type: String,
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
}, },
}, archived: {
boardId: { /**
/** * is the swimlane archived?
* the ID of the board the swimlane is attached to */
*/ type: Boolean,
type: String, // eslint-disable-next-line consistent-return
}, autoValue() {
createdAt: { if (this.isInsert && !this.isSet) {
/** return false;
* creation date of the swimlane }
*/ },
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, boardId: {
sort: { /**
/** * the ID of the board the swimlane is attached to
* the sort value of the swimlane */
*/ type: String,
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
color: {
/**
* the color of the swimlane
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple',
'blue', 'sky', 'lime', 'pink', 'black',
'peachpuff', 'crimson', 'plum', 'darkgreen',
'slateblue', 'magenta', 'gold', 'navy', 'gray',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
],
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
}, createdAt: {
type: { /**
/** * creation date of the swimlane
* The type of swimlane */
*/ type: Date,
type: String, // eslint-disable-next-line consistent-return
defaultValue: 'swimlane', autoValue() {
}, if (this.isInsert) {
})); return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* the sort value of the swimlane
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
color: {
/**
* the color of the swimlane
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isUpdate || this.isUpsert || this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
type: {
/**
* The type of swimlane
*/
type: String,
defaultValue: 'swimlane',
},
})
);
Swimlanes.allow({ 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(
swimlaneId: this._id, Filter.mongoSelector({
archived: false, swimlaneId: this._id,
}), { sort: ['sort'] }); archived: false,
}),
{ sort: ['sort'] }
);
}, },
lists() { lists() {
return Lists.find({ return Lists.find(
boardId: this.boardId, {
swimlaneId: {$in: [this._id, '']}, boardId: this.boardId,
archived: false, swimlaneId: { $in: [this._id, ''] },
}, { sort: ['sort'] }); archived: false,
},
{ 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, {
swimlaneId: {$in: [doc._id, '']}, boardId: doc.boardId,
archived: false, swimlaneId: { $in: [doc._id, ''] },
}, { sort: ['sort'] }); archived: false,
},
{ 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(
return { function(doc) {
_id: doc._id, return {
title: doc.title, _id: doc._id,
}; 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,25 +438,29 @@ 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(
try { 'DELETE',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/swimlanes/:swimlaneId',
const paramBoardId = req.params.boardId; function(req, res) {
const paramSwimlaneId = req.params.swimlaneId; try {
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId }); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramBoardId = req.params.boardId;
code: 200, const paramSwimlaneId = req.params.swimlaneId;
data: { Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
_id: paramSwimlaneId, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
_id: paramSwimlaneId,
},
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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,31 +2,66 @@
// `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(
fieldName: { new SimpleSchema({
type: String, fieldName: {
}, type: String,
docId: {
type: String,
},
value: {
type: String,
},
userId: {
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
}, },
}, docId: {
})); type: String,
},
value: {
type: String,
},
userId: {
type: String,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return this.userId;
}
},
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
UnsavedEditCollection.before.update(
(userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
}
);
if (Meteor.isServer) { 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;

File diff suppressed because it is too large Load diff

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: { {
$exists: false,
},
}, {
$set: {
background: { background: {
type: 'color', $exists: false,
color: defaultColor,
}, },
}, },
}, noValidateMulti); {
$set: {
background: {
type: 'color',
color: defaultColor,
},
},
},
noValidateMulti
);
}); });
Migrations.add('lowercase-board-permission', () => { Migrations.add('lowercase-board-permission', () => {
@ -57,24 +81,28 @@ 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(
$set: { file._id,
'original.type': newTypeForNonImage, {
'copies.attachments.type': newTypeForNonImage, $set: {
'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage,
},
}, },
}, noValidate); noValidate
);
} }
}); });
}); });
Migrations.add('card-covers', () => { 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(
$set: { color: newBoardColor }, board._id,
$unset: { background: '' }, {
}, noValidate); $set: { color: newBoardColor },
$unset: { background: '' },
},
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,324 +251,512 @@ 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: { {
type: 'cardType-card', $set: {
linkedId: null } }, type: 'cardType-card',
linkedId: null,
},
},
noValidate noValidate
); );
}); });
}); });
Migrations.add('add-custom-fields-to-cards', () => { Migrations.add('add-custom-fields-to-cards', () => {
Cards.update({ Cards.update(
customFields: { {
$exists: false, customFields: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
customFields:[], customFields: [],
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-requester-field', () => { Migrations.add('add-requester-field', () => {
Cards.update({ Cards.update(
requestedBy: { {
$exists: false, requestedBy: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
requestedBy:'', requestedBy: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-assigner-field', () => { Migrations.add('add-assigner-field', () => {
Cards.update({ Cards.update(
assignedBy: { {
$exists: false, assignedBy: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
assignedBy:'', assignedBy: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-parent-field-to-cards', () => { Migrations.add('add-parent-field-to-cards', () => {
Cards.update({ Cards.update(
parentId: { {
$exists: false, parentId: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
parentId:'', parentId: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-subtasks-boards', () => { Migrations.add('add-subtasks-boards', () => {
Boards.update({ Boards.update(
subtasksDefaultBoardId: { {
$exists: false, subtasksDefaultBoardId: {
$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: { {
$exists: false, subtaskSort: {
$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: { {
$exists: false, allowsSubtasks: {
$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: { {
$exists: false, presentParentTask: {
$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': { {
$exists: false, authenticationMethod: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
'authenticationMethod': 'password', authenticationMethod: 'password',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('remove-tag', () => { Migrations.add('remove-tag', () => {
Users.update({ Users.update(
}, { {},
$unset: { {
'profile.tags':1, $unset: {
'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: { {
$exists: false, productName: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
productName:'', productName: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-hide-logo', () => { Migrations.add('add-hide-logo', () => {
Settings.update({ Settings.update(
hideLogo: { {
$exists: false, hideLogo: {
$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: { {
$exists: false, customHTMLafterBodyStart: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
customHTMLafterBodyStart:'', customHTMLafterBodyStart: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-custom-html-before-body-end', () => { Migrations.add('add-custom-html-before-body-end', () => {
Settings.update({ Settings.update(
customHTMLbeforeBodyEnd: { {
$exists: false, customHTMLbeforeBodyEnd: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
customHTMLbeforeBodyEnd:'', customHTMLbeforeBodyEnd: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-displayAuthenticationMethod', () => { Migrations.add('add-displayAuthenticationMethod', () => {
Settings.update({ Settings.update(
displayAuthenticationMethod: { {
$exists: false, displayAuthenticationMethod: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
displayAuthenticationMethod: true, displayAuthenticationMethod: true,
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-defaultAuthenticationMethod', () => { Migrations.add('add-defaultAuthenticationMethod', () => {
Settings.update({ Settings.update(
defaultAuthenticationMethod: { {
$exists: false, defaultAuthenticationMethod: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
defaultAuthenticationMethod: 'password', defaultAuthenticationMethod: 'password',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-templates', () => { Migrations.add('add-templates', () => {
Boards.update({ Boards.update(
type: { {
$exists: false, type: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
type: 'board', type: 'board',
},
}, },
}, noValidateMulti); noValidateMulti
Swimlanes.update({ );
type: { Swimlanes.update(
$exists: false, {
type: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
type: 'swimlane', type: 'swimlane',
},
}, },
}, noValidateMulti); noValidateMulti
Lists.update({ );
type: { Lists.update(
$exists: false, {
type: {
$exists: false,
},
swimlaneId: {
$exists: false,
},
}, },
swimlaneId: { {
$exists: false, $set: {
type: 'list',
swimlaneId: '',
},
}, },
}, { noValidateMulti
$set: { );
type: 'list',
swimlaneId: '',
},
}, 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'), {
permission: 'private', title: TAPi18n.__('templates'),
type: 'template-container', permission: 'private',
members: [
{
userId: user._id,
isAdmin: true,
isActive: true,
isNoComments: false,
isCommentOnly: false,
},
],
}, (err, boardId) => {
// Insert the reference to our templates board
Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}});
// Insert the card templates swimlane
Swimlanes.insert({
title: TAPi18n.__('card-templates-swimlane'),
boardId,
sort: 1,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { members: [
{
userId: user._id,
isAdmin: true,
isActive: true,
isNoComments: false,
isCommentOnly: false,
},
],
},
(err, boardId) => {
// Insert the reference to our templates board
Users.update(user._id, {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the reference to out card templates swimlane // Insert the card templates swimlane
Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); Swimlanes.insert(
}); {
title: TAPi18n.__('card-templates-swimlane'),
boardId,
sort: 1,
type: 'template-container',
},
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane
Users.update(user._id, {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the list templates swimlane // Insert the list templates swimlane
Swimlanes.insert({ Swimlanes.insert(
title: TAPi18n.__('list-templates-swimlane'), {
boardId, title: TAPi18n.__('list-templates-swimlane'),
sort: 2, boardId,
type: 'template-container', sort: 2,
}, (err, swimlaneId) => { type: 'template-container',
},
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane
Users.update(user._id, {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the reference to out list templates swimlane // Insert the board templates swimlane
Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); Swimlanes.insert(
}); {
title: TAPi18n.__('board-templates-swimlane'),
// Insert the board templates swimlane boardId,
Swimlanes.insert({ sort: 3,
title: TAPi18n.__('board-templates-swimlane'), type: 'template-container',
boardId, },
sort: 3, (err, swimlaneId) => {
type: 'template-container', // Insert the reference to out board templates swimlane
}, (err, swimlaneId) => { Users.update(user._id, {
$set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
// Insert the reference to out board templates swimlane });
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(
$set: { cf,
boardIds: [cf.boardId], {
$set: {
boardIds: [cf.boardId],
},
$unset: {
boardId: '',
},
}, },
$unset: { noValidateMulti
boardId: '', );
},
}, 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);
});
});