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,
|
||||
dangerouslyGetAllProjectsUserIsMemberOf,
|
||||
isUserInvitedMemberOfProject,
|
||||
isUserInvitedReadWriteMemberOfProject,
|
||||
getPublicShareTokens,
|
||||
userIsTokenMember,
|
||||
userIsReadWriteTokenMember,
|
||||
getAllInvitedMembers,
|
||||
},
|
||||
}
|
||||
|
@ -139,6 +141,23 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
|
|||
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) {
|
||||
const memberInfo = await Project.findOne(
|
||||
{
|
||||
|
@ -253,6 +272,21 @@ async function userIsTokenMember(userId, projectId) {
|
|||
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) {
|
||||
const members = await getMemberIdsWithPrivilegeLevels(projectId)
|
||||
return members.filter(m => m.source !== Sources.TOKEN).length
|
||||
|
|
|
@ -467,6 +467,36 @@ const _ProjectController = {
|
|||
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
|
||||
|
||||
if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) {
|
||||
|
|
|
@ -15,6 +15,9 @@ const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
|||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
|
||||
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const AsyncFormHelper = require('../Helpers/AsyncFormHelper')
|
||||
|
||||
const orderedPrivilegeLevels = [
|
||||
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 = {
|
||||
READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
|
||||
READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
|
||||
|
@ -440,4 +553,10 @@ module.exports = {
|
|||
tokenAccessPage: expressify(tokenAccessPage),
|
||||
grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
|
||||
grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
|
||||
ensureUserCanUseSharingUpdatesConsentPage: expressify(
|
||||
ensureUserCanUseSharingUpdatesConsentPage
|
||||
),
|
||||
sharingUpdatesConsent: expressify(sharingUpdatesConsent),
|
||||
moveReadWriteToCollaborators: expressify(moveReadWriteToCollaborators),
|
||||
moveReadWriteToReadOnly: expressify(moveReadWriteToReadOnly),
|
||||
}
|
||||
|
|
|
@ -187,6 +187,35 @@ const TokenAccessHandler = {
|
|||
).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) {
|
||||
if (!req.session) {
|
||||
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 MetaController = require('./Features/Metadata/MetaController')
|
||||
const TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
|
||||
const TokenAccessRouter = require('./Features/TokenAccess/TokenAccessRouter')
|
||||
const Features = require('./infrastructure/Features')
|
||||
const LinkedFilesRouter = require('./Features/LinkedFiles/LinkedFilesRouter')
|
||||
const TemplatesRouter = require('./Features/Templates/TemplatesRouter')
|
||||
|
@ -270,6 +271,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
TemplatesRouter.apply(webRouter)
|
||||
UserMembershipRouter.apply(webRouter)
|
||||
TokenAccessRouter.apply(webRouter)
|
||||
|
||||
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": "",
|
||||
"official": "",
|
||||
"ok": "",
|
||||
"ok_continue_to_project": "",
|
||||
"ok_join_project": "",
|
||||
"on": "",
|
||||
"on_free_plan_upgrade_to_access_features": "",
|
||||
|
@ -1529,6 +1530,7 @@
|
|||
"update_account_info": "",
|
||||
"update_dropbox_settings": "",
|
||||
"update_your_billing_details": "",
|
||||
"updates_to_project_sharing": "",
|
||||
"updating": "",
|
||||
"upgrade": "",
|
||||
"upgrade_cc_btn": "",
|
||||
|
@ -1594,6 +1596,7 @@
|
|||
"we_sent_new_code": "",
|
||||
"wed_love_you_to_stay": "",
|
||||
"welcome_to_sl": "",
|
||||
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
|
||||
"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_your_project": "",
|
||||
|
@ -1640,6 +1643,7 @@
|
|||
"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_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_log_in_sso": "",
|
||||
"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);
|
||||
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",
|
||||
"official": "Official",
|
||||
"ok": "OK",
|
||||
"ok_continue_to_project": "OK, continue to project",
|
||||
"ok_join_project": "OK, join project",
|
||||
"on": "On",
|
||||
"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_dropbox_settings": "Update Dropbox Settings",
|
||||
"update_your_billing_details": "Update Your Billing Details",
|
||||
"updates_to_project_sharing": "Updates to project sharing",
|
||||
"updating": "Updating",
|
||||
"updating_site": "Updating Site",
|
||||
"upgrade": "Upgrade",
|
||||
|
@ -2231,6 +2233,7 @@
|
|||
"website_status": "Website status",
|
||||
"wed_love_you_to_stay": "We’d love you to stay",
|
||||
"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.",
|
||||
"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.",
|
||||
|
@ -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_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_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_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.",
|
||||
|
|
|
@ -5,6 +5,8 @@ const request = require('./helpers/request')
|
|||
const settings = require('@overleaf/settings')
|
||||
const { db } = require('../../../app/src/infrastructure/mongodb')
|
||||
const expectErrorResponse = require('./helpers/expectErrorResponse')
|
||||
const SplitTestHandler = require('../../../app/src/Features/SplitTests/SplitTestHandler')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const tryEditorAccess = (user, projectId, test, callback) =>
|
||||
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 () {
|
||||
beforeEach(function (done) {
|
||||
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 () {
|
||||
beforeEach(function () {
|
||||
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 () {
|
||||
const userMock = new ObjectId()
|
||||
|
||||
|
|
|
@ -127,6 +127,8 @@ describe('ProjectController', function () {
|
|||
promises: {
|
||||
userIsTokenMember: sinon.stub().resolves(false),
|
||||
isUserInvitedMemberOfProject: sinon.stub().resolves(true),
|
||||
userIsReadWriteTokenMember: sinon.stub().resolves(false),
|
||||
isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(true),
|
||||
},
|
||||
}
|
||||
this.ProjectEntityHandler = {}
|
||||
|
@ -1002,6 +1004,53 @@ describe('ProjectController', function () {
|
|||
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 () {
|
||||
|
|
|
@ -21,6 +21,7 @@ describe('TokenAccessController', function () {
|
|||
}
|
||||
this.req = new MockRequest()
|
||||
this.res = new MockResponse()
|
||||
this.next = sinon.stub().returns()
|
||||
|
||||
this.Settings = {}
|
||||
this.TokenAccessHandler = {
|
||||
|
@ -40,6 +41,8 @@ describe('TokenAccessController', function () {
|
|||
getProjectByToken: sinon.stub().resolves(this.project),
|
||||
getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }),
|
||||
getV1DocInfo: sinon.stub(),
|
||||
removeReadAndWriteUserFromProject: sinon.stub().resolves(),
|
||||
moveReadAndWriteUserToReadOnly: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -77,11 +80,26 @@ describe('TokenAccessController', function () {
|
|||
this.CollaboratorsHandler = {
|
||||
promises: {
|
||||
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.ProjectGetter = {
|
||||
promises: {
|
||||
getProject: sinon.stub().resolves(this.project),
|
||||
},
|
||||
}
|
||||
|
||||
this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.Settings,
|
||||
|
@ -96,7 +114,12 @@ describe('TokenAccessController', function () {
|
|||
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
||||
'../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
|
||||
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
|
||||
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
|
||||
'../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 () {
|
||||
beforeEach(function () {
|
||||
this.req = { session: {}, headers: {} }
|
||||
|
|
Loading…
Reference in a new issue