mirror of
https://github.com/wassname/talk.git
synced 2026-07-31 12:50:48 +08:00
[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:
committed by
Wyatt Johnson
parent
2559d1c0e8
commit
a89dc4675a
@@ -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 site’s 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
|
||||
|
||||
Reference in New Issue
Block a user