mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-03 14:19:01 +02:00
Merge pull request #13850 from overleaf/ab-invite-enrollment
[web] Managed users - combined invite/surrender flow GitOrigin-RevId: 70cb0d81e0019eac69a4a565377447bb6d1a1823
This commit is contained in:
@@ -6,6 +6,8 @@ const ErrorController = require('../Errors/ErrorController')
|
||||
const EmailHelper = require('../Helpers/EmailHelper')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const { expressify } = require('../../util/promises')
|
||||
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
|
||||
const PermissionsManager = require('../Authorization/PermissionsManager')
|
||||
|
||||
function createInvite(req, res, next) {
|
||||
const teamManagerId = SessionManager.getLoggedInUserId(req.session)
|
||||
@@ -53,11 +55,14 @@ async function viewInvite(req, res, next) {
|
||||
const { token } = req.params
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
|
||||
const { invite } = await TeamInvitesHandler.promises.getInvite(token)
|
||||
const { invite, subscription } = await TeamInvitesHandler.promises.getInvite(
|
||||
token
|
||||
)
|
||||
if (!invite) {
|
||||
return ErrorController.notFound(req, res)
|
||||
}
|
||||
|
||||
let validationStatus = new Map()
|
||||
if (userId) {
|
||||
const personalSubscription =
|
||||
await SubscriptionLocator.promises.getUsersSubscription(userId)
|
||||
@@ -69,19 +74,51 @@ async function viewInvite(req, res, next) {
|
||||
personalSubscription.recurlySubscription_id &&
|
||||
personalSubscription.recurlySubscription_id !== ''
|
||||
|
||||
res.render('subscriptions/team/invite', {
|
||||
inviterName: invite.inviterName,
|
||||
inviteToken: invite.token,
|
||||
hasIndividualRecurlySubscription,
|
||||
appName: settings.appName,
|
||||
expired: req.query.expired,
|
||||
})
|
||||
if (subscription?.groupPolicy) {
|
||||
if (!subscription.populated('groupPolicy')) {
|
||||
await subscription.populate('groupPolicy')
|
||||
}
|
||||
|
||||
const user = await UserGetter.promises.getUser(userId)
|
||||
|
||||
if (
|
||||
user.enrollment?.managedBy &&
|
||||
user.enrollment?.managedBy.toString() !== subscription._id.toString()
|
||||
) {
|
||||
return HttpErrorHandler.forbidden(
|
||||
req,
|
||||
res,
|
||||
'User is already managed by a different subscription'
|
||||
)
|
||||
}
|
||||
|
||||
validationStatus =
|
||||
await PermissionsManager.promises.getUserValidationStatus(
|
||||
user,
|
||||
subscription.groupPolicy
|
||||
)
|
||||
|
||||
return res.render('subscriptions/team/invite-managed', {
|
||||
inviterName: invite.inviterName,
|
||||
inviteToken: invite.token,
|
||||
expired: req.query.expired,
|
||||
validationStatus: Object.fromEntries(validationStatus),
|
||||
})
|
||||
} else {
|
||||
return res.render('subscriptions/team/invite', {
|
||||
inviterName: invite.inviterName,
|
||||
inviteToken: invite.token,
|
||||
hasIndividualRecurlySubscription,
|
||||
appName: settings.appName,
|
||||
expired: req.query.expired,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const userByEmail = await UserGetter.promises.getUserByMainEmail(
|
||||
invite.email
|
||||
)
|
||||
|
||||
res.render('subscriptions/team/invite_logged_out', {
|
||||
return res.render('subscriptions/team/invite_logged_out', {
|
||||
inviterName: invite.inviterName,
|
||||
inviteToken: invite.token,
|
||||
appName: settings.appName,
|
||||
@@ -91,16 +128,12 @@ async function viewInvite(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function acceptInvite(req, res, next) {
|
||||
async function acceptInvite(req, res, next) {
|
||||
const { token } = req.params
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
|
||||
TeamInvitesHandler.acceptInvite(token, userId, function (err, results) {
|
||||
if (err) {
|
||||
return next(err)
|
||||
}
|
||||
res.sendStatus(204)
|
||||
})
|
||||
await TeamInvitesHandler.promises.acceptInvite(token, userId)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
function revokeInvite(req, res, next) {
|
||||
@@ -127,6 +160,6 @@ function revokeInvite(req, res, next) {
|
||||
module.exports = {
|
||||
createInvite,
|
||||
viewInvite: expressify(viewInvite),
|
||||
acceptInvite,
|
||||
acceptInvite: expressify(acceptInvite),
|
||||
revokeInvite,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const logger = require('@overleaf/logger')
|
||||
const crypto = require('crypto')
|
||||
const async = require('async')
|
||||
|
||||
const settings = require('@overleaf/settings')
|
||||
const { ObjectId } = require('mongodb')
|
||||
@@ -11,220 +10,164 @@ const UserGetter = require('../User/UserGetter')
|
||||
const SubscriptionLocator = require('./SubscriptionLocator')
|
||||
const SubscriptionUpdater = require('./SubscriptionUpdater')
|
||||
const LimitationsManager = require('./LimitationsManager')
|
||||
const ManagedUsersHandler = require('./ManagedUsersHandler')
|
||||
|
||||
const EmailHandler = require('../Email/EmailHandler')
|
||||
const EmailHelper = require('../Helpers/EmailHelper')
|
||||
|
||||
const Errors = require('../Errors/Errors')
|
||||
const { promisifyMultiResult, promisify } = require('../../util/promises')
|
||||
const { callbackify, callbackifyMultiResult } = require('../../util/promises')
|
||||
|
||||
function getInvite(token, callback) {
|
||||
Subscription.findOne(
|
||||
{ 'teamInvites.token': token },
|
||||
function (err, subscription) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
if (!subscription) {
|
||||
return callback(new Errors.NotFoundError('team not found'))
|
||||
}
|
||||
async function getInvite(token) {
|
||||
const subscription = await Subscription.findOne({
|
||||
'teamInvites.token': token,
|
||||
})
|
||||
if (!subscription) {
|
||||
throw new Errors.NotFoundError('team not found')
|
||||
}
|
||||
|
||||
const invite = subscription.teamInvites.find(i => i.token === token)
|
||||
callback(null, invite, subscription)
|
||||
}
|
||||
)
|
||||
const invite = subscription.teamInvites.find(i => i.token === token)
|
||||
return { invite, subscription }
|
||||
}
|
||||
|
||||
function createInvite(teamManagerId, subscription, email, callback) {
|
||||
async function createInvite(teamManagerId, subscription, email) {
|
||||
email = EmailHelper.parseEmail(email)
|
||||
if (!email) {
|
||||
return callback(new Error('invalid email'))
|
||||
throw new Error('invalid email')
|
||||
}
|
||||
UserGetter.getUser(teamManagerId, function (error, teamManager) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
const teamManager = await UserGetter.promises.getUser(teamManagerId)
|
||||
|
||||
_removeLegacyInvite(subscription.id, email, function (error) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
_createInvite(subscription, email, teamManager, callback)
|
||||
})
|
||||
})
|
||||
await _removeLegacyInvite(subscription.id, email)
|
||||
return _createInvite(subscription, email, teamManager)
|
||||
}
|
||||
|
||||
function importInvite(
|
||||
subscription,
|
||||
inviterName,
|
||||
email,
|
||||
token,
|
||||
sentAt,
|
||||
callback
|
||||
) {
|
||||
_checkIfInviteIsPossible(
|
||||
async function importInvite(subscription, inviterName, email, token, sentAt) {
|
||||
const { possible, reason } = await _checkIfInviteIsPossible(
|
||||
subscription,
|
||||
email,
|
||||
function (error, possible, reason) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
if (!possible) {
|
||||
return callback(reason)
|
||||
}
|
||||
|
||||
subscription.teamInvites.push({
|
||||
email,
|
||||
inviterName,
|
||||
token,
|
||||
sentAt,
|
||||
})
|
||||
|
||||
subscription.save(callback)
|
||||
}
|
||||
email
|
||||
)
|
||||
}
|
||||
|
||||
function acceptInvite(token, userId, callback) {
|
||||
getInvite(token, function (err, invite, subscription) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
if (!invite) {
|
||||
return callback(new Errors.NotFoundError('invite not found'))
|
||||
}
|
||||
|
||||
SubscriptionUpdater.addUserToGroup(
|
||||
subscription._id,
|
||||
userId,
|
||||
function (err) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
_removeInviteFromTeam(subscription.id, invite.email, callback)
|
||||
}
|
||||
)
|
||||
if (!possible) {
|
||||
throw reason
|
||||
}
|
||||
subscription.teamInvites.push({
|
||||
email,
|
||||
inviterName,
|
||||
token,
|
||||
sentAt,
|
||||
})
|
||||
|
||||
return subscription.save()
|
||||
}
|
||||
|
||||
function revokeInvite(teamManagerId, subscription, email, callback) {
|
||||
async function acceptInvite(token, userId) {
|
||||
const { invite, subscription } = await getInvite(token)
|
||||
if (!invite) {
|
||||
throw new Errors.NotFoundError('invite not found')
|
||||
}
|
||||
|
||||
await SubscriptionUpdater.promises.addUserToGroup(subscription._id, userId)
|
||||
|
||||
if (subscription.groupPolicy) {
|
||||
await ManagedUsersHandler.promises.enrollInSubscription(
|
||||
userId,
|
||||
subscription
|
||||
)
|
||||
}
|
||||
|
||||
await _removeInviteFromTeam(subscription.id, invite.email)
|
||||
}
|
||||
|
||||
async function revokeInvite(teamManagerId, subscription, email) {
|
||||
email = EmailHelper.parseEmail(email)
|
||||
if (!email) {
|
||||
return callback(new Error('invalid email'))
|
||||
throw new Error('invalid email')
|
||||
}
|
||||
_removeInviteFromTeam(subscription.id, email, callback)
|
||||
await _removeInviteFromTeam(subscription.id, email)
|
||||
}
|
||||
|
||||
// Legacy method to allow a user to receive a confirmation email if their
|
||||
// email is in Subscription.invited_emails when they join. We'll remove this
|
||||
// after a short while.
|
||||
function createTeamInvitesForLegacyInvitedEmail(email, callback) {
|
||||
SubscriptionLocator.getGroupsWithEmailInvite(email, function (err, teams) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
async function createTeamInvitesForLegacyInvitedEmail(email) {
|
||||
const teams = await SubscriptionLocator.promises.getGroupsWithEmailInvite(
|
||||
email
|
||||
)
|
||||
|
||||
async.map(
|
||||
teams,
|
||||
(team, cb) => createInvite(team.admin_id, team, email, cb),
|
||||
callback
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function _createInvite(subscription, email, inviter, callback) {
|
||||
_checkIfInviteIsPossible(
|
||||
subscription,
|
||||
email,
|
||||
function (error, possible, reason) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
if (!possible) {
|
||||
return callback(reason)
|
||||
}
|
||||
|
||||
// don't send invites when inviting self; add user directly to the group
|
||||
const isInvitingSelf = inviter.emails.some(
|
||||
emailData => emailData.email === email
|
||||
)
|
||||
if (isInvitingSelf) {
|
||||
return SubscriptionUpdater.addUserToGroup(
|
||||
subscription._id,
|
||||
inviter._id,
|
||||
err => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
// legacy: remove any invite that might have been created in the past
|
||||
_removeInviteFromTeam(subscription._id, email, error => {
|
||||
const inviteUserData = {
|
||||
email: inviter.email,
|
||||
first_name: inviter.first_name,
|
||||
last_name: inviter.last_name,
|
||||
invite: false,
|
||||
}
|
||||
callback(error, inviteUserData)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const inviterName = _getInviterName(inviter)
|
||||
let invite = subscription.teamInvites.find(
|
||||
invite => invite.email === email
|
||||
)
|
||||
|
||||
if (invite) {
|
||||
invite = invite.toObject()
|
||||
invite.sentAt = new Date()
|
||||
} else {
|
||||
invite = {
|
||||
email,
|
||||
inviterName,
|
||||
token: crypto.randomBytes(32).toString('hex'),
|
||||
sentAt: new Date(),
|
||||
}
|
||||
subscription.teamInvites.push(invite)
|
||||
}
|
||||
|
||||
subscription.save(function (error) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
const opts = {
|
||||
to: email,
|
||||
inviter,
|
||||
acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
|
||||
appName: settings.appName,
|
||||
}
|
||||
EmailHandler.sendEmail('verifyEmailToJoinTeam', opts, error => {
|
||||
Object.assign(invite, { invite: true })
|
||||
callback(error, invite)
|
||||
})
|
||||
})
|
||||
}
|
||||
return Promise.all(
|
||||
teams.map(team => createInvite(team.admin_id, team, email))
|
||||
)
|
||||
}
|
||||
|
||||
function _removeInviteFromTeam(subscriptionId, email, callback) {
|
||||
async function _createInvite(subscription, email, inviter) {
|
||||
const { possible, reason } = await _checkIfInviteIsPossible(
|
||||
subscription,
|
||||
email
|
||||
)
|
||||
|
||||
if (!possible) {
|
||||
throw reason
|
||||
}
|
||||
|
||||
// don't send invites when inviting self; add user directly to the group
|
||||
const isInvitingSelf = inviter.emails.some(
|
||||
emailData => emailData.email === email
|
||||
)
|
||||
if (isInvitingSelf) {
|
||||
await SubscriptionUpdater.promises.addUserToGroup(
|
||||
subscription._id,
|
||||
inviter._id
|
||||
)
|
||||
|
||||
// legacy: remove any invite that might have been created in the past
|
||||
await _removeInviteFromTeam(subscription._id, email)
|
||||
|
||||
return {
|
||||
email: inviter.email,
|
||||
first_name: inviter.first_name,
|
||||
last_name: inviter.last_name,
|
||||
invite: false,
|
||||
}
|
||||
}
|
||||
|
||||
const inviterName = _getInviterName(inviter)
|
||||
let invite = subscription.teamInvites.find(invite => invite.email === email)
|
||||
|
||||
if (invite) {
|
||||
invite = invite.toObject()
|
||||
invite.sentAt = new Date()
|
||||
} else {
|
||||
invite = {
|
||||
email,
|
||||
inviterName,
|
||||
token: crypto.randomBytes(32).toString('hex'),
|
||||
sentAt: new Date(),
|
||||
}
|
||||
subscription.teamInvites.push(invite)
|
||||
}
|
||||
|
||||
await subscription.save()
|
||||
|
||||
const opts = {
|
||||
to: email,
|
||||
inviter,
|
||||
acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
|
||||
appName: settings.appName,
|
||||
}
|
||||
await EmailHandler.promises.sendEmail('verifyEmailToJoinTeam', opts)
|
||||
Object.assign(invite, { invite: true })
|
||||
return invite
|
||||
}
|
||||
|
||||
async function _removeInviteFromTeam(subscriptionId, email, callback) {
|
||||
const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
|
||||
const removeInvite = { $pull: { teamInvites: { email } } }
|
||||
|
||||
async.series(
|
||||
[
|
||||
cb => Subscription.updateOne(searchConditions, removeInvite, cb),
|
||||
cb => _removeLegacyInvite(subscriptionId, email, cb),
|
||||
],
|
||||
callback
|
||||
)
|
||||
await Subscription.updateOne(searchConditions, removeInvite)
|
||||
await _removeLegacyInvite(subscriptionId, email)
|
||||
}
|
||||
|
||||
const _removeLegacyInvite = (subscriptionId, email, callback) =>
|
||||
Subscription.updateOne(
|
||||
async function _removeLegacyInvite(subscriptionId, email) {
|
||||
await Subscription.updateOne(
|
||||
{
|
||||
_id: new ObjectId(subscriptionId.toString()),
|
||||
},
|
||||
@@ -232,17 +175,17 @@ const _removeLegacyInvite = (subscriptionId, email, callback) =>
|
||||
$pull: {
|
||||
invited_emails: email,
|
||||
},
|
||||
},
|
||||
callback
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function _checkIfInviteIsPossible(subscription, email, callback) {
|
||||
async function _checkIfInviteIsPossible(subscription, email) {
|
||||
if (!subscription.groupPlan) {
|
||||
logger.debug(
|
||||
{ subscriptionId: subscription.id },
|
||||
'can not add members to a subscription that is not in a group plan'
|
||||
)
|
||||
return callback(null, false, { wrongPlan: true })
|
||||
return { possible: false, reason: { wrongPlan: true } }
|
||||
}
|
||||
|
||||
if (LimitationsManager.teamHasReachedMemberLimit(subscription)) {
|
||||
@@ -250,31 +193,27 @@ function _checkIfInviteIsPossible(subscription, email, callback) {
|
||||
{ subscriptionId: subscription.id },
|
||||
'team has reached member limit'
|
||||
)
|
||||
return callback(null, false, { limitReached: true })
|
||||
return { possible: false, reason: { limitReached: true } }
|
||||
}
|
||||
|
||||
UserGetter.getUserByAnyEmail(email, function (error, existingUser) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
if (!existingUser) {
|
||||
return callback(null, true)
|
||||
}
|
||||
const existingUser = await UserGetter.promises.getUserByAnyEmail(email)
|
||||
if (!existingUser) {
|
||||
return { possible: true }
|
||||
}
|
||||
|
||||
const existingMember = subscription.member_ids.find(
|
||||
memberId => memberId.toString() === existingUser._id.toString()
|
||||
const existingMember = subscription.member_ids.find(
|
||||
memberId => memberId.toString() === existingUser._id.toString()
|
||||
)
|
||||
|
||||
if (existingMember) {
|
||||
logger.debug(
|
||||
{ subscriptionId: subscription.id, email },
|
||||
'user already in team'
|
||||
)
|
||||
|
||||
if (existingMember) {
|
||||
logger.debug(
|
||||
{ subscriptionId: subscription.id, email },
|
||||
'user already in team'
|
||||
)
|
||||
callback(null, false, { alreadyInTeam: true })
|
||||
} else {
|
||||
callback(null, true)
|
||||
}
|
||||
})
|
||||
return { possible: false, reason: { alreadyInTeam: true } }
|
||||
} else {
|
||||
return { possible: true }
|
||||
}
|
||||
}
|
||||
|
||||
function _getInviterName(inviter) {
|
||||
@@ -289,20 +228,20 @@ function _getInviterName(inviter) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getInvite,
|
||||
createInvite,
|
||||
importInvite,
|
||||
acceptInvite,
|
||||
revokeInvite,
|
||||
createTeamInvitesForLegacyInvitedEmail,
|
||||
getInvite: callbackifyMultiResult(getInvite, ['invite', 'subscription']),
|
||||
createInvite: callbackify(createInvite),
|
||||
importInvite: callbackify(importInvite),
|
||||
acceptInvite: callbackify(acceptInvite),
|
||||
revokeInvite: callbackify(revokeInvite),
|
||||
createTeamInvitesForLegacyInvitedEmail: callbackify(
|
||||
createTeamInvitesForLegacyInvitedEmail
|
||||
),
|
||||
promises: {
|
||||
getInvite: promisifyMultiResult(getInvite, ['invite', 'subscription']),
|
||||
createInvite: promisify(createInvite),
|
||||
importInvite: promisify(importInvite),
|
||||
acceptInvite: promisify(acceptInvite),
|
||||
revokeInvite: promisify(revokeInvite),
|
||||
createTeamInvitesForLegacyInvitedEmail: promisify(
|
||||
createTeamInvitesForLegacyInvitedEmail
|
||||
),
|
||||
getInvite,
|
||||
createInvite,
|
||||
importInvite,
|
||||
acceptInvite,
|
||||
revokeInvite,
|
||||
createTeamInvitesForLegacyInvitedEmail,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
extends ../../layout-marketing
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/invite-managed'
|
||||
|
||||
block append meta
|
||||
meta(name="ol-inviteToken" content=inviteToken)
|
||||
meta(name="ol-inviterName" content=inviterName)
|
||||
meta(name="ol-expired" data-type="boolean" content=expired)
|
||||
meta(name="ol-alreadyEnrolled" data-type="boolean" content=alreadyEnrolled)
|
||||
meta(name="ol-validationStatus" data-type="json" content=validationStatus)
|
||||
|
||||
block content
|
||||
main.content.content-alt.team-invite#invite-managed-root
|
||||
@@ -28,10 +28,11 @@ block content
|
||||
|
||||
div(ng-show="view =='teamInvite'")
|
||||
p #{translate("join_team_explanation", {appName: appName})}
|
||||
p
|
||||
a.btn.btn-secondary(href="/project") #{translate("not_now")}
|
||||
|
|
||||
a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")}
|
||||
if (!expired)
|
||||
p
|
||||
a.btn.btn-secondary(href="/project") #{translate("not_now")}
|
||||
|
|
||||
a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")}
|
||||
|
||||
div(ng-show="view =='inviteAccepted'")
|
||||
p(ng-non-bindable) #{translate("joined_team", {inviterName: inviterName})}
|
||||
|
||||
@@ -809,6 +809,7 @@ module.exports = {
|
||||
editorLeftMenuManageTemplate: [],
|
||||
oauth2Server: [],
|
||||
managedGroupSubscriptionEnrollmentNotification: [],
|
||||
managedGroupEnrollmentInvite: [],
|
||||
},
|
||||
|
||||
moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
"current_file": "",
|
||||
"current_password": "",
|
||||
"currently_seeing_only_24_hrs_history": "",
|
||||
"currently_signed_in_as_x": "",
|
||||
"currently_subscribed_to_plan": "",
|
||||
"customize_your_group_subscription": "",
|
||||
"customizing_figures": "",
|
||||
@@ -280,6 +281,7 @@
|
||||
"educational_percent_discount_applied": "",
|
||||
"email": "",
|
||||
"email_limit_reached": "",
|
||||
"email_link_expired": "",
|
||||
"email_or_password_wrong_try_again": "",
|
||||
"emails_and_affiliations_explanation": "",
|
||||
"emails_and_affiliations_title": "",
|
||||
@@ -581,6 +583,7 @@
|
||||
"log_entry_maximum_entries_title": "",
|
||||
"log_hint_extra_info": "",
|
||||
"log_in_with_primary_email_address": "",
|
||||
"log_out_lowercase_dot": "",
|
||||
"log_viewer_error": "",
|
||||
"login_to_transfer_account": "",
|
||||
"login_with_service": "",
|
||||
@@ -803,6 +806,8 @@
|
||||
"read_only": "",
|
||||
"read_only_token": "",
|
||||
"read_write_token": "",
|
||||
"ready_to_join_x": "",
|
||||
"ready_to_join_x_in_group_y": "",
|
||||
"realtime_track_changes": "",
|
||||
"reauthorize_github_account": "",
|
||||
"recaptcha_conditions": "",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
|
||||
const [inviteManagedModule] = importOverleafModules(
|
||||
'managedGroupEnrollmentInvite'
|
||||
)
|
||||
const InviteManaged: JSXElementConstructor<Record<string, never>> =
|
||||
inviteManagedModule?.import.default
|
||||
|
||||
export default function InviteManagedRoot() {
|
||||
return <InviteManaged />
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import './base'
|
||||
import ReactDOM from 'react-dom'
|
||||
import InvitedManagedRoot from '../../../features/subscription/components/invite-managed-root'
|
||||
|
||||
const element = document.getElementById('invite-managed-root')
|
||||
if (element) {
|
||||
ReactDOM.render(<InvitedManagedRoot />, element)
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
flex: 1 1 10%;
|
||||
flex: 0 0 32px;
|
||||
> span {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@
|
||||
"current_password": "Current Password",
|
||||
"current_session": "Current Session",
|
||||
"currently_seeing_only_24_hrs_history": "You’re currently seeing the last 24 hours of changes in this project.",
|
||||
"currently_signed_in_as_x": "Currently signed in as <0>__userEmail__</0>.",
|
||||
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.",
|
||||
"custom_resource_portal": "Custom resource portal",
|
||||
"custom_resource_portal_info": "You can have your own custom portal page on Overleaf. This is a great place for your users to find out more about Overleaf, access templates, FAQs and Help resources, and sign up to Overleaf.",
|
||||
@@ -924,6 +925,7 @@
|
||||
"log_in_with_primary_email_address": "This will be the email address to use if you log in with an email address and password. Important __appName__ notifications will be sent to this email address.",
|
||||
"log_out": "Log Out",
|
||||
"log_out_from": "Log out from __email__",
|
||||
"log_out_lowercase_dot": "Log out.",
|
||||
"log_viewer_error": "There was a problem displaying this project’s compilation errors and logs.",
|
||||
"logged_in_with_email": "You are currently logged in to <b>__appName__</b> with the email <b>__email__</b>.",
|
||||
"logging_in": "Logging in",
|
||||
@@ -1266,6 +1268,8 @@
|
||||
"read_only": "Read Only",
|
||||
"read_only_token": "Read-Only Token",
|
||||
"read_write_token": "Read-Write Token",
|
||||
"ready_to_join_x": "You’re ready to join __inviterName__",
|
||||
"ready_to_join_x_in_group_y": "You’re ready to join __inviterName__ in __groupName__",
|
||||
"real_time_track_changes": "Real-time <0>track-changes</0>",
|
||||
"realtime_collab": "Real-time collaboration",
|
||||
"realtime_collab_info": "When you’re working together, you can see your collaborators’ cursors and their changes in real time, so everyone always has the latest version.",
|
||||
|
||||
@@ -33,21 +33,27 @@ describe('TeamInvitesHandler', function () {
|
||||
groupPlan: true,
|
||||
member_ids: [],
|
||||
teamInvites: [this.teamInvite],
|
||||
save: sinon.stub().yields(null),
|
||||
save: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
this.SubscriptionLocator = {
|
||||
getUsersSubscription: sinon.stub(),
|
||||
getSubscription: sinon.stub().yields(null, this.subscription),
|
||||
promises: {
|
||||
getUsersSubscription: sinon.stub(),
|
||||
getSubscription: sinon.stub().resolves(this.subscription),
|
||||
},
|
||||
}
|
||||
|
||||
this.UserGetter = {
|
||||
getUser: sinon.stub().yields(),
|
||||
getUserByAnyEmail: sinon.stub().yields(),
|
||||
promises: {
|
||||
getUser: sinon.stub().resolves(),
|
||||
getUserByAnyEmail: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
this.SubscriptionUpdater = {
|
||||
addUserToGroup: sinon.stub().yields(),
|
||||
promises: {
|
||||
addUserToGroup: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
this.LimitationsManager = {
|
||||
@@ -55,12 +61,20 @@ describe('TeamInvitesHandler', function () {
|
||||
}
|
||||
|
||||
this.Subscription = {
|
||||
findOne: sinon.stub().yields(),
|
||||
updateOne: sinon.stub().yields(),
|
||||
findOne: sinon.stub().resolves(),
|
||||
updateOne: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
this.EmailHandler = {
|
||||
sendEmail: sinon.stub().yields(null),
|
||||
promises: {
|
||||
sendEmail: sinon.stub().resolves(null),
|
||||
},
|
||||
}
|
||||
|
||||
this.ManagedUsersHandler = {
|
||||
promises: {
|
||||
enrollInSubscription: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
this.newToken = 'bbbbbbbbb'
|
||||
@@ -71,18 +85,17 @@ describe('TeamInvitesHandler', function () {
|
||||
},
|
||||
}
|
||||
|
||||
this.UserGetter.getUser
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(this.manager._id)
|
||||
.yields(null, this.manager)
|
||||
this.UserGetter.getUserByAnyEmail
|
||||
.resolves(this.manager)
|
||||
this.UserGetter.promises.getUserByAnyEmail
|
||||
.withArgs(this.manager.email)
|
||||
.yields(null, this.manager)
|
||||
.resolves(this.manager)
|
||||
|
||||
this.SubscriptionLocator.getUsersSubscription.yields(
|
||||
null,
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
this.subscription
|
||||
)
|
||||
this.Subscription.findOne.yields(null, this.subscription)
|
||||
this.Subscription.findOne.resolves(this.subscription)
|
||||
|
||||
this.TeamInvitesHandler = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
@@ -96,6 +109,7 @@ describe('TeamInvitesHandler', function () {
|
||||
'./SubscriptionUpdater': this.SubscriptionUpdater,
|
||||
'./LimitationsManager': this.LimitationsManager,
|
||||
'../Email/EmailHandler': this.EmailHandler,
|
||||
'./ManagedUsersHandler': this.ManagedUsersHandler,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -114,7 +128,7 @@ describe('TeamInvitesHandler', function () {
|
||||
})
|
||||
|
||||
it("returns teamNotFound if there's none", function (done) {
|
||||
this.Subscription.findOne = sinon.stub().yields(null, null)
|
||||
this.Subscription.findOne = sinon.stub().resolves(null)
|
||||
|
||||
this.TeamInvitesHandler.getInvite(
|
||||
this.token,
|
||||
@@ -152,7 +166,7 @@ describe('TeamInvitesHandler', function () {
|
||||
this.subscription,
|
||||
'John.Snow@example.com',
|
||||
(err, invite) => {
|
||||
this.EmailHandler.sendEmail
|
||||
this.EmailHandler.promises.sendEmail
|
||||
.calledWith(
|
||||
'verifyEmailToJoinTeam',
|
||||
sinon.match({
|
||||
@@ -214,7 +228,7 @@ describe('TeamInvitesHandler', function () {
|
||||
this.manager.email,
|
||||
(err, invite) => {
|
||||
sinon.assert.calledWith(
|
||||
this.SubscriptionUpdater.addUserToGroup,
|
||||
this.SubscriptionUpdater.promises.addUserToGroup,
|
||||
this.subscription._id,
|
||||
this.manager._id
|
||||
)
|
||||
@@ -266,9 +280,9 @@ describe('TeamInvitesHandler', function () {
|
||||
email: 'tyrion@example.com',
|
||||
}
|
||||
|
||||
this.UserGetter.getUserByAnyEmail
|
||||
this.UserGetter.promises.getUserByAnyEmail
|
||||
.withArgs(this.user.email)
|
||||
.yields(null, this.user)
|
||||
.resolves(this.user)
|
||||
|
||||
this.subscription.teamInvites.push({
|
||||
email: 'john.snow@example.com',
|
||||
@@ -279,7 +293,7 @@ describe('TeamInvitesHandler', function () {
|
||||
|
||||
it('adds the user to the team', function (done) {
|
||||
this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
|
||||
this.SubscriptionUpdater.addUserToGroup
|
||||
this.SubscriptionUpdater.promises.addUserToGroup
|
||||
.calledWith(this.subscription._id, this.user.id)
|
||||
.should.eq(true)
|
||||
done()
|
||||
@@ -331,10 +345,10 @@ describe('TeamInvitesHandler', function () {
|
||||
'eddard@example.com',
|
||||
'robert@example.com',
|
||||
]
|
||||
this.TeamInvitesHandler.createInvite = sinon.stub().yields(null)
|
||||
this.SubscriptionLocator.getGroupsWithEmailInvite = sinon
|
||||
this.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
|
||||
this.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
|
||||
.stub()
|
||||
.yields(null, [this.subscription])
|
||||
.resolves([this.subscription])
|
||||
})
|
||||
|
||||
it('sends an invitation email to addresses in the legacy invited_emails field', function (done) {
|
||||
@@ -396,9 +410,9 @@ describe('TeamInvitesHandler', function () {
|
||||
}
|
||||
|
||||
this.subscription.member_ids = [member.id]
|
||||
this.UserGetter.getUserByAnyEmail
|
||||
this.UserGetter.promises.getUserByAnyEmail
|
||||
.withArgs(member.email)
|
||||
.yields(null, member)
|
||||
.resolves(member)
|
||||
|
||||
this.TeamInvitesHandler.createInvite(
|
||||
this.manager._id,
|
||||
|
||||
Reference in New Issue
Block a user