mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
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:
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user