Skip to content
Merged
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
2 changes: 1 addition & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export default function App() {
<Route path="/sign-in" component={SignInPage} />

<Route
path="/authenticate/:authenticateToken"
path="/authenticate/:sendMethod/:authenticateToken"
component={TwoFactorAuthenticatePage}
/>

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/AuthenticateField.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,29 @@ import WithPseudoBox from './withPseudoBox'
const AuthenticateField = WithPseudoBox(function AuthenticateField({
name,
forwardedRef,
sendMethod,
...props
}) {
const [field, meta] = useField(name)
const { i18n } = useLingui()

const codeSendMessage =
sendMethod === 'email' ? (
<Trans>
We've sent you an email with an authentication code to sign into
Tracker.
</Trans>
) : (
<Trans>
We've sent an SMS to your registered phone number with an authentication
code to sign into Tracker.
</Trans>
)

return (
<FormControl isInvalid={meta.error && meta.touched}>
<FormLabel htmlFor="twoFactorCode" fontWeight="bold" mb="2">
{codeSendMessage}{' '}
<Trans>Please enter your two factor code below.</Trans>
</FormLabel>
<InputGroup>
Expand All @@ -50,6 +65,7 @@ const AuthenticateField = WithPseudoBox(function AuthenticateField({
AuthenticateField.propTypes = {
name: string.isRequired,
forwardedRef: oneOfType([func, shape({ current: elementType })]),
sendMethod: string.isRequired,
}

const withForwardedRef = React.forwardRef((props, ref) => {
Expand Down
45 changes: 40 additions & 5 deletions frontend/src/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ import EmailField from './EmailField'
import { fieldRequirements } from './fieldRequirements'
import { TrackerButton } from './TrackerButton'
import { LoadingMessage } from './LoadingMessage'
import { useUserState } from './UserState'
import { useLingui } from '@lingui/react'

export default function SignInPage() {
const { login } = useUserState()
const { i18n } = useLingui()
const history = useHistory()
const toast = useToast()

const validationSchema = object().shape({
password: string().required(
fieldRequirements.password.required.message,
),
password: string().required(fieldRequirements.password.required.message),
email: string()
.required(fieldRequirements.email.required.message)
.email(fieldRequirements.email.email.message),
Expand All @@ -45,8 +47,41 @@ export default function SignInPage() {
})
},
onCompleted({ signIn }) {
// redirect to the authenticate page
history.push(`/authenticate/${signIn.authenticateToken}`)
// 2FA not enabled
if (signIn.result.__typename === 'RegularSignInResult') {
login({
jwt: signIn.result.authResult.authToken,
tfa: signIn.result.authResult.user.tfaValidated,
userName: signIn.result.authResult.user.userName,
})
// // redirect to the home page.
history.push('/')
// // Display a welcome message
toast({
title: i18n._(t`Sign In.`),
description: i18n._(t`Welcome, you are successfully signed in!`),
status: 'success',
duration: 9000,
isClosable: true,
})
}
// 2FA enabled
else if (signIn.result.__typename === 'TFASignInResult') {
// redirect to the authenticate page
history.push(
`/authenticate/${signIn.result.sendMethod}/${signIn.result.authenticateToken}`,
)
} else {
toast({
title: t`Incorrect send method received.`,
description: t`Incorrect signIn.result typename.`,
status: 'error',
duration: 9000,
isClosable: true,
position: 'top-left',
})
console.log('Incorrect signIn.result typename.')
}
},
})

Expand Down
8 changes: 6 additions & 2 deletions frontend/src/TwoFactorAuthenticatePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function TwoFactorAuthenticatePage() {
const history = useHistory()
const toast = useToast()
const { i18n } = useLingui()
const { authenticateToken } = useParams()
const { sendMethod, authenticateToken } = useParams()

const validationSchema = object().shape({
twoFactorCode: number()
Expand Down Expand Up @@ -91,7 +91,11 @@ export default function TwoFactorAuthenticatePage() {
<Trans>Two Factor Authentication</Trans>
</Heading>

<AuthenticateField name="twoFactorCode" mb="4" />
<AuthenticateField
name="twoFactorCode"
mb="4"
sendMethod={sendMethod}
/>

<Button isLoading={isSubmitting} type="submit" variantColor="teal">
<Trans>Submit</Trans>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/__tests__/DmarcByDomainPage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('<DmarcByDomainPage />', () => {
query: FORWARD,
variables: {
month: 'LAST30DAYS',
year: '2020',
year: '2021',
first: 10,
},
},
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('<DmarcByDomainPage />', () => {
{
request: {
query: FORWARD,
variables: { first: 10, month: 'LAST30DAYS', year: '2020' },
variables: { first: 10, month: 'LAST30DAYS', year: '2021' },
},
result: rawDmarcReportSummaryTableData,
},
Expand Down Expand Up @@ -96,7 +96,7 @@ describe('<DmarcByDomainPage />', () => {
query: FORWARD,
variables: {
month: 'LAST30DAYS',
year: '2020',
year: '2021',
first: 10,
},
},
Expand Down
207 changes: 147 additions & 60 deletions frontend/src/__tests__/SignInPage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,77 +83,164 @@ describe('<SignInPage />', () => {
})

describe('when sign-in succeeds', () => {
it('redirects to authenticate', async () => {
const values = {
email: 'testuser@testemail.ca',
password: 'testuserpassword',
authenticateToken: 'authenticate-token-test',
}

const mocks = [
{
request: {
query: SIGN_IN,
variables: {
userName: values.email,
password: values.password,
describe('and 2fa is enabled', () => {
it('redirects to authenticate', async () => {
const values = {
email: 'testuser@testemail.ca',
password: 'testuserpassword',
authenticateToken: 'authenticate-token-test',
}

const mocks = [
{
request: {
query: SIGN_IN,
variables: {
userName: values.email,
password: values.password,
},
},
},
result: {
data: {
signIn: {
authenticateToken: values.authenticateToken,
result: {
data: {
signIn: {
result: {
authenticateToken: values.authenticateToken,
sendMethod: 'email',
__typename: 'TFASignInResult',
},
},
},
},
},
},
]

// create a history object and inject it so we can inspect it afterwards
// for the side effects of our form submission (a redirect to /!).
const history = createMemoryHistory({
initialEntries: ['/sign-in'],
initialIndex: 0,
})
]

// create a history object and inject it so we can inspect it afterwards
// for the side effects of our form submission (a redirect to /!).
const history = createMemoryHistory({
initialEntries: ['/sign-in'],
initialIndex: 0,
})

const { container, getByRole } = render(
<UserStateProvider
initialState={{ userName: null, jwt: null, tfa: null }}
>
<ThemeProvider theme={theme}>
<I18nProvider i18n={i18n}>
<Router history={history}>
<MockedProvider mocks={mocks} addTypename={false}>
<SignInPage />
</MockedProvider>
</Router>
</I18nProvider>
</ThemeProvider>
</UserStateProvider>,
)

const { container, getByRole } = render(
<UserStateProvider
initialState={{ userName: null, jwt: null, tfa: null }}
>
<ThemeProvider theme={theme}>
<I18nProvider i18n={i18n}>
<Router history={history}>
<MockedProvider mocks={mocks} addTypename={false}>
<SignInPage />
</MockedProvider>
</Router>
</I18nProvider>
</ThemeProvider>
</UserStateProvider>,
)
const email = container.querySelector('#email')
const password = container.querySelector('#password')
const form = getByRole('form')

const email = container.querySelector('#email')
const password = container.querySelector('#password')
const form = getByRole('form')
fireEvent.change(email, {
target: {
value: values.email,
},
})

fireEvent.change(email, {
target: {
value: values.email,
},
})
fireEvent.change(password, {
target: {
value: values.password,
},
})

fireEvent.submit(form)

fireEvent.change(password, {
target: {
value: values.password,
},
await waitFor(() => {
expect(history.location.pathname).toEqual(
`/authenticate/email/${values.authenticateToken}`,
)
})
})
})
describe('and 2fa is NOT enabled', () => {
it('redirects to home page', async () => {
const values = {
email: 'testuser@testemail.ca',
password: 'testuserpassword',
}

const mocks = [
{
request: {
query: SIGN_IN,
variables: {
userName: values.email,
password: values.password,
},
},
result: {
data: {
signIn: {
result: {
authResult: {
user: {
userName: 'Thalia.Rosenbaum@gmail.com',
tfa: false,
},
authToken: 'test123stringJWT',
},
__typename: 'RegularSignInResult',
},
},
},
},
},
]

// create a history object and inject it so we can inspect it afterwards
// for the side effects of our form submission (a redirect to /!).
const history = createMemoryHistory({
initialEntries: ['/sign-in'],
initialIndex: 0,
})

const { container, getByRole } = render(
<UserStateProvider
initialState={{ userName: null, jwt: null, tfa: null }}
>
<ThemeProvider theme={theme}>
<I18nProvider i18n={i18n}>
<Router history={history}>
<MockedProvider mocks={mocks} addTypename={false}>
<SignInPage />
</MockedProvider>
</Router>
</I18nProvider>
</ThemeProvider>
</UserStateProvider>,
)

fireEvent.submit(form)
const email = container.querySelector('#email')
const password = container.querySelector('#password')
const form = getByRole('form')

await waitFor(() => {
expect(history.location.pathname).toEqual(
`/authenticate/${values.authenticateToken}`,
)
fireEvent.change(email, {
target: {
value: values.email,
},
})

fireEvent.change(password, {
target: {
value: values.password,
},
})

fireEvent.submit(form)

await waitFor(() => {
expect(history.location.pathname).toEqual('/')
})
})
})
})
Expand Down
Loading