diff --git a/services/web/Makefile b/services/web/Makefile
index 370695fa4b..e59c7a4865 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -132,6 +132,9 @@ compile_full:
$(MAKE) compile_modules_full
$(MAKE) compile # ide.js, main.js, share.js, and anything missed
+compile_css_full:
+ $(MAKE) css_full
+
compile_modules: $(MODULE_MAKEFILES)
@set -e; \
for dir in $(MODULE_DIRS); \
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 8d95c9b41b..18948fa0df 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -203,7 +203,7 @@ module.exports = AuthenticationController =
return next()
else
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
- AuthenticationController._setRedirectInSession(req)
+ AuthenticationController.setRedirectInSession(req)
return res.redirect "/login"
httpAuth: basicAuth (user, pass)->
@@ -212,6 +212,16 @@ module.exports = AuthenticationController =
logger.err user:user, pass:pass, "invalid login details"
return isValid
+ setRedirectInSession: (req, value) ->
+ if !value?
+ value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
+ if (
+ req.session? &&
+ !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
+ !/^.*\.(png|jpeg|svg)$/.test(value)
+ )
+ req.session.postLoginRedirect = value
+
_redirectToLoginOrRegisterPage: (req, res)->
if (req.query.zipUrl? or req.query.project_name? or req.path == '/user/subscription/new')
return AuthenticationController._redirectToRegisterPage(req, res)
@@ -220,14 +230,14 @@ module.exports = AuthenticationController =
_redirectToLoginPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to login page"
- AuthenticationController._setRedirectInSession(req)
+ AuthenticationController.setRedirectInSession(req)
url = "/login?#{querystring.stringify(req.query)}"
res.redirect url
Metrics.inc "security.login-redirect"
_redirectToRegisterPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to register page"
- AuthenticationController._setRedirectInSession(req)
+ AuthenticationController.setRedirectInSession(req)
url = "/register?#{querystring.stringify(req.query)}"
res.redirect url
Metrics.inc "security.login-redirect"
@@ -245,16 +255,6 @@ module.exports = AuthenticationController =
Metrics.inc "user.login.failed"
callback()
- _setRedirectInSession: (req, value) ->
- if !value?
- value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
- if (
- req.session? &&
- !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
- !/^.*\.(png|jpeg|svg)$/.test(value)
- )
- req.session.postLoginRedirect = value
-
_getRedirectFromSession: (req) ->
return req?.session?.postLoginRedirect || null
diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
index 10daa41f75..54d54475f3 100644
--- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
+++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
@@ -117,5 +117,5 @@ module.exports = AuthorizationMiddlewear =
logger.log {from: from}, "redirecting to login"
redirect_to = "/login"
if from?
- AuthenticationController._setRedirectInSession(req, from)
+ AuthenticationController.setRedirectInSession(req, from)
res.redirect redirect_to
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index 618fc7c0b4..b3b8e9a6d4 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -270,7 +270,7 @@ module.exports = ProjectController =
project: (cb)->
ProjectGetter.getProject(
project_id,
- { name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, brandVariationId: 1, 'overleaf.history.display': 1 },
+ { name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, brandVariationId: 1, overleaf: 1 },
cb
)
user: (cb)->
@@ -323,6 +323,9 @@ module.exports = ProjectController =
if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
allowedFreeTrial = !!subscription.freeTrial.allowed || true
+ showGitBridge =
+ user.betaProgram && !project.overleaf?.id? # don't support v1 projects yet
+
logger.log project_id:project_id, "rendering editor page"
res.render 'project/editor',
title: project.name
@@ -370,7 +373,7 @@ module.exports = ProjectController =
brandVariation: brandVariation
allowedImageNames: Settings.allowedImageNames || []
gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl
- showGitBridge: req.query?.gitbridge == 'true' || user.isAdmin
+ showGitBridge: showGitBridge
timer.done()
_buildProjectList: (allProjects, v1Projects = [])->
diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
index dedc92e273..240980907a 100644
--- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
@@ -74,13 +74,7 @@ module.exports = ProjectDetailsHandler =
if arguments.length is 3 && typeof suffixes is 'function' # make suffixes an optional argument
callback = suffixes
suffixes = []
- timestamp = new Date().toISOString().replace(/T(\d+):(\d+):(\d+)\..*/,' $1$2$3') # strip out unwanted characters
- ProjectDetailsHandler.ensureProjectNameIsUnique user_id, name, suffixes.concat(" (#{timestamp})"), callback
-
- _addSuffixToProjectName: (name, suffix = '') ->
- # append the suffix and truncate the project title if needed
- truncatedLength = ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH - suffix.length
- return name.substr(0, truncatedLength) + suffix
+ ProjectDetailsHandler.ensureProjectNameIsUnique user_id, name, suffixes, callback
# FIXME: we should put a lock around this to make it completely safe, but we would need to do that at
# the point of project creation, rather than just checking the name at the start of the import.
@@ -106,8 +100,12 @@ module.exports = ProjectDetailsHandler =
candidateName = ProjectDetailsHandler._addSuffixToProjectName(name, suffix)
if isUnique(candidateName)
return callback(null, candidateName, true)
- # we couldn't make the name unique, something is wrong
- return callback new Errors.InvalidNameError("Project name could not be made unique")
+ # if there are no (more) suffixes, use a numeric one
+ uniqueName = ProjectDetailsHandler._addNumericSuffixToProjectName(name, allProjectNames)
+ if uniqueName?
+ callback(null, uniqueName, true)
+ else
+ callback(new Error("Failed to generate a unique name for file: #{name}"))
fixProjectName: (name) ->
if name == "" || !name
@@ -156,3 +154,30 @@ module.exports = ProjectDetailsHandler =
Project.update {_id: project_id}, {$set: {tokens: tokens}}, (err) ->
return callback(err) if err?
callback(null, tokens)
+
+ _addSuffixToProjectName: (name, suffix = '') ->
+ # append the suffix and truncate the project title if needed
+ truncatedLength = ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH - suffix.length
+ return name.substr(0, truncatedLength) + suffix
+
+ _addNumericSuffixToProjectName: (name, allProjectNames) ->
+ NUMERIC_SUFFIX_MATCH = / \((\d+)\)$/
+ suffixedName = (basename, number) ->
+ suffix = " (#{number})"
+ return basename.substr(0, ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH - suffix.length) + suffix
+
+ match = name.match(NUMERIC_SUFFIX_MATCH)
+ basename = name
+ n = 1
+ last = allProjectNames.size + n
+
+ if match?
+ basename = name.replace(NUMERIC_SUFFIX_MATCH, '')
+ n = parseInt(match[1])
+
+ while n <= last
+ candidate = suffixedName(basename, n)
+ return candidate unless allProjectNames.has(candidate)
+ n += 1
+
+ return null
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 3a4280e4ec..bea41ecb6e 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -12,6 +12,7 @@ UserGetter = require "../User/UserGetter"
FeaturesUpdater = require './FeaturesUpdater'
planFeatures = require './planFeatures'
GroupPlansData = require './GroupPlansData'
+V1SubscriptionManager = require "./V1SubscriptionManager"
module.exports = SubscriptionController =
@@ -97,7 +98,8 @@ module.exports = SubscriptionController =
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
- v1Subscriptions
+ v1Subscriptions,
+ v1SubscriptionStatus
} = results
logger.log {
user,
@@ -106,7 +108,8 @@ module.exports = SubscriptionController =
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
- v1Subscriptions
+ v1Subscriptions,
+ v1SubscriptionStatus
}, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
data = {
@@ -118,7 +121,8 @@ module.exports = SubscriptionController =
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
- v1Subscriptions
+ v1Subscriptions,
+ v1SubscriptionStatus
}
res.render "subscriptions/dashboard", data
@@ -158,6 +162,15 @@ module.exports = SubscriptionController =
return next(err)
res.redirect "/user/subscription"
+ cancelV1Subscription: (req, res, next) ->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ logger.log {user_id}, "canceling v1 subscription"
+ V1SubscriptionManager.cancelV1Subscription user_id, (err)->
+ if err?
+ logger.err err:err, user_id:user_id, "something went wrong canceling v1 subscription"
+ return next(err)
+ res.redirect "/user/subscription"
+
updateSubscription: (req, res, next)->
_origin = req?.query?.origin || null
user = AuthenticationController.getSessionUser(req)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
index d81b5f3f59..c4155b58cb 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
@@ -40,6 +40,8 @@ module.exports =
webRouter.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription
webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription
+ webRouter.post '/user/subscription/v1/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelV1Subscription
+
webRouter.put '/user/subscription/extend', AuthenticationController.requireLogin(), SubscriptionController.extendTrial
webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
index f4cdfcf86d..22e2c00987 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
@@ -29,13 +29,7 @@ module.exports = SubscriptionUpdater =
@addUsersToGroup(subscriptionId, [userId], callback)
addUsersToGroup: (subscriptionId, memberIds, callback)->
- logger.log subscriptionId: subscriptionId, memberIds: memberIds, "adding members into mongo subscription"
- searchOps =
- _id: subscriptionId
- insertOperation =
- { $addToSet: { member_ids: { $each: memberIds } } }
-
- Subscription.findAndModify searchOps, insertOperation, (err, subscription) ->
+ @addUsersToGroupWithoutFeaturesRefresh subscriptionId, memberIds, (err) ->
return callback(err) if err?
# Only apply features updates to users, not user stubs
@@ -45,6 +39,14 @@ module.exports = SubscriptionUpdater =
userIds = users.map (u) -> u._id.toString()
async.map userIds, FeaturesUpdater.refreshFeatures, callback
+ addUsersToGroupWithoutFeaturesRefresh: (subscriptionId, memberIds, callback)->
+ logger.log subscriptionId: subscriptionId, memberIds: memberIds, "adding members into mongo subscription"
+ searchOps =
+ _id: subscriptionId
+ insertOperation =
+ { $addToSet: { member_ids: { $each: memberIds } } }
+
+ Subscription.findAndModify searchOps, insertOperation, callback
removeUserFromGroup: (subscriptionId, user_id, callback)->
searchOps =
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
index deebb04aa5..3f65cc5cf5 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
@@ -51,6 +51,10 @@ module.exports =
return cb(error) if error?
# Only return one argument to async.auto, otherwise it returns an array
cb(null, subscriptions)
+ v1SubscriptionStatus: (cb) ->
+ V1SubscriptionManager.getSubscriptionStatusFromV1 user._id, (error, status, v1Id) ->
+ return cb(error) if error?
+ cb(null, status)
}, (err, results) ->
return callback(err) if err?
{
@@ -60,6 +64,7 @@ module.exports =
confirmedMemberInstitutions,
managedInstitutions,
v1Subscriptions,
+ v1SubscriptionStatus,
recurlySubscription,
plan
} = results
@@ -68,6 +73,7 @@ module.exports =
confirmedMemberInstitutions ?= []
managedInstitutions ?= []
v1Subscriptions ?= {}
+ v1SubscriptionStatus ?= {}
if personalSubscription?.toObject?
@@ -97,7 +103,8 @@ module.exports =
memberGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
- v1Subscriptions
+ v1Subscriptions,
+ v1SubscriptionStatus
}
buildViewModel : ->
diff --git a/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee b/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee
index 2d25c8b3e5..0e63259734 100644
--- a/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee
+++ b/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee
@@ -39,6 +39,18 @@ module.exports = V1SubscriptionManager =
url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/subscriptions"
}, callback
+ getSubscriptionStatusFromV1: (userId, callback=(err, status) ->) ->
+ V1SubscriptionManager._v1Request userId, {
+ method: 'GET',
+ url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/subscription_status"
+ }, callback
+
+ cancelV1Subscription: (userId, callback=(err)->) ->
+ V1SubscriptionManager._v1Request userId, {
+ method: 'DELETE',
+ url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/subscription"
+ }, callback
+
v1IdForUser: (userId, callback=(err, v1Id) ->) ->
UserGetter.getUser userId, {'overleaf.id': 1}, (err, user) ->
return callback(err) if err?
@@ -76,7 +88,7 @@ module.exports = V1SubscriptionManager =
pass: settings.apis.v1.pass
sendImmediately: true
json: true,
- timeout: 5 * 1000
+ timeout: 15 * 1000
}, (error, response, body) ->
if error?
# Specially handle no connection err, so warning can be shown
diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee
index 64d238b543..b4f2b1cb91 100644
--- a/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee
+++ b/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee
@@ -21,5 +21,5 @@ module.exports = SudoModeMiddlewear =
return next()
else
logger.log {userId}, "[SudoMode] sudo mode not active, redirecting"
- AuthenticationController._setRedirectInSession(req)
+ AuthenticationController.setRedirectInSession(req)
return res.redirect('/confirm-password')
diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee
index 6a68825b8d..07d3a67f2b 100644
--- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee
+++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee
@@ -60,7 +60,7 @@ module.exports = TokenAccessController =
else
logger.log {token, projectId: project._id},
"[TokenAccess] deny anonymous read-and-write token access"
- AuthenticationController._setRedirectInSession(req)
+ AuthenticationController.setRedirectInSession(req)
return res.redirect('/restricted')
if project.owner_ref.toString() == userId
logger.log {userId, projectId: project._id},
diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee
index 5e6ea7d62b..e906f3570e 100644
--- a/services/web/app/coffee/Features/User/UserPagesController.coffee
+++ b/services/web/app/coffee/Features/User/UserPagesController.coffee
@@ -51,7 +51,7 @@ module.exports =
# such as being sent from the editor to /login, then set the redirect explicitly
if req.query.redir? and !AuthenticationController._getRedirectFromSession(req)?
logger.log {redir: req.query.redir}, "setting explicit redirect from login page"
- AuthenticationController._setRedirectInSession(req, req.query.redir)
+ AuthenticationController.setRedirectInSession(req, req.query.redir)
res.render 'user/login',
title: 'login',
email: req.query.email
diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee
index 1b0e0ca442..a7a5a45ef4 100644
--- a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee
+++ b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee
@@ -6,21 +6,39 @@ Errors = require('../Errors/Errors')
logger = require("logger-sharelatex")
module.exports =
- requireEntityAccess: (entityName, entityIdOverride = null) ->
- (req, res, next) ->
- loggedInUser = AuthenticationController.getSessionUser(req)
- unless loggedInUser
- return AuthorizationMiddlewear.redirectToRestricted req, res, next
+ requireTeamAccess: (req, res, next) ->
+ requireAccessToEntity('team', req.params.id, req, res, next)
- entityId = entityIdOverride or req.params.id
- getEntity entityName, entityId, loggedInUser, (error, entity, entityConfig) ->
- return next(error) if error?
- unless entity?
- return AuthorizationMiddlewear.redirectToRestricted(req, res, next)
+ requireGroupAccess: (req, res, next) ->
+ requireAccessToEntity('group', req.params.id, req, res, next)
- req.entity = entity
- req.entityConfig = entityConfig
- next()
+ requireGroupManagersAccess: (req, res, next) ->
+ requireAccessToEntity('groupManagers', req.params.id, req, res, next)
+
+ requireInstitutionAccess: (req, res, next) ->
+ requireAccessToEntity('institution', req.params.id, req, res, next)
+
+ requirePublisherAccess: (req, res, next) ->
+ requireAccessToEntity('publisher', req.params.id, req, res, next)
+
+ requireGraphAccess: (req, res, next) ->
+ requireAccessToEntity(
+ req.query.resource_type, req.query.resource_id, req, res, next
+ )
+
+requireAccessToEntity = (entityName, entityId, req, res, next) ->
+ loggedInUser = AuthenticationController.getSessionUser(req)
+ unless loggedInUser
+ return AuthorizationMiddlewear.redirectToRestricted req, res, next
+
+ getEntity entityName, entityId, loggedInUser, (error, entity, entityConfig) ->
+ return next(error) if error?
+ unless entity?
+ return AuthorizationMiddlewear.redirectToRestricted(req, res, next)
+
+ req.entity = entity
+ req.entityConfig = entityConfig
+ next()
getEntity = (entityName, entityId, userId, callback = (error, entity, entityConfig)->) ->
entityConfig = EntityConfigs[entityName]
diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee
index 733a82f66d..22d81c8821 100644
--- a/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee
+++ b/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee
@@ -62,3 +62,19 @@ module.exports =
pathsFor: (id) ->
addMember: "/manage/institutions/#{id}/managers"
removeMember: "/manage/institutions/#{id}/managers"
+
+ publisher:
+ modelName: 'Publisher'
+ fields:
+ primaryKey: 'slug'
+ read: ['managerIds']
+ write: 'managerIds'
+ access: 'managerIds'
+ name: 'name'
+ translations:
+ title: 'publisher_account'
+ subtitle: 'managers_management'
+ remove: 'remove_manager'
+ pathsFor: (id) ->
+ addMember: "/manage/publishers/#{id}/managers"
+ removeMember: "/manage/publishers/#{id}/managers"
diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee
index b3f99e8966..ef197b83d9 100644
--- a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee
+++ b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee
@@ -4,6 +4,7 @@ Errors = require('../Errors/Errors')
EntityModels =
Institution: require('../../models/Institution').Institution
Subscription: require('../../models/Subscription').Subscription
+ Publisher: require('../../models/Publisher').Publisher
UserMembershipViewModel = require('./UserMembershipViewModel')
UserGetter = require('../User/UserGetter')
logger = require('logger-sharelatex')
diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee
index c53a5af7c7..eb36f34114 100644
--- a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee
+++ b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee
@@ -5,36 +5,52 @@ TeamInvitesController = require '../Subscription/TeamInvitesController'
module.exports =
apply: (webRouter) ->
+ # group members routes
webRouter.get '/manage/groups/:id/members',
- UserMembershipAuthorization.requireEntityAccess('group'),
+ UserMembershipAuthorization.requireGroupAccess,
UserMembershipController.index
webRouter.post '/manage/groups/:id/invites',
- UserMembershipAuthorization.requireEntityAccess('group'),
+ UserMembershipAuthorization.requireGroupAccess,
TeamInvitesController.createInvite
webRouter.delete '/manage/groups/:id/user/:user_id',
- UserMembershipAuthorization.requireEntityAccess('group'),
+ UserMembershipAuthorization.requireGroupAccess,
SubscriptionGroupController.removeUserFromGroup
webRouter.delete '/manage/groups/:id/invites/:email',
- UserMembershipAuthorization.requireEntityAccess('group'),
+ UserMembershipAuthorization.requireGroupAccess,
TeamInvitesController.revokeInvite
webRouter.get '/manage/groups/:id/members/export',
- UserMembershipAuthorization.requireEntityAccess('group'),
+ UserMembershipAuthorization.requireGroupAccess,
UserMembershipController.exportCsv
+ # group managers routes
+ webRouter.get "/manage/groups/:id/managers",
+ UserMembershipAuthorization.requireGroupManagersAccess,
+ UserMembershipController.index
+ webRouter.post "/manage/groups/:id/managers",
+ UserMembershipAuthorization.requireGroupManagersAccess,
+ UserMembershipController.add
+ webRouter.delete "/manage/groups/:id/managers/:userId",
+ UserMembershipAuthorization.requireGroupManagersAccess,
+ UserMembershipController.remove
- regularEntitites =
- groups: 'groupManagers'
- institutions: 'institution'
- for pathName, entityName of regularEntitites
- do (pathName, entityName) ->
- webRouter.get "/manage/#{pathName}/:id/managers",
- UserMembershipAuthorization.requireEntityAccess(entityName),
- UserMembershipController.index
+ # institution members routes
+ webRouter.get "/manage/institutions/:id/managers",
+ UserMembershipAuthorization.requireInstitutionAccess,
+ UserMembershipController.index
+ webRouter.post "/manage/institutions/:id/managers",
+ UserMembershipAuthorization.requireInstitutionAccess,
+ UserMembershipController.add
+ webRouter.delete "/manage/institutions/:id/managers/:userId",
+ UserMembershipAuthorization.requireInstitutionAccess,
+ UserMembershipController.remove
- webRouter.post "/manage/#{pathName}/:id/managers",
- UserMembershipAuthorization.requireEntityAccess(entityName),
- UserMembershipController.add
-
- webRouter.delete "/manage/#{pathName}/:id/managers/:userId",
- UserMembershipAuthorization.requireEntityAccess(entityName),
- UserMembershipController.remove
+ # publisher members routes
+ webRouter.get "/manage/publishers/:id/managers",
+ UserMembershipAuthorization.requirePublisherAccess,
+ UserMembershipController.index
+ webRouter.post "/manage/publishers/:id/managers",
+ UserMembershipAuthorization.requirePublisherAccess,
+ UserMembershipController.add
+ webRouter.delete "/manage/publishers/:id/managers/:userId",
+ UserMembershipAuthorization.requirePublisherAccess,
+ UserMembershipController.remove
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 9539ce61d1..2eb51b9bf8 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -16,6 +16,7 @@ hashedFiles = {}
Path = require 'path'
Features = require "./Features"
Modules = require "./Modules"
+moment = require 'moment'
jsPath =
if Settings.useMinifiedJs
@@ -126,6 +127,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
res.locals.lib = PackageVersions.lib
+ res.locals.moment = moment
res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile)
diff --git a/services/web/app/coffee/models/Publisher.coffee b/services/web/app/coffee/models/Publisher.coffee
new file mode 100644
index 0000000000..3426b168ed
--- /dev/null
+++ b/services/web/app/coffee/models/Publisher.coffee
@@ -0,0 +1,34 @@
+mongoose = require 'mongoose'
+Schema = mongoose.Schema
+ObjectId = Schema.ObjectId
+settings = require 'settings-sharelatex'
+request = require 'request'
+
+PublisherSchema = new Schema
+ slug: { type: String, required: true }
+ managerIds: [ type:ObjectId, ref:'User' ]
+
+# fetch publisher's (brand on v1) data from v1 API. Errors are ignored
+PublisherSchema.method 'fetchV1Data', (callback = (error, publisher)->) ->
+ request {
+ baseUrl: settings.apis.v1.url
+ url: "/api/v2/brands/#{this.slug}"
+ method: 'GET'
+ auth:
+ user: settings.apis.v1.user
+ pass: settings.apis.v1.pass
+ sendImmediately: true
+ }, (error, response, body) =>
+ try parsedBody = JSON.parse(body) catch e
+ this.name = parsedBody?.name
+ this.partner = parsedBody?.partner
+ callback(null, this)
+
+conn = mongoose.createConnection(settings.mongo.url, {
+ server: {poolSize: settings.mongo.poolSize || 10},
+ config: {autoIndex: false}
+})
+
+Publisher = conn.model 'Publisher', PublisherSchema
+exports.Publisher = Publisher
+exports.PublisherSchema = PublisherSchema
diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee
index fcfb06742b..6ad504b0fa 100644
--- a/services/web/app/coffee/models/User.coffee
+++ b/services/web/app/coffee/models/User.coffee
@@ -50,6 +50,7 @@ UserSchema = new Schema
references: { type:Boolean, default: Settings.defaultFeatures.references }
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
mendeley: { type:Boolean, default: Settings.defaultFeatures.mendeley }
+ zotero: { type:Boolean, default: Settings.defaultFeatures.zotero }
referencesSearch: { type:Boolean, default: Settings.defaultFeatures.referencesSearch }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
diff --git a/services/web/app/views/project/editor/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug
index cbe8124242..969b1e2564 100644
--- a/services/web/app/views/project/editor/new-file-modal.pug
+++ b/services/web/app/views/project/editor/new-file-modal.pug
@@ -162,7 +162,7 @@ script(type='text/ng-template', id='newFileModalTemplate')
on-enter="create()",
name="url"
)
- .row-spaced.small
+ .row-spaced-small
label(for="name") File name in this project
input.form-control(
type="text",
diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug
index f89b26925c..18c39a43e9 100644
--- a/services/web/app/views/project/list/modals.pug
+++ b/services/web/app/views/project/list/modals.pug
@@ -351,7 +351,7 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
| Direct git access to your projects is not yet available, but you can migrate your project to the Overleaf v2 GitHub integration
|
a(href='https://www.overleaf.com/help/343-working-offline-in-overleaf-v2', target='_blank') Read More.
- li There are no Zotero and CiteULike integrations yet
+ li There is no CiteULike integration
li Some Journals and Services in the Submit menu don't support direct submissions yet
.v1-import-cta
p
diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug
index 3becea865a..17e8114036 100644
--- a/services/web/app/views/subscriptions/dashboard.pug
+++ b/services/web/app/views/subscriptions/dashboard.pug
@@ -35,6 +35,9 @@ block content
-if (settings.overleaf && v1Subscriptions)
include ./dashboard/_v1_subscriptions
+ -if (v1SubscriptionStatus)
+ include ./dashboard/_v1_subscription_status
+
-if (!hasAnySubscription)
p You're on the #{settings.appName} Free plan.
|
diff --git a/services/web/app/views/subscriptions/dashboard/_managed_groups.pug b/services/web/app/views/subscriptions/dashboard/_managed_groups.pug
index ce1fda22e8..fd7c354db7 100644
--- a/services/web/app/views/subscriptions/dashboard/_managed_groups.pug
+++ b/services/web/app/views/subscriptions/dashboard/_managed_groups.pug
@@ -5,9 +5,20 @@ each managedGroupSubscription in managedGroupSubscriptions
+teamName(managedGroupSubscription)
p
a.btn.btn-primary(href="/manage/groups/" + managedGroupSubscription._id + "/members")
+ i.fa.fa-fw.fa-users
+ |
| Manage members
|
- a.btn.btn-primary(href="/manage/groups/" + managedGroupSubscription._id + "/managers")
+ p
+ a(href="/manage/groups/" + managedGroupSubscription._id + "/managers")
+ i.fa.fa-fw.fa-users
+ |
| Manage group managers
+ |
+ p
+ a(href="/metrics/groups/" + managedGroupSubscription._id)
+ i.fa.fa-fw.fa-line-chart
+ |
+ | View metrics
hr
diff --git a/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug b/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug
index 31b3e4d225..19a1add36d 100644
--- a/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug
+++ b/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug
@@ -4,6 +4,18 @@ each institution in managedInstitutions
|
strong= institution.name
p
- a.btn.btn-primary(href="/manage/institutions/" + institution.v1Id + "/managers")
+ a.btn.btn-primary(href="/metrics/institutions/" + institution.v1Id)
+ i.fa.fa-fw.fa-line-chart
+ |
+ | View metrics
+ p
+ a(href="/institutions/" + institution.v1Id + "/hub")
+ i.fa.fa-fw.fa-user-circle
+ |
+ | View hub
+ p
+ a(href="/manage/institutions/" + institution.v1Id + "/managers")
+ i.fa.fa-fw.fa-users
+ |
| Manage institution managers
hr
diff --git a/services/web/app/views/subscriptions/dashboard/_v1_subscription_status.pug b/services/web/app/views/subscriptions/dashboard/_v1_subscription_status.pug
new file mode 100644
index 0000000000..4b2d01f7ca
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_v1_subscription_status.pug
@@ -0,0 +1,60 @@
+- if (v1SubscriptionStatus['team'])
+ p
+ | You have a legacy group licence from Overleaf v1.
+ - if (v1SubscriptionStatus['team']['will_end_at'])
+ p
+ | Your current group licence ends on
+ |
+ strong= moment(v1SubscriptionStatus['team']['will_end_at']).format('Do MMM YY')
+ |
+ | and will
+ |
+ - if (v1SubscriptionStatus['team']['will_renew'])
+ | be automatically renewed.
+ - else
+ | not be automatically renewed.
+ - if (v1SubscriptionStatus['can_cancel_team'])
+ p
+ form(method="POST", action="/user/subscription/v1/cancel")
+ input(type="hidden", name="_csrf", value=csrfToken)
+ button().btn.btn-danger Stop automatic renewal
+ - else
+ p
+ | Please
+ |
+ a(href="/contact") contact support
+ |
+ | to make changes to your plan
+ hr
+
+- if (v1SubscriptionStatus['product'])
+ p
+ | You have a legacy Overleaf v1
+ |
+ strong= v1SubscriptionStatus['product']['display_name']
+ |
+ | plan.
+ p
+ | Your plan ends on
+ |
+ strong= moment(v1SubscriptionStatus['product']['will_end_at']).format('Do MMM YY')
+ |
+ | and will
+ |
+ - if (v1SubscriptionStatus['product']['will_renew'])
+ | be automatically renewed.
+ - else
+ | not be automatically renewed.
+ - if (v1SubscriptionStatus['can_cancel'])
+ p
+ form(method="POST", action="/user/subscription/v1/cancel")
+ input(type="hidden", name="_csrf", value=csrfToken)
+ button().btn.btn-danger Stop automatic renewal
+ - else
+ p
+ | Please
+ |
+ a(href="/contact") contact support
+ |
+ | to make changes to your plan
+ hr
diff --git a/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug b/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug
index 1d0377df8c..e543c15960 100644
--- a/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug
+++ b/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug
@@ -1,11 +1,3 @@
-- if (v1Subscriptions.has_subscription)
- -hasAnySubscription = true
- p
- | You are subscribed to Overleaf through Overleaf v1
- p
- a.btn.btn-primary(href='/sign_in_to_v1?return_to=/users/edit%23status') Manage v1 Subscription
- hr
-
- if (v1Subscriptions.teams && v1Subscriptions.teams.length > 0)
-hasAnySubscription = true
for team in v1Subscriptions.teams
diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml
index ebf164f606..fb1fba2b8d 100644
--- a/services/web/docker-compose.yml
+++ b/services/web/docker-compose.yml
@@ -17,7 +17,7 @@ services:
PROJECT_HISTORY_ENABLED: 'true'
ENABLED_LINKED_FILE_TYPES: 'url'
LINKED_URL_PROXY: 'http://localhost:6543'
- ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file,mendeley'
+ ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file,mendeley,zotero'
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
NODE_ENV: production
depends_on:
diff --git a/services/web/public/img/other-brands/logo_ieee_white.png b/services/web/public/img/other-brands/logo_ieee_white.png
new file mode 100644
index 0000000000..509d187104
Binary files /dev/null and b/services/web/public/img/other-brands/logo_ieee_white.png differ
diff --git a/services/web/public/src/ide/file-tree/FileTreeManager.js b/services/web/public/src/ide/file-tree/FileTreeManager.js
index a72a988af8..0717a9039b 100644
--- a/services/web/public/src/ide/file-tree/FileTreeManager.js
+++ b/services/web/public/src/ide/file-tree/FileTreeManager.js
@@ -79,7 +79,8 @@ define([
name: file.name,
id: file._id,
type: 'file',
- linkedFileData
+ linkedFileData,
+ created: file.created
})
return this.recalculateDocList()
})
diff --git a/services/web/public/stylesheets/components/tabs.less b/services/web/public/stylesheets/components/tabs.less
index ed03793ec7..c7ddcf1307 100644
--- a/services/web/public/stylesheets/components/tabs.less
+++ b/services/web/public/stylesheets/components/tabs.less
@@ -16,7 +16,7 @@
color: @link-color-alt;
&:focus, &:hover {
background-color: transparent!important;
- border: 0!important;
+ border: 0;
color: @link-hover-color-alt;
}
}
@@ -36,4 +36,4 @@
background-color: transparent!important;
border: none!important;
}
-}
\ No newline at end of file
+}
diff --git a/services/web/public/stylesheets/core/ol-ieee-variables.less b/services/web/public/stylesheets/core/ol-ieee-variables.less
index 143d948aff..64d3f8f9d5 100644
--- a/services/web/public/stylesheets/core/ol-ieee-variables.less
+++ b/services/web/public/stylesheets/core/ol-ieee-variables.less
@@ -42,3 +42,11 @@
@editor-toggler-hover-bg-color: @ieee-blue;
@toggle-switch-highlight-color: @ieee-blue;
+
+@footer-link-color : @link-color;
+@footer-link-hover-color : @link-hover-color;
+
+@navbar-subdued-hover-color : @ieee-blue;
+@navbar-default-link-hover-bg : @ieee-blue;
+@navbar-default-link-hover-color: @ieee-blue;
+@navbar-default-link-active-bg : @ieee-blue;
diff --git a/services/web/public/stylesheets/ol-ieee-style.less b/services/web/public/stylesheets/ol-ieee-style.less
index 1235633f5b..daa0f85f42 100644
--- a/services/web/public/stylesheets/ol-ieee-style.less
+++ b/services/web/public/stylesheets/ol-ieee-style.less
@@ -4,3 +4,41 @@
@is-overleaf : true;
@is-overleaf-light: false;
@show-rich-text : true;
+
+@ieee-wedge: 30px;
+
+body > .portal-ieee {
+ padding-top: @header-height;
+}
+
+.portal-ieee {
+ .ieee-header {
+ background-color: @ieee-blue;
+ margin-bottom: @margin-xl;
+ padding-bottom: @padding-sm;
+ padding-top: @padding-sm;
+ h1 {
+ margin: 0;
+ }
+ .ieee-logo {
+ width: @navbar-brand-width;
+ }
+ }
+ .ieee-subheader {
+ background-color: @ieee-blue;
+ color: #ffffff;
+ line-height: 1;
+ padding: @padding-md @ieee-wedge;
+ position: relative;
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ border-style: solid;
+ left: -1px;
+ top: -1px;
+ border-color: @content-alt-bg-color transparent;
+ border-width: @ieee-wedge @ieee-wedge 0 0;
+ }
+ }
+}
diff --git a/services/web/test/acceptance/coffee/SubscriptionTests.coffee b/services/web/test/acceptance/coffee/SubscriptionTests.coffee
index bb19c3c779..f68d5ea8f0 100644
--- a/services/web/test/acceptance/coffee/SubscriptionTests.coffee
+++ b/services/web/test/acceptance/coffee/SubscriptionTests.coffee
@@ -234,7 +234,8 @@ describe 'Subscriptions', ->
before (done) ->
v1Id = MockV1Api.nextV1Id()
MockV1Api.setUser v1Id, {
- subscription: {}
+ subscription: {},
+ subscription_status: {}
}
MockV1Api.setAffiliations [{
email: 'confirmed-affiliation-email@stanford.example.edu'
@@ -281,6 +282,10 @@ describe 'Subscriptions', ->
name: 'Test team'
}]
}
+ subscription_status: @subscription_status = {
+ product: { 'mock': 'product' }
+ team: null
+ }
}
@user.setV1Id v1Id, (error) =>
return done(error) if error?
@@ -295,4 +300,29 @@ describe 'Subscriptions', ->
expect(@data.memberGroupSubscriptions).to.deep.equal []
it 'should return a v1Subscriptions', ->
- expect(@data.v1Subscriptions).to.deep.equal @subscription
\ No newline at end of file
+ expect(@data.v1Subscriptions).to.deep.equal @subscription
+
+ it 'should return a v1SubscriptionStatus', ->
+ expect(@data.v1SubscriptionStatus).to.deep.equal @subscription_status
+
+ describe.only 'canceling', ->
+ before (done) ->
+ @user = new User()
+ MockV1Api.setUser v1Id = MockV1Api.nextV1Id(), @v1_user = {}
+ async.series [
+ (cb) => @user.login(cb)
+ (cb) => @user.setV1Id(v1Id, cb)
+ ], (error) =>
+ @user.request {
+ method: 'POST',
+ url: '/user/subscription/v1/cancel'
+ }, (error, @response) =>
+ return done(error) if error?
+ done()
+
+ it 'should tell v1 to cancel the subscription', ->
+ expect(@v1_user.canceled).to.equal true
+
+ it 'should redirect to the subscription dashboard', ->
+ expect(@response.statusCode).to.equal 302
+ expect(@response.headers.location).to.equal '/user/subscription'
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
index 91714af1d7..e7babe54b5 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
@@ -38,6 +38,9 @@ module.exports = MockDocUpdaterApi =
app.post "/project/:project_id/doc/:doc_id", (req, res, next) =>
res.sendStatus 204
+ app.delete "/project/:project_id", (req, res) =>
+ res.sendStatus 204
+
app.post "/project/:project_id/doc/:doc_id/flush", (req, res, next) =>
res.sendStatus 204
diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
index dffebdc929..04c1b6446b 100644
--- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
@@ -53,6 +53,20 @@ module.exports = MockV1Api =
else
res.sendStatus 404
+ app.get "/api/v1/sharelatex/users/:v1_user_id/subscription_status", (req, res, next) =>
+ user = @users[req.params.v1_user_id]
+ if user?.subscription_status?
+ res.json user.subscription_status
+ else
+ res.sendStatus 404
+
+ app.delete "/api/v1/sharelatex/users/:v1_user_id/subscription", (req, res, next) =>
+ user = @users[req.params.v1_user_id]
+ if user?
+ user.canceled = true
+ res.sendStatus 200
+ else
+ res.sendStatus 404
app.post "/api/v1/sharelatex/users/:v1_user_id/sync", (req, res, next) =>
@syncUserFeatures(req.params.v1_user_id)
diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
index 4fb31c7503..3ffd08e08b 100644
--- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -491,10 +491,10 @@ describe "AuthenticationController", ->
beforeEach ->
@req.headers = {}
@AuthenticationController.httpAuth = sinon.stub()
- @_setRedirect = sinon.spy(@AuthenticationController, '_setRedirectInSession')
+ @setRedirect = sinon.spy(@AuthenticationController, 'setRedirectInSession')
afterEach ->
- @_setRedirect.restore()
+ @setRedirect.restore()
describe "with white listed url", ->
beforeEach ->
@@ -540,7 +540,7 @@ describe "AuthenticationController", ->
@AuthenticationController.requireGlobalLogin @req, @res, @next
it 'should have called setRedirectInSession', ->
- @_setRedirect.callCount.should.equal 1
+ @setRedirect.callCount.should.equal 1
it "should redirect to the /login page", ->
@res.redirectedTo.should.equal "/login"
@@ -640,18 +640,18 @@ describe "AuthenticationController", ->
@callback.called.should.equal true
- describe '_setRedirectInSession', ->
+ describe 'setRedirectInSession', ->
beforeEach ->
@req = {session: {}}
@req.path = "/somewhere"
@req.query = {one: "1"}
it 'should set redirect property on session', ->
- @AuthenticationController._setRedirectInSession(@req)
+ @AuthenticationController.setRedirectInSession(@req)
expect(@req.session.postLoginRedirect).to.equal "/somewhere?one=1"
it 'should set the supplied value', ->
- @AuthenticationController._setRedirectInSession(@req, '/somewhere/specific')
+ @AuthenticationController.setRedirectInSession(@req, '/somewhere/specific')
expect(@req.session.postLoginRedirect).to.equal "/somewhere/specific"
describe 'with a png', ->
@@ -659,7 +659,7 @@ describe "AuthenticationController", ->
@req = {session: {}}
it 'should not set the redirect', ->
- @AuthenticationController._setRedirectInSession(@req, '/something.png')
+ @AuthenticationController.setRedirectInSession(@req, '/something.png')
expect(@req.session.postLoginRedirect).to.equal undefined
describe 'with a js path', ->
@@ -668,7 +668,7 @@ describe "AuthenticationController", ->
@req = {session: {}}
it 'should not set the redirect', ->
- @AuthenticationController._setRedirectInSession(@req, '/js/something.js')
+ @AuthenticationController.setRedirectInSession(@req, '/js/something.js')
expect(@req.session.postLoginRedirect).to.equal undefined
describe '_getRedirectFromSession', ->
diff --git a/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee
index 18d73ba77e..116703fa4d 100644
--- a/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee
@@ -163,12 +163,14 @@ describe 'ProjectDetailsHandler', ->
describe "ensureProjectNameIsUnique", ->
beforeEach ->
@result = {
- owned: [{_id: 1, name:"name"}, {_id: 2, name: "name1"}, {_id: 3, name: "name11"}]
+ owned: [{_id: 1, name:"name"}, {_id: 2, name: "name1"}, {_id: 3, name: "name11"}, {_id: 100, name: "numeric"}]
readAndWrite: [{_id: 4, name:"name2"}, {_id: 5, name:"name22"}]
readOnly: [{_id:6, name:"name3"}, {_id:7, name: "name33"}]
tokenReadAndWrite: [{_id:8, name:"name4"}, {_id:9, name:"name44"}]
tokenReadOnly: [{_id:10, name:"name5"}, {_id:11, name:"name55"}, {_id:12, name:"x".repeat(15)}]
}
+ for i in [1..20].concat([30..40])
+ @result.owned.push {_id: 100 + i, name: "numeric (#{i})"}
@ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, @result)
it "should leave a unique name unchanged", (done) ->
@@ -196,9 +198,34 @@ describe 'ProjectDetailsHandler', ->
expect(changed).to.equal true
done()
- it "should return an error if the name cannot be made unique", (done) ->
- @handler.ensureProjectNameIsUnique @user_id, "name", ["1", "5", "55"], (error, name, changed) ->
- expect(error).to.eql new Errors.InvalidNameError("Project name could not be made unique")
+ it "should use a numeric index if no suffix is supplied", (done) ->
+ @handler.ensureProjectNameIsUnique @user_id, "name1", [], (error, name, changed) ->
+ expect(name).to.equal "name1 (1)"
+ expect(changed).to.equal true
+ done()
+
+ it "should use a numeric index if all suffixes are exhausted", (done) ->
+ @handler.ensureProjectNameIsUnique @user_id, "name", ["1", "11"], (error, name, changed) ->
+ expect(name).to.equal "name (1)"
+ expect(changed).to.equal true
+ done()
+
+ it "should find the next lowest available numeric index for the base name", (done) ->
+ @handler.ensureProjectNameIsUnique @user_id, "numeric", [], (error, name, changed) ->
+ expect(name).to.equal "numeric (21)"
+ expect(changed).to.equal true
+ done()
+
+ it "should find the next available numeric index when a numeric index is already present", (done) ->
+ @handler.ensureProjectNameIsUnique @user_id, "numeric (5)", [], (error, name, changed) ->
+ expect(name).to.equal "numeric (21)"
+ expect(changed).to.equal true
+ done()
+
+ it "should not find a numeric index lower than the one already present", (done) ->
+ @handler.ensureProjectNameIsUnique @user_id, "numeric (31)", [], (error, name, changed) ->
+ expect(name).to.equal "numeric (41)"
+ expect(changed).to.equal true
done()
describe "fixProjectName", ->
diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
index 7fbb1df361..74a85967ae 100644
--- a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
+++ b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
@@ -79,6 +79,7 @@ describe "SubscriptionController", ->
"./RecurlyWrapper": @RecurlyWrapper = {}
"./FeaturesUpdater": @FeaturesUpdater = {}
"./GroupPlansData": @GroupPlansData = {}
+ "./V1SubscriptionManager": @V1SubscriptionManager = {}
@res = new MockResponse()
diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee
index cc4f859ab2..51a168fe58 100644
--- a/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee
+++ b/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee
@@ -13,7 +13,7 @@ describe 'SudoModeMiddlewear', ->
isSudoModeActive: sinon.stub()
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@userId)
- _setRedirectInSession: sinon.stub()
+ setRedirectInSession: sinon.stub()
@SudoModeMiddlewear = SandboxedModule.require modulePath, requires:
'./SudoModeHandler': @SudoModeHandler
'../Authentication/AuthenticationController': @AuthenticationController
@@ -54,7 +54,7 @@ describe 'SudoModeMiddlewear', ->
describe 'when sudo mode is not active', ->
beforeEach ->
- @AuthenticationController._setRedirectInSession = sinon.stub()
+ @AuthenticationController.setRedirectInSession = sinon.stub()
@AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId)
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false)
@@ -71,8 +71,8 @@ describe 'SudoModeMiddlewear', ->
it 'should set redirect in session', (done) ->
@call () =>
- @AuthenticationController._setRedirectInSession.callCount.should.equal 1
- @AuthenticationController._setRedirectInSession.calledWith(@req).should.equal true
+ @AuthenticationController.setRedirectInSession.callCount.should.equal 1
+ @AuthenticationController.setRedirectInSession.calledWith(@req).should.equal true
done()
it 'should redirect to the password-prompt page', (done) ->
diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee
index a4c65d1aee..550791cecd 100644
--- a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee
+++ b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee
@@ -61,7 +61,7 @@ describe "TokenAccessController", ->
@TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub()
.callsArgWith(2, null)
@ProjectController.loadEditor = sinon.stub()
- @AuthenticationController._setRedirectInSession = sinon.stub()
+ @AuthenticationController.setRedirectInSession = sinon.stub()
@TokenAccessController.readAndWriteToken @req, @res, @next
it 'should try to find a project with this token', (done) ->
@@ -173,7 +173,7 @@ describe "TokenAccessController", ->
.callsArgWith(2, null)
@ProjectController.loadEditor = sinon.stub()
@TokenAccessHandler.grantSessionTokenAccess = sinon.stub()
- @AuthenticationController._setRedirectInSession = sinon.stub()
+ @AuthenticationController.setRedirectInSession = sinon.stub()
@TokenAccessController.readAndWriteToken @req, @res, @next
it 'should not add the user to the project with read-write access', (done) ->
@@ -192,8 +192,8 @@ describe "TokenAccessController", ->
done()
it 'should set redirect in session', (done) ->
- expect(@AuthenticationController._setRedirectInSession.callCount).to.equal 1
- expect(@AuthenticationController._setRedirectInSession.calledWith(@req)).to.equal true
+ expect(@AuthenticationController.setRedirectInSession.callCount).to.equal 1
+ expect(@AuthenticationController.setRedirectInSession.calledWith(@req)).to.equal true
done()
it 'should redirect to restricted page', (done) ->
diff --git a/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee b/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee
index a0f155846f..6555dce1e0 100644
--- a/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee
+++ b/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee
@@ -28,7 +28,7 @@ describe "UserPagesController", ->
getLoggedInUserId: sinon.stub().returns(@user._id)
getSessionUser: sinon.stub().returns(@user)
_getRedirectFromSession: sinon.stub()
- _setRedirectInSession: sinon.stub()
+ setRedirectInSession: sinon.stub()
@UserPagesController = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings
"logger-sharelatex":
@@ -92,13 +92,13 @@ describe "UserPagesController", ->
beforeEach ->
@AuthenticationController._getRedirectFromSession = sinon.stub().returns(null)
- @AuthenticationController._setRedirectInSession = sinon.stub()
+ @AuthenticationController.setRedirectInSession = sinon.stub()
@req.query.redir = '/somewhere/in/particular'
it 'should set a redirect', (done) ->
@res.render = (page) =>
- @AuthenticationController._setRedirectInSession.callCount.should.equal 1
- expect(@AuthenticationController._setRedirectInSession.lastCall.args[1]).to.equal @req.query.redir
+ @AuthenticationController.setRedirectInSession.callCount.should.equal 1
+ expect(@AuthenticationController.setRedirectInSession.lastCall.args[1]).to.equal @req.query.redir
done()
@UserPagesController.loginPage @req, @res
diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee
index 4630b1c10d..5a35eba898 100644
--- a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee
+++ b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee
@@ -30,10 +30,9 @@ describe "UserMembershipAuthorization", ->
log: ->
err: ->
- describe 'requireEntityAccess', ->
+ describe 'requireAccessToEntity', ->
it 'get entity', (done) ->
- middlewear = @UserMembershipAuthorization.requireEntityAccess 'group'
- middlewear @req, null, (error) =>
+ @UserMembershipAuthorization.requireGroupAccess @req, null, (error) =>
expect(error).to.not.extist
sinon.assert.calledWithMatch(
@UserMembershipHandler.getEntity,
@@ -45,19 +44,9 @@ describe "UserMembershipAuthorization", ->
expect(@req.entityConfig).to.exist
done()
- it 'handle unknown entity', (done) ->
- middlewear = @UserMembershipAuthorization.requireEntityAccess 'foo'
- middlewear @req, null, (error) =>
- expect(error).to.extist
- expect(error).to.be.an.instanceof(Errors.NotFoundError)
- sinon.assert.notCalled(@UserMembershipHandler.getEntity)
- expect(@req.entity).to.not.exist
- done()
-
it 'handle entity not found', (done) ->
@UserMembershipHandler.getEntity.yields(null, null)
- middlewear = @UserMembershipAuthorization.requireEntityAccess 'institution'
- middlewear @req, null, (error) =>
+ @UserMembershipAuthorization.requireGroupAccess @req, null, (error) =>
expect(error).to.extist
sinon.assert.called(@AuthorizationMiddlewear.redirectToRestricted)
sinon.assert.called(@UserMembershipHandler.getEntity)
@@ -66,34 +55,63 @@ describe "UserMembershipAuthorization", ->
it 'handle anonymous user', (done) ->
@AuthenticationController.getSessionUser.returns(null)
- middlewear = @UserMembershipAuthorization.requireEntityAccess 'institution'
- middlewear @req, null, (error) =>
+ @UserMembershipAuthorization.requireGroupAccess @req, null, (error) =>
expect(error).to.extist
sinon.assert.called(@AuthorizationMiddlewear.redirectToRestricted)
sinon.assert.notCalled(@UserMembershipHandler.getEntity)
expect(@req.entity).to.not.exist
done()
- it 'can override entity id', (done) ->
- middlewear = @UserMembershipAuthorization.requireEntityAccess 'group', 'entity-id-override'
+ describe 'requireEntityAccess', ->
+ it 'handle team access', (done) ->
+ @UserMembershipAuthorization.requireTeamAccess @req, null, (error) =>
+ expect(error).to.not.extist
+ sinon.assert.calledWithMatch(
+ @UserMembershipHandler.getEntity,
+ @req.params.id,
+ fields: primaryKey: 'overleaf.id'
+ )
+ done()
+
+ it 'handle group access', (done) ->
+ @UserMembershipAuthorization.requireGroupAccess @req, null, (error) =>
+ expect(error).to.not.extist
+ sinon.assert.calledWithMatch(
+ @UserMembershipHandler.getEntity,
+ @req.params.id,
+ translations: title: 'group_account'
+ )
+ done()
+
+ it 'handle group managers access', (done) ->
+ @UserMembershipAuthorization.requireGroupManagersAccess @req, null, (error) =>
+ expect(error).to.not.extist
+ sinon.assert.calledWithMatch(
+ @UserMembershipHandler.getEntity,
+ @req.params.id,
+ translations: subtitle: 'managers_management'
+ )
+ done()
+
+ it 'handle institution access', (done) ->
+ @UserMembershipAuthorization.requireInstitutionAccess @req, null, (error) =>
+ expect(error).to.not.extist
+ sinon.assert.calledWithMatch(
+ @UserMembershipHandler.getEntity,
+ @req.params.id,
+ modelName: 'Institution',
+ )
+ done()
+
+ it 'handle graph access', (done) ->
+ @req.query.resource_id = 'mock-resource-id'
+ @req.query.resource_type = 'institution'
+ middlewear = @UserMembershipAuthorization.requireGraphAccess
middlewear @req, null, (error) =>
expect(error).to.not.extist
sinon.assert.calledWithMatch(
@UserMembershipHandler.getEntity,
- 'entity-id-override',
+ @req.query.resource_id,
+ modelName: 'Institution',
)
done()
-
- it "doesn't cache entity id between requests", (done) ->
- middlewear = @UserMembershipAuthorization.requireEntityAccess 'group'
- middlewear @req, null, (error) =>
- expect(error).to.not.extist
- lastCallArs = @UserMembershipHandler.getEntity.lastCall.args
- expect(lastCallArs[0]).to.equal @req.params.id
- newEntityId = 'another-mock-id'
- @req.params.id = newEntityId
- middlewear @req, null, (error) =>
- expect(error).to.not.extist
- lastCallArs = @UserMembershipHandler.getEntity.lastCall.args
- expect(lastCallArs[0]).to.equal newEntityId
- done()
diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee
index 55fcb64943..eaea0791b7 100644
--- a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee
+++ b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee
@@ -29,6 +29,11 @@ describe 'UserMembershipHandler', ->
v1Id: 123
managerIds: [ObjectId(), ObjectId(), ObjectId()]
update: sinon.stub().yields(null)
+ @publisher =
+ _id: 'mock-publisher-id'
+ slug: 'slug'
+ managerIds: [ObjectId(), ObjectId()]
+ update: sinon.stub().yields(null)
@UserMembershipViewModel =
buildAsync: sinon.stub().yields(null, { _id: 'mock-member-id'})
@@ -39,12 +44,15 @@ describe 'UserMembershipHandler', ->
findOne: sinon.stub().yields(null, @institution)
@Subscription =
findOne: sinon.stub().yields(null, @subscription)
+ @Publisher =
+ findOne: sinon.stub().yields(null, @publisher)
@UserMembershipHandler = SandboxedModule.require modulePath, requires:
'./UserMembershipViewModel': @UserMembershipViewModel
'../User/UserGetter': @UserGetter
'../Errors/Errors': Errors
'../../models/Institution': Institution: @Institution
'../../models/Subscription': Subscription: @Subscription
+ '../../models/Publisher': Publisher: @Publisher
'logger-sharelatex':
log: ->
err: ->
@@ -94,6 +102,15 @@ describe 'UserMembershipHandler', ->
expect(error).to.not.be.an.instanceof(Errors.NotFoundError)
done()
+ describe 'publishers', ->
+ it 'get publisher', (done) ->
+ @UserMembershipHandler.getEntity @publisher.slug, EntityConfigs.publisher, @user, (error, institution) =>
+ should.not.exist(error)
+ expectedQuery = slug: @publisher.slug, managerIds: ObjectId(@user._id)
+ assertCalledWith(@Publisher.findOne, expectedQuery)
+ expect(institution).to.equal @publisher
+ done()
+
describe 'getUsers', ->
describe 'group', ->
it 'build view model for all users', (done) ->