feat(dashboard-v2): implement account recovery flow
This commit is contained in:
parent
731b1b6d52
commit
de7da6f56b
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
Reference in New Issue