mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-07 16:19:02 +02:00
aab598e044
* Create decapitalize.sh script * Remove `text-capitalize` classes, rely on translations instead * `Account Linking` -> `Account linking` * `Account Settings` -> `Account settings` * `Add Affiliation` -> `Add affiliation` * `Add Email` -> `Add email` * `Add Files` -> `Add files` * `Add to Dictionary` -> `Add to dictionary` * `All Projects` -> `All projects` * `All Templates` -> `All templates` * `Archive Projects` -> `Archive projects` * `Archived Projects` -> `Archived projects` * `Auto Compile` -> `Auto compile` * `Back to Subscription` -> `Back to subscription` * `Blank Project` -> `Blank project` * `Change Password` -> `Change password` * `Change Project Owner` -> `Change project owner` * `Clear Sessions` -> `Clear sessions` * `Company Name` -> `Company name` * `Compile Error Handling` -> `Compile error handling` * `Compile Mode` -> `Compile mode` * `Compromised Password` -> `Compromised password` * `Confirm Affiliation` -> `Confirm affiliation` * `Confirm Email` -> `Confirm email` * `Connected Users` -> `Connected users` * `Contact Sales` -> `Contact sales` * `Contact Support` -> `Contact support` * `Contact Us` -> `Contact us` * `Copy Project` -> `Copy project` * `Delete Account` -> `Delete account` * `Emails and Affiliations` -> `Emails and affiliations` * `Git Integration` -> `Git integration` * `Group Settings` -> `Group settings` * `Link Accounts` -> `Link accounts` * `Make Primary` -> `Make primary` * `Mendeley Integration` -> `Mendeley integration` * `Papers Integration` -> `Papers integration` * `Project Synchronisation` -> `Project synchronisation` * `Sessions Cleared` -> `Sessions cleared` * `Stop Compilation` -> `Stop compilation` * `Update Account Info` -> `Update account info` * `the Sales team` -> `the sales team` * `your Group settings` -> `your group settings` * `Zotero Integration` -> `Zotero integration` * Update decapitalize.sh * Decapitalize some translations * `Example Project` -> `Example project` * `New Project` -> `New project` * `New Tag` -> `New tag` * `Trashed Projects` -> `Trashed projects` * `Upload Project` -> `Upload project` * `Your Projects` -> `Your projects` * Revert "Create decapitalize.sh script" This reverts commit 8c79f367096c206c704c7c01e3572a18f3961d5e. * Revert changes to stories * Fix tests * `Contact us of` -> `Contact us if` * Make `Contact us` bold in tex files * `sales team` -> `Sales team` * `Link accounts and Add email` -> `Link accounts and add email` * `Make Private` -> `Make private` * `contact support` -> `contact Support` * Make `Make primary` tests case sensitive * Use `add_email` translation string * Revert changes to non-english locales * Remove redundant `Account settings` translation * `New project Name` -> `New project name` GitOrigin-RevId: 675c46f96ddbf3d259a8d723fed62aa4a7ed40b7
603 lines
17 KiB
JavaScript
603 lines
17 KiB
JavaScript
import AuthenticationController from '../Authentication/AuthenticationController.js'
|
|
import SessionManager from '../Authentication/SessionManager.js'
|
|
import TokenAccessHandler from './TokenAccessHandler.js'
|
|
import Errors from '../Errors/Errors.js'
|
|
import logger from '@overleaf/logger'
|
|
import OError from '@overleaf/o-error'
|
|
import { expressify } from '@overleaf/promise-utils'
|
|
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
|
|
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
|
|
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
|
|
import CollaboratorsInviteHandler from '../Collaborators/CollaboratorsInviteHandler.mjs'
|
|
import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.js'
|
|
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
|
|
import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.js'
|
|
import ProjectGetter from '../Project/ProjectGetter.js'
|
|
import AsyncFormHelper from '../Helpers/AsyncFormHelper.js'
|
|
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
|
|
import { canRedirectToAdminDomain } from '../Helpers/AdminAuthorizationHelper.js'
|
|
import { getSafeAdminDomainRedirect } from '../Helpers/UrlHelper.js'
|
|
import UserGetter from '../User/UserGetter.js'
|
|
import Settings from '@overleaf/settings'
|
|
import LimitationsManager from '../Subscription/LimitationsManager.js'
|
|
|
|
const orderedPrivilegeLevels = [
|
|
PrivilegeLevels.NONE,
|
|
PrivilegeLevels.READ_ONLY,
|
|
PrivilegeLevels.REVIEW,
|
|
PrivilegeLevels.READ_AND_WRITE,
|
|
PrivilegeLevels.OWNER,
|
|
]
|
|
|
|
async function _userAlreadyHasHigherPrivilege(userId, projectId, tokenType) {
|
|
if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
|
|
throw new Error('bad token type')
|
|
}
|
|
if (!userId) {
|
|
return false
|
|
}
|
|
const privilegeLevel =
|
|
await AuthorizationManager.promises.getPrivilegeLevelForProject(
|
|
userId,
|
|
projectId
|
|
)
|
|
return (
|
|
orderedPrivilegeLevels.indexOf(privilegeLevel) >=
|
|
orderedPrivilegeLevels.indexOf(tokenType)
|
|
)
|
|
}
|
|
|
|
const makePostUrl = token => {
|
|
if (TokenAccessHandler.isReadAndWriteToken(token)) {
|
|
return `/${token}/grant`
|
|
} else if (TokenAccessHandler.isReadOnlyToken(token)) {
|
|
return `/read/${token}/grant`
|
|
} else {
|
|
throw new Error('invalid token type')
|
|
}
|
|
}
|
|
|
|
async function _handleV1Project(token, userId) {
|
|
if (!userId) {
|
|
return { v1Import: { status: 'mustLogin' } }
|
|
} else {
|
|
const docInfo = await TokenAccessHandler.promises.getV1DocInfo(
|
|
token,
|
|
userId
|
|
)
|
|
// This should not happen anymore, but it does show
|
|
// a nice "contact Support" message, so it can stay
|
|
if (!docInfo) {
|
|
return { v1Import: { status: 'cannotImport' } }
|
|
}
|
|
if (!docInfo.exists) {
|
|
return null
|
|
}
|
|
if (docInfo.exported) {
|
|
return null
|
|
}
|
|
return {
|
|
v1Import: {
|
|
status: 'canDownloadZip',
|
|
projectId: token,
|
|
hasOwner: docInfo.has_owner,
|
|
name: docInfo.name || 'Untitled',
|
|
brandInfo: docInfo.brand_info,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
async function _isOverleafStaff(userId) {
|
|
const emails = await UserGetter.promises.getUserConfirmedEmails(userId)
|
|
const adminDomains = Settings.adminDomains ?? []
|
|
return emails.some(email =>
|
|
adminDomains.some(adminDomain => email.email.endsWith(`@${adminDomain}`))
|
|
)
|
|
}
|
|
|
|
async function tokenAccessPage(req, res, next) {
|
|
const { token } = req.params
|
|
if (!TokenAccessHandler.isValidToken(token)) {
|
|
return next(new Errors.NotFoundError())
|
|
}
|
|
|
|
try {
|
|
if (TokenAccessHandler.isReadOnlyToken(token)) {
|
|
const docPublishedInfo =
|
|
await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
|
|
if (docPublishedInfo.allow === false) {
|
|
return res.redirect(302, docPublishedInfo.published_path)
|
|
}
|
|
}
|
|
|
|
res.render('project/token/access-react', {
|
|
postUrl: makePostUrl(token),
|
|
})
|
|
} catch (err) {
|
|
return next(
|
|
OError.tag(err, 'error while rendering token access page', { token })
|
|
)
|
|
}
|
|
}
|
|
|
|
async function checkAndGetProjectOrResponseAction(
|
|
tokenType,
|
|
token,
|
|
userId,
|
|
tokenHashPrefix,
|
|
req,
|
|
res,
|
|
next
|
|
) {
|
|
const isAnonymousUser = !userId
|
|
if (
|
|
isAnonymousUser &&
|
|
tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
|
|
!TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
|
|
) {
|
|
logger.warn('[TokenAccess] deny anonymous read-and-write token access')
|
|
|
|
let projectUrlWithToken = TokenAccessHandler.makeTokenUrl(token)
|
|
|
|
if (tokenHashPrefix && tokenHashPrefix.startsWith('#')) {
|
|
projectUrlWithToken += `${tokenHashPrefix}`
|
|
}
|
|
|
|
AuthenticationController.setRedirectInSession(req, projectUrlWithToken)
|
|
return [
|
|
null,
|
|
() => {
|
|
res.json({
|
|
redirect: '/restricted',
|
|
anonWriteAccessDenied: true,
|
|
})
|
|
},
|
|
{ action: 'denied anonymous read-and-write token access' },
|
|
]
|
|
}
|
|
|
|
// Try to get the project, and/or an alternative action to take.
|
|
// Returns a tuple of [project, action]
|
|
const project = await TokenAccessHandler.promises.getProjectByToken(
|
|
tokenType,
|
|
token
|
|
)
|
|
if (!project) {
|
|
if (Settings.overleaf) {
|
|
const v1ImportData = await _handleV1Project(token, userId)
|
|
return [
|
|
null,
|
|
() => {
|
|
if (v1ImportData) {
|
|
res.json(v1ImportData)
|
|
} else {
|
|
res.sendStatus(404)
|
|
}
|
|
},
|
|
{ action: v1ImportData ? 'import v1' : '404' },
|
|
]
|
|
} else {
|
|
return [null, null, { action: '404' }]
|
|
}
|
|
}
|
|
|
|
const projectId = project._id
|
|
|
|
const tokenAccessEnabled =
|
|
TokenAccessHandler.tokenAccessEnabledForProject(project)
|
|
if (isAnonymousUser && tokenAccessEnabled) {
|
|
if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
|
|
if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) {
|
|
logger.debug({ projectId }, 'granting read-write anonymous access')
|
|
TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
|
|
return [
|
|
null,
|
|
() => {
|
|
res.json({
|
|
redirect: `/project/${projectId}`,
|
|
grantAnonymousAccess: tokenType,
|
|
})
|
|
},
|
|
{ projectId, action: 'granting read-write anonymous access' },
|
|
]
|
|
} else {
|
|
// anonymous read-and-write token access should have been denied already
|
|
throw new Error(
|
|
'unreachable: anonymous read-and-write token access bug'
|
|
)
|
|
}
|
|
} else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
|
|
logger.debug({ projectId }, 'granting read-only anonymous access')
|
|
TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
|
|
return [
|
|
null,
|
|
() => {
|
|
res.json({
|
|
redirect: `/project/${projectId}`,
|
|
grantAnonymousAccess: tokenType,
|
|
})
|
|
},
|
|
{ projectId, action: 'granting read-only anonymous access' },
|
|
]
|
|
} else {
|
|
throw new Error('unreachable')
|
|
}
|
|
}
|
|
const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
|
|
userId,
|
|
projectId,
|
|
tokenType
|
|
)
|
|
if (userHasPrivilege) {
|
|
return [
|
|
null,
|
|
() => {
|
|
res.json({ redirect: `/project/${project._id}`, higherAccess: true })
|
|
},
|
|
{ projectId, action: 'user already has higher or same privilege' },
|
|
]
|
|
}
|
|
|
|
// Handle admin redirect
|
|
// If the project owner is an internal staff (using @overleaf.com email),
|
|
// the admin will join the project "for real".
|
|
// If the project owner is a external user
|
|
// the admin will be redirect to admin domain to view the project.
|
|
if (canRedirectToAdminDomain(SessionManager.getSessionUser(req.session))) {
|
|
const isProjectOwnerOverleafStaff = await _isOverleafStaff(
|
|
project.owner_ref
|
|
)
|
|
if (isProjectOwnerOverleafStaff) {
|
|
logger.warn(
|
|
{ projectId, userId },
|
|
'letting admin user join staff project'
|
|
)
|
|
} else {
|
|
let projectUrlWithToken = TokenAccessHandler.makeTokenUrl(token)
|
|
if (tokenHashPrefix && tokenHashPrefix.startsWith('#')) {
|
|
projectUrlWithToken += `${tokenHashPrefix}`
|
|
}
|
|
return [
|
|
null,
|
|
() =>
|
|
res.json({
|
|
redirect: getSafeAdminDomainRedirect(projectUrlWithToken),
|
|
}),
|
|
{ projectId, action: 'redirect admin user to admin domain' },
|
|
]
|
|
}
|
|
}
|
|
|
|
if (!tokenAccessEnabled) {
|
|
return [
|
|
null,
|
|
() => {
|
|
next(new Errors.NotFoundError())
|
|
},
|
|
{ projectId, action: 'token access not enabled' },
|
|
]
|
|
}
|
|
return [project, null, { projectId, action: 'continue' }]
|
|
}
|
|
|
|
async function grantTokenAccessReadAndWrite(req, res, next) {
|
|
const { token } = req.params
|
|
const { confirmedByUser, tokenHashPrefix } = req.body
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
if (!TokenAccessHandler.isReadAndWriteToken(token)) {
|
|
return res.sendStatus(400)
|
|
}
|
|
const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
|
|
|
|
try {
|
|
const [project, action, logData] = await checkAndGetProjectOrResponseAction(
|
|
tokenType,
|
|
token,
|
|
userId,
|
|
tokenHashPrefix,
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
|
|
TokenAccessHandler.checkTokenHashPrefix(
|
|
token,
|
|
tokenHashPrefix,
|
|
tokenType,
|
|
userId,
|
|
logData
|
|
)
|
|
|
|
if (action) {
|
|
return action()
|
|
}
|
|
if (!project) {
|
|
return next(new Errors.NotFoundError())
|
|
}
|
|
|
|
if (!confirmedByUser) {
|
|
return res.json({
|
|
requireAccept: {
|
|
projectName: project.name,
|
|
},
|
|
})
|
|
}
|
|
|
|
const pendingEditor =
|
|
!(await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
|
|
project._id
|
|
))
|
|
await ProjectAuditLogHandler.promises.addEntry(
|
|
project._id,
|
|
'accept-via-link-sharing',
|
|
userId,
|
|
req.ip,
|
|
{
|
|
privileges: pendingEditor ? 'readOnly' : 'readAndWrite',
|
|
...(pendingEditor && { pendingEditor: true }),
|
|
}
|
|
)
|
|
AnalyticsManager.recordEventForUserInBackground(userId, 'project-joined', {
|
|
role: pendingEditor
|
|
? PrivilegeLevels.READ_ONLY
|
|
: PrivilegeLevels.READ_AND_WRITE,
|
|
ownerId: project.owner_ref.toString(),
|
|
source: 'link-sharing',
|
|
mode: pendingEditor ? 'view' : 'edit',
|
|
projectId: project._id.toString(),
|
|
...(pendingEditor && { pendingEditor: true }),
|
|
})
|
|
await CollaboratorsHandler.promises.addUserIdToProject(
|
|
project._id,
|
|
undefined,
|
|
userId,
|
|
pendingEditor
|
|
? PrivilegeLevels.READ_ONLY
|
|
: PrivilegeLevels.READ_AND_WRITE,
|
|
{ pendingEditor }
|
|
)
|
|
|
|
// remove pending invite and notification
|
|
const userEmails = await UserGetter.promises.getUserConfirmedEmails(userId)
|
|
await CollaboratorsInviteHandler.promises.revokeInviteForUser(
|
|
project._id,
|
|
userEmails
|
|
)
|
|
// Should be a noop if the user is already a member,
|
|
// and would redirect transparently into the project.
|
|
EditorRealTimeController.emitToRoom(
|
|
project._id,
|
|
'project:membership:changed',
|
|
{ members: true, invites: true }
|
|
)
|
|
|
|
return res.json({
|
|
redirect: `/project/${project._id}`,
|
|
})
|
|
} catch (err) {
|
|
return next(
|
|
OError.tag(
|
|
err,
|
|
'error while trying to grant read-and-write token access',
|
|
{ token }
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
async function grantTokenAccessReadOnly(req, res, next) {
|
|
const { token } = req.params
|
|
const { confirmedByUser, tokenHashPrefix } = req.body
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
if (!TokenAccessHandler.isReadOnlyToken(token)) {
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_ONLY
|
|
|
|
const docPublishedInfo =
|
|
await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
|
|
if (docPublishedInfo.allow === false) {
|
|
return res.json({ redirect: docPublishedInfo.published_path })
|
|
}
|
|
try {
|
|
const [project, action, logData] = await checkAndGetProjectOrResponseAction(
|
|
tokenType,
|
|
token,
|
|
userId,
|
|
tokenHashPrefix,
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
|
|
TokenAccessHandler.checkTokenHashPrefix(
|
|
token,
|
|
tokenHashPrefix,
|
|
tokenType,
|
|
userId,
|
|
logData
|
|
)
|
|
|
|
if (action) {
|
|
return action()
|
|
}
|
|
if (!project) {
|
|
return next(new Errors.NotFoundError())
|
|
}
|
|
|
|
if (!confirmedByUser) {
|
|
return res.json({
|
|
requireAccept: {
|
|
projectName: project.name,
|
|
},
|
|
})
|
|
}
|
|
|
|
if (!project.tokenAccessReadOnly_refs.some(id => id.equals(userId))) {
|
|
await ProjectAuditLogHandler.promises.addEntry(
|
|
project._id,
|
|
'join-via-token',
|
|
userId,
|
|
req.ip,
|
|
{ privileges: 'readOnly' }
|
|
)
|
|
}
|
|
|
|
await TokenAccessHandler.promises.addReadOnlyUserToProject(
|
|
userId,
|
|
project._id,
|
|
project.owner_ref
|
|
)
|
|
|
|
return res.json({
|
|
redirect: `/project/${project._id}`,
|
|
tokenAccessGranted: tokenType,
|
|
})
|
|
} catch (err) {
|
|
return next(
|
|
OError.tag(err, 'error while trying to grant read-only token access', {
|
|
token,
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
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 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
|
|
AnalyticsManager.recordEventForSession(req.session, 'notification-prompt', {
|
|
page: req.path,
|
|
name: 'link-sharing-collaborator',
|
|
})
|
|
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 project = await ProjectGetter.promises.getProject(projectId, {
|
|
owner_ref: 1,
|
|
})
|
|
const isInvitedMember =
|
|
await CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
|
|
userId,
|
|
projectId
|
|
)
|
|
const pendingEditor =
|
|
!(await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
|
|
project._id
|
|
))
|
|
await ProjectAuditLogHandler.promises.addEntry(
|
|
projectId,
|
|
'accept-via-link-sharing',
|
|
userId,
|
|
req.ip,
|
|
{
|
|
privileges: pendingEditor
|
|
? PrivilegeLevels.READ_ONLY
|
|
: PrivilegeLevels.READ_AND_WRITE,
|
|
tokenMember: true,
|
|
invitedMember: isInvitedMember,
|
|
...(pendingEditor && { pendingEditor: true }),
|
|
}
|
|
)
|
|
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,
|
|
pendingEditor
|
|
? PrivilegeLevels.READ_ONLY
|
|
: PrivilegeLevels.READ_AND_WRITE,
|
|
{ pendingEditor }
|
|
)
|
|
} else {
|
|
// Normal case, not invited, joining via link sharing
|
|
await TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
|
|
userId,
|
|
projectId
|
|
)
|
|
await CollaboratorsHandler.promises.addUserIdToProject(
|
|
projectId,
|
|
undefined,
|
|
userId,
|
|
pendingEditor
|
|
? PrivilegeLevels.READ_ONLY
|
|
: PrivilegeLevels.READ_AND_WRITE,
|
|
{ pendingEditor }
|
|
)
|
|
}
|
|
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)
|
|
}
|
|
|
|
export default {
|
|
READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
|
|
READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
|
|
|
|
tokenAccessPage: expressify(tokenAccessPage),
|
|
grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
|
|
grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
|
|
ensureUserCanUseSharingUpdatesConsentPage: expressify(
|
|
ensureUserCanUseSharingUpdatesConsentPage
|
|
),
|
|
sharingUpdatesConsent: expressify(sharingUpdatesConsent),
|
|
moveReadWriteToCollaborators: expressify(moveReadWriteToCollaborators),
|
|
moveReadWriteToReadOnly: expressify(moveReadWriteToReadOnly),
|
|
}
|