fix(client): put /update-email behind authentication (#54128)

Co-authored-by: Just-A-Pixel <Raj.Anand0511@gmail.com>
Co-authored-by: sembauke <semboot699@gmail.com>
This commit is contained in:
Huyen Nguyen
2024-07-02 08:08:52 -07:00
committed by GitHub
parent b2dba74e28
commit f8fa98ec3a
5 changed files with 195 additions and 155 deletions
@@ -0,0 +1,140 @@
import { Link } from 'gatsby';
import React, {
useState,
useEffect,
type FormEvent,
type ChangeEvent
} from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import isEmail from 'validator/lib/isEmail';
import {
Container,
FormGroup,
FormControl,
ControlLabel,
Col,
Row,
Button
} from '@freecodecamp/ui';
import envData from '../../config/env.json';
import { Spacer, Loader } from '../components/helpers';
import './show-update-email.css';
import { isSignedInSelector, userSelector } from '../redux/selectors';
import { hardGoTo as navigate } from '../redux/actions';
import { updateMyEmail } from '../redux/settings/actions';
import { maybeEmailRE } from '../utils';
const { apiLocation } = envData;
interface ShowUpdateEmailProps {
isNewEmail: boolean;
updateMyEmail: (e: string) => void;
path?: string;
isSignedIn: boolean;
navigate: (location: string) => void;
}
const mapStateToProps = createSelector(
userSelector,
isSignedInSelector,
(
{ email, emailVerified }: { email: string; emailVerified: boolean },
isSignedIn
) => ({
isNewEmail: !email || emailVerified,
isSignedIn
})
);
const mapDispatchToProps = { updateMyEmail, navigate };
function ShowUpdateEmail({
isNewEmail,
updateMyEmail,
isSignedIn,
navigate
}: ShowUpdateEmailProps) {
const { t } = useTranslation();
const [emailValue, setEmailValue] = useState('');
useEffect(() => {
if (!isSignedIn) navigate(`${apiLocation}/signin`);
}, [isSignedIn, navigate]);
if (!isSignedIn) return <Loader fullScreen={true} />;
function handleSubmit(event: FormEvent) {
event.preventDefault();
updateMyEmail(emailValue);
}
function onChange(event: ChangeEvent<HTMLInputElement>) {
setEmailValue(event.target.value);
return null;
}
const emailValidationState = maybeEmailRE.test(emailValue)
? isEmail(emailValue)
? 'success'
: 'error'
: null;
return (
<>
<Helmet>
<title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
</Helmet>
<Container>
<Spacer size='medium' />
<h2 className='text-center'>{t('misc.update-email-2')}</h2>
<Row>
<Col sm={6} smOffset={3}>
<Row>
<form
onSubmit={handleSubmit}
data-playwright-test-label='update-email-form'
>
<FormGroup
className='update-email-field'
validationState={emailValidationState}
>
<ControlLabel htmlFor='emailInput'>
{t('misc.email')}
</ControlLabel>
<FormControl
id='emailInput'
onChange={onChange}
placeholder='camperbot@example.com'
required={true}
type='email'
/>
</FormGroup>
<Button
block={true}
size='large'
variant='primary'
disabled={emailValidationState !== 'success'}
type='submit'
>
{isNewEmail
? t('buttons.update-email')
: t('buttons.verify-email')}
</Button>
</form>
<p className='text-center'>
<Link to='/signout'>{t('buttons.sign-out')}</Link>
</p>
</Row>
</Col>
</Row>
</Container>
</>
);
}
ShowUpdateEmail.displayName = 'ShowUpdateEmail';
export default connect(mapStateToProps, mapDispatchToProps)(ShowUpdateEmail);
+13 -124
View File
@@ -1,131 +1,20 @@
import { Link } from 'gatsby';
import { isString } from 'lodash-es';
import React, { useState, type FormEvent, type ChangeEvent } from 'react';
import Helmet from 'react-helmet';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import isEmail from 'validator/lib/isEmail';
import {
Container,
FormGroup,
FormControl,
ControlLabel,
Col,
Row,
Button
} from '@freecodecamp/ui';
import { Router } from '@reach/router';
import { withPrefix } from 'gatsby';
import React from 'react';
import { Spacer } from '../components/helpers';
import './update-email.css';
import { userSelector } from '../redux/selectors';
import { updateMyEmail } from '../redux/settings/actions';
import { maybeEmailRE } from '../utils';
interface UpdateEmailProps {
isNewEmail: boolean;
t: TFunction;
updateMyEmail: (e: string) => void;
}
const mapStateToProps = createSelector(
userSelector,
({ email, emailVerified }: { email: string; emailVerified: boolean }) => ({
isNewEmail: !email || emailVerified
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ updateMyEmail }, dispatch);
function UpdateEmail({ isNewEmail, t, updateMyEmail }: UpdateEmailProps) {
const [emailValue, setEmailValue] = useState('');
function handleSubmit(event: FormEvent) {
event.preventDefault();
updateMyEmail(emailValue);
}
function onChange(event: ChangeEvent<HTMLInputElement>) {
const newEmailValue = event.target.value;
if (!isString(newEmailValue)) {
return null;
}
setEmailValue(newEmailValue);
return null;
}
function getEmailValidationState() {
if (maybeEmailRE.test(emailValue)) {
return isEmail(emailValue) ? 'success' : 'error';
}
return null;
}
import ShowUpdateEmail from '../client-only-routes/show-update-email';
import RedirectHome from '../components/redirect-home';
function UpdateEmail(): JSX.Element {
return (
<>
<Helmet>
<title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
</Helmet>
<Container>
<Spacer size='medium' />
<h2
className='text-center'
data-playwright-test-label='update-email-heading'
>
{t('misc.update-email-2')}
</h2>
<Row>
<Col sm={6} smOffset={3}>
<Row>
<form
onSubmit={handleSubmit}
data-playwright-test-label='update-email-form'
>
<FormGroup
className='update-email-field'
validationState={getEmailValidationState()}
>
<ControlLabel htmlFor='emailInput'>
{t('misc.email')}
</ControlLabel>
<FormControl
id='emailInput'
onChange={onChange}
placeholder='camperbot@example.com'
required={true}
type='email'
/>
</FormGroup>
<Button
block={true}
size='large'
variant='primary'
disabled={getEmailValidationState() !== 'success'}
type='submit'
>
{isNewEmail
? t('buttons.update-email')
: t('buttons.verify-email')}
</Button>
</form>
<p className='text-center'>
<Link to='/signout'>{t('buttons.sign-out')}</Link>
</p>
</Row>
</Col>
</Row>
</Container>
</>
<Router>
<ShowUpdateEmail path={withPrefix('/update-email')} />
<RedirectHome default />
</Router>
);
}
UpdateEmail.displayName = 'Update-Email';
UpdateEmail.displayName = 'UpdateEmail';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(UpdateEmail));
export default UpdateEmail;
-1
View File
@@ -9,7 +9,6 @@ import { Container, Row, Col, Button } from '@freecodecamp/ui';
import BigCallToAction from '../components/landing/components/big-call-to-action';
import { Spacer } from '../components/helpers';
import './update-email.css';
import {
isSignedInSelector,
isDonatingSelector,
+42 -30
View File
@@ -1,33 +1,24 @@
import { test, expect, type Page } from '@playwright/test';
import { test, expect } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
let page: Page;
test.describe('The update-email page when the user is signed in', () => {
test.use({ storageState: 'playwright/.auth/certified-user.json' });
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
await page.goto('/update-email');
});
test.beforeEach(async ({ page }) => {
await page.goto('/update-email');
});
test.describe('The update-email page', () => {
test('The page renders with correct title', async () => {
test('should display the content correctly', async ({ page }) => {
await expect(page).toHaveTitle(
'Update your email address | freeCodeCamp.org'
);
});
await expect(
page.getByRole('heading', { name: 'Update your email address here' })
).toBeVisible();
test('The page has correct header', async () => {
const header = page.getByTestId('update-email-heading');
await expect(header).toBeVisible();
await expect(header).toContainText(translations.misc['update-email-2']);
});
test('The page has update email form', async () => {
const form = page.getByTestId('update-email-form');
const emailInput = page.getByLabel(translations.misc.email);
const submitButton = page.getByRole('button', {
name: translations.buttons['update-email']
});
const emailInput = page.getByLabel('Email');
const submitButton = page.getByRole('button', { name: 'Update my Email' });
await expect(form).toBeVisible();
await expect(emailInput).toBeVisible();
@@ -38,22 +29,19 @@ test.describe('The update-email page', () => {
);
await expect(submitButton).toBeVisible();
await expect(submitButton).toHaveAttribute('type', 'submit');
});
test('The page has sign out button', async () => {
const signOutButton = page.getByRole('link', {
name: translations.buttons['sign-out']
});
const signOutButton = page.getByRole('link', { name: 'Sign out' });
await expect(signOutButton).toBeVisible();
await expect(signOutButton).toHaveAttribute('href', '/signout');
});
test('should enable the submit button if the email input is valid', async () => {
test('should enable the submit button if the email input is valid', async ({
page
}) => {
const emailInput = page.getByLabel(translations.misc.email);
const submitButton = page.getByRole('button', {
name: translations.buttons['update-email']
});
const submitButton = page.getByRole('button', { name: 'Update my Email' });
await expect(submitButton).toBeDisabled();
await emailInput.fill('123');
await expect(submitButton).toBeDisabled();
@@ -61,3 +49,27 @@ test.describe('The update-email page', () => {
await expect(submitButton).toBeEnabled();
});
});
test.describe('The update-email page when the user is not signed in', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test.beforeEach(async ({ page }) => {
await page.goto('/update-email');
});
test('should sign the user in and redirect them to /learn', async ({
page,
browserName
}) => {
// The signin step involves multiple navigations, which results a network error in Firefox.
// The error is harmless but Playwright doesn't suppress it, causing the test to fail.
// Ref: https://github.com/microsoft/playwright/issues/20749
test.skip(browserName === 'firefox');
await page.waitForURL(/\/learn\/?/);
await expect(
page.getByRole('heading', { name: 'Welcome back, Full Stack User' })
).toBeVisible();
});
});