feat(dashboard-v2): implement account recovery flow

This commit is contained in:
Michał Leszczyk 2022-03-23 13:14:52 +01:00
parent 731b1b6d52
commit de7da6f56b
No known key found for this signature in database
GPG Key ID: FA123CA8BAA2FBF4
4 changed files with 235 additions and 0 deletions

View File

@ -0,0 +1,57 @@
import PropTypes from "prop-types";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { TextField } from "../Form/TextField";
import { Button } from "../Button";
import accountsService from "../../services/accountsService";
const recoverySchema = Yup.object().shape({
email: Yup.string().required("Email is required").email("Please provide a valid email address"),
});
export const RecoveryForm = ({ onSuccess, onFailure }) => (
<Formik
initialValues={{
email: "",
}}
validationSchema={recoverySchema}
onSubmit={async (values) => {
try {
await accountsService.post("user/recover/request", {
json: values,
});
onSuccess();
} catch {
onFailure();
}
}}
>
{({ errors, touched }) => (
<Form className="flex flex-col gap-4">
<h3 className="mt-4 mb-8">Request account recovery</h3>
<TextField
type="text"
id="email"
name="email"
label="Email address"
error={errors.email}
touched={touched.email}
/>
<div className="flex w-full justify-center mt-4">
<Button type="submit" className="px-12" $primary>
Send recovery link
</Button>
</div>
</Form>
)}
</Formik>
);
RecoveryForm.propTypes = {
onFailure: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,72 @@
import PropTypes from "prop-types";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { TextField } from "../Form/TextField";
import { Button } from "../Button";
import accountsService from "../../services/accountsService";
const resetPasswordSchema = Yup.object().shape({
password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"),
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
});
export const ResetPasswordForm = ({ token, onSuccess, onFailure }) => (
<Formik
initialValues={{
password: "",
confirmPassword: "",
}}
validationSchema={resetPasswordSchema}
onSubmit={async ({ password, confirmPassword }) => {
try {
await accountsService.post("user/recover", {
json: {
token,
password,
confirmPassword,
},
});
onSuccess();
} catch {
onFailure();
}
}}
>
{({ errors, touched }) => (
<Form className="flex flex-col gap-4">
<h3 className="mt-4 mb-8">Set your new password</h3>
<TextField
type="password"
id="password"
name="password"
label="New password"
error={errors.password}
touched={touched.password}
/>
<TextField
type="password"
id="confirmPassword"
name="confirmPassword"
label="Confirm new password"
error={errors.confirmPassword}
touched={touched.confirmPassword}
/>
<div className="flex w-full justify-center mt-4">
<Button type="submit" className="px-12" $primary>
Confirm
</Button>
</div>
</Form>
)}
</Formik>
);
ResetPasswordForm.propTypes = {
token: PropTypes.string.isRequired,
onFailure: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,50 @@
import { useState } from "react";
import AuthLayout from "../../layouts/AuthLayout";
import { RecoveryForm } from "../../components/forms/RecoveryForm";
import HighlightedLink from "../../components/HighlightedLink";
const State = {
Pure: "PURE",
Success: "SUCCESS",
Failure: "FAILURE",
};
const ResetPasswordPage = () => {
const [state, setState] = useState(State.Pure);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 h-screen">
<div className="mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<RecoveryForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)}
{state === State.Success && (
<p className="text-primary text-center font-semibold">
Please check your email inbox for further instructions.
</p>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<div className="text-sm text-center mt-8">
<p>
Suddenly remembered your password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
<p>
Don't actually have an account? <HighlightedLink to="/auth/register">Create one!</HighlightedLink>
</p>
</div>
</div>
);
};
ResetPasswordPage.Layout = AuthLayout;
export default ResetPasswordPage;

View File

@ -0,0 +1,56 @@
import { useState } from "react";
import { navigate } from "gatsby";
import AuthLayout from "../../layouts/AuthLayout";
import { ResetPasswordForm } from "../../components/forms/ResetPasswordForm";
import HighlightedLink from "../../components/HighlightedLink";
const State = {
Pure: "PURE",
Success: "SUCCESS",
Failure: "FAILURE",
};
const RecoverPage = ({ location }) => {
const query = new URLSearchParams(location.search);
const token = query.get("token");
const [state, setState] = useState(State.Pure);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 h-screen">
<div className="mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<ResetPasswordForm
token={token}
onSuccess={() => {
setState(State.Success);
navigate("/");
}}
onFailure={() => setState(State.Failure)}
/>
)}
{state === State.Success && (
<p className="text-primary text-center font-semibold">
All done! You will be redirected to your dashboard shortly.
</p>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<p className="text-sm text-center mt-8">
Suddenly remembered your old password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
</div>
);
};
RecoverPage.Layout = AuthLayout;
export default RecoverPage;