mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Add new interstitial 'Join project' consent page for existing link sharing editors when opening a project (#19066)
* Add helpers for checking and removing user readwrite token membership * Add sharing-updates page and handlers * Redirect read write token members to sharing-updates on project load GitOrigin-RevId: d552a2cd74a9843c6103923b03f137131a48877a
This commit is contained in:
parent
260fdf1307
commit
94be372b24
17 changed files with 1235 additions and 0 deletions
|
@ -42,8 +42,10 @@ module.exports = {
|
||||||
getProjectsUserIsMemberOf,
|
getProjectsUserIsMemberOf,
|
||||||
dangerouslyGetAllProjectsUserIsMemberOf,
|
dangerouslyGetAllProjectsUserIsMemberOf,
|
||||||
isUserInvitedMemberOfProject,
|
isUserInvitedMemberOfProject,
|
||||||
|
isUserInvitedReadWriteMemberOfProject,
|
||||||
getPublicShareTokens,
|
getPublicShareTokens,
|
||||||
userIsTokenMember,
|
userIsTokenMember,
|
||||||
|
userIsReadWriteTokenMember,
|
||||||
getAllInvitedMembers,
|
getAllInvitedMembers,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -139,6 +141,23 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
async function getPublicShareTokens(userId, projectId) {
|
async function getPublicShareTokens(userId, projectId) {
|
||||||
const memberInfo = await Project.findOne(
|
const memberInfo = await Project.findOne(
|
||||||
{
|
{
|
||||||
|
@ -253,6 +272,21 @@ async function userIsTokenMember(userId, projectId) {
|
||||||
return project != null
|
return project != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function userIsReadWriteTokenMember(userId, projectId) {
|
||||||
|
userId = new ObjectId(userId.toString())
|
||||||
|
projectId = new ObjectId(projectId.toString())
|
||||||
|
const project = await Project.findOne(
|
||||||
|
{
|
||||||
|
_id: projectId,
|
||||||
|
tokenAccessReadAndWrite_refs: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 1,
|
||||||
|
}
|
||||||
|
).exec()
|
||||||
|
return project != null
|
||||||
|
}
|
||||||
|
|
||||||
async function _getInvitedMemberCount(projectId) {
|
async function _getInvitedMemberCount(projectId) {
|
||||||
const members = await getMemberIdsWithPrivilegeLevels(projectId)
|
const members = await getMemberIdsWithPrivilegeLevels(projectId)
|
||||||
return members.filter(m => m.source !== Sources.TOKEN).length
|
return members.filter(m => m.source !== Sources.TOKEN).length
|
||||||
|
|
|
@ -467,6 +467,36 @@ const _ProjectController = {
|
||||||
anonRequestToken
|
anonRequestToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const linkSharingChanges =
|
||||||
|
await SplitTestHandler.promises.getAssignmentForUser(
|
||||||
|
project.owner_ref,
|
||||||
|
'link-sharing-warning'
|
||||||
|
)
|
||||||
|
if (linkSharingChanges?.variant === 'active') {
|
||||||
|
if (isTokenMember) {
|
||||||
|
// 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
|
||||||
|
const isReadWriteTokenMember =
|
||||||
|
await CollaboratorsGetter.promises.userIsReadWriteTokenMember(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
if (isReadWriteTokenMember) {
|
||||||
|
// Check for an edge case where a user is both in read write token access refs but also
|
||||||
|
// an invited read write member. Ensure they are not redirected to the sharing updates page
|
||||||
|
// We could also delete the token access ref if the user is already a member of the project
|
||||||
|
const isInvitedReadWriteMember =
|
||||||
|
await CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
if (!isInvitedReadWriteMember) {
|
||||||
|
return res.redirect(`/project/${projectId}/sharing-updates`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let allowedFreeTrial = true
|
let allowedFreeTrial = true
|
||||||
|
|
||||||
if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) {
|
if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) {
|
||||||
|
|
|
@ -15,6 +15,9 @@ const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
||||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||||
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||||
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
|
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
|
||||||
|
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
|
||||||
|
const ProjectGetter = require('../Project/ProjectGetter')
|
||||||
|
const AsyncFormHelper = require('../Helpers/AsyncFormHelper')
|
||||||
|
|
||||||
const orderedPrivilegeLevels = [
|
const orderedPrivilegeLevels = [
|
||||||
PrivilegeLevels.NONE,
|
PrivilegeLevels.NONE,
|
||||||
|
@ -433,6 +436,116 @@ async function grantTokenAccessReadOnly(req, res, next) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureUserCanUseSharingUpdatesConsentPage(req, res, next) {
|
||||||
|
const { Project_id: projectId } = req.params
|
||||||
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
|
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||||
|
owner_ref: 1,
|
||||||
|
})
|
||||||
|
if (!project) {
|
||||||
|
throw new Errors.NotFoundError()
|
||||||
|
}
|
||||||
|
const linkSharingChanges =
|
||||||
|
await SplitTestHandler.promises.getAssignmentForUser(
|
||||||
|
project.owner_ref,
|
||||||
|
'link-sharing-warning'
|
||||||
|
)
|
||||||
|
if (linkSharingChanges?.variant !== 'active') {
|
||||||
|
return AsyncFormHelper.redirect(req, res, `/project/${projectId}`)
|
||||||
|
}
|
||||||
|
const isReadWriteTokenMember =
|
||||||
|
await CollaboratorsGetter.promises.userIsReadWriteTokenMember(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
if (!isReadWriteTokenMember) {
|
||||||
|
// If the user is not a read write token member, there are no actions to take
|
||||||
|
return AsyncFormHelper.redirect(req, res, `/project/${projectId}`)
|
||||||
|
}
|
||||||
|
const isReadWriteMember =
|
||||||
|
await CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
if (isReadWriteMember) {
|
||||||
|
// If the user is already an invited editor, the actions don't make sense
|
||||||
|
return AsyncFormHelper.redirect(req, res, `/project/${projectId}`)
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sharingUpdatesConsent(req, res, next) {
|
||||||
|
const { Project_id: projectId } = req.params
|
||||||
|
res.render('project/token/sharing-updates', {
|
||||||
|
projectId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveReadWriteToCollaborators(req, res, next) {
|
||||||
|
const { Project_id: projectId } = req.params
|
||||||
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
|
const isInvitedMember =
|
||||||
|
await CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
await ProjectAuditLogHandler.promises.addEntry(
|
||||||
|
projectId,
|
||||||
|
'accept-via-link-sharing',
|
||||||
|
userId,
|
||||||
|
req.ip,
|
||||||
|
{
|
||||||
|
privileges: 'readAndWrite',
|
||||||
|
tokenMember: true,
|
||||||
|
invitedMember: isInvitedMember,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (isInvitedMember) {
|
||||||
|
// Read only invited viewer who is gaining edit access via link sharing
|
||||||
|
await TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
await CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
PrivilegeLevels.READ_AND_WRITE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Normal case, not invited, joining via link sharing
|
||||||
|
await TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
await CollaboratorsHandler.promises.addUserIdToProject(
|
||||||
|
projectId,
|
||||||
|
undefined,
|
||||||
|
userId,
|
||||||
|
PrivilegeLevels.READ_AND_WRITE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {
|
||||||
|
members: true,
|
||||||
|
})
|
||||||
|
res.sendStatus(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveReadWriteToReadOnly(req, res, next) {
|
||||||
|
const { Project_id: projectId } = req.params
|
||||||
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
|
await ProjectAuditLogHandler.promises.addEntry(
|
||||||
|
projectId,
|
||||||
|
'readonly-via-sharing-updates',
|
||||||
|
userId,
|
||||||
|
req.ip
|
||||||
|
)
|
||||||
|
await TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly(
|
||||||
|
userId,
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
res.sendStatus(204)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
|
READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
|
||||||
READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
|
READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
|
||||||
|
@ -440,4 +553,10 @@ module.exports = {
|
||||||
tokenAccessPage: expressify(tokenAccessPage),
|
tokenAccessPage: expressify(tokenAccessPage),
|
||||||
grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
|
grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
|
||||||
grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
|
grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
|
||||||
|
ensureUserCanUseSharingUpdatesConsentPage: expressify(
|
||||||
|
ensureUserCanUseSharingUpdatesConsentPage
|
||||||
|
),
|
||||||
|
sharingUpdatesConsent: expressify(sharingUpdatesConsent),
|
||||||
|
moveReadWriteToCollaborators: expressify(moveReadWriteToCollaborators),
|
||||||
|
moveReadWriteToReadOnly: expressify(moveReadWriteToReadOnly),
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,6 +187,35 @@ const TokenAccessHandler = {
|
||||||
).exec()
|
).exec()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async removeReadAndWriteUserFromProject(userId, projectId) {
|
||||||
|
userId = new ObjectId(userId.toString())
|
||||||
|
projectId = new ObjectId(projectId.toString())
|
||||||
|
|
||||||
|
return await Project.updateOne(
|
||||||
|
{
|
||||||
|
_id: projectId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$pull: { tokenAccessReadAndWrite_refs: userId },
|
||||||
|
}
|
||||||
|
).exec()
|
||||||
|
},
|
||||||
|
|
||||||
|
async moveReadAndWriteUserToReadOnly(userId, projectId) {
|
||||||
|
userId = new ObjectId(userId.toString())
|
||||||
|
projectId = new ObjectId(projectId.toString())
|
||||||
|
|
||||||
|
return await Project.updateOne(
|
||||||
|
{
|
||||||
|
_id: projectId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$pull: { tokenAccessReadAndWrite_refs: userId },
|
||||||
|
$addToSet: { tokenAccessReadOnly_refs: userId },
|
||||||
|
}
|
||||||
|
).exec()
|
||||||
|
},
|
||||||
|
|
||||||
grantSessionTokenAccess(req, projectId, token) {
|
grantSessionTokenAccess(req, projectId, token) {
|
||||||
if (!req.session) {
|
if (!req.session) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||||
|
const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware')
|
||||||
|
const TokenAccessController = require('./TokenAccessController')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apply(webRouter) {
|
||||||
|
webRouter.get(
|
||||||
|
`/project/:Project_id/sharing-updates`,
|
||||||
|
AuthenticationController.requireLogin(),
|
||||||
|
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||||
|
TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage,
|
||||||
|
TokenAccessController.sharingUpdatesConsent
|
||||||
|
)
|
||||||
|
|
||||||
|
webRouter.post(
|
||||||
|
`/project/:Project_id/sharing-updates/join`,
|
||||||
|
AuthenticationController.requireLogin(),
|
||||||
|
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||||
|
TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage,
|
||||||
|
TokenAccessController.moveReadWriteToCollaborators
|
||||||
|
)
|
||||||
|
|
||||||
|
webRouter.post(
|
||||||
|
`/project/:Project_id/sharing-updates/view`,
|
||||||
|
AuthenticationController.requireLogin(),
|
||||||
|
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||||
|
TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage,
|
||||||
|
TokenAccessController.moveReadWriteToReadOnly
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ const BetaProgramController = require('./Features/BetaProgram/BetaProgramControl
|
||||||
const AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
const AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
||||||
const MetaController = require('./Features/Metadata/MetaController')
|
const MetaController = require('./Features/Metadata/MetaController')
|
||||||
const TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
|
const TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
|
||||||
|
const TokenAccessRouter = require('./Features/TokenAccess/TokenAccessRouter')
|
||||||
const Features = require('./infrastructure/Features')
|
const Features = require('./infrastructure/Features')
|
||||||
const LinkedFilesRouter = require('./Features/LinkedFiles/LinkedFilesRouter')
|
const LinkedFilesRouter = require('./Features/LinkedFiles/LinkedFilesRouter')
|
||||||
const TemplatesRouter = require('./Features/Templates/TemplatesRouter')
|
const TemplatesRouter = require('./Features/Templates/TemplatesRouter')
|
||||||
|
@ -270,6 +271,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||||
TemplatesRouter.apply(webRouter)
|
TemplatesRouter.apply(webRouter)
|
||||||
UserMembershipRouter.apply(webRouter)
|
UserMembershipRouter.apply(webRouter)
|
||||||
|
TokenAccessRouter.apply(webRouter)
|
||||||
|
|
||||||
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
|
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
|
||||||
|
|
||||||
|
|
16
services/web/app/views/project/token/sharing-updates.pug
Normal file
16
services/web/app/views/project/token/sharing-updates.pug
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
extends ../../layout-marketing
|
||||||
|
|
||||||
|
block entrypointVar
|
||||||
|
- entrypoint = 'pages/sharing-updates'
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- var suppressFooter = true
|
||||||
|
- var suppressCookieBanner = true
|
||||||
|
- var suppressSkipToContent = true
|
||||||
|
|
||||||
|
block append meta
|
||||||
|
meta(name="ol-project_id" data-type="string" content=projectId)
|
||||||
|
|
||||||
|
block content
|
||||||
|
.content.content-alt#main-content
|
||||||
|
div#sharing-updates-page
|
|
@ -874,6 +874,7 @@
|
||||||
"off": "",
|
"off": "",
|
||||||
"official": "",
|
"official": "",
|
||||||
"ok": "",
|
"ok": "",
|
||||||
|
"ok_continue_to_project": "",
|
||||||
"ok_join_project": "",
|
"ok_join_project": "",
|
||||||
"on": "",
|
"on": "",
|
||||||
"on_free_plan_upgrade_to_access_features": "",
|
"on_free_plan_upgrade_to_access_features": "",
|
||||||
|
@ -1529,6 +1530,7 @@
|
||||||
"update_account_info": "",
|
"update_account_info": "",
|
||||||
"update_dropbox_settings": "",
|
"update_dropbox_settings": "",
|
||||||
"update_your_billing_details": "",
|
"update_your_billing_details": "",
|
||||||
|
"updates_to_project_sharing": "",
|
||||||
"updating": "",
|
"updating": "",
|
||||||
"upgrade": "",
|
"upgrade": "",
|
||||||
"upgrade_cc_btn": "",
|
"upgrade_cc_btn": "",
|
||||||
|
@ -1594,6 +1596,7 @@
|
||||||
"we_sent_new_code": "",
|
"we_sent_new_code": "",
|
||||||
"wed_love_you_to_stay": "",
|
"wed_love_you_to_stay": "",
|
||||||
"welcome_to_sl": "",
|
"welcome_to_sl": "",
|
||||||
|
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
|
||||||
"were_performing_maintenance": "",
|
"were_performing_maintenance": "",
|
||||||
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "",
|
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "",
|
||||||
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "",
|
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "",
|
||||||
|
@ -1640,6 +1643,7 @@
|
||||||
"you_are_on_a_paid_plan_contact_support_to_find_out_more": "",
|
"you_are_on_a_paid_plan_contact_support_to_find_out_more": "",
|
||||||
"you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "",
|
"you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "",
|
||||||
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
|
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
|
||||||
|
"you_can_also_choose_to_view_anonymously_or_leave_the_project": "",
|
||||||
"you_can_now_enable_sso": "",
|
"you_can_now_enable_sso": "",
|
||||||
"you_can_now_log_in_sso": "",
|
"you_can_now_log_in_sso": "",
|
||||||
"you_can_only_add_n_people_to_edit_a_project": "",
|
"you_can_only_add_n_people_to_edit_a_project": "",
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||||
|
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import getMeta from '@/utils/meta'
|
||||||
|
import { postJSON } from '@/infrastructure/fetch-json'
|
||||||
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
import useAsync from '@/shared/hooks/use-async'
|
||||||
|
import Notification from '@/shared/components/notification'
|
||||||
|
|
||||||
|
function SharingUpdatesRoot() {
|
||||||
|
const { isReady } = useWaitForI18n()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isLoading, isSuccess, isError, runAsync } = useAsync()
|
||||||
|
const projectId = getMeta('ol-project_id')
|
||||||
|
|
||||||
|
const joinProject = useCallback(() => {
|
||||||
|
runAsync(postJSON(`/project/${projectId}/sharing-updates/join`))
|
||||||
|
.then(() => {
|
||||||
|
location.assign(`/project/${projectId}`)
|
||||||
|
})
|
||||||
|
.catch(debugConsole.error)
|
||||||
|
}, [runAsync, projectId])
|
||||||
|
|
||||||
|
const viewProject = useCallback(() => {
|
||||||
|
runAsync(postJSON(`/project/${projectId}/sharing-updates/view`))
|
||||||
|
.then(() => {
|
||||||
|
location.assign(`/project/${projectId}`)
|
||||||
|
})
|
||||||
|
.catch(debugConsole.error)
|
||||||
|
}, [runAsync, projectId])
|
||||||
|
|
||||||
|
const leaveProject = useCallback(() => {
|
||||||
|
runAsync(postJSON(`/project/${projectId}/leave`))
|
||||||
|
.then(() => {
|
||||||
|
location.assign('/project')
|
||||||
|
})
|
||||||
|
.catch(debugConsole.error)
|
||||||
|
}, [runAsync, projectId])
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6 col-md-offset-3">
|
||||||
|
<div className="card sharing-updates">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<h1 className="sharing-updates-h1">
|
||||||
|
{t('updates_to_project_sharing')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row row-spaced">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="were_making_some_changes_to_project_sharing_this_means_you_will_be_visible"
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||||
|
<a
|
||||||
|
href="/blog/changes-to-project-sharing"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row row-spaced">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => joinProject()}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{t('ok_continue_to_project')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<div className="row row-spaced">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<Notification
|
||||||
|
type="error"
|
||||||
|
content={t('generic_something_went_wrong')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="row row-spaced">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<p>
|
||||||
|
<small>
|
||||||
|
<Trans
|
||||||
|
i18nKey="you_can_also_choose_to_view_anonymously_or_leave_the_project"
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line react/jsx-key
|
||||||
|
<button
|
||||||
|
className="btn btn-inline-link"
|
||||||
|
onClick={() => viewProject()}
|
||||||
|
disabled={isLoading || isSuccess}
|
||||||
|
/>,
|
||||||
|
// eslint-disable-next-line react/jsx-key
|
||||||
|
<button
|
||||||
|
className="btn btn-inline-link"
|
||||||
|
onClick={() => leaveProject()}
|
||||||
|
disabled={isLoading || isSuccess}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withErrorBoundary(
|
||||||
|
SharingUpdatesRoot,
|
||||||
|
GenericErrorBoundaryFallback
|
||||||
|
)
|
11
services/web/frontend/js/pages/sharing-updates.tsx
Normal file
11
services/web/frontend/js/pages/sharing-updates.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import './../utils/meta'
|
||||||
|
import './../utils/webpack-public-path'
|
||||||
|
import './../infrastructure/error-reporter'
|
||||||
|
import './../i18n'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import SharingUpdatesRoot from '../features/token-access/components/sharing-updates-root'
|
||||||
|
|
||||||
|
const element = document.getElementById('sharing-updates-page')
|
||||||
|
if (element) {
|
||||||
|
ReactDOM.render(<SharingUpdatesRoot />, element)
|
||||||
|
}
|
|
@ -27,3 +27,17 @@
|
||||||
font-size: var(--font-size-07);
|
font-size: var(--font-size-07);
|
||||||
line-height: var(--line-height-06);
|
line-height: var(--line-height-06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sharing-updates {
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
h1 {
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
// heading-sm
|
||||||
|
font-size: var(--font-size-05);
|
||||||
|
line-height: var(--line-height-04);
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size: var(--font-size-02);
|
||||||
|
line-height: var(--line-height-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1284,6 +1284,7 @@
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"official": "Official",
|
"official": "Official",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
|
"ok_continue_to_project": "OK, continue to project",
|
||||||
"ok_join_project": "OK, join project",
|
"ok_join_project": "OK, join project",
|
||||||
"on": "On",
|
"on": "On",
|
||||||
"on_free_plan_upgrade_to_access_features": "You are on the __appName__ Free plan. Upgrade to access these <0>Premium Features</0>",
|
"on_free_plan_upgrade_to_access_features": "You are on the __appName__ Free plan. Upgrade to access these <0>Premium Features</0>",
|
||||||
|
@ -2151,6 +2152,7 @@
|
||||||
"update_account_info": "Update Account Info",
|
"update_account_info": "Update Account Info",
|
||||||
"update_dropbox_settings": "Update Dropbox Settings",
|
"update_dropbox_settings": "Update Dropbox Settings",
|
||||||
"update_your_billing_details": "Update Your Billing Details",
|
"update_your_billing_details": "Update Your Billing Details",
|
||||||
|
"updates_to_project_sharing": "Updates to project sharing",
|
||||||
"updating": "Updating",
|
"updating": "Updating",
|
||||||
"updating_site": "Updating Site",
|
"updating_site": "Updating Site",
|
||||||
"upgrade": "Upgrade",
|
"upgrade": "Upgrade",
|
||||||
|
@ -2231,6 +2233,7 @@
|
||||||
"website_status": "Website status",
|
"website_status": "Website status",
|
||||||
"wed_love_you_to_stay": "We’d love you to stay",
|
"wed_love_you_to_stay": "We’d love you to stay",
|
||||||
"welcome_to_sl": "Welcome to __appName__",
|
"welcome_to_sl": "Welcome to __appName__",
|
||||||
|
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "We’re making some <0>changes to project sharing</0>. This means, as someone with edit access, your name and email address will be visible to the project owner and other editors.",
|
||||||
"were_performing_maintenance": "We’re performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
|
"were_performing_maintenance": "We’re performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
|
||||||
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "We’ve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected this project.",
|
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "We’ve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected this project.",
|
||||||
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "We’ve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected your project.",
|
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "We’ve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected your project.",
|
||||||
|
@ -2292,6 +2295,7 @@
|
||||||
"you_are_on_a_paid_plan_contact_support_to_find_out_more": "You’re on an __appName__ Paid plan. <0>Contact support</0> to find out more.",
|
"you_are_on_a_paid_plan_contact_support_to_find_out_more": "You’re on an __appName__ Paid plan. <0>Contact support</0> to find out more.",
|
||||||
"you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "You are on our <0>__planName__</0> plan as a <1>confirmed member</1> of <1>__institutionName__</1>",
|
"you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "You are on our <0>__planName__</0> plan as a <1>confirmed member</1> of <1>__institutionName__</1>",
|
||||||
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are on our <0>__planName__</0> plan as a <1>member</1> of the group subscription <1>__groupName__</1> administered by <1>__adminEmail__</1>",
|
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are on our <0>__planName__</0> plan as a <1>member</1> of the group subscription <1>__groupName__</1> administered by <1>__adminEmail__</1>",
|
||||||
|
"you_can_also_choose_to_view_anonymously_or_leave_the_project": "You can also choose to <0>view anonymously</0> (you will lose edit access) or <1>leave the project</1>.",
|
||||||
"you_can_now_enable_sso": "You can now enable SSO on your Group settings page.",
|
"you_can_now_enable_sso": "You can now enable SSO on your Group settings page.",
|
||||||
"you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ Professional features</0>.",
|
"you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ Professional features</0>.",
|
||||||
"you_can_only_add_n_people_to_edit_a_project": "You can only add X people to edit a project with you on your current plan. Upgrade to add more.",
|
"you_can_only_add_n_people_to_edit_a_project": "You can only add X people to edit a project with you on your current plan. Upgrade to add more.",
|
||||||
|
|
|
@ -5,6 +5,8 @@ const request = require('./helpers/request')
|
||||||
const settings = require('@overleaf/settings')
|
const settings = require('@overleaf/settings')
|
||||||
const { db } = require('../../../app/src/infrastructure/mongodb')
|
const { db } = require('../../../app/src/infrastructure/mongodb')
|
||||||
const expectErrorResponse = require('./helpers/expectErrorResponse')
|
const expectErrorResponse = require('./helpers/expectErrorResponse')
|
||||||
|
const SplitTestHandler = require('../../../app/src/Features/SplitTests/SplitTestHandler')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
|
||||||
const tryEditorAccess = (user, projectId, test, callback) =>
|
const tryEditorAccess = (user, projectId, test, callback) =>
|
||||||
async.series(
|
async.series(
|
||||||
|
@ -237,6 +239,47 @@ const tryFetchProjectTokens = (user, projectId, callback) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trySharingUpdatesPage = (user, projectId, test, callback) =>
|
||||||
|
user.request.get(
|
||||||
|
`/project/${projectId}/sharing-updates`,
|
||||||
|
(error, response, body) => {
|
||||||
|
if (error != null) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
test(response, body, projectId)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const trySharingUpdatesJoin = (user, projectId, test, callback) =>
|
||||||
|
user.request.post(
|
||||||
|
`/project/${projectId}/sharing-updates/join`,
|
||||||
|
(error, response, body) => {
|
||||||
|
if (error != null) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
test(response, body, projectId)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const trySharingUpdatesView = (user, projectId, test, callback) =>
|
||||||
|
user.request.post(
|
||||||
|
`/project/${projectId}/sharing-updates/view`,
|
||||||
|
(error, response, body) => {
|
||||||
|
if (error != null) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
test(response, body, projectId)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectRedirectToProject = (response, body, projectId) => {
|
||||||
|
expect(response.statusCode).to.equal(302)
|
||||||
|
expect(response.headers.location).to.equal(`/project/${projectId}`)
|
||||||
|
}
|
||||||
|
|
||||||
describe('TokenAccess', function () {
|
describe('TokenAccess', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
this.timeout(90000)
|
this.timeout(90000)
|
||||||
|
@ -1782,4 +1825,388 @@ describe('TokenAccess', function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('link sharing changes', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.getAssignmentForUser = sinon.stub(
|
||||||
|
SplitTestHandler.promises,
|
||||||
|
'getAssignmentForUser'
|
||||||
|
)
|
||||||
|
this.getAssignmentForUser.resolves({ variant: 'default' })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
this.getAssignmentForUser.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('not a member of the project', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.projectName = `token-link-sharing-changes${Math.random()}`
|
||||||
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.projectId = projectId
|
||||||
|
this.owner.makeTokenBased(this.projectId, err => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.owner.getProject(this.projectId, (err, project) => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.tokens = project.tokens
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should deny access', function (done) {
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesPage(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectErrorResponse.restricted.html,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesJoin(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectErrorResponse.restricted.html,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesView(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectErrorResponse.restricted.html,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('read and write token member of project', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.projectName = `token-link-sharing-changes${Math.random()}`
|
||||||
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.projectId = projectId
|
||||||
|
this.owner.makeTokenBased(this.projectId, err => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.owner.getProject(this.projectId, (err, project) => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.tokens = project.tokens
|
||||||
|
// must do token accept before split test enabled
|
||||||
|
// otherwise would be automatically added to named collaborators
|
||||||
|
tryReadAndWriteTokenAccept(
|
||||||
|
this.other1,
|
||||||
|
this.tokens.readAndWrite,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
||||||
|
expect(body.tokenAccessGranted).to.equal('readAndWrite')
|
||||||
|
},
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('link sharing changes test not active', function () {
|
||||||
|
it('should redirect to project, same permissions as before', function (done) {
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesPage(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectRedirectToProject,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesJoin(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectRedirectToProject,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesView(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectRedirectToProject,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryContentAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(body.privilegeLevel).to.equal('readAndWrite')
|
||||||
|
expect(body.isRestrictedUser).to.equal(false)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
|
expect(body.project.owner).to.have.all.keys(
|
||||||
|
'_id',
|
||||||
|
'email',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'privileges',
|
||||||
|
'signUpDate'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryEditorAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('link sharing changes test is active', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.getAssignmentForUser.resolves({ variant: 'active' })
|
||||||
|
})
|
||||||
|
it('should show sharing updates page', function (done) {
|
||||||
|
trySharingUpdatesPage(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow join to named collaborator', function (done) {
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesJoin(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(204)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryContentAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(body.privilegeLevel).to.equal('readAndWrite')
|
||||||
|
expect(body.isRestrictedUser).to.equal(false)
|
||||||
|
expect(body.isTokenMember).to.equal(false)
|
||||||
|
expect(body.isInvitedMember).to.equal(true) // now collaborator
|
||||||
|
expect(body.project.owner).to.have.all.keys(
|
||||||
|
'_id',
|
||||||
|
'email',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'privileges',
|
||||||
|
'signUpDate'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryEditorAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow move to anonymous viewer', function (done) {
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesView(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(204)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryContentAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(body.privilegeLevel).to.equal('readOnly')
|
||||||
|
expect(body.isRestrictedUser).to.equal(true)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
|
expect(body.project.owner).to.have.keys('_id')
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryEditorAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('read-only token member of project', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.projectName = `token-link-sharing-changes${Math.random()}`
|
||||||
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.projectId = projectId
|
||||||
|
this.owner.makeTokenBased(this.projectId, err => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.owner.getProject(this.projectId, (err, project) => {
|
||||||
|
if (err != null) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
this.tokens = project.tokens
|
||||||
|
tryReadOnlyTokenAccept(
|
||||||
|
this.other1,
|
||||||
|
this.tokens.readOnly,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
||||||
|
expect(body.tokenAccessGranted).to.equal('readOnly')
|
||||||
|
},
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('link sharing changes test is active', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.getAssignmentForUser.resolves({ variant: 'active' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect to project, same view permissions as before', function (done) {
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesPage(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectRedirectToProject,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesJoin(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectRedirectToProject,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
trySharingUpdatesView(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectRedirectToProject,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
// allow content access read-only
|
||||||
|
tryContentAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(body.privilegeLevel).to.equal('readOnly')
|
||||||
|
expect(body.isRestrictedUser).to.equal(true)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
|
expect(body.project.owner).to.have.keys('_id')
|
||||||
|
expect(body.project.owner).to.not.have.any.keys(
|
||||||
|
'email',
|
||||||
|
'first_name',
|
||||||
|
'last_name'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
tryEditorAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -231,6 +231,38 @@ describe('CollaboratorsGetter', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isUserInvitedReadWriteMemberOfProject', function () {
|
||||||
|
describe('when user is a read write member of the project', function () {
|
||||||
|
it('should return true', async function () {
|
||||||
|
const isMember =
|
||||||
|
await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
|
||||||
|
this.readWriteRef1
|
||||||
|
)
|
||||||
|
expect(isMember).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user is a read only member of the project', function () {
|
||||||
|
it('should return false', async function () {
|
||||||
|
const isMember =
|
||||||
|
await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
|
||||||
|
this.readOnlyRef1
|
||||||
|
)
|
||||||
|
expect(isMember).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user is not a member of the project', function () {
|
||||||
|
it('should return false', async function () {
|
||||||
|
const isMember =
|
||||||
|
await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
|
||||||
|
this.nonMemberRef
|
||||||
|
)
|
||||||
|
expect(isMember).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('getProjectsUserIsMemberOf', function () {
|
describe('getProjectsUserIsMemberOf', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.fields = 'mock fields'
|
this.fields = 'mock fields'
|
||||||
|
@ -365,6 +397,28 @@ describe('CollaboratorsGetter', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('userIsReadWriteTokenMember', function () {
|
||||||
|
it('should return true when the project is found', async function () {
|
||||||
|
this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
|
||||||
|
const isMember =
|
||||||
|
await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
|
||||||
|
this.userId,
|
||||||
|
this.project._id
|
||||||
|
)
|
||||||
|
expect(isMember).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when the project is not found', async function () {
|
||||||
|
this.ProjectMock.expects('findOne').chain('exec').resolves(null)
|
||||||
|
const isMember =
|
||||||
|
await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
|
||||||
|
this.userId,
|
||||||
|
this.project._id
|
||||||
|
)
|
||||||
|
expect(isMember).to.be.false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('getPublicShareTokens', function () {
|
describe('getPublicShareTokens', function () {
|
||||||
const userMock = new ObjectId()
|
const userMock = new ObjectId()
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,8 @@ describe('ProjectController', function () {
|
||||||
promises: {
|
promises: {
|
||||||
userIsTokenMember: sinon.stub().resolves(false),
|
userIsTokenMember: sinon.stub().resolves(false),
|
||||||
isUserInvitedMemberOfProject: sinon.stub().resolves(true),
|
isUserInvitedMemberOfProject: sinon.stub().resolves(true),
|
||||||
|
userIsReadWriteTokenMember: sinon.stub().resolves(false),
|
||||||
|
isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
this.ProjectEntityHandler = {}
|
this.ProjectEntityHandler = {}
|
||||||
|
@ -1002,6 +1004,53 @@ describe('ProjectController', function () {
|
||||||
this.ProjectController.loadEditor(this.req, this.res)
|
this.ProjectController.loadEditor(this.req, this.res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('link sharing changes active', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.SplitTestHandler.promises.getAssignmentForUser.resolves({
|
||||||
|
variant: 'active',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user is a read write token member (and not already a named editor)', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true)
|
||||||
|
this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect to the sharing-updates page', function (done) {
|
||||||
|
this.res.redirect = url => {
|
||||||
|
expect(url).to.equal(`/project/${this.project_id}/sharing-updates`)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
this.ProjectController.loadEditor(this.req, this.res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user is a read write token member but also a named editor', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true)
|
||||||
|
this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not redirect to the sharing-updates page, and should load the editor', function (done) {
|
||||||
|
this.res.render = (pageName, opts) => {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
this.ProjectController.loadEditor(this.req, this.res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('userProjectsJson', function () {
|
describe('userProjectsJson', function () {
|
||||||
|
|
|
@ -21,6 +21,7 @@ describe('TokenAccessController', function () {
|
||||||
}
|
}
|
||||||
this.req = new MockRequest()
|
this.req = new MockRequest()
|
||||||
this.res = new MockResponse()
|
this.res = new MockResponse()
|
||||||
|
this.next = sinon.stub().returns()
|
||||||
|
|
||||||
this.Settings = {}
|
this.Settings = {}
|
||||||
this.TokenAccessHandler = {
|
this.TokenAccessHandler = {
|
||||||
|
@ -40,6 +41,8 @@ describe('TokenAccessController', function () {
|
||||||
getProjectByToken: sinon.stub().resolves(this.project),
|
getProjectByToken: sinon.stub().resolves(this.project),
|
||||||
getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }),
|
getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }),
|
||||||
getV1DocInfo: sinon.stub(),
|
getV1DocInfo: sinon.stub(),
|
||||||
|
removeReadAndWriteUserFromProject: sinon.stub().resolves(),
|
||||||
|
moveReadAndWriteUserToReadOnly: sinon.stub().resolves(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,11 +80,26 @@ describe('TokenAccessController', function () {
|
||||||
this.CollaboratorsHandler = {
|
this.CollaboratorsHandler = {
|
||||||
promises: {
|
promises: {
|
||||||
addUserIdToProject: sinon.stub().resolves(),
|
addUserIdToProject: sinon.stub().resolves(),
|
||||||
|
setCollaboratorPrivilegeLevel: sinon.stub().resolves(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.CollaboratorsGetter = {
|
||||||
|
promises: {
|
||||||
|
userIsReadWriteTokenMember: sinon.stub().resolves(),
|
||||||
|
isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(),
|
||||||
|
isUserInvitedMemberOfProject: sinon.stub().resolves(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.EditorRealTimeController = { emitToRoom: sinon.stub() }
|
this.EditorRealTimeController = { emitToRoom: sinon.stub() }
|
||||||
|
|
||||||
|
this.ProjectGetter = {
|
||||||
|
promises: {
|
||||||
|
getProject: sinon.stub().resolves(this.project),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
|
this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
|
||||||
requires: {
|
requires: {
|
||||||
'@overleaf/settings': this.Settings,
|
'@overleaf/settings': this.Settings,
|
||||||
|
@ -96,7 +114,12 @@ describe('TokenAccessController', function () {
|
||||||
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
||||||
'../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
|
'../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
|
||||||
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
|
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
|
||||||
|
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
|
||||||
'../Editor/EditorRealTimeController': this.EditorRealTimeController,
|
'../Editor/EditorRealTimeController': this.EditorRealTimeController,
|
||||||
|
'../Project/ProjectGetter': this.ProjectGetter,
|
||||||
|
'../Helpers/AsyncFormHelper': (this.AsyncFormHelper = {
|
||||||
|
redirect: sinon.stub(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -721,4 +744,204 @@ describe('TokenAccessController', function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('ensureUserCanUseSharingUpdatesConsentPage', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.req.params = { Project_id: this.project._id }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when not in link sharing changes test', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done())
|
||||||
|
this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to the project/editor', function () {
|
||||||
|
expect(this.AsyncFormHelper.redirect).to.have.been.calledWith(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
`/project/${this.project._id}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when link sharing changes test active', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.SplitTestHandler.promises.getAssignmentForUser.resolves({
|
||||||
|
variant: 'active',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user is not an invited editor and is a read write token member', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
this.next.callsFake(() => done())
|
||||||
|
this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
this.next
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls next', function () {
|
||||||
|
expect(
|
||||||
|
this.CollaboratorsGetter.promises
|
||||||
|
.isUserInvitedReadWriteMemberOfProject
|
||||||
|
).to.have.been.calledWith(this.user._id, this.project._id)
|
||||||
|
expect(
|
||||||
|
this.CollaboratorsGetter.promises.userIsReadWriteTokenMember
|
||||||
|
).to.have.been.calledWith(this.user._id, this.project._id)
|
||||||
|
expect(this.next).to.have.been.calledOnce
|
||||||
|
expect(this.next.firstCall.args[0]).to.not.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user is already an invited editor', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done())
|
||||||
|
this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to the project/editor', function () {
|
||||||
|
expect(this.AsyncFormHelper.redirect).to.have.been.calledWith(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
`/project/${this.project._id}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when user not a read write token member', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done())
|
||||||
|
this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to the project/editor', function () {
|
||||||
|
expect(this.AsyncFormHelper.redirect).to.have.been.calledWith(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
`/project/${this.project._id}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moveReadWriteToCollaborators', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.req.params = { Project_id: this.project._id }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('read only invited viewer gaining edit access via link sharing', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
this.res.callback = done
|
||||||
|
this.TokenAccessController.moveReadWriteToCollaborators(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the privilege level to read and write for the invited viewer', function () {
|
||||||
|
expect(
|
||||||
|
this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel
|
||||||
|
).to.have.been.calledWith(
|
||||||
|
this.project._id,
|
||||||
|
this.user._id,
|
||||||
|
PrivilegeLevels.READ_AND_WRITE
|
||||||
|
)
|
||||||
|
expect(this.res.sendStatus).to.have.been.calledWith(204)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('previously joined token access user moving to named collaborator', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
this.res.callback = done
|
||||||
|
this.TokenAccessController.moveReadWriteToCollaborators(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the privilege level to read and write for the invited viewer', function () {
|
||||||
|
expect(
|
||||||
|
this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject
|
||||||
|
).to.have.been.calledWith(this.user._id, this.project._id)
|
||||||
|
expect(
|
||||||
|
this.CollaboratorsHandler.promises.addUserIdToProject
|
||||||
|
).to.have.been.calledWith(
|
||||||
|
this.project._id,
|
||||||
|
undefined,
|
||||||
|
this.user._id,
|
||||||
|
PrivilegeLevels.READ_AND_WRITE
|
||||||
|
)
|
||||||
|
expect(this.res.sendStatus).to.have.been.calledWith(204)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moveReadWriteToReadOnly', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.req.params = { Project_id: this.project._id }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('previously joined token access user moving to anonymous viewer', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.res.callback = done
|
||||||
|
this.TokenAccessController.moveReadWriteToReadOnly(
|
||||||
|
this.req,
|
||||||
|
this.res,
|
||||||
|
done
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes them from read write token access refs and adds them to read only token access refs', function () {
|
||||||
|
expect(
|
||||||
|
this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly
|
||||||
|
).to.have.been.calledWith(this.user._id, this.project._id)
|
||||||
|
expect(this.res.sendStatus).to.have.been.calledWith(204)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('writes a project audit log', function () {
|
||||||
|
expect(
|
||||||
|
this.ProjectAuditLogHandler.promises.addEntry
|
||||||
|
).to.have.been.calledWith(
|
||||||
|
this.project._id,
|
||||||
|
'readonly-via-sharing-updates',
|
||||||
|
this.user._id,
|
||||||
|
this.req.ip
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -200,6 +200,59 @@ describe('TokenAccessHandler', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('removeReadAndWriteUserFromProject', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.Project.updateOne = sinon
|
||||||
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves(null) })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call Project.updateOne', async function () {
|
||||||
|
await this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
|
||||||
|
this.userId,
|
||||||
|
this.projectId
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||||
|
expect(
|
||||||
|
this.Project.updateOne.calledWith({
|
||||||
|
_id: this.projectId,
|
||||||
|
})
|
||||||
|
).to.equal(true)
|
||||||
|
expect(this.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
|
||||||
|
'tokenAccessReadAndWrite_refs'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('moveReadAndWriteUserToReadOnly', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.Project.updateOne = sinon
|
||||||
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves(null) })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call Project.updateOne', async function () {
|
||||||
|
await this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly(
|
||||||
|
this.userId,
|
||||||
|
this.projectId
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||||
|
expect(
|
||||||
|
this.Project.updateOne.calledWith({
|
||||||
|
_id: this.projectId,
|
||||||
|
})
|
||||||
|
).to.equal(true)
|
||||||
|
expect(this.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
|
||||||
|
'tokenAccessReadAndWrite_refs'
|
||||||
|
)
|
||||||
|
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||||
|
'tokenAccessReadOnly_refs'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('grantSessionTokenAccess', function () {
|
describe('grantSessionTokenAccess', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.req = { session: {}, headers: {} }
|
this.req = { session: {}, headers: {} }
|
||||||
|
|
Loading…
Reference in a new issue