[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 12:30:19 -04: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
@@ -119,6 +119,9 @@ class AddOrganizationStep extends React.Component<Props> {
placeholder="Organization Contact Email"
color={colorFromMeta(meta)}
disabled={submitting}
type="email"
autoCapitalize="off"
autoCorrect="off"
fullWidth
{...input}
/>
@@ -89,6 +89,9 @@ class CreateYourAccountStep extends Component<Props> {
placeholder="Email"
color={colorFromMeta(meta)}
disabled={submitting}
type="email"
autoCapitalize="off"
autoCorrect="off"
fullWidth
{...input}
/>
@@ -121,6 +124,8 @@ class CreateYourAccountStep extends Component<Props> {
placeholder="Username"
color={colorFromMeta(meta)}
disabled={submitting}
autoCapitalize="off"
autoCorrect="off"
fullWidth
{...input}
/>
+41 -26
View File
@@ -59,6 +59,7 @@ login-signIn-passwordTextField =
login-signIn-signInWithEmail = Sign in with Email
login-signIn-orSeparator = Or
login-signIn-forgot-password = Forgot your password?
login-signInWithFacebook = Sign in with Facebook
login-signInWithGoogle = Sign in with Google
@@ -87,9 +88,9 @@ configure-permissionField-dontAllow = Don't allow
### General
configure-general-guidelines-title = Community guidelines summary
configure-general-guidelines-explanation =
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
<externalLink>here</externalLink>.
configure-general-guidelines-showCommunityGuidelines = Show community guidelines summary
@@ -100,9 +101,9 @@ configure-general-locale-chooseLanguage = Choose the language for your Coral com
#### Sitewide Commenting
configure-general-sitewideCommenting-title = Sitewide commenting
configure-general-sitewideCommenting-explanation =
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.
configure-general-sitewideCommenting-enableNewCommentsSitewide =
Enable new comments sitewide
@@ -242,7 +243,7 @@ configure-auth-oidc-tokenURL = Token URL
configure-auth-oidc-jwksURI = JWKS URI
configure-auth-oidc-useLoginOn = Use OpenID Connect login on
configure-auth-settings = Session settings
configure-auth-settings = Session settings
configure-auth-settings-session-duration-label = Session duration
### Moderation
@@ -255,14 +256,14 @@ configure-moderation-recentCommentHistory-timeFrame-description =
Amount of time to calculate a commenter's rejection rate.
configure-moderation-recentCommentHistory-enabled = Recent history filter
configure-moderation-recentCommentHistory-enabled-description =
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.
configure-moderation-recentCommentHistory-triggerRejectionRate = Rejection rate threshold
configure-moderation-recentCommentHistory-triggerRejectionRate-description =
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.
#### Pre-Moderation
@@ -279,9 +280,9 @@ configure-moderation-apiKey = API key
configure-moderation-akismet-title = Spam detection filter
configure-moderation-akismet-explanation =
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.
#### Akismet
@@ -295,11 +296,11 @@ configure-moderation-akismet-siteURL = Site URL
#### Perspective
configure-moderation-perspective-title = Toxic comment filter
configure-moderation-perspective-explanation =
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</strong> and are placed in
the <strong>Pending Queue for review by a moderator</strong>.
the <strong>Pending Queue for review by a moderator</strong>.
If approved by a moderator, the comment will be published.
configure-moderation-perspective-filter = Toxic comment filter
configure-moderation-perspective-toxicityThreshold = Toxicity threshold
@@ -308,7 +309,7 @@ configure-moderation-perspective-toxicityThresholdDescription =
comment is toxic, according to Perspective API. By default the threshold is set to { $default }.
configure-moderation-perspective-toxicityModel = Toxicity model
configure-moderation-perspective-toxicityModelDescription =
Choose your Perspective Model. The default is { $default }.
Choose your Perspective Model. The default is { $default }.
You can find out more about model choices <externalLink>here</externalLink>.
configure-moderation-perspective-allowStoreCommentData = Allow Google to store comment data
configure-moderation-perspective-allowStoreCommentDataDescription =
@@ -337,7 +338,7 @@ configure-wordList-suspect-explanation =
published (if comments are not pre-moderated).</strong>
configure-wordList-suspect-wordList = Suspect word list
configure-wordList-suspect-wordListDetail =
Separate suspect words or phrases with a new line.
Separate suspect words or phrases with a new line.
### Advanced
configure-advanced-customCSS = Custom CSS
@@ -352,7 +353,7 @@ configure-advanced-permittedDomains-description =
configure-advanced-liveUpdates = Comment stream live updates
configure-advanced-liveUpdates-explanation =
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.
configure-advanced-embedCode-title = Embed code
@@ -524,7 +525,7 @@ moderate-user-drawer-recent-history-calculated =
moderate-user-drawer-recent-history-rejected = Rejected
moderate-user-drawer-recent-history-tooltip-title = How is this calculated?
moderate-user-drawer-recent-history-tooltip-body =
Rejected comments ÷ (rejected comments + published comments).
Rejected comments ÷ (rejected comments + published comments).
The threshold can be changed by administrators in Configure > Moderation.
moderate-user-drawer-recent-history-tooltip-button =
.aria-label = Toggle recent comment history tooltip
@@ -736,8 +737,8 @@ userDetails-suspension-end = <strong>End:</strong> { $timestamp }
configure-general-reactions-title = Reactions
configure-general-reactions-explanation =
Allow your community to engage with one another and express themselves
with one-click reactions. By default, Coral allows commenters to "Respect"
each other's comments.
with one-click reactions. By default, Coral allows commenters to "Respect"
each other's comments.
configure-general-reactions-label = Reaction label
configure-general-reactions-input =
.placeholder = E.g. Respect
@@ -766,7 +767,7 @@ configure-account-features-delete-account = Delete their account
configure-account-features-delete-account-details =
Removes all of their comment data, username, and email address from the site and the database.
configure-account-features-delete-account-fieldDescriptions =
configure-account-features-delete-account-fieldDescriptions =
Removes all of their comment data, username, and email
address from the site and the database.
@@ -781,3 +782,17 @@ configure-advanced-stories-proxy-detail =
When specified, allows scraping requests to use the provided
proxy. All requests will then be passed through the appropriote
proxy as parsed by the <externalLink>npm proxy-agent</externalLink> package.
forgotPassword-forgotPasswordHeader = Forgot password?
forgotPassword-checkEmailHeader = Check your email
forgotPassword-gotBackToSignIn = Go back to sign in page
forgotPassword-checkEmail-receiveEmail =
If there is an account associated with <strong>{ $email }</strong>,
you will receive an email with a link to create a new password.
forgotPassword-enterEmailAndGetALink =
Enter your email address below and we will send you a link
to reset your password.
forgotPassword-emailAddressLabel = Email address
forgotPassword-emailAddressTextField =
.placeholder = Email Address
forgotPassword-sendEmailButton = Send email