dispatch =>
localStorage.bookwormJWT = user.token;
dispatch(userLoggedIn(user));
});
+
+export const resetPasswordRequest = ({ email }) => () =>
+ api.user.resetPasswordRequest(email);
+
+export const validateToken = token => () => api.user.validateToken(token);
+
+export const resetPassword = data => () => api.user.resetPassword(data);
diff --git a/src/api.js b/src/api.js
index 6a5276a..38456df 100644
--- a/src/api.js
+++ b/src/api.js
@@ -7,6 +7,12 @@ export default {
signup: user =>
axios.post("/api/users", { user }).then(res => res.data.user),
confirm: token =>
- axios.post("/api/auth/confirmation", { token }).then(res => res.data.user)
+ axios
+ .post("/api/auth/confirmation", { token })
+ .then(res => res.data.user),
+ resetPasswordRequest: email =>
+ axios.post("/api/auth/reset_password_request", { email }),
+ validateToken: token => axios.post("/api/auth/validate_token", { token }),
+ resetPassword: data => axios.post("/api/auth/reset_password", { data })
}
};
diff --git a/src/components/forms/ForgotPasswordForm.js b/src/components/forms/ForgotPasswordForm.js
new file mode 100644
index 0000000..2ac573f
--- /dev/null
+++ b/src/components/forms/ForgotPasswordForm.js
@@ -0,0 +1,70 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Form, Button, Message } from "semantic-ui-react";
+import isEmail from "validator/lib/isEmail";
+import InlineError from "../messages/InlineError";
+
+class ForgotPasswordForm extends React.Component {
+ state = {
+ data: {
+ email: ""
+ },
+ loading: false,
+ errors: {}
+ };
+
+ onChange = e =>
+ this.setState({
+ ...this.state,
+ data: { ...this.state.data, [e.target.name]: e.target.value }
+ });
+
+ onSubmit = e => {
+ e.preventDefault();
+ const errors = this.validate(this.state.data);
+ this.setState({ errors });
+ if (Object.keys(errors).length === 0) {
+ this.setState({ loading: true });
+ this.props
+ .submit(this.state.data)
+ .catch(err =>
+ this.setState({ errors: err.response.data.errors, loading: false })
+ );
+ }
+ };
+
+ validate = data => {
+ const errors = {};
+ if (!isEmail(data.email)) errors.email = "Invalid email";
+ return errors;
+ };
+
+ render() {
+ const { errors, data, loading } = this.state;
+
+ return (
+
+
+
+ {errors.email && }
+
+
+
+ );
+ }
+}
+
+ForgotPasswordForm.propTypes = {
+ submit: PropTypes.func.isRequired
+};
+
+export default ForgotPasswordForm;
diff --git a/src/components/forms/ResetPasswordForm.js b/src/components/forms/ResetPasswordForm.js
new file mode 100644
index 0000000..032a0a2
--- /dev/null
+++ b/src/components/forms/ResetPasswordForm.js
@@ -0,0 +1,91 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Form, Button } from "semantic-ui-react";
+import InlineError from "../messages/InlineError";
+
+class ResetPasswordForm extends React.Component {
+ state = {
+ data: {
+ token: this.props.token,
+ password: "",
+ passwordConfirmation: ""
+ },
+ loading: false,
+ errors: {}
+ };
+
+ onChange = e =>
+ this.setState({
+ ...this.state,
+ data: { ...this.state.data, [e.target.name]: e.target.value }
+ });
+
+ onSubmit = e => {
+ e.preventDefault();
+ const errors = this.validate(this.state.data);
+ this.setState({ errors });
+ if (Object.keys(errors).length === 0) {
+ this.setState({ loading: true });
+ this.props
+ .submit(this.state.data)
+ .catch(err =>
+ this.setState({ errors: err.response.data.errors, loading: false })
+ );
+ }
+ };
+
+ validate = data => {
+ const errors = {};
+ if (!data.password) errors.password = "Can't be blank";
+ if (data.password !== data.passwordConfirmation)
+ errors.password = "Passwords must match";
+ return errors;
+ };
+
+ render() {
+ const { errors, data, loading } = this.state;
+
+ return (
+
+
+
+ {errors.password && }
+
+
+
+
+
+ {errors.passwordConfirmation && (
+
+ )}
+
+
+
+
+ );
+ }
+}
+
+ResetPasswordForm.propTypes = {
+ submit: PropTypes.func.isRequired,
+ token: PropTypes.string.isRequired
+};
+
+export default ResetPasswordForm;
diff --git a/src/components/pages/ForgotPasswordPage.js b/src/components/pages/ForgotPasswordPage.js
new file mode 100644
index 0000000..06a64d3
--- /dev/null
+++ b/src/components/pages/ForgotPasswordPage.js
@@ -0,0 +1,35 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { Message } from "semantic-ui-react";
+import ForgotPasswordForm from "../forms/ForgotPasswordForm";
+import { resetPasswordRequest } from "../../actions/auth";
+
+class ForgotPasswordPage extends React.Component {
+ state = {
+ success: false
+ };
+
+ submit = data =>
+ this.props
+ .resetPasswordRequest(data)
+ .then(() => this.setState({ success: true }));
+
+ render() {
+ return (
+
+ {this.state.success ? (
+ Email has been sent.
+ ) : (
+
+ )}
+
+ );
+ }
+}
+
+ForgotPasswordPage.propTypes = {
+ resetPasswordRequest: PropTypes.func.isRequired
+};
+
+export default connect(null, { resetPasswordRequest })(ForgotPasswordPage);
diff --git a/src/components/pages/LoginPage.js b/src/components/pages/LoginPage.js
index cd8473f..14aa474 100644
--- a/src/components/pages/LoginPage.js
+++ b/src/components/pages/LoginPage.js
@@ -1,6 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
+import { Link } from "react-router-dom";
import LoginForm from "../forms/LoginForm";
import { login } from "../../actions/auth";
@@ -14,6 +15,8 @@ class LoginPage extends React.Component {
Login page
+
+ Forgot Password?
);
}
diff --git a/src/components/pages/ResetPasswordPage.js b/src/components/pages/ResetPasswordPage.js
new file mode 100644
index 0000000..3d77afb
--- /dev/null
+++ b/src/components/pages/ResetPasswordPage.js
@@ -0,0 +1,56 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { Message } from "semantic-ui-react";
+import ResetPasswordForm from "../forms/ResetPasswordForm";
+import { validateToken, resetPassword } from "../../actions/auth";
+
+class ResetPasswordPage extends React.Component {
+ state = {
+ loading: true,
+ success: false
+ };
+
+ componentDidMount() {
+ this.props
+ .validateToken(this.props.match.params.token)
+ .then(() => this.setState({ loading: false, success: true }))
+ .catch(() => this.setState({ loading: false, success: false }));
+ }
+
+ submit = data =>
+ this.props
+ .resetPassword(data)
+ .then(() => this.props.history.push("/login"));
+
+ render() {
+ const { loading, success } = this.state;
+ const token = this.props.match.params.token;
+
+ return (
+
+ {loading && Loading}
+ {!loading &&
+ success && }
+ {!loading && !success && Invalid Token}
+
+ );
+ }
+}
+
+ResetPasswordPage.propTypes = {
+ validateToken: PropTypes.func.isRequired,
+ resetPassword: PropTypes.func.isRequired,
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ token: PropTypes.string.isRequired
+ }).isRequired
+ }).isRequired,
+ history: PropTypes.shape({
+ push: PropTypes.func.isRequired
+ }).isRequired
+};
+
+export default connect(null, { validateToken, resetPassword })(
+ ResetPasswordPage
+);