[CORL-516] forgot password flow in admin (#2558)

* add forgot password page and link to admin

* add translations

* update snaps

* fix: install patch

* fix: adapted localizations

* fix: added email type to email field

* fix: updated snapshots
This commit is contained in:
Tessa Thornton
2019-09-12 16:30:19 +00:00
committed by Wyatt Johnson
parent 2559d1c0e8
commit a89dc4675a
18 changed files with 416 additions and 47 deletions
+2
View File
@@ -16,6 +16,7 @@ import {
OrganizationConfigRoute,
WordListConfigRoute,
} from "./routes/Configure/sections";
import ForgotPasswordRoute from "./routes/ForgotPassword";
import InviteRoute from "./routes/Invite";
import LoginRoute from "./routes/Login";
import ModerateRoute from "./routes/Moderate";
@@ -79,5 +80,6 @@ export default makeRouteConfig(
</Route>
<Route path="invite" {...InviteRoute.routeConfig} />
<Route path="login" {...LoginRoute.routeConfig} />
<Route path="forgot-password" {...ForgotPasswordRoute.routeConfig} />
</Route>
);
@@ -0,0 +1,30 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import Main from "coral-auth/components/Main";
import { HorizontalGutter, Typography } from "coral-ui/components";
interface Props {
email: string;
}
const CheckEmail: FunctionComponent<Props> = ({ email }) => (
<div data-testid="forgotPassword-checkEmail-container">
<Main data-testid="forgotPassword-checkEmail-main">
<HorizontalGutter size="full">
<Localized
id="forgotPassword-checkEmail-receiveEmail"
$email={email}
strong={<strong />}
>
<Typography variant="bodyCopy">
If there is an account associated with <strong>{email}</strong>, you
will receive an email with a link to create a new password.
</Typography>
</Localized>
</HorizontalGutter>
</Main>
</div>
);
export default CheckEmail;
@@ -0,0 +1,11 @@
.container {
width: 350px;
background-color: #f5f5f5;
border: 1px solid var(--palette-grey-lighter);
margin-top: 70px;
box-sizing: border-box;
}
.textLink {
color: var(--palette-primary-main);
}
@@ -0,0 +1,47 @@
import { Localized } from "fluent-react/compat";
import { Link } from "found";
import React, { FunctionComponent, useState } from "react";
import { Bar, SubBar, Title } from "coral-auth/components/Header";
import { Flex, Typography } from "coral-ui/components";
import CheckEmail from "./CheckEmail";
import ForgotPasswordForm from "./ForgotPasswordForm";
import styles from "./ForgotPasswordContainer.css";
const ForgotPasswordContainer: FunctionComponent = () => {
const [checkEmail, setCheckEmail] = useState<string | null>(null);
return (
<Flex justifyContent="center">
<div className={styles.container}>
<Bar>
{!checkEmail ? (
<Localized id="forgotPassword-forgotPasswordHeader">
<Title>Forgot Password?</Title>
</Localized>
) : (
<Localized id="forgotPassword-checkEmailHeader">
<Title>Check Email</Title>
</Localized>
)}
</Bar>
<SubBar>
<Typography variant="bodyCopy" container={Flex}>
<Localized id="forgotPassword-gotBackToSignIn">
<Link className={styles.textLink} to="/admin/login">
Go back to sign in page
</Link>
</Localized>
</Typography>
</SubBar>
{checkEmail ? (
<CheckEmail email={checkEmail} />
) : (
<ForgotPasswordForm onCheckEmail={setCheckEmail} />
)}
</div>
</Flex>
);
};
export default ForgotPasswordContainer;
@@ -0,0 +1,120 @@
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback } from "react";
import { Field, Form } from "react-final-form";
import Main from "coral-auth/components/Main";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
import { useMutation } from "coral-framework/lib/relay";
import {
composeValidators,
required,
validateEmail,
} from "coral-framework/lib/validation";
import {
Button,
CallOut,
FormField,
HorizontalGutter,
InputLabel,
TextField,
Typography,
} from "coral-ui/components";
import ForgotPasswordMutation from "./ForgotPasswordMutation";
interface FormProps {
email: string;
}
interface Props {
onCheckEmail: (email: string) => void;
}
const ForgotPasswordForm: FunctionComponent<Props> = props => {
const forgotPassword = useMutation(ForgotPasswordMutation);
const onSubmit = useCallback(
async (form: FormProps) => {
try {
await forgotPassword(form);
props.onCheckEmail(form.email);
} catch (error) {
if (error instanceof InvalidRequestError) {
return error.invalidArgs;
}
return { [FORM_ERROR]: error.message };
}
return;
},
[forgotPassword]
);
return (
<div data-testid="admin-forgotPassword-container">
<Main data-testid="admin-forgotPassword-main">
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="full">
<Localized id="forgotPassword-enterEmailAndGetALink">
<Typography variant="bodyCopy">
Enter your email address below and we will send you a link
to reset your password.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="email"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="forgotPassword-emailAddressLabel">
<InputLabel htmlFor={input.name}>
Email Address
</InputLabel>
</Localized>
<Localized
id="forgotPassword-emailAddressTextField"
attrs={{ placeholder: true }}
>
<TextField
id={input.name}
placeholder="Email Address"
color={colorFromMeta(meta)}
disabled={submitting}
type="email"
fullWidth
{...input}
/>
</Localized>
<ValidationMessage meta={meta} fullWidth />
</FormField>
)}
</Field>
<Localized id="forgotPassword-sendEmailButton">
<Button
variant="filled"
color="primary"
size="large"
fullWidth
type="submit"
disabled={submitting}
>
Send Email
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
export default ForgotPasswordForm;
@@ -0,0 +1,12 @@
import { pick } from "lodash";
import { createMutation } from "coral-framework/lib/relay";
import { forgotPassword, ForgotPasswordInput } from "coral-framework/rest";
const ForgotPasswordMutation = createMutation(
"forgotPassword",
(_, input: ForgotPasswordInput, { rest }) =>
forgotPassword(rest, pick(input, ["email"]))
);
export default ForgotPasswordMutation;
@@ -0,0 +1,11 @@
import { withRouteConfig } from "coral-framework/lib/router";
import React from "react";
import ForgotPasswordContainer from "./ForgotPasswordContainer";
const ForgotPasswordRoute: React.FunctionComponent = () => {
return <ForgotPasswordContainer />;
};
const enhanced = withRouteConfig({})(ForgotPasswordRoute);
export default enhanced;
@@ -0,0 +1 @@
export { default, default as ForgotPasswordRoute } from "./ForgotPasswordRoute";
@@ -0,0 +1,4 @@
.textLink {
color: var(--palette-primary-main);
text-decoration: underline;
}
@@ -1,4 +1,5 @@
import { Localized } from "fluent-react/compat";
import { Link } from "found";
import React, { FunctionComponent } from "react";
import { Field, Form } from "react-final-form";
@@ -13,13 +14,17 @@ import {
Button,
ButtonIcon,
CallOut,
Flex,
FormField,
HorizontalGutter,
InputLabel,
Typography,
} from "coral-ui/components";
import EmailField from "../../EmailField";
import styles from "./SignInWithEmail.css";
interface FormProps {
email: string;
password: string;
@@ -62,6 +67,18 @@ const SignInWithEmail: FunctionComponent<SignInWithEmailForm> = props => {
/>
</Localized>
<ValidationMessage meta={meta} fullWidth />
<Flex justifyContent="flex-end">
<Typography>
<Localized id="login-signIn-forgot-password">
<Link
className={styles.textLink}
to="/admin/forgot-password"
>
Forgot your password?
</Link>
</Localized>
</Typography>
</Flex>
</FormField>
)}
</Field>
@@ -74,6 +91,7 @@ const SignInWithEmail: FunctionComponent<SignInWithEmailForm> = props => {
fullWidth
>
<ButtonIcon size="md">email</ButtonIcon>
<Localized id="login-signIn-signInWithEmail">
<span>Sign in with Email</span>
</Localized>
@@ -94,6 +94,21 @@ exports[`accepts correct password 1`] = `
</div>
</div>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<a
className="SignInWithEmail-textLink"
href="/admin/forgot-password"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
@@ -215,6 +230,21 @@ exports[`accepts valid email 1`] = `
This field is required.
</span>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<a
className="SignInWithEmail-textLink"
href="/admin/forgot-password"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
@@ -349,6 +379,21 @@ exports[`checks for invalid email 1`] = `
This field is required.
</span>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<a
className="SignInWithEmail-textLink"
href="/admin/forgot-password"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
@@ -504,6 +549,21 @@ exports[`renders sign in form 1`] = `
</div>
</div>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<a
className="SignInWithEmail-textLink"
href="/admin/forgot-password"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
@@ -652,6 +712,21 @@ exports[`shows error when submitting empty form 1`] = `
This field is required.
</span>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<a
className="SignInWithEmail-textLink"
href="/admin/forgot-password"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
@@ -767,6 +842,21 @@ exports[`shows server error 1`] = `
</div>
</div>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<a
className="SignInWithEmail-textLink"
href="/admin/forgot-password"
onClick={[Function]}
>
Forgot your password?
</a>
</p>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
@@ -245,7 +245,7 @@ each of your sites stories.
<p
className="Box-root Typography-root Typography-bodyShort Typography-colorTextPrimary"
>
When enabled, there will be real-time loading and updating of comments.
When enabled, there will be real-time loading and updating of comments.
When disabled, users will have to refresh the page to see new comments.
</p>
<div>
@@ -211,9 +211,9 @@ exports[`renders configure general 1`] = `
<p
className="Box-root Typography-root Typography-bodyShort Typography-colorTextPrimary"
>
Open or close comment streams for new comments sitewide.
When new comments are turned off, new comments cannot be
submitted, but existing comments can continue to receive
Open or close comment streams for new comments sitewide.
When new comments are turned off, new comments cannot be
submitted, but existing comments can continue to receive
reactions, be reported, and be shared.
</p>
<fieldset
@@ -386,9 +386,9 @@ reactions, be reported, and be shared.
<p
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
>
This will appear above the comments sitewide.
You can format the text using Markdown.
More information on how to use Markdown
This will appear above the comments sitewide.
You can format the text using Markdown.
More information on how to use Markdown
<a
className="ExternalLink-root"
@@ -858,7 +858,7 @@ moderation panel.
className="Box-root Typography-root Typography-bodyShort Typography-colorTextPrimary"
>
Allow your community to engage with one another and express themselves
with one-click reactions. By default, Coral allows commenters to "Respect"
with one-click reactions. By default, Coral allows commenters to "Respect"
each other's comments.
</p>
<div
@@ -318,9 +318,9 @@ approved by a moderator.
<p
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
>
Prevents repeat offenders from publishing comments without approval.
When a commenter's rejection rate is above the threshold, their
comments are sent to Pending for moderator approval. This does not
Prevents repeat offenders from publishing comments without approval.
When a commenter's rejection rate is above the threshold, their
comments are sent to Pending for moderator approval. This does not
apply to Staff comments.
</p>
<div>
@@ -385,8 +385,8 @@ apply to Staff comments.
<p
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
>
Rejected comments ÷ (rejected comments + published comments)
over the timeframe above, as a percentage. It does not include
Rejected comments ÷ (rejected comments + published comments)
over the timeframe above, as a percentage. It does not include
comments pending for toxicity, spam or pre-moderation.
</p>
<div
@@ -435,9 +435,9 @@ comments pending for toxicity, spam or pre-moderation.
<p
className="Box-root Typography-root Typography-bodyShort Typography-colorTextPrimary"
>
Using the Perspective API, the Toxic Comment filter warns users
when comments exceed the predefined toxicity threshold.
Comments with a toxicity score above the threshold
Using the Perspective API, the Toxic Comment filter warns users
when comments exceed the predefined toxicity threshold.
Comments with a toxicity score above the threshold
<strong>
will not be published
@@ -447,7 +447,7 @@ the
<strong>
Pending Queue for review by a moderator
</strong>
.
.
If approved by a moderator, the comment will be published.
</p>
<fieldset
@@ -566,7 +566,7 @@ comment is toxic, according to Perspective API. By default the threshold is set
<p
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
>
Choose your Perspective Model. The default is TOXICITY.
Choose your Perspective Model. The default is TOXICITY.
You can find out more about model choices
<a
className="ExternalLink-root"
@@ -783,9 +783,9 @@ improve the API over time.
<p
className="Box-root Typography-root Typography-bodyShort Typography-colorTextPrimary"
>
The Akismet API filter warns users when a comment is determined likely
to be spam. Comments that Akismet thinks are spam will not be published
and are placed in the Pending Queue for review by a moderator.
The Akismet API filter warns users when a comment is determined likely
to be spam. Comments that Akismet thinks are spam will not be published
and are placed in the Pending Queue for review by a moderator.
If approved by a moderator, the comment will be published.
</p>
<fieldset