mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-17 21:05:04 -04:00
[web] Enforce collaborator limit (#19619)
* Enables collaborator limit enforcement * Add pendingEditor_refs for editors downgraded during limit enforcement * Add isPendingEditor to useEditorContext --------- Co-authored-by: Thomas Mees <thomas.mees@overleaf.com> GitOrigin-RevId: b622213f6282ccd8ee85a23ceb29b8c6f8ff6a96
This commit is contained in:
parent
18ee2bc87a
commit
5e2662adc4
13 changed files with 227 additions and 28 deletions
|
@ -40,6 +40,7 @@ module.exports = {
|
||||||
getMemberIdPrivilegeLevel,
|
getMemberIdPrivilegeLevel,
|
||||||
getInvitedCollaboratorCount,
|
getInvitedCollaboratorCount,
|
||||||
getInvitedEditCollaboratorCount,
|
getInvitedEditCollaboratorCount,
|
||||||
|
getInvitedPendingEditorCount,
|
||||||
getProjectsUserIsMemberOf,
|
getProjectsUserIsMemberOf,
|
||||||
dangerouslyGetAllProjectsUserIsMemberOf,
|
dangerouslyGetAllProjectsUserIsMemberOf,
|
||||||
isUserInvitedMemberOfProject,
|
isUserInvitedMemberOfProject,
|
||||||
|
@ -59,6 +60,7 @@ async function getMemberIdsWithPrivilegeLevels(projectId) {
|
||||||
tokenAccessReadOnly_refs: 1,
|
tokenAccessReadOnly_refs: 1,
|
||||||
tokenAccessReadAndWrite_refs: 1,
|
tokenAccessReadAndWrite_refs: 1,
|
||||||
publicAccesLevel: 1,
|
publicAccesLevel: 1,
|
||||||
|
pendingEditor_refs: 1,
|
||||||
})
|
})
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Errors.NotFoundError(`no project found with id ${projectId}`)
|
throw new Errors.NotFoundError(`no project found with id ${projectId}`)
|
||||||
|
@ -69,7 +71,8 @@ async function getMemberIdsWithPrivilegeLevels(projectId) {
|
||||||
project.readOnly_refs,
|
project.readOnly_refs,
|
||||||
project.tokenAccessReadAndWrite_refs,
|
project.tokenAccessReadAndWrite_refs,
|
||||||
project.tokenAccessReadOnly_refs,
|
project.tokenAccessReadOnly_refs,
|
||||||
project.publicAccesLevel
|
project.publicAccesLevel,
|
||||||
|
project.pendingEditor_refs
|
||||||
)
|
)
|
||||||
return memberIds
|
return memberIds
|
||||||
}
|
}
|
||||||
|
@ -136,6 +139,17 @@ async function getInvitedEditCollaboratorCount(projectId) {
|
||||||
).length
|
).length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getInvitedPendingEditorCount(projectId) {
|
||||||
|
// Only counts invited members that are readonly pending editors
|
||||||
|
const members = await getMemberIdsWithPrivilegeLevels(projectId)
|
||||||
|
return members.filter(
|
||||||
|
m =>
|
||||||
|
m.source === Sources.INVITE &&
|
||||||
|
m.privilegeLevel === PrivilegeLevels.READ_ONLY &&
|
||||||
|
m.pendingEditor === true
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
|
||||||
async function isUserInvitedMemberOfProject(userId, projectId) {
|
async function isUserInvitedMemberOfProject(userId, projectId) {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return false
|
return false
|
||||||
|
@ -309,7 +323,8 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
|
||||||
readOnlyIds,
|
readOnlyIds,
|
||||||
tokenAccessIds,
|
tokenAccessIds,
|
||||||
tokenAccessReadOnlyIds,
|
tokenAccessReadOnlyIds,
|
||||||
publicAccessLevel
|
publicAccessLevel,
|
||||||
|
pendingEditorIds
|
||||||
) {
|
) {
|
||||||
const members = []
|
const members = []
|
||||||
members.push({
|
members.push({
|
||||||
|
@ -329,6 +344,9 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
|
||||||
id: memberId.toString(),
|
id: memberId.toString(),
|
||||||
privilegeLevel: PrivilegeLevels.READ_ONLY,
|
privilegeLevel: PrivilegeLevels.READ_ONLY,
|
||||||
source: Sources.INVITE,
|
source: Sources.INVITE,
|
||||||
|
...(pendingEditorIds?.some(pe => memberId.equals(pe)) && {
|
||||||
|
pendingEditor: true,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) {
|
if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) {
|
||||||
|
@ -364,7 +382,11 @@ async function _loadMembers(members) {
|
||||||
signUpDate: 1,
|
signUpDate: 1,
|
||||||
})
|
})
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
return { user, privilegeLevel: member.privilegeLevel }
|
return {
|
||||||
|
user,
|
||||||
|
privilegeLevel: member.privilegeLevel,
|
||||||
|
...(member.pendingEditor && { pendingEditor: true }),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ async function removeUserFromProject(projectId, userId) {
|
||||||
$pull: {
|
$pull: {
|
||||||
collaberator_refs: userId,
|
collaberator_refs: userId,
|
||||||
readOnly_refs: userId,
|
readOnly_refs: userId,
|
||||||
|
pendingEditor_refs: userId,
|
||||||
tokenAccessReadOnly_refs: userId,
|
tokenAccessReadOnly_refs: userId,
|
||||||
tokenAccessReadAndWrite_refs: userId,
|
tokenAccessReadAndWrite_refs: userId,
|
||||||
trashed: userId,
|
trashed: userId,
|
||||||
|
@ -62,6 +63,7 @@ async function removeUserFromProject(projectId, userId) {
|
||||||
$pull: {
|
$pull: {
|
||||||
collaberator_refs: userId,
|
collaberator_refs: userId,
|
||||||
readOnly_refs: userId,
|
readOnly_refs: userId,
|
||||||
|
pendingEditor_refs: userId,
|
||||||
tokenAccessReadOnly_refs: userId,
|
tokenAccessReadOnly_refs: userId,
|
||||||
tokenAccessReadAndWrite_refs: userId,
|
tokenAccessReadAndWrite_refs: userId,
|
||||||
archived: userId,
|
archived: userId,
|
||||||
|
@ -196,6 +198,19 @@ async function transferProjects(fromUserId, toUserId) {
|
||||||
}
|
}
|
||||||
).exec()
|
).exec()
|
||||||
|
|
||||||
|
await Project.updateMany(
|
||||||
|
{ pendingEditor_refs: fromUserId },
|
||||||
|
{
|
||||||
|
$addToSet: { pendingEditor_refs: toUserId },
|
||||||
|
}
|
||||||
|
).exec()
|
||||||
|
await Project.updateMany(
|
||||||
|
{ pendingEditor_refs: fromUserId },
|
||||||
|
{
|
||||||
|
$pull: { pendingEditor_refs: fromUserId },
|
||||||
|
}
|
||||||
|
).exec()
|
||||||
|
|
||||||
// Flush in background, no need to block on this
|
// Flush in background, no need to block on this
|
||||||
_flushProjects(projectIds).catch(err => {
|
_flushProjects(projectIds).catch(err => {
|
||||||
logger.err(
|
logger.err(
|
||||||
|
@ -220,14 +235,14 @@ async function setCollaboratorPrivilegeLevel(
|
||||||
switch (privilegeLevel) {
|
switch (privilegeLevel) {
|
||||||
case PrivilegeLevels.READ_AND_WRITE: {
|
case PrivilegeLevels.READ_AND_WRITE: {
|
||||||
update = {
|
update = {
|
||||||
$pull: { readOnly_refs: userId },
|
$pull: { readOnly_refs: userId, pendingEditor_refs: userId },
|
||||||
$addToSet: { collaberator_refs: userId },
|
$addToSet: { collaberator_refs: userId },
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case PrivilegeLevels.READ_ONLY: {
|
case PrivilegeLevels.READ_ONLY: {
|
||||||
update = {
|
update = {
|
||||||
$pull: { collaberator_refs: userId },
|
$pull: { collaberator_refs: userId, pendingEditor_refs: userId },
|
||||||
$addToSet: { readOnly_refs: userId },
|
$addToSet: { readOnly_refs: userId },
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
|
@ -415,6 +415,7 @@ const _ProjectController = {
|
||||||
tokens: 1,
|
tokens: 1,
|
||||||
tokenAccessReadAndWrite_refs: 1, // used for link sharing analytics
|
tokenAccessReadAndWrite_refs: 1, // used for link sharing analytics
|
||||||
collaberator_refs: 1, // used for link sharing analytics
|
collaberator_refs: 1, // used for link sharing analytics
|
||||||
|
pendingEditor_refs: 1, // used for link sharing analytics
|
||||||
}),
|
}),
|
||||||
userIsMemberOfGroupSubscription: sessionUser
|
userIsMemberOfGroupSubscription: sessionUser
|
||||||
? (async () =>
|
? (async () =>
|
||||||
|
@ -488,12 +489,24 @@ const _ProjectController = {
|
||||||
anonRequestToken
|
anonRequestToken
|
||||||
)
|
)
|
||||||
|
|
||||||
const linkSharingChanges =
|
const [linkSharingChanges, linkSharingEnforcement] = await Promise.all([
|
||||||
await SplitTestHandler.promises.getAssignmentForUser(
|
SplitTestHandler.promises.getAssignmentForUser(
|
||||||
project.owner_ref,
|
project.owner_ref,
|
||||||
'link-sharing-warning'
|
'link-sharing-warning'
|
||||||
)
|
),
|
||||||
|
SplitTestHandler.promises.getAssignmentForUser(
|
||||||
|
project.owner_ref,
|
||||||
|
'link-sharing-enforcement'
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
if (linkSharingChanges?.variant === 'active') {
|
if (linkSharingChanges?.variant === 'active') {
|
||||||
|
if (linkSharingEnforcement?.variant === 'active') {
|
||||||
|
await Modules.promises.hooks.fire(
|
||||||
|
'enforceCollaboratorLimit',
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
}
|
||||||
if (isTokenMember) {
|
if (isTokenMember) {
|
||||||
// Check explicitly that the user is in read write token refs, while this could be inferred
|
// Check explicitly that the user is in read write token refs, while this could be inferred
|
||||||
// from the privilege level, the privilege level of token members might later be restricted
|
// from the privilege level, the privilege level of token members might later be restricted
|
||||||
|
@ -570,12 +583,14 @@ const _ProjectController = {
|
||||||
)
|
)
|
||||||
const planLimit = ownerFeatures?.collaborators || 0
|
const planLimit = ownerFeatures?.collaborators || 0
|
||||||
const namedEditors = project.collaberator_refs?.length || 0
|
const namedEditors = project.collaberator_refs?.length || 0
|
||||||
|
const pendingEditors = project.pendingEditor_refs?.length || 0
|
||||||
const exceedAtLimit = planLimit > -1 && namedEditors >= planLimit
|
const exceedAtLimit = planLimit > -1 && namedEditors >= planLimit
|
||||||
const projectOpenedSegmentation = {
|
const projectOpenedSegmentation = {
|
||||||
projectId: project._id,
|
projectId: project._id,
|
||||||
// temporary link sharing segmentation:
|
// temporary link sharing segmentation:
|
||||||
linkSharingWarning: linkSharingChanges?.variant,
|
linkSharingWarning: linkSharingChanges?.variant,
|
||||||
namedEditors,
|
namedEditors,
|
||||||
|
pendingEditors,
|
||||||
tokenEditors: project.tokenAccessReadAndWrite_refs?.length || 0,
|
tokenEditors: project.tokenAccessReadAndWrite_refs?.length || 0,
|
||||||
planLimit,
|
planLimit,
|
||||||
exceedAtLimit,
|
exceedAtLimit,
|
||||||
|
@ -1034,7 +1049,6 @@ const ProjectController = {
|
||||||
),
|
),
|
||||||
updateProjectSettings: expressify(_ProjectController.updateProjectSettings),
|
updateProjectSettings: expressify(_ProjectController.updateProjectSettings),
|
||||||
userProjectsJson: expressify(_ProjectController.userProjectsJson),
|
userProjectsJson: expressify(_ProjectController.userProjectsJson),
|
||||||
|
|
||||||
_buildProjectList: _ProjectController._buildProjectList,
|
_buildProjectList: _ProjectController._buildProjectList,
|
||||||
_buildProjectViewModel: _ProjectController._buildProjectViewModel,
|
_buildProjectViewModel: _ProjectController._buildProjectViewModel,
|
||||||
_injectProjectUsers: _ProjectController._injectProjectUsers,
|
_injectProjectUsers: _ProjectController._injectProjectUsers,
|
||||||
|
|
|
@ -83,11 +83,9 @@ module.exports = ProjectEditorHandler = {
|
||||||
for (const member of members || []) {
|
for (const member of members || []) {
|
||||||
if (member.privilegeLevel === 'owner') {
|
if (member.privilegeLevel === 'owner') {
|
||||||
ownerFeatures = member.user.features
|
ownerFeatures = member.user.features
|
||||||
owner = this.buildUserModelView(member.user, 'owner')
|
owner = this.buildUserModelView(member)
|
||||||
} else {
|
} else {
|
||||||
filteredMembers.push(
|
filteredMembers.push(this.buildUserModelView(member))
|
||||||
this.buildUserModelView(member.user, member.privilegeLevel)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -97,14 +95,16 @@ module.exports = ProjectEditorHandler = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildUserModelView(user, privileges) {
|
buildUserModelView(member) {
|
||||||
|
const user = member.user
|
||||||
return {
|
return {
|
||||||
_id: user._id,
|
_id: user._id,
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
privileges,
|
privileges: member.privilegeLevel,
|
||||||
signUpDate: user.signUpDate,
|
signUpDate: user.signUpDate,
|
||||||
|
pendingEditor: member.pendingEditor,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -30,14 +30,14 @@ async function allowedNumberOfCollaboratorsForUser(userId) {
|
||||||
|
|
||||||
async function canAddXCollaborators(projectId, numberOfNewCollaborators) {
|
async function canAddXCollaborators(projectId, numberOfNewCollaborators) {
|
||||||
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
|
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
|
||||||
|
if (allowedNumber < 0) {
|
||||||
|
return true // -1 means unlimited
|
||||||
|
}
|
||||||
const currentNumber =
|
const currentNumber =
|
||||||
await CollaboratorsGetter.promises.getInvitedCollaboratorCount(projectId)
|
await CollaboratorsGetter.promises.getInvitedCollaboratorCount(projectId)
|
||||||
const inviteCount =
|
const inviteCount =
|
||||||
await CollaboratorsInvitesHandler.promises.getInviteCount(projectId)
|
await CollaboratorsInvitesHandler.promises.getInviteCount(projectId)
|
||||||
return (
|
return currentNumber + inviteCount + numberOfNewCollaborators <= allowedNumber
|
||||||
currentNumber + inviteCount + numberOfNewCollaborators <= allowedNumber ||
|
|
||||||
allowedNumber < 0 // -1 means unlimited
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function canAddXEditCollaborators(
|
async function canAddXEditCollaborators(
|
||||||
|
@ -45,6 +45,9 @@ async function canAddXEditCollaborators(
|
||||||
numberOfNewEditCollaborators
|
numberOfNewEditCollaborators
|
||||||
) {
|
) {
|
||||||
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
|
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
|
||||||
|
if (allowedNumber < 0) {
|
||||||
|
return true // -1 means unlimited
|
||||||
|
}
|
||||||
const currentEditors =
|
const currentEditors =
|
||||||
await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
|
await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
|
||||||
projectId
|
projectId
|
||||||
|
@ -53,7 +56,7 @@ async function canAddXEditCollaborators(
|
||||||
await CollaboratorsInvitesHandler.promises.getEditInviteCount(projectId)
|
await CollaboratorsInvitesHandler.promises.getEditInviteCount(projectId)
|
||||||
return (
|
return (
|
||||||
currentEditors + editInviteCount + numberOfNewEditCollaborators <=
|
currentEditors + editInviteCount + numberOfNewEditCollaborators <=
|
||||||
allowedNumber || allowedNumber < 0 // -1 means unlimited
|
allowedNumber
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ const ProjectSchema = new Schema(
|
||||||
owner_ref: { type: ObjectId, ref: 'User' },
|
owner_ref: { type: ObjectId, ref: 'User' },
|
||||||
collaberator_refs: [{ type: ObjectId, ref: 'User' }],
|
collaberator_refs: [{ type: ObjectId, ref: 'User' }],
|
||||||
readOnly_refs: [{ type: ObjectId, ref: 'User' }],
|
readOnly_refs: [{ type: ObjectId, ref: 'User' }],
|
||||||
|
pendingEditor_refs: [{ type: ObjectId, ref: 'User' }],
|
||||||
rootDoc_id: { type: ObjectId },
|
rootDoc_id: { type: ObjectId },
|
||||||
rootFolder: [FolderSchema],
|
rootFolder: [FolderSchema],
|
||||||
version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
|
version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
|
||||||
|
|
|
@ -124,9 +124,17 @@ export default function EditMember({
|
||||||
<FormGroup className="project-member">
|
<FormGroup className="project-member">
|
||||||
<Col xs={7}>
|
<Col xs={7}>
|
||||||
<div className="project-member-email-icon">
|
<div className="project-member-email-icon">
|
||||||
<Icon type={shouldWarnMember() ? 'warning' : 'user'} fw />
|
<Icon
|
||||||
|
type={
|
||||||
|
shouldWarnMember() || member.pendingEditor ? 'warning' : 'user'
|
||||||
|
}
|
||||||
|
fw
|
||||||
|
/>
|
||||||
<div className="email-warning">
|
<div className="email-warning">
|
||||||
{member.email}
|
{member.email}
|
||||||
|
{member.pendingEditor && (
|
||||||
|
<div className="subtitle">Pending editor</div>
|
||||||
|
)}
|
||||||
{shouldWarnMember() && (
|
{shouldWarnMember() && (
|
||||||
<div className="subtitle">
|
<div className="subtitle">
|
||||||
{t('will_lose_edit_access_on_date', {
|
{t('will_lose_edit_access_on_date', {
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const EditorContext = createContext<
|
||||||
insertSymbol?: (symbol: string) => void
|
insertSymbol?: (symbol: string) => void
|
||||||
isProjectOwner: boolean
|
isProjectOwner: boolean
|
||||||
isRestrictedTokenMember?: boolean
|
isRestrictedTokenMember?: boolean
|
||||||
|
isPendingEditor: boolean
|
||||||
permissionsLevel: 'readOnly' | 'readAndWrite' | 'owner'
|
permissionsLevel: 'readOnly' | 'readAndWrite' | 'owner'
|
||||||
deactivateTutorial: (tutorial: string) => void
|
deactivateTutorial: (tutorial: string) => void
|
||||||
inactiveTutorials: string[]
|
inactiveTutorials: string[]
|
||||||
|
@ -62,7 +63,7 @@ export const EditorProvider: FC = ({ children }) => {
|
||||||
const { role } = useDetachContext()
|
const { role } = useDetachContext()
|
||||||
const { showGenericMessageModal } = useModalsContext()
|
const { showGenericMessageModal } = useModalsContext()
|
||||||
|
|
||||||
const { owner, features, _id: projectId } = useProjectContext()
|
const { owner, features, _id: projectId, members } = useProjectContext()
|
||||||
|
|
||||||
const cobranding = useMemo(() => {
|
const cobranding = useMemo(() => {
|
||||||
const brandVariation = getMeta('ol-brandVariation')
|
const brandVariation = getMeta('ol-brandVariation')
|
||||||
|
@ -98,6 +99,17 @@ export const EditorProvider: FC = ({ children }) => {
|
||||||
|
|
||||||
const [currentPopup, setCurrentPopup] = useState<string | null>(null)
|
const [currentPopup, setCurrentPopup] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const isPendingEditor = useMemo(
|
||||||
|
() =>
|
||||||
|
members?.some(
|
||||||
|
member =>
|
||||||
|
member._id === userId &&
|
||||||
|
member.pendingEditor &&
|
||||||
|
member.privileges === 'readAndWrite'
|
||||||
|
),
|
||||||
|
[members, userId]
|
||||||
|
)
|
||||||
|
|
||||||
const deactivateTutorial = useCallback(
|
const deactivateTutorial = useCallback(
|
||||||
tutorialKey => {
|
tutorialKey => {
|
||||||
setInactiveTutorials([...inactiveTutorials, tutorialKey])
|
setInactiveTutorials([...inactiveTutorials, tutorialKey])
|
||||||
|
@ -174,6 +186,7 @@ export const EditorProvider: FC = ({ children }) => {
|
||||||
setPermissionsLevel,
|
setPermissionsLevel,
|
||||||
isProjectOwner: owner?._id === userId,
|
isProjectOwner: owner?._id === userId,
|
||||||
isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
|
isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
|
||||||
|
isPendingEditor,
|
||||||
showSymbolPalette,
|
showSymbolPalette,
|
||||||
toggleSymbolPalette,
|
toggleSymbolPalette,
|
||||||
insertSymbol,
|
insertSymbol,
|
||||||
|
@ -194,6 +207,7 @@ export const EditorProvider: FC = ({ children }) => {
|
||||||
renameProject,
|
renameProject,
|
||||||
permissionsLevel,
|
permissionsLevel,
|
||||||
setPermissionsLevel,
|
setPermissionsLevel,
|
||||||
|
isPendingEditor,
|
||||||
showSymbolPalette,
|
showSymbolPalette,
|
||||||
toggleSymbolPalette,
|
toggleSymbolPalette,
|
||||||
insertSymbol,
|
insertSymbol,
|
||||||
|
|
|
@ -7,6 +7,7 @@ export type ProjectContextMember = {
|
||||||
email: string
|
email: string
|
||||||
first_name: string
|
first_name: string
|
||||||
last_name: string
|
last_name: string
|
||||||
|
pendingEditor?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectContextValue = {
|
export type ProjectContextValue = {
|
||||||
|
|
|
@ -946,6 +946,10 @@ class User {
|
||||||
updateOp = { $addToSet: { collaberator_refs: user._id } }
|
updateOp = { $addToSet: { collaberator_refs: user._id } }
|
||||||
} else if (privileges === 'readOnly') {
|
} else if (privileges === 'readOnly') {
|
||||||
updateOp = { $addToSet: { readOnly_refs: user._id } }
|
updateOp = { $addToSet: { readOnly_refs: user._id } }
|
||||||
|
} else if (privileges === 'pendingEditor') {
|
||||||
|
updateOp = {
|
||||||
|
$addToSet: { readOnly_refs: user._id, pendingEditor_refs: user._id },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
db.projects.updateOne({ _id: new ObjectId(projectId) }, updateOp, callback)
|
db.projects.updateOne({ _id: new ObjectId(projectId) }, updateOp, callback)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ describe('CollaboratorsGetter', function () {
|
||||||
this.ownerRef = new ObjectId()
|
this.ownerRef = new ObjectId()
|
||||||
this.readOnlyRef1 = new ObjectId()
|
this.readOnlyRef1 = new ObjectId()
|
||||||
this.readOnlyRef2 = new ObjectId()
|
this.readOnlyRef2 = new ObjectId()
|
||||||
|
this.pendingEditorRef = new ObjectId()
|
||||||
this.readWriteRef1 = new ObjectId()
|
this.readWriteRef1 = new ObjectId()
|
||||||
this.readWriteRef2 = new ObjectId()
|
this.readWriteRef2 = new ObjectId()
|
||||||
this.readOnlyTokenRef = new ObjectId()
|
this.readOnlyTokenRef = new ObjectId()
|
||||||
|
@ -25,7 +26,12 @@ describe('CollaboratorsGetter', function () {
|
||||||
this.project = {
|
this.project = {
|
||||||
_id: new ObjectId(),
|
_id: new ObjectId(),
|
||||||
owner_ref: [this.ownerRef],
|
owner_ref: [this.ownerRef],
|
||||||
readOnly_refs: [this.readOnlyRef1, this.readOnlyRef2],
|
readOnly_refs: [
|
||||||
|
this.readOnlyRef1,
|
||||||
|
this.readOnlyRef2,
|
||||||
|
this.pendingEditorRef,
|
||||||
|
],
|
||||||
|
pendingEditor_refs: [this.pendingEditorRef],
|
||||||
collaberator_refs: [this.readWriteRef1, this.readWriteRef2],
|
collaberator_refs: [this.readWriteRef1, this.readWriteRef2],
|
||||||
tokenAccessReadAndWrite_refs: [this.readWriteTokenRef],
|
tokenAccessReadAndWrite_refs: [this.readWriteTokenRef],
|
||||||
tokenAccessReadOnly_refs: [this.readOnlyTokenRef],
|
tokenAccessReadOnly_refs: [this.readOnlyTokenRef],
|
||||||
|
@ -99,6 +105,12 @@ describe('CollaboratorsGetter', function () {
|
||||||
privilegeLevel: 'readOnly',
|
privilegeLevel: 'readOnly',
|
||||||
source: 'invite',
|
source: 'invite',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: this.pendingEditorRef.toString(),
|
||||||
|
privilegeLevel: 'readOnly',
|
||||||
|
source: 'invite',
|
||||||
|
pendingEditor: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: this.readOnlyTokenRef.toString(),
|
id: this.readOnlyTokenRef.toString(),
|
||||||
privilegeLevel: 'readOnly',
|
privilegeLevel: 'readOnly',
|
||||||
|
@ -139,6 +151,7 @@ describe('CollaboratorsGetter', function () {
|
||||||
this.readOnlyRef2.toString(),
|
this.readOnlyRef2.toString(),
|
||||||
this.readWriteRef1.toString(),
|
this.readWriteRef1.toString(),
|
||||||
this.readWriteRef2.toString(),
|
this.readWriteRef2.toString(),
|
||||||
|
this.pendingEditorRef.toString(),
|
||||||
this.readWriteTokenRef.toString(),
|
this.readWriteTokenRef.toString(),
|
||||||
this.readOnlyTokenRef.toString(),
|
this.readOnlyTokenRef.toString(),
|
||||||
])
|
])
|
||||||
|
@ -157,6 +170,7 @@ describe('CollaboratorsGetter', function () {
|
||||||
this.readOnlyRef2.toString(),
|
this.readOnlyRef2.toString(),
|
||||||
this.readWriteRef1.toString(),
|
this.readWriteRef1.toString(),
|
||||||
this.readWriteRef2.toString(),
|
this.readWriteRef2.toString(),
|
||||||
|
this.pendingEditorRef.toString(),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -484,4 +498,14 @@ describe('CollaboratorsGetter', function () {
|
||||||
expect(count).to.equal(2)
|
expect(count).to.equal(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getInvitedPendingEditorCount', function () {
|
||||||
|
it('should return the count of pending editors', async function () {
|
||||||
|
const count =
|
||||||
|
await this.CollaboratorsGetter.promises.getInvitedPendingEditorCount(
|
||||||
|
this.project._id
|
||||||
|
)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -106,6 +106,7 @@ describe('CollaboratorsHandler', function () {
|
||||||
$pull: {
|
$pull: {
|
||||||
collaberator_refs: this.userId,
|
collaberator_refs: this.userId,
|
||||||
readOnly_refs: this.userId,
|
readOnly_refs: this.userId,
|
||||||
|
pendingEditor_refs: this.userId,
|
||||||
tokenAccessReadOnly_refs: this.userId,
|
tokenAccessReadOnly_refs: this.userId,
|
||||||
tokenAccessReadAndWrite_refs: this.userId,
|
tokenAccessReadAndWrite_refs: this.userId,
|
||||||
archived: this.userId,
|
archived: this.userId,
|
||||||
|
@ -148,6 +149,7 @@ describe('CollaboratorsHandler', function () {
|
||||||
$pull: {
|
$pull: {
|
||||||
collaberator_refs: this.userId,
|
collaberator_refs: this.userId,
|
||||||
readOnly_refs: this.userId,
|
readOnly_refs: this.userId,
|
||||||
|
pendingEditor_refs: this.userId,
|
||||||
tokenAccessReadOnly_refs: this.userId,
|
tokenAccessReadOnly_refs: this.userId,
|
||||||
tokenAccessReadAndWrite_refs: this.userId,
|
tokenAccessReadAndWrite_refs: this.userId,
|
||||||
trashed: this.userId,
|
trashed: this.userId,
|
||||||
|
@ -182,6 +184,7 @@ describe('CollaboratorsHandler', function () {
|
||||||
$pull: {
|
$pull: {
|
||||||
collaberator_refs: this.userId,
|
collaberator_refs: this.userId,
|
||||||
readOnly_refs: this.userId,
|
readOnly_refs: this.userId,
|
||||||
|
pendingEditor_refs: this.userId,
|
||||||
tokenAccessReadOnly_refs: this.userId,
|
tokenAccessReadOnly_refs: this.userId,
|
||||||
tokenAccessReadAndWrite_refs: this.userId,
|
tokenAccessReadAndWrite_refs: this.userId,
|
||||||
archived: this.userId,
|
archived: this.userId,
|
||||||
|
@ -377,6 +380,7 @@ describe('CollaboratorsHandler', function () {
|
||||||
$pull: {
|
$pull: {
|
||||||
collaberator_refs: this.userId,
|
collaberator_refs: this.userId,
|
||||||
readOnly_refs: this.userId,
|
readOnly_refs: this.userId,
|
||||||
|
pendingEditor_refs: this.userId,
|
||||||
tokenAccessReadOnly_refs: this.userId,
|
tokenAccessReadOnly_refs: this.userId,
|
||||||
tokenAccessReadAndWrite_refs: this.userId,
|
tokenAccessReadAndWrite_refs: this.userId,
|
||||||
archived: this.userId,
|
archived: this.userId,
|
||||||
|
@ -457,6 +461,24 @@ describe('CollaboratorsHandler', function () {
|
||||||
)
|
)
|
||||||
.chain('exec')
|
.chain('exec')
|
||||||
.resolves()
|
.resolves()
|
||||||
|
this.ProjectMock.expects('updateMany')
|
||||||
|
.withArgs(
|
||||||
|
{ pendingEditor_refs: this.fromUserId },
|
||||||
|
{
|
||||||
|
$addToSet: { pendingEditor_refs: this.toUserId },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.chain('exec')
|
||||||
|
.resolves()
|
||||||
|
this.ProjectMock.expects('updateMany')
|
||||||
|
.withArgs(
|
||||||
|
{ pendingEditor_refs: this.fromUserId },
|
||||||
|
{
|
||||||
|
$pull: { pendingEditor_refs: this.fromUserId },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.chain('exec')
|
||||||
|
.resolves()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('successfully', function () {
|
describe('successfully', function () {
|
||||||
|
@ -501,7 +523,10 @@ describe('CollaboratorsHandler', function () {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$pull: { collaberator_refs: this.userId },
|
$pull: {
|
||||||
|
collaberator_refs: this.userId,
|
||||||
|
pendingEditor_refs: this.userId,
|
||||||
|
},
|
||||||
$addToSet: { readOnly_refs: this.userId },
|
$addToSet: { readOnly_refs: this.userId },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -526,7 +551,10 @@ describe('CollaboratorsHandler', function () {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$addToSet: { collaberator_refs: this.userId },
|
$addToSet: { collaberator_refs: this.userId },
|
||||||
$pull: { readOnly_refs: this.userId },
|
$pull: {
|
||||||
|
readOnly_refs: this.userId,
|
||||||
|
pendingEditor_refs: this.userId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.chain('exec')
|
.chain('exec')
|
||||||
|
|
|
@ -131,6 +131,11 @@ describe('ProjectController', function () {
|
||||||
isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(true),
|
isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
this.CollaboratorsHandler = {
|
||||||
|
promises: {
|
||||||
|
setCollaboratorPrivilegeLevel: sinon.stub().resolves(),
|
||||||
|
},
|
||||||
|
}
|
||||||
this.ProjectEntityHandler = {}
|
this.ProjectEntityHandler = {}
|
||||||
this.UserGetter = {
|
this.UserGetter = {
|
||||||
getUserFullEmails: sinon.stub().yields(null, []),
|
getUserFullEmails: sinon.stub().yields(null, []),
|
||||||
|
@ -200,12 +205,16 @@ describe('ProjectController', function () {
|
||||||
this.OnboardingDataCollectionManager = {
|
this.OnboardingDataCollectionManager = {
|
||||||
getOnboardingDataValue: sinon.stub().resolves(null),
|
getOnboardingDataValue: sinon.stub().resolves(null),
|
||||||
}
|
}
|
||||||
|
this.Modules = {
|
||||||
|
promises: { hooks: { fire: sinon.stub().resolves() } },
|
||||||
|
}
|
||||||
|
|
||||||
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
||||||
requires: {
|
requires: {
|
||||||
'mongodb-legacy': { ObjectId },
|
'mongodb-legacy': { ObjectId },
|
||||||
'@overleaf/settings': this.settings,
|
'@overleaf/settings': this.settings,
|
||||||
'@overleaf/metrics': this.Metrics,
|
'@overleaf/metrics': this.Metrics,
|
||||||
|
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
|
||||||
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
||||||
'../SplitTests/SplitTestSessionHandler': this.SplitTestSessionHandler,
|
'../SplitTests/SplitTestSessionHandler': this.SplitTestSessionHandler,
|
||||||
'./ProjectDeleter': this.ProjectDeleter,
|
'./ProjectDeleter': this.ProjectDeleter,
|
||||||
|
@ -255,6 +264,7 @@ describe('ProjectController', function () {
|
||||||
updateUser: sinon.stub().resolves(),
|
updateUser: sinon.stub().resolves(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'../../infrastructure/Modules': this.Modules,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1015,9 +1025,13 @@ describe('ProjectController', function () {
|
||||||
|
|
||||||
describe('link sharing changes active', function () {
|
describe('link sharing changes active', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.SplitTestHandler.promises.getAssignmentForUser.resolves({
|
this.SplitTestHandler.promises.getAssignmentForUser.callsFake(
|
||||||
variant: 'active',
|
async (userId, test) => {
|
||||||
})
|
if (test === 'link-sharing-warning') {
|
||||||
|
return { variant: 'active' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when user is a read write token member (and not already a named editor)', function () {
|
describe('when user is a read write token member (and not already a named editor)', function () {
|
||||||
|
@ -1059,6 +1073,57 @@ describe('ProjectController', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('link sharing enforcement', function () {
|
||||||
|
describe('when not active (default)', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.SplitTestHandler.promises.getAssignmentForUser.callsFake(
|
||||||
|
async (userId, test) => {
|
||||||
|
if (test === 'link-sharing-warning') {
|
||||||
|
return { variant: 'active' }
|
||||||
|
} else if (test === 'link-sharing-enforcement') {
|
||||||
|
return { variant: 'default' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not call the collaborator limit enforcement check', function (done) {
|
||||||
|
this.res.render = (pageName, opts) => {
|
||||||
|
this.Modules.promises.hooks.fire.should.not.have.been.calledWith(
|
||||||
|
'enforceCollaboratorLimit'
|
||||||
|
)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
this.ProjectController.loadEditor(this.req, this.res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when active', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.SplitTestHandler.promises.getAssignmentForUser.callsFake(
|
||||||
|
async (userId, test) => {
|
||||||
|
if (test === 'link-sharing-warning') {
|
||||||
|
return { variant: 'active' }
|
||||||
|
} else if (test === 'link-sharing-enforcement') {
|
||||||
|
return { variant: 'active' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call the collaborator limit enforcement check', function (done) {
|
||||||
|
this.res.render = (pageName, opts) => {
|
||||||
|
this.Modules.promises.hooks.fire.should.have.been.calledWith(
|
||||||
|
'enforceCollaboratorLimit',
|
||||||
|
this.project_id
|
||||||
|
)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
this.ProjectController.loadEditor(this.req, this.res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('userProjectsJson', function () {
|
describe('userProjectsJson', function () {
|
||||||
|
|
Loading…
Reference in a new issue