[web] add ProjectAccess helper class (#25663)

* [web] add ProjectAccess helper class

* [web] remove ts-ignore for calling OError.tag with try/catch error

GitOrigin-RevId: e097a95b4d929a3927a3eeb70635590680c93007
This commit is contained in:
Jakob Ackermann
2025-06-02 13:33:07 +02:00
committed by Copybot
parent 988d826c75
commit e352249847
@@ -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<LoadedProjectMember[]>}
*/
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<LoadedProjectMember[]>}
* @private
*/
async function _loadMembers(members) {
const userIds = Array.from(new Set(members.map(m => m.id)))
const users = new Map()