Merge branch 'justinr1234-created-modified' into meteor-1.8

This commit is contained in:
Lauri Ojansivu 2019-06-27 15:27:14 -04:00
commit a0a482aa8e
28 changed files with 3403 additions and 2175 deletions

View file

@ -8,3 +8,20 @@ end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{js,html}]
charset = utf-8
end_of_line = lf
indent_brace_style = 1TBS
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
quote_type = auto
spaces_around_operators = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
packages/*

View file

@ -1,9 +1,15 @@
{
"extends": "eslint:recommended",
"extends": [
"eslint:recommended",
"plugin:meteor/recommended",
"prettier",
"prettier/standard"
],
"env": {
"es6": true,
"node": true,
"browser": true
"browser": true,
"meteor": true
},
"parserOptions": {
"ecmaVersion": 2017,
@ -33,6 +39,7 @@
"comma-style": 2,
"eol-last": 2,
"linebreak-style": [2, "unix"],
"meteor/audit-argument-checks": 0,
"new-parens": 2,
"no-lonely-if": 2,
"no-multiple-empty-lines": 2,
@ -52,8 +59,26 @@
"prefer-const": 2,
"prefer-spread": 2,
"prefer-template": 2,
"no-unused-vars" : "warn"
"no-unused-vars": "warn",
"prettier/prettier": [
"error",
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "all"
}
]
},
"settings": {
"import/resolver": {
"meteor": {
"extensions": [".js", ".jsx"]
}
}
},
"plugins": ["prettier", "meteor"],
"globals": {
"Meteor": false,
"Session": false,

7
.prettierignore Normal file
View file

@ -0,0 +1,7 @@
packages/
node_modules/
.build/
.meteor/
.vscode/
.tx/
.github/

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}

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;

View file

@ -4,9 +4,29 @@
"description": "Open-Source kanban",
"private": true,
"scripts": {
"lint": "eslint --ignore-pattern 'packages/*' .",
"lint": "eslint --cache --ext .js --ignore-path .eslintignore .",
"lint:eslint:fix": "eslint --ext .js --ignore-path .eslintignore --fix .",
"lint:staged": "lint-staged",
"prettify": "prettier --write '**/*.js' '**/*.jsx'",
"test": "npm run --silent lint"
},
"lint-staged": {
"*.js": [
"meteor npm run prettify",
"meteor npm run lint:eslint:fix",
"git add --force"
],
"*.jsx": [
"meteor npm run prettify",
"meteor npm run lint:eslint:fix",
"git add --force"
],
"*.json": [
"prettier --write",
"git add --force"
]
},
"pre-commit": "lint:staged",
"eslintConfig": {
"extends": "@meteorjs/eslint-config-meteor"
},
@ -20,7 +40,17 @@
},
"homepage": "https://wekan.github.io",
"devDependencies": {
"eslint": "^5.16.0"
"eslint": "^5.16.0",
"eslint-config-meteor": "0.0.9",
"eslint-config-prettier": "^3.6.0",
"eslint-import-resolver-meteor": "^0.4.0",
"eslint-plugin-import": "^2.18.0",
"eslint-plugin-meteor": "^4.2.2",
"eslint-plugin-prettier": "^3.1.0",
"lint-staged": "^7.3.0",
"pre-commit": "^1.2.2",
"prettier": "^1.18.2",
"prettier-eslint": "^8.8.2"
},
"dependencies": {
"@babel/runtime": "^7.4.3",

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