Merge pull request #14442 from overleaf/mf-resend-group-invite

[web] Add an option to resend group invite in managed users setting

GitOrigin-RevId: 75625c5a50dfc74b48b3a465c9f713e2d6179db8
This commit is contained in:
M Fahru 2023-08-23 11:53:01 -07:00 committed by Copybot
parent 52b487e7be
commit a834e02cd5
8 changed files with 221 additions and 37 deletions

View file

@ -362,12 +362,16 @@ templates.verifyEmailToJoinTeam = ctaTemplate({
templates.verifyEmailToJoinManagedUsers = ctaTemplate({
subject(opts) {
return `Youve been invited by ${_.escape(
return `${
opts.reminder ? 'Reminder: ' : ''
}Youve been invited by ${_.escape(
_formatUserNameAndEmail(opts.inviter, 'a collaborator')
)} to join an ${settings.appName} group subscription.`
},
title(opts) {
return `Youve been invited by ${_.escape(
return `${
opts.reminder ? 'Reminder: ' : ''
}Youve been invited by ${_.escape(
_formatUserNameAndEmail(opts.inviter, 'a collaborator')
)} to join an ${settings.appName} group subscription.`
},
@ -404,12 +408,16 @@ templates.verifyEmailToJoinManagedUsers = ctaTemplate({
templates.inviteNewUserToJoinManagedUsers = ctaTemplate({
subject(opts) {
return `Youve been invited by ${_.escape(
return `${
opts.reminder ? 'Reminder: ' : ''
}Youve been invited by ${_.escape(
_formatUserNameAndEmail(opts.inviter, 'a collaborator')
)} to join an ${settings.appName} group subscription.`
},
title(opts) {
return `Youve been invited by ${_.escape(
return `${
opts.reminder ? 'Reminder: ' : ''
}Youve been invited by ${_.escape(
_formatUserNameAndEmail(opts.inviter, 'a collaborator')
)} to join an ${settings.appName} group subscription.`
},

View file

@ -1,5 +1,6 @@
const settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
const OError = require('@overleaf/o-error')
const TeamInvitesHandler = require('./TeamInvitesHandler')
const SessionManager = require('../Authentication/SessionManager')
const SubscriptionLocator = require('./SubscriptionLocator')
@ -9,8 +10,17 @@ const UserGetter = require('../User/UserGetter')
const { expressify } = require('../../util/promises')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const PermissionsManager = require('../Authorization/PermissionsManager')
const EmailHandler = require('../Email/EmailHandler')
const { RateLimiter } = require('../../infrastructure/RateLimiter')
function createInvite(req, res, next) {
const rateLimiters = {
resendGroupInvite: new RateLimiter('resend-group-invite', {
points: 1,
duration: 60 * 60,
}),
}
async function createInvite(req, res, next) {
const teamManagerId = SessionManager.getLoggedInUserId(req.session)
const subscription = req.entity
const email = EmailHelper.parseEmail(req.body.email)
@ -23,33 +33,31 @@ function createInvite(req, res, next) {
})
}
TeamInvitesHandler.createInvite(
teamManagerId,
subscription,
email,
function (err, inviteUserData) {
if (err) {
if (err.alreadyInTeam) {
return res.status(400).json({
error: {
code: 'user_already_added',
message: req.i18n.translate('user_already_added'),
},
})
}
if (err.limitReached) {
return res.status(400).json({
error: {
code: 'group_full',
message: req.i18n.translate('group_full'),
},
})
}
return next(err)
}
res.json({ user: inviteUserData })
try {
const invitedUserData = await TeamInvitesHandler.promises.createInvite(
teamManagerId,
subscription,
email
)
return res.json({ user: invitedUserData })
} catch (err) {
if (err.alreadyInTeam) {
return res.status(400).json({
error: {
code: 'user_already_added',
message: req.i18n.translate('user_already_added'),
},
})
}
)
if (err.limitReached) {
return res.status(400).json({
error: {
code: 'group_full',
message: req.i18n.translate('group_full'),
},
})
}
}
}
async function viewInvite(req, res, next) {
@ -178,9 +186,55 @@ function revokeInvite(req, res, next) {
)
}
async function resendInvite(req, res, next) {
const { entity: subscription } = req
const userEmail = EmailHelper.parseEmail(req.body.email)
await subscription.populate('admin_id', ['email', 'first_name', 'last_name'])
if (!userEmail) {
throw new Error('invalid email')
}
const currentInvite = subscription.teamInvites.find(
invite => invite?.email === userEmail
)
if (!currentInvite) {
return await createInvite(req, res)
}
const opts = {
to: userEmail,
admin: subscription.admin_id,
inviter: currentInvite.inviterName,
acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${currentInvite.token}/`,
reminder: true,
}
try {
await rateLimiters.resendGroupInvite.consume(userEmail)
const existingUser = await UserGetter.promises.getUserByAnyEmail(userEmail)
const emailTemplate = existingUser
? 'verifyEmailToJoinManagedUsers'
: 'inviteNewUserToJoinManagedUsers'
EmailHandler.sendDeferredEmail(emailTemplate, opts)
} catch (err) {
if (err?.remainingPoints === 0) {
return res.sendStatus(429)
} else {
throw OError.tag(err, 'Failed to resend group invite email')
}
}
return res.status(200).json({ success: true })
}
module.exports = {
createInvite,
createInvite: expressify(createInvite),
viewInvite: expressify(viewInvite),
acceptInvite: expressify(acceptInvite),
revokeInvite,
resendInvite: expressify(resendInvite),
}

View file

@ -30,6 +30,12 @@ module.exports = {
RateLimiterMiddleware.rateLimit(rateLimiters.createTeamInvite),
TeamInvitesController.createInvite
)
webRouter.post(
'/manage/groups/:id/resendInvite',
UserMembershipMiddleware.requireGroupManagementAccess,
RateLimiterMiddleware.rateLimit(rateLimiters.createTeamInvite),
TeamInvitesController.resendInvite
)
webRouter.delete(
'/manage/groups/:id/user/:user_id',
UserMembershipMiddleware.requireGroupManagementAccess,

View file

@ -310,6 +310,7 @@
"expires": "",
"export_csv": "",
"export_project_to_github": "",
"failed_to_send_group_invite_to_email": "",
"failed_to_send_managed_user_invite_to_email": "",
"fast": "",
"faster_compiles_feedback_question": "",
@ -404,6 +405,7 @@
"go_to_pdf_location_in_code": "",
"go_to_settings": "",
"group_admin": "",
"group_invite_has_been_sent_to_email": "",
"group_managed_by_group_administrator": "",
"group_plan_tooltip": "",
"group_plan_with_name_tooltip": "",
@ -850,6 +852,7 @@
"resend": "",
"resend_confirmation_email": "",
"resend_email": "",
"resend_group_invite": "",
"resend_managed_user_invite": "",
"resending_confirmation_email": "",
"resolve": "",

View file

@ -17,7 +17,7 @@ import Icon from '../../../../shared/components/icon'
import { ManagedUserAlert } from '../../utils/types'
import { useGroupMembersContext } from '../../context/group-members-context'
type ResendManagedUserInviteResponse = {
type resendInviteResponse = {
success: boolean
}
@ -40,11 +40,18 @@ export default function ManagedUserDropdownButton({
const {
runAsync: runResendManagedUserInviteAsync,
isLoading: isResendingManagedUserInvite,
} = useAsync<ResendManagedUserInviteResponse>()
} = useAsync<resendInviteResponse>()
const {
runAsync: runResendGroupInviteAsync,
isLoading: isResendingGroupInvite,
} = useAsync<resendInviteResponse>()
const userNotManaged =
!user.isEntityAdmin && !user.invite && !user.enrollment?.managedBy
const userPending = user.invite
const handleResendManagedUserInvite = useCallback(
async user => {
try {
@ -64,7 +71,7 @@ export default function ManagedUserDropdownButton({
} catch (err) {
if ((err as FetchError)?.response?.status === 429) {
setManagedUserAlert({
variant: 'resendManagedUserInviteTooManyRequests',
variant: 'resendInviteTooManyRequests',
email: user.email,
})
} else {
@ -80,10 +87,49 @@ export default function ManagedUserDropdownButton({
[setManagedUserAlert, groupId, runResendManagedUserInviteAsync]
)
const handleResendGroupInvite = useCallback(
async user => {
try {
await runResendGroupInviteAsync(
postJSON(`/manage/groups/${groupId}/resendInvite/`, {
body: {
email: user.email,
},
})
)
setManagedUserAlert({
variant: 'resendGroupInviteSuccess',
email: user.email,
})
setIsOpened(false)
} catch (err) {
if ((err as FetchError)?.response?.status === 429) {
setManagedUserAlert({
variant: 'resendInviteTooManyRequests',
email: user.email,
})
} else {
setManagedUserAlert({
variant: 'resendGroupInviteFailed',
email: user.email,
})
}
setIsOpened(false)
}
},
[setManagedUserAlert, groupId, runResendGroupInviteAsync]
)
const onResendManagedUserInviteClick = () => {
handleResendManagedUserInvite(user)
}
const onResendGroupInviteClick = () => {
handleResendGroupInvite(user)
}
const onDeleteUserClick = () => {
openOffboardingModalForUser(user)
}
@ -111,6 +157,14 @@ export default function ManagedUserDropdownButton({
/>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right managed-user-dropdown-menu">
{userPending ? (
<MenuItemButton onClick={onResendGroupInviteClick}>
{t('resend_group_invite')}
{isResendingGroupInvite ? (
<Icon type="spinner" spin style={{ marginLeft: '5px' }} />
) : null}
</MenuItemButton>
) : null}
{userNotManaged ? (
<MenuItemButton onClick={onResendManagedUserInviteClick}>
{t('resend_managed_user_invite')}

View file

@ -29,7 +29,21 @@ export default function ManagedUsersListAlert({
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendManagedUserInviteTooManyRequests':
case 'resendGroupInviteSuccess':
return (
<ResendGroupInviteSuccess
onDismiss={onDismiss}
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendGroupInviteFailed':
return (
<FailedToResendGroupInvite
onDismiss={onDismiss}
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendInviteTooManyRequests':
return (
<TooManyRequests
onDismiss={onDismiss}
@ -84,6 +98,46 @@ function FailedToResendManagedInvite({
)
}
function ResendGroupInviteSuccess({
onDismiss,
invitedUserEmail,
}: ManagedUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="success" onDismiss={onDismiss}>
<Trans
i18nKey="group_invite_has_been_sent_to_email"
values={{
email: invitedUserEmail,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
)
}
function FailedToResendGroupInvite({
onDismiss,
invitedUserEmail,
}: ManagedUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="danger" onDismiss={onDismiss}>
<Trans
i18nKey="failed_to_send_group_invite_to_email"
values={{
email: invitedUserEmail,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
)
}
function TooManyRequests({
onDismiss,
invitedUserEmail,

View file

@ -1,7 +1,9 @@
export type ManagedUserAlertVariant =
| 'resendManagedUserInviteSuccess'
| 'resendManagedUserInviteFailed'
| 'resendManagedUserInviteTooManyRequests'
| 'resendGroupInviteSuccess'
| 'resendGroupInviteFailed'
| 'resendInviteTooManyRequests'
export type ManagedUserAlert =
| {

View file

@ -494,6 +494,7 @@
"expiry": "Expiry Date",
"export_csv": "Export CSV",
"export_project_to_github": "Export Project to GitHub",
"failed_to_send_group_invite_to_email": "Failed to send Group invite to <0>__email__</0>. Please try again later.",
"failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__</0>. Please try again later.",
"faq_change_plans_or_cancel_answer": "Yes, you can do this at any time via your subscription settings. You can change plans, switch between monthly and annual billing options, or cancel to downgrade to the free plan. When cancelling, your subscription will continue until the end of the billing period. If your account temporarily does not have a subscription, the only change will be to the features available to you. Your projects will always be available on your account.",
"faq_change_plans_or_cancel_question": "Can I change plans or cancel later?",
@ -674,6 +675,7 @@
"group_admins_get_access_to": "Group admins get access to",
"group_admins_get_access_to_info": "Special features available only on group plans.",
"group_full": "This group is already full",
"group_invite_has_been_sent_to_email": "Group invite has been sent to <0>__email__</0>",
"group_managed_by_group_administrator": "User accounts in this group are managed by the group administrator.",
"group_members_and_collaborators_get_access_to": "Group members and their project collaborators get access to",
"group_members_and_collaborators_get_access_to_info": "These features are available to group members and their collaborators (other Overleaf users invited to projects owned a group member).",
@ -1391,6 +1393,7 @@
"resend": "Resend",
"resend_confirmation_email": "Resend confirmation email",
"resend_email": "Resend email",
"resend_group_invite": "Resend group invite",
"resend_managed_user_invite": "Resend managed user invite",
"resending_confirmation_email": "Resending confirmation email",
"reset_password": "Reset Password",