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.attachSchema(new SimpleSchema({
_id: {
type: String,
},
booleanValue: {
type: Boolean,
optional: true,
},
sort: {
type: Number,
decimal: true,
},
}));
AccountSettings.attachSchema(
new SimpleSchema({
_id: {
type: String,
},
booleanValue: {
type: Boolean,
optional: true,
},
sort: {
type: Number,
decimal: true,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
AccountSettings.allow({
update(userId) {
@ -21,19 +47,33 @@ AccountSettings.allow({
},
});
AccountSettings.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, {
$setOnInsert: {
booleanValue: false,
sort: 0,
},
});
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, {
$setOnInsert: {
booleanValue: false,
sort: 1,
},
});
AccountSettings._collection._ensureIndex({ modifiedAt: -1 });
AccountSettings.upsert(
{ _id: 'accounts-allowEmailChange' },
{
$setOnInsert: {
booleanValue: false,
sort: 0,
},
}
);
AccountSettings.upsert(
{ _id: 'accounts-allowUserNameChange' },
{
$setOnInsert: {
booleanValue: false,
sort: 1,
},
}
);
});
}
export default AccountSettings;

View file

@ -1,3 +1,5 @@
import { Meteor } from 'meteor/meteor';
Actions = new Mongo.Collection('actions');
Actions.allow({
@ -17,3 +19,16 @@ Actions.helpers({
return this.desc;
},
});
Actions.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Actions._collection._ensureIndex({ modifiedAt: -1 });
});
}
export default Actions;

View file

@ -69,7 +69,11 @@ Activities.before.insert((userId, doc) => {
Activities.after.insert((userId, doc) => {
const activity = Activities._transform(doc);
RulesHelper.executeRules(activity);
});
Activities.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
@ -78,11 +82,21 @@ if (Meteor.isServer) {
// are largely used in the App. See #524.
Meteor.startup(() => {
Activities._collection._ensureIndex({ createdAt: -1 });
Activities._collection._ensureIndex({ modifiedAt: -1 });
Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ commentId: 1 }, { partialFilterExpression: { commentId: { $exists: true } } });
Activities._collection._ensureIndex({ attachmentId: 1 }, { partialFilterExpression: { attachmentId: { $exists: true } } });
Activities._collection._ensureIndex({ customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } });
Activities._collection._ensureIndex(
{ commentId: 1 },
{ partialFilterExpression: { commentId: { $exists: true } } }
);
Activities._collection._ensureIndex(
{ attachmentId: 1 },
{ partialFilterExpression: { attachmentId: { $exists: true } } }
);
Activities._collection._ensureIndex(
{ customFieldId: 1 },
{ partialFilterExpression: { customFieldId: { $exists: true } } }
);
// Label activity did not work yet, unable to edit labels when tried this.
//Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 });
//Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
@ -189,18 +203,35 @@ if (Meteor.isServer) {
// params.labelId = activity.labelId;
//}
if (board) {
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
watchers = _.union(watchers, watchingUsers, _.intersection(participants, trackingUsers));
const watchingUsers = _.pluck(
_.where(board.watchers, { level: 'watching' }),
'userId'
);
const trackingUsers = _.pluck(
_.where(board.watchers, { level: 'tracking' }),
'userId'
);
watchers = _.union(
watchers,
watchingUsers,
_.intersection(participants, trackingUsers)
);
}
Notifications.getUsers(watchers).forEach((user) => {
Notifications.notify(user, title, description, params);
});
const integrations = Integrations.find({ boardId: board._id, type: 'outgoing-webhooks', enabled: true, activities: { '$in': [description, 'all'] } }).fetch();
const integrations = Integrations.find({
boardId: board._id,
type: 'outgoing-webhooks',
enabled: true,
activities: { $in: [description, 'all'] },
}).fetch();
if (integrations.length > 0) {
Meteor.call('outgoingWebhooks', integrations, description, params);
}
});
}
export default Activities;

View file

@ -1,23 +1,49 @@
Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({
enabled: {
type: Boolean,
defaultValue: false,
},
title: {
type: String,
optional: true,
},
body: {
type: String,
optional: true,
},
sort: {
type: Number,
decimal: true,
},
}));
Announcements.attachSchema(
new SimpleSchema({
enabled: {
type: Boolean,
defaultValue: false,
},
title: {
type: String,
optional: true,
},
body: {
type: String,
optional: true,
},
sort: {
type: Number,
decimal: true,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Announcements.allow({
update(userId) {
@ -26,11 +52,19 @@ Announcements.allow({
},
});
Announcements.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Announcements._collection._ensureIndex({ modifiedAt: -1 });
const announcements = Announcements.findOne({});
if(!announcements){
Announcements.insert({enabled: false, sort: 0});
if (!announcements) {
Announcements.insert({ enabled: false, sort: 0 });
}
});
}
export default Announcements;

View file

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

View file

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

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

View file

@ -81,7 +81,8 @@ Cards.attachSchema(new SimpleSchema({
* creation date
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
@ -89,6 +90,18 @@ Cards.attachSchema(new SimpleSchema({
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
customFields: {
/**
* list of custom fields
@ -1539,7 +1552,8 @@ if (Meteor.isServer) {
// Cards are often fetched within a board, so we create an index to make these
// queries more efficient.
Meteor.startup(() => {
Cards._collection._ensureIndex({boardId: 1, createdAt: -1});
Cards._collection._ensureIndex({ modifiedAt: -1 });
Cards._collection._ensureIndex({ boardId: 1, createdAt: -1 });
// https://github.com/wekan/wekan/issues/1863
// Swimlane added a new field in the cards collection of mongodb named parentId.
// When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection).
@ -1581,6 +1595,11 @@ if (Meteor.isServer) {
cardCustomFields(userId, doc, fieldNames, modifier);
});
Cards.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
// Remove all activities associated with a card if we remove the card
// Remove also card_comments / checklists / attachments
Cards.before.remove((userId, doc) => {
@ -1980,3 +1999,5 @@ if (Meteor.isServer) {
});
}
export default Cards;

View file

@ -3,40 +3,66 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/**
* An item in a checklist
*/
ChecklistItems.attachSchema(new SimpleSchema({
title: {
/**
* the text of the item
*/
type: String,
},
sort: {
/**
* the sorting field of the item
*/
type: Number,
decimal: true,
},
isFinished: {
/**
* Is the item checked?
*/
type: Boolean,
defaultValue: false,
},
checklistId: {
/**
* the checklist ID the item is attached to
*/
type: String,
},
cardId: {
/**
* the card ID the item is attached to
*/
type: String,
},
}));
ChecklistItems.attachSchema(
new SimpleSchema({
title: {
/**
* the text of the item
*/
type: String,
},
sort: {
/**
* the sorting field of the item
*/
type: Number,
decimal: true,
},
isFinished: {
/**
* Is the item checked?
*/
type: Boolean,
defaultValue: false,
},
checklistId: {
/**
* the checklist ID the item is attached to
*/
type: String,
},
cardId: {
/**
* the card ID the item is attached to
*/
type: String,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
ChecklistItems.allow({
insert(userId, doc) {
@ -62,10 +88,10 @@ ChecklistItems.mutations({
setTitle(title) {
return { $set: { title } };
},
check(){
check() {
return { $set: { isFinished: true } };
},
uncheck(){
uncheck() {
return { $set: { isFinished: false } };
},
toggleItem() {
@ -79,7 +105,7 @@ ChecklistItems.mutations({
sort: sortIndex,
};
return {$set: mutatedFields};
return { $set: mutatedFields };
},
});
@ -106,13 +132,13 @@ function itemRemover(userId, doc) {
});
}
function publishCheckActivity(userId, doc){
function publishCheckActivity(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
let activityType;
if(doc.isFinished){
if (doc.isFinished) {
activityType = 'checkedItem';
}else{
} else {
activityType = 'uncheckedItem';
}
const act = {
@ -122,19 +148,19 @@ function publishCheckActivity(userId, doc){
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
checklistItemName:doc.title,
checklistItemName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
};
Activities.insert(act);
}
function publishChekListCompleted(userId, doc){
function publishChekListCompleted(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
const checklistId = doc.checklistId;
const checkList = Checklists.findOne({_id:checklistId});
if(checkList.isFinished()){
const checkList = Checklists.findOne({ _id: checklistId });
if (checkList.isFinished()) {
const act = {
userId,
activityType: 'completeChecklist',
@ -149,11 +175,11 @@ function publishChekListCompleted(userId, doc){
}
}
function publishChekListUncompleted(userId, doc){
function publishChekListUncompleted(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
const checklistId = doc.checklistId;
const checkList = Checklists.findOne({_id:checklistId});
const checkList = Checklists.findOne({ _id: checklistId });
// BUGS in IFTTT Rules: https://github.com/wekan/wekan/issues/1972
// Currently in checklist all are set as uncompleted/not checked,
// IFTTT Rule does not move card to other list.
@ -167,7 +193,7 @@ function publishChekListUncompleted(userId, doc){
// find . | xargs grep 'count' -sl | grep -v .meteor | grep -v node_modules | grep -v .build
// Maybe something related here?
// wekan/client/components/rules/triggers/checklistTriggers.js
if(checkList.isFinished()){
if (checkList.isFinished()) {
const act = {
userId,
activityType: 'uncompleteChecklist',
@ -185,6 +211,7 @@ function publishChekListUncompleted(userId, doc){
// Activities
if (Meteor.isServer) {
Meteor.startup(() => {
ChecklistItems._collection._ensureIndex({ modifiedAt: -1 });
ChecklistItems._collection._ensureIndex({ checklistId: 1 });
ChecklistItems._collection._ensureIndex({ cardId: 1 });
});
@ -198,6 +225,10 @@ if (Meteor.isServer) {
publishChekListUncompleted(userId, doc, fieldNames);
});
ChecklistItems.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
ChecklistItems.after.insert((userId, doc) => {
itemCreation(userId, doc);
@ -214,7 +245,7 @@ if (Meteor.isServer) {
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
checklistItemName:doc.title,
checklistItemName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
@ -233,21 +264,25 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item
* @return_type ChecklistItems
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramItemId = req.params.itemId;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklistItem,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklistItem,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
}
});
);
/**
* @operation edit_checklist_item
@ -262,25 +297,35 @@ if (Meteor.isServer) {
* @param {string} [title] the new text of the item
* @return_type {_id: string}
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add(
'PUT',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId;
const paramItemId = req.params.itemId;
if (req.body.hasOwnProperty('isFinished')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}});
if (req.body.hasOwnProperty('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
@ -295,15 +340,21 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item to be removed
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramItemId = req.params.itemId;
ChecklistItems.direct.remove({ _id: paramItemId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId;
ChecklistItems.direct.remove({ _id: paramItemId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
}
);
}
export default ChecklistItems;

View file

@ -3,49 +3,64 @@ Checklists = new Mongo.Collection('checklists');
/**
* A Checklist
*/
Checklists.attachSchema(new SimpleSchema({
cardId: {
/**
* The ID of the card the checklist is in
*/
type: String,
},
title: {
/**
* the title of the checklist
*/
type: String,
defaultValue: 'Checklist',
},
finishedAt: {
/**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
Checklists.attachSchema(
new SimpleSchema({
cardId: {
/**
* The ID of the card the checklist is in
*/
type: String,
},
},
sort: {
/**
* sorting value of the checklist
*/
type: Number,
decimal: true,
},
}));
title: {
/**
* the title of the checklist
*/
type: String,
defaultValue: 'Checklist',
},
finishedAt: {
/**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* sorting value of the checklist
*/
type: Number,
decimal: true,
},
})
);
Checklists.helpers({
copy(newCardId) {
@ -53,7 +68,7 @@ Checklists.helpers({
this._id = null;
this.cardId = newCardId;
const newChecklistId = Checklists.insert(this);
ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => {
ChecklistItems.find({ checklistId: oldChecklistId }).forEach((item) => {
item._id = null;
item.checklistId = newChecklistId;
item.cardId = newCardId;
@ -65,9 +80,12 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count();
},
items() {
return ChecklistItems.find({
checklistId: this._id,
}, { sort: ['sort'] });
return ChecklistItems.find(
{
checklistId: this._id,
},
{ sort: ['sort'] }
);
},
finishedCount() {
return ChecklistItems.find({
@ -78,20 +96,20 @@ Checklists.helpers({
isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
},
checkAllItems(){
const checkItems = ChecklistItems.find({checklistId: this._id});
checkItems.forEach(function(item){
checkAllItems() {
const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item) {
item.check();
});
},
uncheckAllItems(){
const checkItems = ChecklistItems.find({checklistId: this._id});
checkItems.forEach(function(item){
uncheckAllItems() {
const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item) {
item.uncheck();
});
},
itemIndex(itemId) {
const items = self.findOne({_id : this._id}).items;
const items = self.findOne({ _id: this._id }).items;
return _.pluck(items, '_id').indexOf(itemId);
},
});
@ -124,6 +142,7 @@ Checklists.mutations({
if (Meteor.isServer) {
Meteor.startup(() => {
Checklists._collection._ensureIndex({ modifiedAt: -1 });
Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 });
});
@ -135,12 +154,17 @@ if (Meteor.isServer) {
cardId: doc.cardId,
boardId: card.boardId,
checklistId: doc._id,
checklistName:doc.title,
checklistName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
});
Checklists.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Checklists.before.remove((userId, doc) => {
const activities = Activities.find({ checklistId: doc._id });
const card = Cards.findOne(doc.cardId);
@ -155,7 +179,7 @@ if (Meteor.isServer) {
cardId: doc.cardId,
boardId: Cards.findOne(doc.cardId).boardId,
checklistId: doc._id,
checklistName:doc.title,
checklistName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
@ -172,26 +196,32 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
};
});
if (checklists) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklists,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function(
doc
) {
return {
_id: doc._id,
title: doc.title,
};
});
if (checklists) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklists,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
}
});
);
/**
* @operation get_checklist
@ -209,29 +239,38 @@ if (Meteor.isServer) {
* title: string,
* isFinished: boolean}]}
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId });
if (checklist) {
checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
isFinished: doc.isFinished,
};
});
JsonRoutes.sendResult(res, {
code: 200,
data: checklist,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({
_id: paramChecklistId,
cardId: paramCardId,
});
if (checklist) {
checklist.items = ChecklistItems.find({
checklistId: checklist._id,
}).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
isFinished: doc.isFinished,
};
});
JsonRoutes.sendResult(res, {
code: 200,
data: checklist,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
}
});
);
/**
* @operation new_checklist
@ -242,36 +281,40 @@ if (Meteor.isServer) {
* @param {string} title the title of the new checklist
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId;
const id = Checklists.insert({
title: req.body.title,
cardId: paramCardId,
sort: 0,
});
if (id) {
req.body.items.forEach(function (item, idx) {
ChecklistItems.insert({
cardId: paramCardId,
checklistId: id,
title: item.title,
sort: idx,
const paramCardId = req.params.cardId;
const id = Checklists.insert({
title: req.body.title,
cardId: paramCardId,
sort: 0,
});
if (id) {
req.body.items.forEach(function(item, idx) {
ChecklistItems.insert({
cardId: paramCardId,
checklistId: id,
title: item.title,
sort: idx,
});
});
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
} else {
JsonRoutes.sendResult(res, {
code: 400,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
} else {
JsonRoutes.sendResult(res, {
code: 400,
});
}
}
});
);
/**
* @operation delete_checklist
@ -284,15 +327,21 @@ if (Meteor.isServer) {
* @param {string} checklistId the ID of the checklist to remove
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramChecklistId = req.params.checklistId;
Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramChecklistId,
},
});
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId;
Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramChecklistId,
},
});
}
);
}
export default Checklists;

View file

@ -3,74 +3,100 @@ CustomFields = new Mongo.Collection('customFields');
/**
* A custom field on a card in the board
*/
CustomFields.attachSchema(new SimpleSchema({
boardIds: {
/**
* the ID of the board
*/
type: [String],
},
name: {
/**
* name of the custom field
*/
type: String,
},
type: {
/**
* type of the custom field
*/
type: String,
allowedValues: ['text', 'number', 'date', 'dropdown'],
},
settings: {
/**
* settings of the custom field
*/
type: Object,
},
'settings.dropdownItems': {
/**
* list of drop down items objects
*/
type: [Object],
optional: true,
},
'settings.dropdownItems.$': {
type: new SimpleSchema({
_id: {
/**
* ID of the drop down item
*/
type: String,
CustomFields.attachSchema(
new SimpleSchema({
boardIds: {
/**
* the ID of the board
*/
type: [String],
},
name: {
/**
* name of the custom field
*/
type: String,
},
type: {
/**
* type of the custom field
*/
type: String,
allowedValues: ['text', 'number', 'date', 'dropdown'],
},
settings: {
/**
* settings of the custom field
*/
type: Object,
},
'settings.dropdownItems': {
/**
* list of drop down items objects
*/
type: [Object],
optional: true,
},
'settings.dropdownItems.$': {
type: new SimpleSchema({
_id: {
/**
* ID of the drop down item
*/
type: String,
},
name: {
/**
* name of the drop down item
*/
type: String,
},
}),
},
showOnCard: {
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
name: {
/**
* name of the drop down item
*/
type: String,
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
}),
},
showOnCard: {
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
}));
},
})
);
CustomFields.mutations({
addBoard(boardId) {
@ -88,19 +114,28 @@ CustomFields.mutations({
CustomFields.allow({
insert(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
_id: {$in: doc.boardIds},
}).fetch());
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
},
update(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
_id: {$in: doc.boardIds},
}).fetch());
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
},
remove(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
_id: {$in: doc.boardIds},
}).fetch());
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
},
fetch: ['userId', 'boardIds'],
});
@ -108,7 +143,7 @@ CustomFields.allow({
// not sure if we need this?
//CustomFields.hookOptions.after.update = { fetchPrevious: false };
function customFieldCreation(userId, doc){
function customFieldCreation(userId, doc) {
Activities.insert({
userId,
activityType: 'createCustomField',
@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){
if (Meteor.isServer) {
Meteor.startup(() => {
CustomFields._collection._ensureIndex({ modifiedAt: -1 });
CustomFields._collection._ensureIndex({ boardIds: 1 });
});
@ -149,12 +185,17 @@ if (Meteor.isServer) {
customFieldCreation(userId, doc);
});
CustomFields.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
CustomFields.before.update((userId, doc, fieldNames, modifier) => {
if (_.contains(fieldNames, 'boardIds') && modifier.$pull) {
Cards.update(
{boardId: modifier.$pull.boardIds, 'customFields._id': doc._id},
{$pull: {'customFields': {'_id': doc._id}}},
{multi: true}
{ boardId: modifier.$pull.boardIds, 'customFields._id': doc._id },
{ $pull: { customFields: { _id: doc._id } } },
{ multi: true }
);
customFieldEdit(userId, doc);
Activities.remove({
@ -180,9 +221,9 @@ if (Meteor.isServer) {
});
Cards.update(
{boardId: {$in: doc.boardIds}, 'customFields._id': doc._id},
{$pull: {'customFields': {'_id': doc._id}}},
{multi: true}
{ boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
{ $pull: { customFields: { _id: doc._id } } },
{ multi: true }
);
});
}
@ -198,18 +239,23 @@ if (Meteor.isServer) {
* name: string,
* type: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function(
req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.find({ boardIds: {$in: [paramBoardId]} }).map(function (cf) {
return {
_id: cf._id,
name: cf.name,
type: cf.type,
};
}),
data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
function(cf) {
return {
_id: cf._id,
name: cf.name,
type: cf.type,
};
}
),
});
});
@ -221,15 +267,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field
* @return_type CustomFields
*/
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }),
});
});
JsonRoutes.add(
'GET',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.findOne({
_id: paramCustomFieldId,
boardIds: { $in: [paramBoardId] },
}),
});
}
);
/**
* @operation new_custom_field
@ -244,8 +297,11 @@ if (Meteor.isServer) {
* @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function(
req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const id = CustomFields.direct.insert({
name: req.body.name,
@ -254,10 +310,13 @@ if (Meteor.isServer) {
showOnCard: req.body.showOnCard,
automaticallyOnCard: req.body.automaticallyOnCard,
showLabelOnMiniCard: req.body.showLabelOnMiniCard,
boardIds: {$in: [paramBoardId]},
boardIds: { $in: [paramBoardId] },
});
const customField = CustomFields.findOne({_id: id, boardIds: {$in: [paramBoardId]} });
const customField = CustomFields.findOne({
_id: id,
boardIds: { $in: [paramBoardId] },
});
customFieldCreation(req.body.authorId, customField);
JsonRoutes.sendResult(res, {
@ -278,16 +337,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
}
);
}
export default CustomFields;

View file

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

View file

@ -1,45 +1,78 @@
InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({
code: {
type: String,
},
email: {
type: String,
unique: true,
regEx: SimpleSchema.RegEx.Email,
},
createdAt: {
type: Date,
denyUpdate: false,
},
// always be the admin if only one admin
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
defaultValue: true,
},
}));
InvitationCodes.attachSchema(
new SimpleSchema({
code: {
type: String,
},
email: {
type: String,
unique: true,
regEx: SimpleSchema.RegEx.Email,
},
createdAt: {
type: Date,
denyUpdate: false,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
// always be the admin if only one admin
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
defaultValue: true,
},
})
);
InvitationCodes.helpers({
author(){
author() {
return Users.findOne(this.authorId);
},
});
InvitationCodes.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
// InvitationCodes.before.insert((userId, doc) => {
// doc.createdAt = new Date();
// doc.authorId = userId;
// });
if (Meteor.isServer) {
Meteor.startup(() => {
InvitationCodes._collection._ensureIndex({ modifiedAt: -1 });
});
Boards.deny({
fetch: ['members'],
});
}
export default InvitationCodes;

View file

@ -3,125 +3,161 @@ Lists = new Mongo.Collection('lists');
/**
* A list (column) in the Wekan board.
*/
Lists.attachSchema(new SimpleSchema({
title: {
/**
* the title of the list
*/
type: String,
},
archived: {
/**
* is the list archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
Lists.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the list
*/
type: String,
},
},
boardId: {
/**
* the board associated to this list
*/
type: String,
},
swimlaneId: {
/**
* the swimlane associated to this list. Used for templates
*/
type: String,
defaultValue: '',
},
createdAt: {
/**
* creation date
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
archived: {
/**
* is the list archived
*/
type: Boolean,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return false;
}
},
},
},
sort: {
/**
* is the list sorted
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* last update of the list
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
boardId: {
/**
* the board associated to this list
*/
type: String,
},
},
wipLimit: {
/**
* WIP object, see below
*/
type: Object,
optional: true,
},
'wipLimit.value': {
/**
* value of the WIP
*/
type: Number,
decimal: false,
defaultValue: 1,
},
'wipLimit.enabled': {
/**
* is the WIP enabled
*/
type: Boolean,
defaultValue: false,
},
'wipLimit.soft': {
/**
* is the WIP a soft or hard requirement
*/
type: Boolean,
defaultValue: false,
},
color: {
/**
* the color of the list
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple',
'blue', 'sky', 'lime', 'pink', 'black',
'peachpuff', 'crimson', 'plum', 'darkgreen',
'slateblue', 'magenta', 'gold', 'navy', 'gray',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
],
},
type: {
/**
* The type of list
*/
type: String,
defaultValue: 'list',
},
}));
swimlaneId: {
/**
* the swimlane associated to this list. Used for templates
*/
type: String,
defaultValue: '',
},
createdAt: {
/**
* creation date
*/
type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* is the list sorted
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* last update of the list
*/
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isUpdate || this.isUpsert || this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
wipLimit: {
/**
* WIP object, see below
*/
type: Object,
optional: true,
},
'wipLimit.value': {
/**
* value of the WIP
*/
type: Number,
decimal: false,
defaultValue: 1,
},
'wipLimit.enabled': {
/**
* is the WIP enabled
*/
type: Boolean,
defaultValue: false,
},
'wipLimit.soft': {
/**
* is the WIP a soft or hard requirement
*/
type: Boolean,
defaultValue: false,
},
color: {
/**
* the color of the list
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
},
type: {
/**
* The type of list
*/
type: String,
defaultValue: 'list',
},
})
);
Lists.allow({
insert(userId, doc) {
@ -172,10 +208,8 @@ Lists.helpers({
listId: this._id,
archived: false,
};
if (swimlaneId)
selector.swimlaneId = swimlaneId;
return Cards.find(Filter.mongoSelector(selector),
{ sort: ['sort'] });
if (swimlaneId) selector.swimlaneId = swimlaneId;
return Cards.find(Filter.mongoSelector(selector), { sort: ['sort'] });
},
cardsUnfiltered(swimlaneId) {
@ -183,10 +217,8 @@ Lists.helpers({
listId: this._id,
archived: false,
};
if (swimlaneId)
selector.swimlaneId = swimlaneId;
return Cards.find(selector,
{ sort: ['sort'] });
if (swimlaneId) selector.swimlaneId = swimlaneId;
return Cards.find(selector, { sort: ['sort'] });
},
allCards() {
@ -197,11 +229,12 @@ Lists.helpers({
return Boards.findOne(this.boardId);
},
getWipLimit(option){
getWipLimit(option) {
const list = Lists.findOne({ _id: this._id });
if(!list.wipLimit) { // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
if (!list.wipLimit) {
// Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
return 0;
} else if(!option) {
} else if (!option) {
return list.wipLimit;
} else {
return list.wipLimit[option] ? list.wipLimit[option] : 0; // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
@ -209,8 +242,7 @@ Lists.helpers({
},
colorClass() {
if (this.color)
return this.color;
if (this.color) return this.color;
return '';
},
@ -219,7 +251,7 @@ Lists.helpers({
},
remove() {
Lists.remove({ _id: this._id});
Lists.remove({ _id: this._id });
},
});
@ -271,10 +303,10 @@ Lists.mutations({
});
Meteor.methods({
applyWipLimit(listId, limit){
applyWipLimit(listId, limit) {
check(listId, String);
check(limit, Number);
if(limit === 0){
if (limit === 0) {
limit = 1;
}
Lists.findOne({ _id: listId }).setWipLimit(limit);
@ -283,7 +315,7 @@ Meteor.methods({
enableWipLimit(listId) {
check(listId, String);
const list = Lists.findOne({ _id: listId });
if(list.getWipLimit('value') === 0){
if (list.getWipLimit('value') === 0) {
list.setWipLimit(1);
}
list.toggleWipLimit(!list.getWipLimit('enabled'));
@ -300,6 +332,7 @@ Lists.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) {
Meteor.startup(() => {
Lists._collection._ensureIndex({ modifiedAt: -1 });
Lists._collection._ensureIndex({ boardId: 1 });
});
@ -313,6 +346,11 @@ if (Meteor.isServer) {
});
});
Lists.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Lists.before.remove((userId, doc) => {
const cards = Cards.find({ listId: doc._id });
if (cards) {
@ -353,22 +391,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function(req, res) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
};
}),
data: Lists.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return {
_id: doc._id,
title: doc.title,
};
}
),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -384,17 +423,23 @@ if (Meteor.isServer) {
* @param {string} listId the List ID
* @return_type Lists
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function(
req,
res
) {
try {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }),
data: Lists.findOne({
_id: paramListId,
boardId: paramBoardId,
archived: false,
}),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -410,9 +455,9 @@ if (Meteor.isServer) {
* @param {string} title the title of the List
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) {
JsonRoutes.add('POST', '/api/boards/:boardId/lists', function(req, res) {
try {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId);
const id = Lists.insert({
@ -426,8 +471,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -446,9 +490,12 @@ if (Meteor.isServer) {
* @param {string} listId the ID of the list to remove
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) {
JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function(
req,
res
) {
try {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Lists.remove({ _id: paramListId, boardId: paramBoardId });
@ -458,13 +505,13 @@ if (Meteor.isServer) {
_id: paramListId,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
}
export default Lists;

View file

@ -1,23 +1,51 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({
title: {
type: String,
optional: false,
},
triggerId: {
type: String,
optional: false,
},
actionId: {
type: String,
optional: false,
},
boardId: {
type: String,
optional: false,
},
}));
Rules.attachSchema(
new SimpleSchema({
title: {
type: String,
optional: false,
},
triggerId: {
type: String,
optional: false,
},
actionId: {
type: String,
optional: false,
},
boardId: {
type: String,
optional: false,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Rules.mutations({
rename(description) {
@ -26,15 +54,14 @@ Rules.mutations({
});
Rules.helpers({
getAction(){
return Actions.findOne({_id:this.actionId});
getAction() {
return Actions.findOne({ _id: this.actionId });
},
getTrigger(){
return Triggers.findOne({_id:this.triggerId});
getTrigger() {
return Triggers.findOne({ _id: this.triggerId });
},
});
Rules.allow({
insert(userId, doc) {
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
@ -46,3 +73,16 @@ Rules.allow({
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
},
});
Rules.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Rules._collection._ensureIndex({ modifiedAt: -1 });
});
}
export default Rules;

View file

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

View file

@ -3,89 +3,125 @@ Swimlanes = new Mongo.Collection('swimlanes');
/**
* A swimlane is an line in the kaban board.
*/
Swimlanes.attachSchema(new SimpleSchema({
title: {
/**
* the title of the swimlane
*/
type: String,
},
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
Swimlanes.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the swimlane
*/
type: String,
},
},
boardId: {
/**
* the ID of the board the swimlane is attached to
*/
type: String,
},
createdAt: {
/**
* creation date of the swimlane
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return false;
}
},
},
},
sort: {
/**
* the sort value of the swimlane
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
color: {
/**
* the color of the swimlane
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple',
'blue', 'sky', 'lime', 'pink', 'black',
'peachpuff', 'crimson', 'plum', 'darkgreen',
'slateblue', 'magenta', 'gold', 'navy', 'gray',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
],
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
boardId: {
/**
* the ID of the board the swimlane is attached to
*/
type: String,
},
},
type: {
/**
* The type of swimlane
*/
type: String,
defaultValue: 'swimlane',
},
}));
createdAt: {
/**
* creation date of the swimlane
*/
type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* the sort value of the swimlane
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
color: {
/**
* the color of the swimlane
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isUpdate || this.isUpsert || this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
type: {
/**
* The type of swimlane
*/
type: String,
defaultValue: 'swimlane',
},
})
);
Swimlanes.allow({
insert(userId, doc) {
@ -109,7 +145,7 @@ Swimlanes.helpers({
const _id = Swimlanes.insert(this);
const query = {
swimlaneId: {$in: [oldId, '']},
swimlaneId: { $in: [oldId, ''] },
archived: false,
};
if (oldBoardId) {
@ -126,18 +162,24 @@ Swimlanes.helpers({
},
cards() {
return Cards.find(Filter.mongoSelector({
swimlaneId: this._id,
archived: false,
}), { sort: ['sort'] });
return Cards.find(
Filter.mongoSelector({
swimlaneId: this._id,
archived: false,
}),
{ sort: ['sort'] }
);
},
lists() {
return Lists.find({
boardId: this.boardId,
swimlaneId: {$in: [this._id, '']},
archived: false,
}, { sort: ['sort'] });
return Lists.find(
{
boardId: this.boardId,
swimlaneId: { $in: [this._id, ''] },
archived: false,
},
{ sort: ['sort'] }
);
},
myLists() {
@ -153,8 +195,7 @@ Swimlanes.helpers({
},
colorClass() {
if (this.color)
return this.color;
if (this.color) return this.color;
return '';
},
@ -182,7 +223,7 @@ Swimlanes.helpers({
},
remove() {
Swimlanes.remove({ _id: this._id});
Swimlanes.remove({ _id: this._id });
},
});
@ -221,10 +262,16 @@ Swimlanes.mutations({
},
});
Swimlanes.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Swimlanes.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) {
Meteor.startup(() => {
Swimlanes._collection._ensureIndex({ modifiedAt: -1 });
Swimlanes._collection._ensureIndex({ boardId: 1 });
});
@ -239,18 +286,21 @@ if (Meteor.isServer) {
});
Swimlanes.before.remove(function(userId, doc) {
const lists = Lists.find({
boardId: doc.boardId,
swimlaneId: {$in: [doc._id, '']},
archived: false,
}, { sort: ['sort'] });
const lists = Lists.find(
{
boardId: doc.boardId,
swimlaneId: { $in: [doc._id, ''] },
archived: false,
},
{ sort: ['sort'] }
);
if (lists.count() < 2) {
lists.forEach((list) => {
list.remove();
});
} else {
Cards.remove({swimlaneId: doc._id});
Cards.remove({ swimlaneId: doc._id });
}
Activities.insert({
@ -287,22 +337,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
};
}),
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return {
_id: doc._id,
title: doc.title,
};
}
),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -319,17 +370,23 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane
* @return_type Swimlanes
*/
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function(
req,
res
) {
try {
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }),
data: Swimlanes.findOne({
_id: paramSwimlaneId,
boardId: paramBoardId,
archived: false,
}),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -346,9 +403,9 @@ if (Meteor.isServer) {
* @param {string} title the new title of the swimlane
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) {
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
try {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId);
const id = Swimlanes.insert({
@ -362,8 +419,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -382,25 +438,29 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
try {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramSwimlaneId,
},
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/swimlanes/:swimlaneId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramSwimlaneId,
},
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
}
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.mutations({
@ -23,7 +25,6 @@ Triggers.allow({
});
Triggers.helpers({
description() {
return this.desc;
},
@ -56,3 +57,16 @@ Triggers.helpers({
return cardLabels;
},
});
Triggers.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Triggers._collection._ensureIndex({ modifiedAt: -1 });
});
}
export default Triggers;

View file

@ -2,31 +2,66 @@
// `UnsavedEdits` API on the client.
UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
UnsavedEditCollection.attachSchema(new SimpleSchema({
fieldName: {
type: String,
},
docId: {
type: String,
},
value: {
type: String,
},
userId: {
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
UnsavedEditCollection.attachSchema(
new SimpleSchema({
fieldName: {
type: String,
},
},
}));
docId: {
type: String,
},
value: {
type: String,
},
userId: {
type: String,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return this.userId;
}
},
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
UnsavedEditCollection.before.update(
(userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
}
);
if (Meteor.isServer) {
function isAuthor(userId, doc, fieldNames = []) {
return userId === doc.userId && fieldNames.indexOf('userId') === -1;
}
Meteor.startup(() => {
UnsavedEditCollection._collection._ensureIndex({ modifiedAt: -1 });
UnsavedEditCollection._collection._ensureIndex({ userId: 1 });
});
UnsavedEditCollection.allow({
@ -36,3 +71,5 @@ if (Meteor.isServer) {
fetch: ['userId'],
});
}
export default UnsavedEditCollection;

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