Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions frontend/e2e/tests/invite-test.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,32 @@ test.describe('Invite Tests', () => {
await setText("[name='currentPassword']", PASSWORD)
await click(byId('delete-account'))
});

test('An invite opened while signed in asks before joining @oss', async ({ page }) => {
const { click, getInputValue, login, waitForElementVisible } = createHelpers(page);

log('Login')
await login(E2E_USER, PASSWORD)
log('Get Invite url')
await waitForElementVisible(byId('organisation-link'))
await click(byId('organisation-link'))
await waitForElementVisible(byId('org-settings-link'))
await click(byId('org-settings-link'))
await getInputValue(byId('organisation-name'))
await click(byId('users-and-permissions'))
const hasAdminLink = await page.locator(byId('invite-link')).waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false)
if (!hasAdminLink) {
await click(byId('invite-role-select-option-1'))
await waitForElementVisible(byId('invite-link'))
}
const inviteLink = await getInputValue(byId('invite-link'))

log('Open the invite with the session still live')
await page.goto(inviteLink)
// The signup form is what this used to show, despite being signed in.
await expect(page.getByText('Accept your invitation')).toBeVisible({ timeout: LONG_TIMEOUT })
await expect(page.getByText(E2E_USER)).toBeVisible()
await expect(page.locator(byId('firstName'))).toHaveCount(0)
await expect(page.getByRole('button', { name: 'Use a different account' })).toBeVisible()
});
});
65 changes: 0 additions & 65 deletions frontend/web/components/pages/InvitePage.js

This file was deleted.

126 changes: 126 additions & 0 deletions frontend/web/components/pages/InvitePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React, { FC, useEffect, useState } from 'react'
import { useHistory, useParams } from 'react-router-dom'
import AccountProvider from 'common/providers/AccountProvider'
import AppActions from 'common/dispatcher/app-actions'
import Constants from 'common/constants'
import Utils from 'common/utils/utils'
import API from 'project/api'
import Button from 'components/base/forms/Button'
import Card from 'components/Card'
import Loader from 'components/Loader'

const getErrorMessage = (error: string) => {
switch (error) {
case 'No Invite matches the given query.':
case 'Not found.':
return 'We could not validate your invite, please check the invite URL and email address you have entered is correct.'
case 'Please upgrade your plan to add additional seats/users':
return 'The organisation you have been invited to has no seats available. Please contact the organisation administrator to resolve this before trying again.'
default:
return error
}
}

const InvitePage: FC = () => {
const { id } = useParams<{ id: string }>()
const history = useHistory()
const [isAccepting, setIsAccepting] = useState(false)

// Carried in the URL rather than in storage, because switching account clears
// storage on the way out.
const signInUrl = `/?redirect=${encodeURIComponent(
document.location.pathname,
)}`
const hasSession = !!API.getCookie('t')

useEffect(() => {
// Recorded so a session restoring in the background knows an invite is in
// play. Without it, a user who belongs to no organisation yet gets routed
// to organisation creation instead of this page. acceptInvite clears it.
API.setInviteType(
document.location.pathname.includes('/invite-link/')
? 'INVITE_LINK'
: 'INVITE_EMAIL',
)
API.setInvite(id)
API.trackPage(Constants.pages.INVITE)
}, [id])

useEffect(() => {
if (!hasSession) {
history.replace(signInUrl)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasSession])

const onSave = (organisationId: string) => {
AppActions.selectOrganisation(organisationId)
history.replace(Utils.getOrganisationHomePage(organisationId))
}

return (
<div className='app-container'>
<AccountProvider
onSave={onSave}
onLogout={() => history.replace(signInUrl)}
>
{({ error, user }: { error?: string; user?: { email?: string } }) => {
if (error) {
return (
<div className='centered-container'>
<div>
<h3 className='pt-5'>Oops</h3>
<p>{getErrorMessage(error)}</p>
<Button onClick={() => history.replace(signInUrl)}>
Sign in
</Button>
</div>
</div>
)
}

// Either the session is still being restored, or we are on our way to
// sign in, or the invite is being accepted.
if (!user?.email || isAccepting) {
return (
<div className='centered-container'>
<Loader />
</div>
)
}

return (
<div className='centered-container'>
<Card className='p-4'>
<h5>Accept your invitation</h5>
<p className='mb-4'>
You are signed in as <strong>{user.email}</strong>. Joining
adds this organisation to that account.
</p>
<Button
data-test='accept-invite-btn'
className='full-width'
onClick={() => {
setIsAccepting(true)
AppActions.acceptInvite(id)
}}
>
Accept invitation
</Button>
<Button
theme='text'
className='mt-3'
onClick={() => AppActions.logout()}
>
Use a different account
</Button>
</Card>
</div>
)
}}
</AccountProvider>
</div>
)
}

export default InvitePage
8 changes: 6 additions & 2 deletions frontend/web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,10 @@ if (event) {
} catch (e) {}
}

const isInvite = document.location.href.includes('invite')
const isOauth =
document.location.href.includes('/oauth') &&
!document.location.pathname.startsWith('/oauth/authorize')
if (res && !isInvite && !isOauth) {
if (res && !isOauth) {
AppActions.setToken(res)
}

Expand All @@ -75,6 +74,11 @@ function isPublicURL() {
'/saml',
'/signup',
'/login',
// Reachable either way: InvitePage sends you to sign in if you have no
// session, and asks before joining if you do. Bouncing from here instead
// would flash the signup form at someone already signed in.
'/invite',
'/invite-link',
]

return publicPaths.some(
Expand Down
Loading