Add createdAt and modifiedAt to all collections

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

View file

@ -1,6 +1,7 @@
AccountSettings = new Mongo.Collection('accountSettings');
AccountSettings.attachSchema(new SimpleSchema({
AccountSettings.attachSchema(
new SimpleSchema({
_id: {
type: String,
},
@ -12,7 +13,32 @@ AccountSettings.attachSchema(new SimpleSchema({
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'}, {
AccountSettings._collection._ensureIndex({ modifiedAt: -1 });
AccountSettings.upsert(
{ _id: 'accounts-allowEmailChange' },
{
$setOnInsert: {
booleanValue: false,
sort: 0,
},
});
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, {
}
);
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,6 +1,7 @@
Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({
Announcements.attachSchema(
new SimpleSchema({
enabled: {
type: Boolean,
defaultValue: false,
@ -17,7 +18,32 @@ Announcements.attachSchema(new SimpleSchema({
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 });
}
});
}
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({
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;

View file

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

View file

@ -3,7 +3,8 @@ CardComments = new Mongo.Collection('card_comments');
/**
* A comment on a card
*/
CardComments.attachSchema(new SimpleSchema({
CardComments.attachSchema(
new SimpleSchema({
boardId: {
/**
* the board ID
@ -23,15 +24,14 @@ CardComments.attachSchema(new SimpleSchema({
*/
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
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
@ -39,19 +39,33 @@ CardComments.attachSchema(new SimpleSchema({
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
// XXX Should probably be called `authorId`
userId: {
/**
* the author ID of the comment
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return this.userId;
}
},
},
}));
})
);
CardComments.allow({
insert(userId, doc) {
@ -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);
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,7 +210,10 @@ 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) {
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
@ -193,16 +221,20 @@ if (Meteor.isServer) {
const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, {
code: 200,
data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }),
data: CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
}),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
}
);
/**
* @operation new_comment
@ -214,7 +246,10 @@ 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) {
JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/comments',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
@ -233,16 +268,20 @@ if (Meteor.isServer) {
},
});
const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId });
const cardComment = CardComments.findOne({
_id: id,
cardId: paramCardId,
boardId: paramBoardId,
});
commentCreation(req.body.authorId, cardComment);
}
catch (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) {
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 });
CardComments.remove({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
},
});
}
catch (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,6 +1552,7 @@ 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({ 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.
@ -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,7 +3,8 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/**
* An item in a checklist
*/
ChecklistItems.attachSchema(new SimpleSchema({
ChecklistItems.attachSchema(
new SimpleSchema({
title: {
/**
* the text of the item
@ -36,7 +37,32 @@ ChecklistItems.attachSchema(new SimpleSchema({
*/
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) {
@ -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);
@ -233,7 +264,10 @@ 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) {
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 });
@ -247,7 +281,8 @@ if (Meteor.isServer) {
code: 500,
});
}
});
}
);
/**
* @operation edit_checklist_item
@ -262,16 +297,25 @@ 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) {
JsonRoutes.add(
'PUT',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId;
if (req.body.hasOwnProperty('isFinished')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}});
ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { isFinished: req.body.isFinished } }
);
}
if (req.body.hasOwnProperty('title')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}});
ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { title: req.body.title } }
);
}
JsonRoutes.sendResult(res, {
@ -280,7 +324,8 @@ if (Meteor.isServer) {
_id: paramItemId,
},
});
});
}
);
/**
* @operation delete_checklist_item
@ -295,7 +340,10 @@ 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) {
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 });
@ -305,5 +353,8 @@ if (Meteor.isServer) {
_id: paramItemId,
},
});
});
}
);
}
export default ChecklistItems;

View file

@ -3,7 +3,8 @@ Checklists = new Mongo.Collection('checklists');
/**
* A Checklist
*/
Checklists.attachSchema(new SimpleSchema({
Checklists.attachSchema(
new SimpleSchema({
cardId: {
/**
* The ID of the card the checklist is in
@ -30,7 +31,8 @@ Checklists.attachSchema(new SimpleSchema({
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
@ -38,6 +40,18 @@ Checklists.attachSchema(new SimpleSchema({
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* sorting value of the checklist
@ -45,7 +59,8 @@ Checklists.attachSchema(new SimpleSchema({
type: Number,
decimal: true,
},
}));
})
);
Checklists.helpers({
copy(newCardId) {
@ -65,9 +80,12 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count();
},
items() {
return ChecklistItems.find({
return ChecklistItems.find(
{
checklistId: this._id,
}, { sort: ['sort'] });
},
{ sort: ['sort'] }
);
},
finishedCount() {
return ChecklistItems.find({
@ -124,6 +142,7 @@ Checklists.mutations({
if (Meteor.isServer) {
Meteor.startup(() => {
Checklists._collection._ensureIndex({ modifiedAt: -1 });
Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 });
});
@ -141,6 +160,11 @@ if (Meteor.isServer) {
});
});
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);
@ -172,10 +196,15 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
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) {
const checklists = Checklists.find({ cardId: paramCardId }).map(function(
doc
) {
return {
_id: doc._id,
title: doc.title,
@ -191,7 +220,8 @@ if (Meteor.isServer) {
code: 500,
});
}
});
}
);
/**
* @operation get_checklist
@ -209,13 +239,21 @@ if (Meteor.isServer) {
* title: string,
* isFinished: boolean}]}
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
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 });
const checklist = Checklists.findOne({
_id: paramChecklistId,
cardId: paramCardId,
});
if (checklist) {
checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) {
checklist.items = ChecklistItems.find({
checklistId: checklist._id,
}).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
@ -231,7 +269,8 @@ if (Meteor.isServer) {
code: 500,
});
}
});
}
);
/**
* @operation new_checklist
@ -242,7 +281,10 @@ 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) {
JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId;
@ -271,7 +313,8 @@ if (Meteor.isServer) {
code: 400,
});
}
});
}
);
/**
* @operation delete_checklist
@ -284,7 +327,10 @@ 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) {
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 });
@ -294,5 +340,8 @@ if (Meteor.isServer) {
_id: paramChecklistId,
},
});
});
}
);
}
export default Checklists;

View file

@ -3,7 +3,8 @@ CustomFields = new Mongo.Collection('customFields');
/**
* A custom field on a card in the board
*/
CustomFields.attachSchema(new SimpleSchema({
CustomFields.attachSchema(
new SimpleSchema({
boardIds: {
/**
* the ID of the board
@ -70,7 +71,32 @@ CustomFields.attachSchema(new SimpleSchema({
*/
type: Boolean,
},
}));
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
CustomFields.mutations({
addBoard(boardId) {
@ -88,19 +114,28 @@ CustomFields.mutations({
CustomFields.allow({
insert(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch());
}).fetch()
);
},
update(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch());
}).fetch()
);
},
remove(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch());
}).fetch()
);
},
fetch: ['userId', 'boardIds'],
});
@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){
if (Meteor.isServer) {
Meteor.startup(() => {
CustomFields._collection._ensureIndex({ modifiedAt: -1 });
CustomFields._collection._ensureIndex({ boardIds: 1 });
});
@ -149,11 +185,16 @@ 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}}},
{ $pull: { customFields: { _id: doc._id } } },
{ multi: true }
);
customFieldEdit(userId, doc);
@ -181,7 +222,7 @@ if (Meteor.isServer) {
Cards.update(
{ boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
{$pull: {'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) {
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) {
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) {
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]} }),
});
data: CustomFields.findOne({
_id: paramCustomFieldId,
boardIds: { $in: [paramBoardId] },
}),
});
}
);
/**
* @operation new_custom_field
@ -244,7 +297,10 @@ 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) {
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({
@ -257,7 +313,10 @@ if (Meteor.isServer) {
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,7 +337,10 @@ 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) {
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;
@ -289,5 +351,8 @@ if (Meteor.isServer) {
_id: id,
},
});
});
}
);
}
export default CustomFields;

View file

@ -3,7 +3,8 @@ Integrations = new Mongo.Collection('integrations');
/**
* Integration with third-party applications
*/
Integrations.attachSchema(new SimpleSchema({
Integrations.attachSchema(
new SimpleSchema({
enabled: {
/**
* is the integration enabled?
@ -32,7 +33,8 @@ Integrations.attachSchema(new SimpleSchema({
type: [String],
defaultValue: ['all'],
},
url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex)
url: {
// URL validation regex (https://mathiasbynens.be/demo/url-regex)
/**
* URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/
@ -57,7 +59,8 @@ Integrations.attachSchema(new SimpleSchema({
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
@ -65,13 +68,31 @@ Integrations.attachSchema(new SimpleSchema({
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
userId: {
/**
* 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) {
} 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) {
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}}),
data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { _id: 1, activities: 1 } }
),
});
}
catch (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) {
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}}),
data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { _id: 1, activities: 1 } }
),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
}
);
/**
* @operation delete_integration
@ -311,7 +373,10 @@ 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;
@ -324,8 +389,7 @@ if (Meteor.isServer) {
_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,6 +1,7 @@
InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({
InvitationCodes.attachSchema(
new SimpleSchema({
code: {
type: String,
},
@ -12,6 +13,27 @@ InvitationCodes.attachSchema(new SimpleSchema({
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: {
@ -25,7 +47,8 @@ InvitationCodes.attachSchema(new SimpleSchema({
type: Boolean,
defaultValue: true,
},
}));
})
);
InvitationCodes.helpers({
author() {
@ -33,13 +56,23 @@ InvitationCodes.helpers({
},
});
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,7 +3,8 @@ Lists = new Mongo.Collection('lists');
/**
* A list (column) in the Wekan board.
*/
Lists.attachSchema(new SimpleSchema({
Lists.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the list
@ -15,7 +16,8 @@ Lists.attachSchema(new SimpleSchema({
* is the list archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return false;
}
@ -39,7 +41,8 @@ Lists.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 {
@ -62,8 +65,21 @@ Lists.attachSchema(new SimpleSchema({
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
// 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();
@ -107,11 +123,30 @@ Lists.attachSchema(new SimpleSchema({
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',
'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: {
@ -121,7 +156,8 @@ Lists.attachSchema(new SimpleSchema({
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() {
@ -199,7 +231,8 @@ Lists.helpers({
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) {
return list.wipLimit;
@ -209,8 +242,7 @@ Lists.helpers({
},
colorClass() {
if (this.color)
return this.color;
if (this.color) return this.color;
return '';
},
@ -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) {
@ -360,15 +398,16 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, {
code: 200,
data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
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);
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,
@ -426,8 +471,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -446,7 +490,10 @@ 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);
const paramBoardId = req.params.boardId;
@ -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,6 +1,9 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({
Rules.attachSchema(
new SimpleSchema({
title: {
type: String,
optional: false,
@ -17,7 +20,32 @@ Rules.attachSchema(new SimpleSchema({
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) {
@ -34,7 +62,6 @@ Rules.helpers({
},
});
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,6 +1,7 @@
Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({
Settings.attachSchema(
new SimpleSchema({
disableRegistration: {
type: Boolean,
},
@ -55,11 +56,28 @@ Settings.attachSchema(new SimpleSchema({
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() {
if (!this.mailServer.host) {
@ -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,30 +106,52 @@ Settings.before.update((userId, doc, fieldNames, modifier) => {
if (Meteor.isServer) {
Meteor.startup(() => {
Settings._collection._ensureIndex({ modifiedAt: -1 });
const setting = Settings.findOne({});
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;
}
@ -118,7 +160,7 @@ if (Meteor.isServer) {
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) {
@ -126,7 +168,10 @@ if (Meteor.isServer) {
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) {
@ -180,22 +225,39 @@ if (Meteor.isServer) {
// 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.`);
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}});
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){
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);
throw new Meteor.Error(
'invitation-generated-fail',
err.message
);
}
});
}
);
}
}
});
@ -219,7 +281,11 @@ if (Meteor.isServer) {
text: TAPi18n.__('email-smtp-test-text', { lng: lang }),
});
} catch ({ message }) {
throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message);
throw new Meteor.Error(
'email-fail',
`${TAPi18n.__('email-fail-text', { lng: lang })}: ${message}`,
message
);
}
return {
message: 'email-sent',
@ -275,3 +341,5 @@ if (Meteor.isServer) {
},
});
}
export default Settings;

View file

@ -3,7 +3,8 @@ Swimlanes = new Mongo.Collection('swimlanes');
/**
* A swimlane is an line in the kaban board.
*/
Swimlanes.attachSchema(new SimpleSchema({
Swimlanes.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the swimlane
@ -15,7 +16,8 @@ Swimlanes.attachSchema(new SimpleSchema({
* is the swimlane archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return false;
}
@ -32,7 +34,8 @@ Swimlanes.attachSchema(new SimpleSchema({
* creation date of the swimlane
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
@ -57,11 +60,30 @@ Swimlanes.attachSchema(new SimpleSchema({
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',
'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: {
@ -70,8 +92,21 @@ Swimlanes.attachSchema(new SimpleSchema({
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
// 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();
@ -85,7 +120,8 @@ Swimlanes.attachSchema(new SimpleSchema({
type: String,
defaultValue: 'swimlane',
},
}));
})
);
Swimlanes.allow({
insert(userId, doc) {
@ -126,18 +162,24 @@ Swimlanes.helpers({
},
cards() {
return Cards.find(Filter.mongoSelector({
return Cards.find(
Filter.mongoSelector({
swimlaneId: this._id,
archived: false,
}), { sort: ['sort'] });
}),
{ sort: ['sort'] }
);
},
lists() {
return Lists.find({
return Lists.find(
{
boardId: this.boardId,
swimlaneId: { $in: [this._id, ''] },
archived: false,
}, { sort: ['sort'] });
},
{ sort: ['sort'] }
);
},
myLists() {
@ -153,8 +195,7 @@ Swimlanes.helpers({
},
colorClass() {
if (this.color)
return this.color;
if (this.color) return this.color;
return '';
},
@ -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,11 +286,14 @@ if (Meteor.isServer) {
});
Swimlanes.before.remove(function(userId, doc) {
const lists = Lists.find({
const lists = Lists.find(
{
boardId: doc.boardId,
swimlaneId: { $in: [doc._id, ''] },
archived: false,
}, { sort: ['sort'] });
},
{ sort: ['sort'] }
);
if (lists.count() < 2) {
lists.forEach((list) => {
@ -294,15 +344,16 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, {
code: 200,
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
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);
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,
@ -362,8 +419,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -382,7 +438,10 @@ 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) {
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/swimlanes/:swimlaneId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
@ -394,13 +453,14 @@ if (Meteor.isServer) {
_id: paramSwimlaneId,
},
});
}
catch (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,7 +2,8 @@
// `UnsavedEdits` API on the client.
UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
UnsavedEditCollection.attachSchema(new SimpleSchema({
UnsavedEditCollection.attachSchema(
new SimpleSchema({
fieldName: {
type: String,
},
@ -14,19 +15,53 @@ UnsavedEditCollection.attachSchema(new SimpleSchema({
},
userId: {
type: String,
autoValue() { // eslint-disable-line consistent-return
// 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;

View file

@ -1,20 +1,22 @@
// Sandstorm context is detected using the METEOR_SETTINGS environment variable
// in the package definition.
const isSandstorm = Meteor.settings && Meteor.settings.public &&
Meteor.settings.public.sandstorm;
const isSandstorm =
Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
Users = Meteor.users;
/**
* A User in wekan
*/
Users.attachSchema(new SimpleSchema({
Users.attachSchema(
new SimpleSchema({
username: {
/**
* the username of the user
*/
type: String,
optional: true,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
const name = this.field('profile.fullname');
if (name.isSet) {
@ -48,7 +50,8 @@ Users.attachSchema(new SimpleSchema({
* creation date of the user
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
@ -56,13 +59,26 @@ Users.attachSchema(new SimpleSchema({
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
profile: {
/**
* profile settings
*/
type: Object,
optional: true,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return {
boardView: 'board-view-lists',
@ -223,7 +239,7 @@ Users.attachSchema(new SimpleSchema({
type: Boolean,
optional: true,
},
'authenticationMethod': {
authenticationMethod: {
/**
* authentication method of the user
*/
@ -231,7 +247,8 @@ Users.attachSchema(new SimpleSchema({
optional: false,
defaultValue: 'password',
},
}));
})
);
Users.allow({
update(userId) {
@ -240,7 +257,10 @@ Users.allow({
},
remove(userId, doc) {
const adminsNumber = Users.find({ isAdmin: true }).count();
const { isAdmin } = Users.findOne({ _id: userId }, { fields: { 'isAdmin': 1 } });
const { isAdmin } = Users.findOne(
{ _id: userId },
{ fields: { isAdmin: 1 } }
);
// Prevents remove of the only one administrator
if (adminsNumber === 1 && isAdmin && userId === doc._id) {
@ -270,7 +290,9 @@ if (Meteor.isClient) {
isNotNoComments() {
const board = Boards.findOne(Session.get('currentBoard'));
return board && board.hasMember(this._id) && !board.hasNoComments(this._id);
return (
board && board.hasMember(this._id) && !board.hasNoComments(this._id)
);
},
isNoComments() {
@ -280,7 +302,9 @@ if (Meteor.isClient) {
isNotCommentOnly() {
const board = Boards.findOne(Session.get('currentBoard'));
return board && board.hasMember(this._id) && !board.hasCommentOnly(this._id);
return (
board && board.hasMember(this._id) && !board.hasCommentOnly(this._id)
);
},
isCommentOnly() {
@ -342,14 +366,14 @@ Users.helpers({
getInitials() {
const profile = this.profile || {};
if (profile.initials)
return profile.initials;
if (profile.initials) return profile.initials;
else if (profile.fullname) {
return profile.fullname.split(/\s+/).reduce((memo, word) => {
return profile.fullname
.split(/\s+/)
.reduce((memo, word) => {
return memo + word[0];
}, '').toUpperCase();
}, '')
.toUpperCase();
} else {
return this.username[0].toUpperCase();
}
@ -426,10 +450,8 @@ Users.mutations({
},
toggleTag(tag) {
if (this.hasTag(tag))
this.removeTag(tag);
else
this.addTag(tag);
if (this.hasTag(tag)) this.removeTag(tag);
else this.addTag(tag);
},
toggleSystem(value = false) {
@ -509,16 +531,21 @@ Meteor.methods({
},
setEmail(email, userId) {
check(email, String);
const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}});
const existingUser = Users.findOne(
{ 'emails.address': email },
{ fields: { _id: 1 } }
);
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
Users.update(userId, {
$set: {
emails: [{
emails: [
{
address: email,
verified: false,
}],
},
],
},
});
}
@ -548,7 +575,8 @@ if (Meteor.isServer) {
const inviter = Meteor.user();
const board = Boards.findOne(boardId);
const allowInvite = inviter &&
const allowInvite =
inviter &&
board &&
board.members &&
_.contains(_.pluck(board.members, 'userId'), inviter._id) &&
@ -566,10 +594,12 @@ if (Meteor.isServer) {
user = Users.findOne(username) || Users.findOne({ username });
}
if (user) {
if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
if (user._id === inviter._id)
throw new Meteor.Error('error-user-notAllowSelf');
} else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
if (Settings.findOne().disableRegistration) throw new Meteor.Error('error-user-notCreated');
if (Settings.findOne().disableRegistration)
throw new Meteor.Error('error-user-notCreated');
// Set in lowercase email before creating account
const email = username.toLowerCase();
username = email.substring(0, posAt);
@ -621,14 +651,22 @@ if (Meteor.isServer) {
const email = user.services.oidc.email.toLowerCase();
user.username = user.services.oidc.username;
user.emails = [{ address: email, verified: true }];
const initials = user.services.oidc.fullname.match(/\b[a-zA-Z]/g).join('').toUpperCase();
user.profile = { initials, fullname: user.services.oidc.fullname, boardView: 'board-view-lists' };
const initials = user.services.oidc.fullname
.match(/\b[a-zA-Z]/g)
.join('')
.toUpperCase();
user.profile = {
initials,
fullname: user.services.oidc.fullname,
boardView: 'board-view-lists',
};
user.authenticationMethod = 'oauth2';
// see if any existing user has this email address or username, otherwise create new
const existingUser = Meteor.users.findOne({$or: [{'emails.address': email}, {'username':user.username}]});
if (!existingUser)
return user;
const existingUser = Meteor.users.findOne({
$or: [{ 'emails.address': email }, { username: user.username }],
});
if (!existingUser) return user;
// copy across new service info
const service = _.keys(user.services)[0];
@ -660,7 +698,10 @@ if (Meteor.isServer) {
}
if (!options || !options.profile) {
throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required');
throw new Meteor.Error(
'error-invitation-code-blank',
'The invitation code is required'
);
}
const invitationCode = InvitationCodes.findOne({
code: options.profile.invitationcode,
@ -668,26 +709,41 @@ if (Meteor.isServer) {
valid: true,
});
if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist');
throw new Meteor.Error(
'error-invitation-code-not-exist',
'The invitation code doesn\'t exist'
);
} else {
user.profile = { icode: options.profile.invitationcode };
user.profile.boardView = 'board-view-lists';
// Deletes the invitation code after the user was created successfully.
setTimeout(Meteor.bindEnvironment(() => {
InvitationCodes.remove({'_id': invitationCode._id});
}), 200);
setTimeout(
Meteor.bindEnvironment(() => {
InvitationCodes.remove({ _id: invitationCode._id });
}),
200
);
return user;
}
});
}
Users.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
// Let mongoDB ensure username unicity
Meteor.startup(() => {
Users._collection._ensureIndex({
Users._collection._ensureIndex({ modifiedAt: -1 });
Users._collection._ensureIndex(
{
username: 1,
}, {unique: true});
},
{ unique: true }
);
});
// OLD WAY THIS CODE DID WORK: When user is last admin of board,
@ -715,8 +771,7 @@ if (Meteor.isServer) {
Users.after.update(function(userId, user, fieldNames) {
// The `starredBoards` list is hosted on the `profile` field. If this
// field hasn't been modificated we don't need to run this hook.
if (!_.contains(fieldNames, 'profile'))
return;
if (!_.contains(fieldNames, 'profile')) return;
// To calculate a diff of board starred ids, we get both the previous
// and the newly board ids list
@ -773,57 +828,76 @@ if (Meteor.isServer) {
});
*/
Boards.insert({
Boards.insert(
{
title: TAPi18n.__('templates'),
permission: 'private',
type: 'template-container',
}, fakeUser, (err, boardId) => {
},
fakeUser,
(err, boardId) => {
// Insert the reference to our templates board
Users.update(fakeUserId.get(), {$set: {'profile.templatesBoardId': boardId}});
Users.update(fakeUserId.get(), {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the card templates swimlane
Swimlanes.insert({
Swimlanes.insert(
{
title: TAPi18n.__('card-templates-swimlane'),
boardId,
sort: 1,
type: 'template-container',
}, fakeUser, (err, swimlaneId) => {
},
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}});
Users.update(fakeUserId.get(), {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the list templates swimlane
Swimlanes.insert({
Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'),
boardId,
sort: 2,
type: 'template-container',
}, fakeUser, (err, swimlaneId) => {
},
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}});
Users.update(fakeUserId.get(), {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the board templates swimlane
Swimlanes.insert({
Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'),
boardId,
sort: 3,
type: 'template-container',
}, fakeUser, (err, swimlaneId) => {
},
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}});
});
Users.update(fakeUserId.get(), {
$set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
});
}
);
}
);
});
});
}
Users.after.insert((userId, doc) => {
if (doc.createdThroughApi) {
// The admin user should be able to create a user despite disabling registration because
// it is two different things (registration and creation).
@ -840,7 +914,10 @@ if (Meteor.isServer) {
// If ldap, bypass the inviation code if the self registration isn't allowed.
// TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type
if (doc.authenticationMethod !== 'ldap' && disableRegistration) {
const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true});
const invitationCode = InvitationCodes.findOne({
code: doc.profile.icode,
valid: true,
});
if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist');
} else {
@ -864,10 +941,9 @@ if (Meteor.isServer) {
// Middleware which checks that API is enabled.
JsonRoutes.Middleware.use(function(req, res, next) {
const api = req.url.search('api');
if (api === 1 && process.env.WITH_API === 'true' || api === -1){
if ((api === 1 && process.env.WITH_API === 'true') || api === -1) {
return next();
}
else {
} else {
res.writeHead(301, { Location: '/' });
return res.end();
}
@ -888,8 +964,7 @@ if (Meteor.isServer) {
code: 200,
data,
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -915,8 +990,7 @@ if (Meteor.isServer) {
return { _id: doc._id, username: doc.username };
}),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -942,8 +1016,7 @@ if (Meteor.isServer) {
code: 200,
data: Meteor.users.findOne({ _id: id }),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -990,8 +1063,16 @@ if (Meteor.isServer) {
};
});
} else {
if ((action === 'disableLogin') && (id !== req.userId)) {
Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } });
if (action === 'disableLogin' && id !== req.userId) {
Users.update(
{ _id: id },
{
$set: {
loginDisabled: true,
'services.resume.loginTokens': '',
},
}
);
} else if (action === 'enableLogin') {
Users.update({ _id: id }, { $set: { loginDisabled: '' } });
}
@ -1002,8 +1083,7 @@ if (Meteor.isServer) {
code: 200,
data,
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -1030,7 +1110,10 @@ if (Meteor.isServer) {
* @return_type {_id: string,
* title: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function (req, res) {
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function(
req,
res
) {
try {
Authentication.checkUserId(req.userId);
const userId = req.params.userId;
@ -1048,7 +1131,13 @@ if (Meteor.isServer) {
function isTrue(data) {
return data.toLowerCase() === 'true';
}
board.setMemberPermission(userId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), userId);
board.setMemberPermission(
userId,
isTrue(isAdmin),
isTrue(isNoComments),
isTrue(isCommentOnly),
userId
);
}
return {
_id: board._id,
@ -1061,8 +1150,7 @@ if (Meteor.isServer) {
code: 200,
data: query,
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -1084,7 +1172,10 @@ if (Meteor.isServer) {
* @return_type {_id: string,
* title: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/remove', function (req, res) {
JsonRoutes.add(
'POST',
'/api/boards/:boardId/members/:userId/remove',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const userId = req.params.userId;
@ -1110,14 +1201,14 @@ if (Meteor.isServer) {
code: 200,
data: query,
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
}
);
/**
* @operation new_user
@ -1146,8 +1237,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -1176,8 +1266,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -1185,3 +1274,5 @@ if (Meteor.isServer) {
}
});
}
export default Users;

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({
Boards.update(
{
background: {
$exists: false,
},
}, {
},
{
$set: {
background: {
type: 'color',
color: defaultColor,
},
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('lowercase-board-permission', () => {
@ -57,12 +81,16 @@ Migrations.add('change-attachments-type-for-non-images', () => {
const newTypeForNonImage = 'application/octet-stream';
Attachments.find().forEach((file) => {
if (!file.isImage()) {
Attachments.update(file._id, {
Attachments.update(
file._id,
{
$set: {
'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage,
},
}, noValidate);
},
noValidate
);
}
});
});
@ -89,10 +117,14 @@ 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, {
Boards.update(
board._id,
{
$set: { color: newBoardColor },
$unset: { background: '' },
}, noValidate);
},
noValidate
);
});
});
@ -108,7 +140,8 @@ Migrations.add('denormalize-star-number-per-board', () => {
Migrations.add('add-member-isactive-field', () => {
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();
@ -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,7 +226,8 @@ Migrations.add('add-checklist-items', () => {
});
// Delete old ones
Checklists.direct.update({ _id: checklist._id },
Checklists.direct.update(
{ _id: checklist._id },
{ $unset: { items: 1 } },
noValidate
);
@ -217,251 +251,334 @@ Migrations.add('add-card-types', () => {
Cards.find().forEach((card) => {
Cards.direct.update(
{ _id: card._id },
{ $set: {
{
$set: {
type: 'cardType-card',
linkedId: null } },
linkedId: null,
},
},
noValidate
);
});
});
Migrations.add('add-custom-fields-to-cards', () => {
Cards.update({
Cards.update(
{
customFields: {
$exists: false,
},
}, {
},
{
$set: {
customFields: [],
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-requester-field', () => {
Cards.update({
Cards.update(
{
requestedBy: {
$exists: false,
},
}, {
},
{
$set: {
requestedBy: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-assigner-field', () => {
Cards.update({
Cards.update(
{
assignedBy: {
$exists: false,
},
}, {
},
{
$set: {
assignedBy: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-parent-field-to-cards', () => {
Cards.update({
Cards.update(
{
parentId: {
$exists: false,
},
}, {
},
{
$set: {
parentId: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-subtasks-boards', () => {
Boards.update({
Boards.update(
{
subtasksDefaultBoardId: {
$exists: false,
},
}, {
},
{
$set: {
subtasksDefaultBoardId: null,
subtasksDefaultListId: null,
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-subtasks-sort', () => {
Boards.update({
Boards.update(
{
subtaskSort: {
$exists: false,
},
}, {
},
{
$set: {
subtaskSort: -1,
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-subtasks-allowed', () => {
Boards.update({
Boards.update(
{
allowsSubtasks: {
$exists: false,
},
}, {
},
{
$set: {
allowsSubtasks: true,
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-subtasks-allowed', () => {
Boards.update({
Boards.update(
{
presentParentTask: {
$exists: false,
},
}, {
},
{
$set: {
presentParentTask: 'no-parent',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-authenticationMethod', () => {
Users.update({
'authenticationMethod': {
Users.update(
{
authenticationMethod: {
$exists: false,
},
}, {
$set: {
'authenticationMethod': 'password',
},
}, noValidateMulti);
{
$set: {
authenticationMethod: 'password',
},
},
noValidateMulti
);
});
Migrations.add('remove-tag', () => {
Users.update({
}, {
Users.update(
{},
{
$unset: {
'profile.tags': 1,
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('remove-customFields-references-broken', () => {
Cards.update({'customFields.$value': null},
{ $pull: {
Cards.update(
{ 'customFields.$value': null },
{
$pull: {
customFields: { value: null },
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-product-name', () => {
Settings.update({
Settings.update(
{
productName: {
$exists: false,
},
}, {
},
{
$set: {
productName: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-hide-logo', () => {
Settings.update({
Settings.update(
{
hideLogo: {
$exists: false,
},
}, {
},
{
$set: {
hideLogo: false,
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-custom-html-after-body-start', () => {
Settings.update({
Settings.update(
{
customHTMLafterBodyStart: {
$exists: false,
},
}, {
},
{
$set: {
customHTMLafterBodyStart: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-custom-html-before-body-end', () => {
Settings.update({
Settings.update(
{
customHTMLbeforeBodyEnd: {
$exists: false,
},
}, {
},
{
$set: {
customHTMLbeforeBodyEnd: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-displayAuthenticationMethod', () => {
Settings.update({
Settings.update(
{
displayAuthenticationMethod: {
$exists: false,
},
}, {
},
{
$set: {
displayAuthenticationMethod: true,
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-defaultAuthenticationMethod', () => {
Settings.update({
Settings.update(
{
defaultAuthenticationMethod: {
$exists: false,
},
}, {
},
{
$set: {
defaultAuthenticationMethod: 'password',
},
}, noValidateMulti);
},
noValidateMulti
);
});
Migrations.add('add-templates', () => {
Boards.update({
Boards.update(
{
type: {
$exists: false,
},
}, {
},
{
$set: {
type: 'board',
},
}, noValidateMulti);
Swimlanes.update({
},
noValidateMulti
);
Swimlanes.update(
{
type: {
$exists: false,
},
}, {
},
{
$set: {
type: 'swimlane',
},
}, noValidateMulti);
Lists.update({
},
noValidateMulti
);
Lists.update(
{
type: {
$exists: false,
},
swimlaneId: {
$exists: false,
},
}, {
},
{
$set: {
type: 'list',
swimlaneId: '',
},
}, noValidateMulti);
},
noValidateMulti
);
Users.find({
'profile.templatesBoardId': {
$exists: false,
},
}).forEach((user) => {
// Create board and swimlanes
Boards.insert({
Boards.insert(
{
title: TAPi18n.__('templates'),
permission: 'private',
type: 'template-container',
@ -474,47 +591,62 @@ Migrations.add('add-templates', () => {
isCommentOnly: false,
},
],
}, (err, boardId) => {
},
(err, boardId) => {
// Insert the reference to our templates board
Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}});
Users.update(user._id, {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the card templates swimlane
Swimlanes.insert({
Swimlanes.insert(
{
title: TAPi18n.__('card-templates-swimlane'),
boardId,
sort: 1,
type: 'template-container',
}, (err, swimlaneId) => {
},
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane
Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}});
Users.update(user._id, {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the list templates swimlane
Swimlanes.insert({
Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'),
boardId,
sort: 2,
type: 'template-container',
}, (err, swimlaneId) => {
},
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane
Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}});
Users.update(user._id, {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the board templates swimlane
Swimlanes.insert({
Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'),
boardId,
sort: 3,
type: 'template-container',
}, (err, swimlaneId) => {
},
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane
Users.update(user._id, {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}});
});
Users.update(user._id, {
$set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
});
}
);
}
);
});
});
@ -528,13 +660,103 @@ Migrations.add('fix-circular-reference_', () => {
Migrations.add('mutate-boardIds-in-customfields', () => {
CustomFields.find().forEach((cf) => {
CustomFields.update(cf, {
CustomFields.update(
cf,
{
$set: {
boardIds: [cf.boardId],
},
$unset: {
boardId: '',
},
}, noValidateMulti);
},
noValidateMulti
);
});
});
const firstBatchOfDbsToAddCreatedAndUpdated = [
AccountSettings,
Actions,
Activities,
Announcements,
Boards,
CardComments,
Cards,
ChecklistItems,
Checklists,
CustomFields,
Integrations,
InvitationCodes,
Lists,
Rules,
Settings,
Swimlanes,
Triggers,
UnsavedEdits,
];
firstBatchOfDbsToAddCreatedAndUpdated.forEach((db) => {
db.before.insert((userId, doc) => {
doc.createdAt = Date.now();
doc.updatedAt = doc.createdAt;
});
db.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.updatedAt = new Date();
});
});
const modifiedAtTables = [
AccountSettings,
Actions,
Activities,
Announcements,
Boards,
CardComments,
Cards,
ChecklistItems,
Checklists,
CustomFields,
Integrations,
InvitationCodes,
Lists,
Rules,
Settings,
Swimlanes,
Triggers,
UnsavedEdits,
Users,
];
Migrations.add('add-missing-created-and-modified', () => {
Promise.all(
modifiedAtTables.map((db) =>
db
.rawCollection()
.update(
{ modifiedAt: { $exists: false } },
{ $set: { modifiedAt: new Date() } },
{ multi: true }
)
.then(() =>
db
.rawCollection()
.update(
{ createdAt: { $exists: false } },
{ $set: { createdAt: new Date() } },
{ multi: true }
)
)
)
)
.then(() => {
// eslint-disable-next-line no-console
console.info('Successfully added createdAt and updatedAt to all tables');
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
});
});