diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js index caa6ef159d..2906edad4e 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js @@ -1,3 +1,4 @@ +// @ts-check const { callbackify } = require('util') const pLimit = require('p-limit') const { ObjectId } = require('mongodb-legacy') @@ -50,7 +51,155 @@ module.exports = { }, } -async function getMemberIdsWithPrivilegeLevels(projectId) { +/** + * @typedef ProjectMember + * @property {string} id + * @property {typeof PrivilegeLevels[keyof PrivilegeLevels]} privilegeLevel + * @property {typeof Sources[keyof Sources]} source + * @property {boolean} [pendingEditor] + * @property {boolean} [pendingReviewer] + */ + +/** + * @typedef LoadedProjectMember + * @property {typeof PrivilegeLevels[keyof PrivilegeLevels]} privilegeLevel + * @property {{_id: ObjectId, email: string, features: any, first_name: string, last_name: string, signUpDate: Date}} user + * @property {boolean} [pendingEditor] + * @property {boolean} [pendingReviewer] + */ + +// Wrapper for determining multiple dimensions of project access. +class ProjectAccess { + /** @type {ProjectMember[]} */ + #members + + /** @type {typeof PublicAccessLevels[keyof PublicAccessLevels]} */ + #publicAccessLevel + + /** + * @param {{ owner_ref: ObjectId; collaberator_refs: ObjectId[]; readOnly_refs: ObjectId[]; tokenAccessReadAndWrite_refs: ObjectId[]; tokenAccessReadOnly_refs: ObjectId[]; publicAccesLevel: typeof PublicAccessLevels[keyof PublicAccessLevels]; pendingEditor_refs: ObjectId[]; reviewer_refs: ObjectId[]; pendingReviewer_refs: ObjectId[]; }} project + */ + constructor(project) { + this.#members = _getMemberIdsWithPrivilegeLevelsFromFields( + project.owner_ref, + project.collaberator_refs, + project.readOnly_refs, + project.tokenAccessReadAndWrite_refs, + project.tokenAccessReadOnly_refs, + project.publicAccesLevel, + project.pendingEditor_refs, + project.reviewer_refs, + project.pendingReviewer_refs + ) + this.#publicAccessLevel = project.publicAccesLevel + } + + /** + * @return {Promise} + */ + async loadInvitedMembers() { + return _loadMembers(this.#members.filter(m => m.source !== Sources.TOKEN)) + } + + /** + * @return {ProjectMember[]} + */ + allMembers() { + return this.#members + } + + /** + * @return {typeof PublicAccessLevels[keyof PublicAccessLevels]} + */ + publicAccessLevel() { + return this.#publicAccessLevel + } + + /** + * @return {string[]} + */ + memberIds() { + return this.#members.map(m => m.id) + } + + /** + * @return {string[]} + */ + invitedMemberIds() { + return this.#members.filter(m => m.source !== Sources.TOKEN).map(m => m.id) + } + + /** + * @param {string | ObjectId} userId + * @return {typeof PrivilegeLevels[keyof PrivilegeLevels]} + */ + privilegeLevelForUser(userId) { + for (const member of this.#members) { + if (member.id === userId.toString()) { + return member.privilegeLevel + } + } + return PrivilegeLevels.NONE + } + + /** + * @param {string | ObjectId} userId + * @return {boolean} + */ + isUserInvitedMember(userId) { + for (const member of this.#members) { + if (member.id === userId.toString() && member.source !== Sources.TOKEN) { + return true + } + } + return false + } + + /** + * @param {string | ObjectId} userId + * @return {boolean} + */ + isUserInvitedReadWriteMember(userId) { + for (const member of this.#members) { + if ( + member.id.toString() === userId.toString() && + member.source !== Sources.TOKEN && + member.privilegeLevel === PrivilegeLevels.READ_AND_WRITE + ) { + return true + } + } + return false + } + + /** + * Counts invited members with editor or reviewer roles + * @return {number} + */ + countInvitedEditCollaborators() { + return this.#members.filter( + m => + m.source === Sources.INVITE && + (m.privilegeLevel === PrivilegeLevels.READ_AND_WRITE || + m.privilegeLevel === PrivilegeLevels.REVIEW) + ).length + } + + /** + * Counts invited members that are readonly pending editors or pending reviewers + * @return {number} + */ + countInvitedPendingEditors() { + return this.#members.filter( + m => + m.source === Sources.INVITE && + m.privilegeLevel === PrivilegeLevels.READ_ONLY && + (m.pendingEditor || m.pendingReviewer) + ).length + } +} + +async function getProjectAccess(projectId) { const project = await ProjectGetter.promises.getProject(projectId, { owner_ref: 1, collaberator_refs: 1, @@ -65,34 +214,23 @@ async function getMemberIdsWithPrivilegeLevels(projectId) { if (!project) { throw new Errors.NotFoundError(`no project found with id ${projectId}`) } - const memberIds = _getMemberIdsWithPrivilegeLevelsFromFields( - project.owner_ref, - project.collaberator_refs, - project.readOnly_refs, - project.tokenAccessReadAndWrite_refs, - project.tokenAccessReadOnly_refs, - project.publicAccesLevel, - project.pendingEditor_refs, - project.reviewer_refs, - project.pendingReviewer_refs - ) - return memberIds + return new ProjectAccess(project) +} + +async function getMemberIdsWithPrivilegeLevels(projectId) { + return (await getProjectAccess(projectId)).allMembers() } async function getMemberIds(projectId) { - const members = await getMemberIdsWithPrivilegeLevels(projectId) - return members.map(m => m.id) + return (await getProjectAccess(projectId)).memberIds() } async function getInvitedMemberIds(projectId) { - const members = await getMemberIdsWithPrivilegeLevels(projectId) - return members.filter(m => m.source !== Sources.TOKEN).map(m => m.id) + return (await getProjectAccess(projectId)).invitedMemberIds() } async function getInvitedMembersWithPrivilegeLevels(projectId) { - let members = await getMemberIdsWithPrivilegeLevels(projectId) - members = members.filter(m => m.source !== Sources.TOKEN) - return _loadMembers(members) + return await (await getProjectAccess(projectId)).loadInvitedMembers() } async function getInvitedMembersWithPrivilegeLevelsFromFields( @@ -107,7 +245,7 @@ async function getInvitedMembersWithPrivilegeLevelsFromFields( readOnlyIds, [], [], - null, + 'private', [], reviewerIds, [] @@ -121,69 +259,31 @@ async function getMemberIdPrivilegeLevel(userId, projectId) { if (userId == null) { return PrivilegeLevels.NONE } - const members = await getMemberIdsWithPrivilegeLevels(projectId) - for (const member of members) { - if (member.id === userId.toString()) { - return member.privilegeLevel - } - } - return PrivilegeLevels.NONE + return (await getProjectAccess(projectId)).privilegeLevelForUser(userId) } async function getInvitedEditCollaboratorCount(projectId) { - // Counts invited members with editor or reviewer roles - const members = await getMemberIdsWithPrivilegeLevels(projectId) - return members.filter( - m => - m.source === Sources.INVITE && - (m.privilegeLevel === PrivilegeLevels.READ_AND_WRITE || - m.privilegeLevel === PrivilegeLevels.REVIEW) - ).length + return (await getProjectAccess(projectId)).countInvitedEditCollaborators() } async function getInvitedPendingEditorCount(projectId) { - // Only counts invited members that are readonly pending editors or pending - // reviewers - const members = await getMemberIdsWithPrivilegeLevels(projectId) - return members.filter( - m => - m.source === Sources.INVITE && - m.privilegeLevel === PrivilegeLevels.READ_ONLY && - (m.pendingEditor || m.pendingReviewer) - ).length + return (await getProjectAccess(projectId)).countInvitedPendingEditors() } async function isUserInvitedMemberOfProject(userId, projectId) { if (!userId) { return false } - const members = await getMemberIdsWithPrivilegeLevels(projectId) - for (const member of members) { - if ( - member.id.toString() === userId.toString() && - member.source !== Sources.TOKEN - ) { - return true - } - } - return false + return (await getProjectAccess(projectId)).isUserInvitedMember(userId) } async function isUserInvitedReadWriteMemberOfProject(userId, projectId) { if (!userId) { return false } - const members = await getMemberIdsWithPrivilegeLevels(projectId) - for (const member of members) { - if ( - member.id.toString() === userId.toString() && - member.source !== Sources.TOKEN && - member.privilegeLevel === PrivilegeLevels.READ_AND_WRITE - ) { - return true - } - } - return false + return (await getProjectAccess(projectId)).isUserInvitedReadWriteMember( + userId + ) } async function getPublicShareTokens(userId, projectId) { @@ -209,10 +309,13 @@ async function getPublicShareTokens(userId, projectId) { return null } + // @ts-ignore if (memberInfo.isOwner) { return memberInfo.tokens + // @ts-ignore } else if (memberInfo.hasTokenReadOnlyAccess) { return { + // @ts-ignore readOnly: memberInfo.tokens.readOnly, } } else { @@ -224,6 +327,7 @@ async function getPublicShareTokens(userId, projectId) { // excluding projects where the user is listed in the token access fields when // token access has been disabled. async function getProjectsUserIsMemberOf(userId, fields) { + // @ts-ignore const limit = pLimit(2) const [readAndWrite, review, readOnly, tokenReadAndWrite, tokenReadOnly] = await Promise.all([ @@ -274,9 +378,9 @@ async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) { async function getAllInvitedMembers(projectId) { try { - const rawMembers = await getInvitedMembersWithPrivilegeLevels(projectId) - const { members } = - ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) + const { members } = ProjectEditorHandler.buildOwnerAndMembersViews( + await (await getProjectAccess(projectId)).loadInvitedMembers() + ) return members } catch (err) { throw OError.tag(err, 'error getting members for project', { projectId }) @@ -316,6 +420,19 @@ async function userIsReadWriteTokenMember(userId, projectId) { return project != null } +/** + * @param {ObjectId} ownerId + * @param {ObjectId[]} collaboratorIds + * @param {ObjectId[]} readOnlyIds + * @param {ObjectId[]} tokenAccessIds + * @param {ObjectId[]} tokenAccessReadOnlyIds + * @param {typeof PublicAccessLevels[keyof PublicAccessLevels]} publicAccessLevel + * @param {ObjectId[]} pendingEditorIds + * @param {ObjectId[]} reviewerIds + * @param {ObjectId[]} pendingReviewerIds + * @return {ProjectMember[]} + * @private + */ function _getMemberIdsWithPrivilegeLevelsFromFields( ownerId, collaboratorIds, @@ -384,6 +501,11 @@ function _getMemberIdsWithPrivilegeLevelsFromFields( return members } +/** + * @param {ProjectMember[]} members + * @return {Promise} + * @private + */ async function _loadMembers(members) { const userIds = Array.from(new Set(members.map(m => m.id))) const users = new Map()