diff --git a/src/App.js b/src/App.js index 9c2c566..7afd536 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,8 @@ import LoginPage from "./components/pages/LoginPage"; import DashboardPage from "./components/pages/DashboardPage"; import SignupPage from "./components/pages/SignupPage"; import ConfirmationPage from "./components/pages/ConfirmationPage"; +import ForgotPasswordPage from "./components/pages/ForgotPasswordPage"; +import ResetPasswordPage from "./components/pages/ResetPasswordPage"; import UserRoute from "./components/routes/UserRoute"; import GuestRoute from "./components/routes/GuestRoute"; @@ -25,6 +27,18 @@ const App = ({ location }) => ( exact component={SignupPage} /> + + 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.global && {errors.global}} + + + + {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 +);