diff --git a/models/export.js b/models/export.js index 7b74c20fe..da8e3a491 100644 --- a/models/export.js +++ b/models/export.js @@ -22,21 +22,35 @@ if (Meteor.isServer) { * @param {string} boardId the ID of the board we are exporting * @param {string} authToken the loginToken */ - JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) { + JsonRoutes.add('get', '/api/boards/:boardId/export', function (req, res) { const boardId = req.params.boardId; let user = null; + let impersonateDone = false; + let adminId = null; const loginToken = req.query.authToken; if (loginToken) { const hashToken = Accounts._hashLoginToken(loginToken); user = Meteor.users.findOne({ 'services.resume.loginTokens.hashedToken': hashToken, }); + adminId = user._id.toString(); + impersonateDone = ImpersonatedUsers.findOne({ + adminId: adminId, + }); } else if (!Meteor.settings.public.sandstorm) { Authentication.checkUserId(req.userId); user = Users.findOne({ _id: req.userId, isAdmin: true }); } const exporter = new Exporter(boardId); - if (exporter.canExport(user)) { + if (exporter.canExport(user) || impersonateDone) { + if (impersonateDone) { + ImpersonatedUsers.insert({ + adminId: adminId, + boardId: boardId, + reason: 'exportJSON', + }); + } + JsonRoutes.sendResult(res, { code: 200, data: exporter.build(), @@ -71,22 +85,36 @@ if (Meteor.isServer) { JsonRoutes.add( 'get', '/api/boards/:boardId/attachments/:attachmentId/export', - function(req, res) { + function (req, res) { const boardId = req.params.boardId; const attachmentId = req.params.attachmentId; let user = null; + let impersonateDone = false; + let adminId = null; const loginToken = req.query.authToken; if (loginToken) { const hashToken = Accounts._hashLoginToken(loginToken); user = Meteor.users.findOne({ 'services.resume.loginTokens.hashedToken': hashToken, }); + adminId = user._id.toString(); + impersonateDone = ImpersonatedUsers.findOne({ + adminId: adminId, + }); } else if (!Meteor.settings.public.sandstorm) { Authentication.checkUserId(req.userId); user = Users.findOne({ _id: req.userId, isAdmin: true }); } const exporter = new Exporter(boardId, attachmentId); - if (exporter.canExport(user)) { + if (exporter.canExport(user) || impersonateDone) { + if (impersonateDone) { + ImpersonatedUsers.insert({ + adminId: adminId, + boardId: boardId, + attachmentId: attachmentId, + reason: 'exportJSONattachment', + }); + } JsonRoutes.sendResult(res, { code: 200, data: exporter.build(), @@ -114,15 +142,21 @@ if (Meteor.isServer) { * @param {string} authToken the loginToken * @param {string} delimiter delimiter to use while building export. Default is comma ',' */ - Picker.route('/api/boards/:boardId/export/csv', function(params, req, res) { + Picker.route('/api/boards/:boardId/export/csv', function (params, req, res) { const boardId = params.boardId; let user = null; + let impersonateDone = false; + let adminId = null; const loginToken = params.query.authToken; if (loginToken) { const hashToken = Accounts._hashLoginToken(loginToken); user = Meteor.users.findOne({ 'services.resume.loginTokens.hashedToken': hashToken, }); + adminId = user._id.toString(); + impersonateDone = ImpersonatedUsers.findOne({ + adminId: adminId, + }); } else if (!Meteor.settings.public.sandstorm) { Authentication.checkUserId(req.userId); user = Users.findOne({ @@ -131,19 +165,31 @@ if (Meteor.isServer) { }); } const exporter = new Exporter(boardId); - //if (exporter.canExport(user)) { - body = params.query.delimiter - ? exporter.buildCsv(params.query.delimiter) - : exporter.buildCsv(); - //'Content-Length': body.length, - res.writeHead(200, { - 'Content-Type': params.query.delimiter ? 'text/csv' : 'text/tsv', - }); - res.write(body); - res.end(); - //} else { - // res.writeHead(403); - // res.end('Permission Error'); - //} + if (exporter.canExport(user) || impersonateDone) { + if (impersonateDone) { + // TODO: Checking for CSV or TSV export type does not work: + // let exportType = 'export' + params.query.delimiter ? 'CSV' : 'TSV'; + // So logging export to CSV: + let exportType = 'exportCSV'; + ImpersonatedUsers.insert({ + adminId: adminId, + boardId: boardId, + reason: exportType, + }); + } + + body = params.query.delimiter + ? exporter.buildCsv(params.query.delimiter) + : exporter.buildCsv(); + //'Content-Length': body.length, + res.writeHead(200, { + 'Content-Type': params.query.delimiter ? 'text/csv' : 'text/tsv', + }); + res.write(body); + res.end(); + } else { + res.writeHead(403); + res.end('Permission Error'); + } }); } diff --git a/models/exportExcel.js b/models/exportExcel.js index c8eceb1e4..75d8401cd 100644 --- a/models/exportExcel.js +++ b/models/exportExcel.js @@ -21,16 +21,21 @@ if (Meteor.isServer) { * @param {string} authToken the loginToken */ const Excel = require('exceljs'); - Picker.route('/api/boards/:boardId/exportExcel', function(params, req, res) { + Picker.route('/api/boards/:boardId/exportExcel', function (params, req, res) { const boardId = params.boardId; let user = null; - + let impersonateDone = false; + let adminId = null; const loginToken = params.query.authToken; if (loginToken) { const hashToken = Accounts._hashLoginToken(loginToken); user = Meteor.users.findOne({ 'services.resume.loginTokens.hashedToken': hashToken, }); + adminId = user._id.toString(); + impersonateDone = ImpersonatedUsers.findOne({ + adminId: adminId, + }); } else if (!Meteor.settings.public.sandstorm) { Authentication.checkUserId(req.userId); user = Users.findOne({ @@ -39,7 +44,14 @@ if (Meteor.isServer) { }); } const exporterExcel = new ExporterExcel(boardId); - if (exporterExcel.canExport(user)) { + if (exporterExcel.canExport(user) || impersonateDone) { + if (impersonateDone) { + ImpersonatedUsers.insert({ + adminId: adminId, + boardId: boardId, + reason: 'exportExcel', + }); + } exporterExcel.build(res); } else { res.end(TAPi18n.__('user-can-not-export-excel')); @@ -108,7 +120,7 @@ export class ExporterExcel { result.subtaskItems = []; result.triggers = []; result.actions = []; - result.cards.forEach(card => { + result.cards.forEach((card) => { result.checklists.push( ...Checklists.find({ cardId: card._id, @@ -125,7 +137,7 @@ export class ExporterExcel { }).fetch(), ); }); - result.rules.forEach(rule => { + result.rules.forEach((rule) => { result.triggers.push( ...Triggers.find( { @@ -149,32 +161,32 @@ export class ExporterExcel { // 1- only exports users that are linked somehow to that board // 2- do not export any sensitive information const users = {}; - result.members.forEach(member => { + result.members.forEach((member) => { users[member.userId] = true; }); - result.lists.forEach(list => { + result.lists.forEach((list) => { users[list.userId] = true; }); - result.cards.forEach(card => { + result.cards.forEach((card) => { users[card.userId] = true; if (card.members) { - card.members.forEach(memberId => { + card.members.forEach((memberId) => { users[memberId] = true; }); } if (card.assignees) { - card.assignees.forEach(memberId => { + card.assignees.forEach((memberId) => { users[memberId] = true; }); } }); - result.comments.forEach(comment => { + result.comments.forEach((comment) => { users[comment.userId] = true; }); - result.activities.forEach(activity => { + result.activities.forEach((activity) => { users[activity.userId] = true; }); - result.checklists.forEach(checklist => { + result.checklists.forEach((checklist) => { users[checklist.userId] = true; }); const byUserIds = { @@ -194,7 +206,7 @@ export class ExporterExcel { }; result.users = Users.find(byUserIds, userFields) .fetch() - .map(user => { + .map((user) => { // user avatar is stored as a relative url, we export absolute if ((user.profile || {}).avatarUrl) { user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl); @@ -389,7 +401,7 @@ export class ExporterExcel { const jlabel = {}; var isFirst = 1; for (const klabel in result.labels) { - console.log(klabel); + // console.log(klabel); if (isFirst == 0) { jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`; } else { @@ -589,7 +601,7 @@ export class ExporterExcel { //get parent name if (jcard.parentId) { const parentCard = result.cards.find( - card => card._id === jcard.parentId, + (card) => card._id === jcard.parentId, ); jcard.parentCardTitle = parentCard ? parentCard.title : ''; } @@ -653,7 +665,7 @@ export class ExporterExcel { wrapText: true, }; } - workbook.xlsx.write(res).then(function() {}); + workbook.xlsx.write(res).then(function () {}); } canExport(user) { diff --git a/models/exporter.js b/models/exporter.js index 999f30606..3a671fce4 100644 --- a/models/exporter.js +++ b/models/exporter.js @@ -38,7 +38,7 @@ export class Exporter { // [Old] for attachments we only export IDs and absolute url to original doc // [New] Encode attachment to base64 - const getBase64Data = function(doc, callback) { + const getBase64Data = function (doc, callback) { let buffer = Buffer.allocUnsafe(0); buffer.fill(0); @@ -49,14 +49,14 @@ export class Exporter { ); const tmpWriteable = fs.createWriteStream(tmpFile); const readStream = doc.createReadStream(); - readStream.on('data', function(chunk) { + readStream.on('data', function (chunk) { buffer = Buffer.concat([buffer, chunk]); }); - readStream.on('error', function() { + readStream.on('error', function () { callback(null, null); }); - readStream.on('end', function() { + readStream.on('end', function () { // done fs.unlink(tmpFile, () => { //ignored @@ -72,7 +72,7 @@ export class Exporter { : byBoard; result.attachments = Attachments.find(byBoardAndAttachment) .fetch() - .map(attachment => { + .map((attachment) => { let filebase64 = null; filebase64 = getBase64DataSync(attachment); @@ -105,7 +105,7 @@ export class Exporter { result.subtaskItems = []; result.triggers = []; result.actions = []; - result.cards.forEach(card => { + result.cards.forEach((card) => { result.checklists.push( ...Checklists.find({ cardId: card._id, @@ -122,7 +122,7 @@ export class Exporter { }).fetch(), ); }); - result.rules.forEach(rule => { + result.rules.forEach((rule) => { result.triggers.push( ...Triggers.find( { @@ -146,27 +146,27 @@ export class Exporter { // 1- only exports users that are linked somehow to that board // 2- do not export any sensitive information const users = {}; - result.members.forEach(member => { + result.members.forEach((member) => { users[member.userId] = true; }); - result.lists.forEach(list => { + result.lists.forEach((list) => { users[list.userId] = true; }); - result.cards.forEach(card => { + result.cards.forEach((card) => { users[card.userId] = true; if (card.members) { - card.members.forEach(memberId => { + card.members.forEach((memberId) => { users[memberId] = true; }); } }); - result.comments.forEach(comment => { + result.comments.forEach((comment) => { users[comment.userId] = true; }); - result.activities.forEach(activity => { + result.activities.forEach((activity) => { users[activity.userId] = true; }); - result.checklists.forEach(checklist => { + result.checklists.forEach((checklist) => { users[checklist.userId] = true; }); const byUserIds = { @@ -187,7 +187,7 @@ export class Exporter { }; result.users = Users.find(byUserIds, userFields) .fetch() - .map(user => { + .map((user) => { // user avatar is stored as a relative url, we export absolute if ((user.profile || {}).avatarUrl) { user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl); @@ -259,14 +259,14 @@ export class Exporter { ); const customFieldMap = {}; let i = 0; - result.customFields.forEach(customField => { + result.customFields.forEach((customField) => { customFieldMap[customField._id] = { position: i, type: customField.type, }; if (customField.type === 'dropdown') { let options = ''; - customField.settings.dropdownItems.forEach(item => { + customField.settings.dropdownItems.forEach((item) => { options = options === '' ? item.name : `${`${options}/${item.name}`}`; }); columnHeaders.push( @@ -308,7 +308,7 @@ export class Exporter { TAPi18n.__('archived'), */ - result.cards.forEach(card => { + result.cards.forEach((card) => { const currentRow = []; currentRow.push(card.title); currentRow.push(card.description); @@ -324,19 +324,19 @@ export class Exporter { currentRow.push(card.requestedBy ? card.requestedBy : ' '); currentRow.push(card.assignedBy ? card.assignedBy : ' '); let usernames = ''; - card.members.forEach(memberId => { + card.members.forEach((memberId) => { const user = result.users.find(({ _id }) => _id === memberId); usernames = `${usernames + user.username} `; }); currentRow.push(usernames.trim()); let assignees = ''; - card.assignees.forEach(assigneeId => { + card.assignees.forEach((assigneeId) => { const user = result.users.find(({ _id }) => _id === assigneeId); assignees = `${assignees + user.username} `; }); currentRow.push(assignees.trim()); let labels = ''; - card.labelIds.forEach(labelId => { + card.labelIds.forEach((labelId) => { const label = result.labels.find(({ _id }) => _id === labelId); labels = `${labels + label.name}-${label.color} `; }); @@ -354,11 +354,11 @@ export class Exporter { if (card.vote && card.vote.question !== '') { let positiveVoters = ''; let negativeVoters = ''; - card.vote.positive.forEach(userId => { + card.vote.positive.forEach((userId) => { const user = result.users.find(({ _id }) => _id === userId); positiveVoters = `${positiveVoters + user.username} `; }); - card.vote.negative.forEach(userId => { + card.vote.negative.forEach((userId) => { const user = result.users.find(({ _id }) => _id === userId); negativeVoters = `${negativeVoters + user.username} `; }); @@ -378,12 +378,11 @@ export class Exporter { currentRow.push(card.archived ? 'true' : 'false'); //Custom fields const customFieldValuesToPush = new Array(result.customFields.length); - card.customFields.forEach(field => { + card.customFields.forEach((field) => { if (field.value !== null) { if (customFieldMap[field._id].type === 'date') { - customFieldValuesToPush[ - customFieldMap[field._id].position - ] = moment(field.value).format(); + customFieldValuesToPush[customFieldMap[field._id].position] = + moment(field.value).format(); } else if (customFieldMap[field._id].type === 'dropdown') { const dropdownOptions = result.customFields.find( ({ _id }) => _id === field._id, @@ -391,9 +390,8 @@ export class Exporter { const fieldValue = dropdownOptions.find( ({ _id }) => _id === field.value, ).name; - customFieldValuesToPush[ - customFieldMap[field._id].position - ] = fieldValue; + customFieldValuesToPush[customFieldMap[field._id].position] = + fieldValue; } else { customFieldValuesToPush[customFieldMap[field._id].position] = field.value; diff --git a/models/impersonatedUsers.js b/models/impersonatedUsers.js new file mode 100644 index 000000000..97d15f10b --- /dev/null +++ b/models/impersonatedUsers.js @@ -0,0 +1,79 @@ +ImpersonatedUsers = new Mongo.Collection('impersonatedUsers'); + +/** + * A Impersonated User in wekan + */ +ImpersonatedUsers.attachSchema( + new SimpleSchema({ + adminId: { + /** + * the admin userid that impersonates + */ + type: String, + optional: true, + }, + userId: { + /** + * the userId that is impersonated + */ + type: String, + optional: true, + }, + boardId: { + /** + * the boardId that was exported by anyone that has sometime impersonated + */ + type: String, + optional: true, + }, + attachmentId: { + /** + * the attachmentId that was exported by anyone that has sometime impersonated + */ + type: String, + optional: true, + }, + reason: { + /** + * the reason why impersonated, like exportJSON + */ + type: String, + optional: true, + }, + createdAt: { + /** + * creation date of the impersonation + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else if (this.isUpsert) { + return { + $setOnInsert: new Date(), + }; + } else { + this.unset(); + } + }, + }, + modifiedAt: { + /** + * modified date of the impersonation + */ + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }), +); + +export default ImpersonatedUsers; diff --git a/models/users.js b/models/users.js index 13e623318..d2f2d6ef9 100644 --- a/models/users.js +++ b/models/users.js @@ -1,4 +1,5 @@ import { SyncedCron } from 'meteor/percolate:synced-cron'; +import ImpersonatedUsers from './impersonatedUsers'; // Sandstorm context is detected using the METEOR_SETTINGS environment variable // in the package definition. @@ -67,7 +68,9 @@ Users.attachSchema( if (this.isInsert) { return new Date(); } else if (this.isUpsert) { - return { $setOnInsert: new Date() }; + return { + $setOnInsert: new Date(), + }; } else { this.unset(); } @@ -350,7 +353,9 @@ Users.attachSchema( Users.allow({ update(userId, doc) { - const user = Users.findOne({ _id: userId }); + const user = Users.findOne({ + _id: userId, + }); if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin)) return true; if (!user) { @@ -359,10 +364,18 @@ Users.allow({ return doc._id === userId; }, remove(userId, doc) { - const adminsNumber = Users.find({ isAdmin: true }).count(); + const adminsNumber = Users.find({ + isAdmin: true, + }).count(); const { isAdmin } = Users.findOne( - { _id: userId }, - { fields: { isAdmin: 1 } }, + { + _id: userId, + }, + { + fields: { + isAdmin: 1, + }, + }, ); // Prevents remove of the only one administrator @@ -440,7 +453,7 @@ if (Meteor.isClient) { }); } -Users.parseImportUsernames = usernamesString => { +Users.parseImportUsernames = (usernamesString) => { return usernamesString.trim().split(new RegExp('\\s*[,;]\\s*')); }; @@ -454,17 +467,30 @@ Users.helpers({ boards() { return Boards.find( - { 'members.userId': this._id }, - { sort: { sort: 1 /* boards default sorting */ } }, + { + 'members.userId': this._id, + }, + { + sort: { + sort: 1 /* boards default sorting */, + }, + }, ); }, starredBoards() { const { starredBoards = [] } = this.profile || {}; return Boards.find( - { archived: false, _id: { $in: starredBoards } }, { - sort: { sort: 1 /* boards default sorting */ }, + archived: false, + _id: { + $in: starredBoards, + }, + }, + { + sort: { + sort: 1 /* boards default sorting */, + }, }, ); }, @@ -477,9 +503,16 @@ Users.helpers({ invitedBoards() { const { invitedBoards = [] } = this.profile || {}; return Boards.find( - { archived: false, _id: { $in: invitedBoards } }, { - sort: { sort: 1 /* boards default sorting */ }, + archived: false, + _id: { + $in: invitedBoards, + }, + }, + { + sort: { + sort: 1 /* boards default sorting */, + }, }, ); }, @@ -611,7 +644,9 @@ Users.helpers({ }, remove() { - User.remove({ _id: this._id }); + User.remove({ + _id: this._id, + }); }, }); @@ -714,7 +749,9 @@ Users.mutations({ addNotification(activityId) { return { $addToSet: { - 'profile.notifications': { activity: activityId }, + 'profile.notifications': { + activity: activityId, + }, }, }; }, @@ -722,7 +759,9 @@ Users.mutations({ removeNotification(activityId) { return { $pull: { - 'profile.notifications': { activity: activityId }, + 'profile.notifications': { + activity: activityId, + }, }, }; }, @@ -744,15 +783,27 @@ Users.mutations({ }, setAvatarUrl(avatarUrl) { - return { $set: { 'profile.avatarUrl': avatarUrl } }; + return { + $set: { + 'profile.avatarUrl': avatarUrl, + }, + }; }, setShowCardsCountAt(limit) { - return { $set: { 'profile.showCardsCountAt': limit } }; + return { + $set: { + 'profile.showCardsCountAt': limit, + }, + }; }, setStartDayOfWeek(startDay) { - return { $set: { 'profile.startDayOfWeek': startDay } }; + return { + $set: { + 'profile.startDayOfWeek': startDay, + }, + }; }, setBoardView(view) { @@ -801,15 +852,33 @@ if (Meteor.isServer) { if (Meteor.user() && Meteor.user().isAdmin) { // If setting is missing, add it Users.update( - { 'profile.hiddenSystemMessages': { $exists: false } }, - { $set: { 'profile.hiddenSystemMessages': true } }, - { multi: true }, + { + 'profile.hiddenSystemMessages': { + $exists: false, + }, + }, + { + $set: { + 'profile.hiddenSystemMessages': true, + }, + }, + { + multi: true, + }, ); // If setting is false, set it to true Users.update( - { 'profile.hiddenSystemMessages': false }, - { $set: { 'profile.hiddenSystemMessages': true } }, - { multi: true }, + { + 'profile.hiddenSystemMessages': false, + }, + { + $set: { + 'profile.hiddenSystemMessages': true, + }, + }, + { + multi: true, + }, ); return true; } else { @@ -836,8 +905,12 @@ if (Meteor.isServer) { check(email, String); check(importUsernames, Array); - const nUsersWithUsername = Users.find({ username }).count(); - const nUsersWithEmail = Users.find({ email }).count(); + const nUsersWithUsername = Users.find({ + username, + }).count(); + const nUsersWithEmail = Users.find({ + email, + }).count(); if (nUsersWithUsername > 0) { throw new Meteor.Error('username-already-taken'); } else if (nUsersWithEmail > 0) { @@ -851,7 +924,11 @@ if (Meteor.isServer) { email: email.toLowerCase(), from: 'admin', }); - const user = Users.findOne(username) || Users.findOne({ username }); + const user = + Users.findOne(username) || + Users.findOne({ + username, + }); if (user) { Users.update(user._id, { $set: { @@ -868,11 +945,17 @@ if (Meteor.isServer) { if (Meteor.user() && Meteor.user().isAdmin) { check(username, String); check(userId, String); - const nUsersWithUsername = Users.find({ username }).count(); + const nUsersWithUsername = Users.find({ + username, + }).count(); if (nUsersWithUsername > 0) { throw new Meteor.Error('username-already-taken'); } else { - Users.update(userId, { $set: { username } }); + Users.update(userId, { + $set: { + username, + }, + }); } } }, @@ -883,8 +966,14 @@ if (Meteor.isServer) { } check(email, String); const existingUser = Users.findOne( - { 'emails.address': email }, - { fields: { _id: 1 } }, + { + 'emails.address': email, + }, + { + fields: { + _id: 1, + }, + }, ); if (existingUser) { throw new Meteor.Error('email-already-taken'); @@ -963,7 +1052,9 @@ if (Meteor.isServer) { board && board.members && _.contains(_.pluck(board.members, 'userId'), inviter._id) && - _.where(board.members, { userId: inviter._id })[0].isActive; + _.where(board.members, { + userId: inviter._id, + })[0].isActive; // GitHub issue 2060 //_.where(board.members, { userId: inviter._id })[0].isAdmin; if (!allowInvite) throw new Meteor.Error('error-board-notAMember'); @@ -973,22 +1064,39 @@ if (Meteor.isServer) { const posAt = username.indexOf('@'); let user = null; if (posAt >= 0) { - user = Users.findOne({ emails: { $elemMatch: { address: username } } }); + user = Users.findOne({ + emails: { + $elemMatch: { + address: username, + }, + }, + }); } else { - user = Users.findOne(username) || Users.findOne({ username }); + user = + Users.findOne(username) || + Users.findOne({ + username, + }); } if (user) { 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: true })) { + if ( + Settings.findOne({ + disableRegistration: true, + }) + ) { throw new Meteor.Error('error-user-notCreated'); } // Set in lowercase email before creating account const email = username.toLowerCase(); username = email.substring(0, posAt); - const newUserId = Accounts.createUser({ username, email }); + const newUserId = Accounts.createUser({ + username, + email, + }); if (!newUserId) throw new Meteor.Error('error-user-notCreated'); // assume new user speak same language with inviter if (inviter.profile && inviter.profile.language) { @@ -1032,7 +1140,10 @@ if (Meteor.isServer) { } catch (e) { throw new Meteor.Error('email-fail', e.message); } - return { username: user.username, email: user.emails[0].address }; + return { + username: user.username, + email: user.emails[0].address, + }; }, impersonate(userId) { check(userId, String); @@ -1042,8 +1153,16 @@ if (Meteor.isServer) { if (!Meteor.user().isAdmin) throw new Meteor.Error(403, 'Permission denied'); + ImpersonatedUsers.insert({ adminId: Meteor.user()._id, userId: userId, reason: 'clickedImpersonate' }); this.setUserId(userId); }, + isImpersonated(userId) { + check(userId, String); + const isImpersonated = ImpersonatedUsers.findOne({ + userId: userId, + }); + return isImpersonated; + }, }); Accounts.onCreateUser((options, user) => { const userCount = Users.find().count(); @@ -1059,7 +1178,12 @@ if (Meteor.isServer) { } email = email.toLowerCase(); user.username = user.services.oidc.username; - user.emails = [{ address: email, verified: true }]; + user.emails = [ + { + address: email, + verified: true, + }, + ]; const initials = user.services.oidc.fullname .split(/\s+/) .reduce((memo, word) => { @@ -1075,7 +1199,14 @@ if (Meteor.isServer) { // 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 }], + $or: [ + { + 'emails.address': email, + }, + { + username: user.username, + }, + ], }); if (!existingUser) return user; @@ -1087,8 +1218,12 @@ if (Meteor.isServer) { existingUser.profile = user.profile; existingUser.authenticationMethod = user.authenticationMethod; - Meteor.users.remove({ _id: user._id }); - Meteor.users.remove({ _id: existingUser._id }); // is going to be created again + Meteor.users.remove({ + _id: user._id, + }); + Meteor.users.remove({ + _id: existingUser._id, + }); // is going to be created again return existingUser; } @@ -1127,13 +1262,17 @@ if (Meteor.isServer) { "The invitation code doesn't exist", ); } else { - user.profile = { icode: options.profile.invitationcode }; + user.profile = { + icode: options.profile.invitationcode, + }; user.profile.boardView = 'board-view-swimlanes'; // Deletes the invitation code after the user was created successfully. setTimeout( Meteor.bindEnvironment(() => { - InvitationCodes.remove({ _id: invitationCode._id }); + InvitationCodes.remove({ + _id: invitationCode._id, + }); }), 200, ); @@ -1153,7 +1292,7 @@ const addCronJob = _.debounce( SyncedCron.add({ name: 'notification_cleanup', - schedule: parser => parser.text('every 1 days'), + schedule: (parser) => parser.text('every 1 days'), job: () => { for (const user of Users.find()) { if (!user.profile || !user.profile.notifications) continue; @@ -1178,15 +1317,19 @@ const addCronJob = _.debounce( if (Meteor.isServer) { // Let mongoDB ensure username unicity Meteor.startup(() => { - allowedSortValues.forEach(value => { + allowedSortValues.forEach((value) => { Lists._collection._ensureIndex(value); }); - Users._collection._ensureIndex({ modifiedAt: -1 }); + Users._collection._ensureIndex({ + modifiedAt: -1, + }); Users._collection._ensureIndex( { username: 1, }, - { unique: true }, + { + unique: true, + }, ); Meteor.defer(() => { addCronJob(); @@ -1215,7 +1358,7 @@ if (Meteor.isServer) { // counter. // We need to run this code on the server only, otherwise the incrementation // will be done twice. - Users.after.update(function(userId, user, fieldNames) { + Users.after.update(function (userId, user, fieldNames) { // The `starredBoards` list is hosted on the `profile` field. If this // field hasn't been modificated we don't need to run this hook. if (!_.contains(fieldNames, 'profile')) return; @@ -1233,8 +1376,12 @@ if (Meteor.isServer) { // b. We use it to find deleted and newly inserted ids by using it in one // direction and then in the other. function incrementBoards(boardsIds, inc) { - boardsIds.forEach(boardId => { - Boards.update(boardId, { $inc: { stars: inc } }); + boardsIds.forEach((boardId) => { + Boards.update(boardId, { + $inc: { + stars: inc, + }, + }); }); } @@ -1258,23 +1405,23 @@ if (Meteor.isServer) { fakeUserId.withValue(doc._id, () => { /* - // Insert the Welcome Board - Boards.insert({ - title: TAPi18n.__('welcome-board'), - permission: 'private', - }, fakeUser, (err, boardId) => { + // Insert the Welcome Board + Boards.insert({ + title: TAPi18n.__('welcome-board'), + permission: 'private', + }, fakeUser, (err, boardId) => { - Swimlanes.insert({ - title: TAPi18n.__('welcome-swimlane'), - boardId, - sort: 1, - }, fakeUser); + Swimlanes.insert({ + title: TAPi18n.__('welcome-swimlane'), + boardId, + sort: 1, + }, fakeUser); - ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => { - Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser); - }); - }); - */ + ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => { + Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser); + }); + }); + */ const Future = require('fibers/future'); const future1 = new Future(); @@ -1290,7 +1437,9 @@ if (Meteor.isServer) { (err, boardId) => { // Insert the reference to our templates board Users.update(fakeUserId.get(), { - $set: { 'profile.templatesBoardId': boardId }, + $set: { + 'profile.templatesBoardId': boardId, + }, }); // Insert the card templates swimlane @@ -1305,7 +1454,9 @@ if (Meteor.isServer) { (err, swimlaneId) => { // Insert the reference to out card templates swimlane Users.update(fakeUserId.get(), { - $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId }, + $set: { + 'profile.cardTemplatesSwimlaneId': swimlaneId, + }, }); future1.return(); }, @@ -1323,7 +1474,9 @@ if (Meteor.isServer) { (err, swimlaneId) => { // Insert the reference to out list templates swimlane Users.update(fakeUserId.get(), { - $set: { 'profile.listTemplatesSwimlaneId': swimlaneId }, + $set: { + 'profile.listTemplatesSwimlaneId': swimlaneId, + }, }); future2.return(); }, @@ -1341,7 +1494,9 @@ if (Meteor.isServer) { (err, swimlaneId) => { // Insert the reference to out board templates swimlane Users.update(fakeUserId.get(), { - $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId }, + $set: { + 'profile.boardTemplatesSwimlaneId': swimlaneId, + }, }); future3.return(); }, @@ -1358,7 +1513,9 @@ if (Meteor.isServer) { Users.after.insert((userId, doc) => { // HACK - doc = Users.findOne({ _id: doc._id }); + doc = Users.findOne({ + _id: doc._id, + }); 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). @@ -1366,7 +1523,11 @@ if (Meteor.isServer) { // the disableRegistration check. // Issue : https://github.com/wekan/wekan/issues/1232 // PR : https://github.com/wekan/wekan/pull/1251 - Users.update(doc._id, { $set: { createdThroughApi: '' } }); + Users.update(doc._id, { + $set: { + createdThroughApi: '', + }, + }); return; } @@ -1382,7 +1543,7 @@ if (Meteor.isServer) { if (!invitationCode) { throw new Meteor.Error('error-invitation-code-not-exist'); } else { - invitationCode.boardsToBeInvited.forEach(boardId => { + invitationCode.boardsToBeInvited.forEach((boardId) => { const board = Boards.findOne(boardId); board.addMember(doc._id); }); @@ -1390,8 +1551,16 @@ if (Meteor.isServer) { doc.profile = {}; } doc.profile.invitedBoards = invitationCode.boardsToBeInvited; - Users.update(doc._id, { $set: { profile: doc.profile } }); - InvitationCodes.update(invitationCode._id, { $set: { valid: false } }); + Users.update(doc._id, { + $set: { + profile: doc.profile, + }, + }); + InvitationCodes.update(invitationCode._id, { + $set: { + valid: false, + }, + }); } } }); @@ -1400,12 +1569,14 @@ if (Meteor.isServer) { // USERS REST API if (Meteor.isServer) { // Middleware which checks that API is enabled. - JsonRoutes.Middleware.use(function(req, res, next) { + JsonRoutes.Middleware.use(function (req, res, next) { const api = req.url.startsWith('/api'); if ((api === true && process.env.WITH_API === 'true') || api === false) { return next(); } else { - res.writeHead(301, { Location: '/' }); + res.writeHead(301, { + Location: '/', + }); return res.end(); } }); @@ -1416,10 +1587,12 @@ if (Meteor.isServer) { * @summary returns the current user * @return_type Users */ - JsonRoutes.add('GET', '/api/user', function(req, res) { + JsonRoutes.add('GET', '/api/user', function (req, res) { try { Authentication.checkLoggedIn(req.userId); - const data = Meteor.users.findOne({ _id: req.userId }); + const data = Meteor.users.findOne({ + _id: req.userId, + }); delete data.services; // get all boards where the user is member of @@ -1429,11 +1602,14 @@ if (Meteor.isServer) { 'members.userId': req.userId, }, { - fields: { _id: 1, members: 1 }, + fields: { + _id: 1, + members: 1, + }, }, ); - boards = boards.map(b => { - const u = b.members.find(m => m.userId === req.userId); + boards = boards.map((b) => { + const u = b.members.find((m) => m.userId === req.userId); delete u.userId; u.boardId = b._id; return u; @@ -1461,13 +1637,16 @@ if (Meteor.isServer) { * @return_type [{ _id: string, * username: string}] */ - JsonRoutes.add('GET', '/api/users', function(req, res) { + JsonRoutes.add('GET', '/api/users', function (req, res) { try { Authentication.checkUserId(req.userId); JsonRoutes.sendResult(res, { code: 200, - data: Meteor.users.find({}).map(function(doc) { - return { _id: doc._id, username: doc.username }; + data: Meteor.users.find({}).map(function (doc) { + return { + _id: doc._id, + username: doc.username, + }; }), }); } catch (error) { @@ -1488,13 +1667,17 @@ if (Meteor.isServer) { * @param {string} userId the user ID or username * @return_type Users */ - JsonRoutes.add('GET', '/api/users/:userId', function(req, res) { + JsonRoutes.add('GET', '/api/users/:userId', function (req, res) { try { Authentication.checkUserId(req.userId); let id = req.params.userId; - let user = Meteor.users.findOne({ _id: id }); + let user = Meteor.users.findOne({ + _id: id, + }); if (!user) { - user = Meteor.users.findOne({ username: id }); + user = Meteor.users.findOne({ + username: id, + }); id = user._id; } @@ -1505,11 +1688,14 @@ if (Meteor.isServer) { 'members.userId': id, }, { - fields: { _id: 1, members: 1 }, + fields: { + _id: 1, + members: 1, + }, }, ); - boards = boards.map(b => { - const u = b.members.find(m => m.userId === id); + boards = boards.map((b) => { + const u = b.members.find((m) => m.userId === id); delete u.userId; u.boardId = b._id; return u; @@ -1545,12 +1731,14 @@ if (Meteor.isServer) { * @return_type {_id: string, * title: string} */ - JsonRoutes.add('PUT', '/api/users/:userId', function(req, res) { + JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.userId; const action = req.body.action; - let data = Meteor.users.findOne({ _id: id }); + let data = Meteor.users.findOne({ + _id: id, + }); if (data !== undefined) { if (action === 'takeOwnership') { data = Boards.find( @@ -1558,8 +1746,12 @@ if (Meteor.isServer) { 'members.userId': id, 'members.isAdmin': true, }, - { sort: { sort: 1 /* boards default sorting */ } }, - ).map(function(board) { + { + sort: { + sort: 1 /* boards default sorting */, + }, + }, + ).map(function (board) { if (board.hasMember(req.userId)) { board.removeMember(req.userId); } @@ -1572,7 +1764,9 @@ if (Meteor.isServer) { } else { if (action === 'disableLogin' && id !== req.userId) { Users.update( - { _id: id }, + { + _id: id, + }, { $set: { loginDisabled: true, @@ -1581,9 +1775,20 @@ if (Meteor.isServer) { }, ); } else if (action === 'enableLogin') { - Users.update({ _id: id }, { $set: { loginDisabled: '' } }); + Users.update( + { + _id: id, + }, + { + $set: { + loginDisabled: '', + }, + }, + ); } - data = Meteor.users.findOne({ _id: id }); + data = Meteor.users.findOne({ + _id: id, + }); } } JsonRoutes.sendResult(res, { @@ -1617,53 +1822,57 @@ if (Meteor.isServer) { * @return_type {_id: string, * title: string} */ - JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function( - req, - res, - ) { - try { - Authentication.checkUserId(req.userId); - const userId = req.params.userId; - const boardId = req.params.boardId; - const action = req.body.action; - const { isAdmin, isNoComments, isCommentOnly } = req.body; - let data = Meteor.users.findOne({ _id: userId }); - if (data !== undefined) { - if (action === 'add') { - data = Boards.find({ - _id: boardId, - }).map(function(board) { - if (!board.hasMember(userId)) { - board.addMember(userId); - function isTrue(data) { - return data.toLowerCase() === 'true'; + JsonRoutes.add( + 'POST', + '/api/boards/:boardId/members/:userId/add', + function (req, res) { + try { + Authentication.checkUserId(req.userId); + const userId = req.params.userId; + const boardId = req.params.boardId; + const action = req.body.action; + const { isAdmin, isNoComments, isCommentOnly } = req.body; + let data = Meteor.users.findOne({ + _id: userId, + }); + if (data !== undefined) { + if (action === 'add') { + data = Boards.find({ + _id: boardId, + }).map(function (board) { + if (!board.hasMember(userId)) { + board.addMember(userId); + + 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, - title: board.title, - }; - }); + return { + _id: board._id, + title: board.title, + }; + }); + } } + JsonRoutes.sendResult(res, { + code: 200, + data: query, + }); + } catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); } - JsonRoutes.sendResult(res, { - code: 200, - data: query, - }); - } catch (error) { - JsonRoutes.sendResult(res, { - code: 200, - data: error, - }); - } - }); + }, + ); /** * @operation remove_board_member @@ -1682,18 +1891,20 @@ if (Meteor.isServer) { JsonRoutes.add( 'POST', '/api/boards/:boardId/members/:userId/remove', - function(req, res) { + function (req, res) { try { Authentication.checkUserId(req.userId); const userId = req.params.userId; const boardId = req.params.boardId; const action = req.body.action; - let data = Meteor.users.findOne({ _id: userId }); + let data = Meteor.users.findOne({ + _id: userId, + }); if (data !== undefined) { if (action === 'remove') { data = Boards.find({ _id: boardId, - }).map(function(board) { + }).map(function (board) { if (board.hasMember(userId)) { board.removeMember(userId); } @@ -1729,7 +1940,7 @@ if (Meteor.isServer) { * @param {string} password the password of the new user * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/users/', function(req, res) { + JsonRoutes.add('POST', '/api/users/', function (req, res) { try { Authentication.checkUserId(req.userId); const id = Accounts.createUser({ @@ -1762,7 +1973,7 @@ if (Meteor.isServer) { * @param {string} userId the ID of the user to delete * @return_type {_id: string} */ - JsonRoutes.add('DELETE', '/api/users/:userId', function(req, res) { + JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.userId; @@ -1800,7 +2011,7 @@ if (Meteor.isServer) { * @param {string} userId the ID of the user to create token for. * @return_type {_id: string} */ - JsonRoutes.add('POST', '/api/createtoken/:userId', function(req, res) { + JsonRoutes.add('POST', '/api/createtoken/:userId', function (req, res) { try { Authentication.checkUserId(req.userId); const id = req.params.userId;