From 2cb559b5375a07765ae242f538876a85dd81ef5b Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 3 Sep 2019 15:42:15 +0200 Subject: [PATCH] Make password reset work --- src/auth/AuthProvider.tsx | 66 ++++++++++++------- src/auth/components/LoginPage/LoginPage.tsx | 5 +- .../NewPasswordPage/NewPasswordPage.tsx | 5 +- .../ResetPasswordPage/ResetPasswordPage.tsx | 5 +- .../ResetPasswordSuccessPage.tsx | 5 +- src/auth/index.tsx | 35 ++++------ src/auth/mutations.ts | 21 ++++++ src/auth/types/SetPassword.ts | 55 ++++++++++++++++ src/auth/urls.ts | 4 +- src/auth/utils.ts | 15 +++++ src/auth/views/NewPassword.tsx | 51 ++++++++++++++ src/auth/views/ResetPassword.tsx | 9 ++- src/auth/views/ResetPasswordSuccess.tsx | 3 +- src/index.tsx | 1 + 14 files changed, 216 insertions(+), 64 deletions(-) create mode 100644 src/auth/types/SetPassword.ts create mode 100644 src/auth/utils.ts diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 5fa3a5d22..b09edcf96 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { getMutationProviderData } from "../misc"; -import { PartialMutationProviderOutput } from "../types"; -import { getAuthToken, removeAuthToken, setAuthToken, UserContext } from "./"; +import { MutationFunction, MutationResult } from "react-apollo"; +import { UserContext } from "./"; import { TypedTokenAuthMutation, TypedVerifyTokenMutation } from "./mutations"; import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { User } from "./types/User"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; +import { getAuthToken, removeAuthToken, setAuthToken } from "./utils"; interface AuthProviderOperationsProps { children: (props: { @@ -25,10 +25,7 @@ const AuthProviderOperations: React.StatelessComponent< {(...tokenAuth) => ( {(...tokenVerify) => ( - + {children} )} @@ -46,8 +43,14 @@ interface AuthProviderProps { tokenVerifyLoading: boolean; user: User; }) => React.ReactNode; - tokenAuth: PartialMutationProviderOutput; - tokenVerify: PartialMutationProviderOutput; + tokenAuth: [ + MutationFunction, + MutationResult + ]; + tokenVerify: [ + MutationFunction, + MutationResult + ]; } interface AuthProviderState { @@ -66,23 +69,26 @@ class AuthProvider extends React.Component< componentWillReceiveProps(props: AuthProviderProps) { const { tokenAuth, tokenVerify } = props; - if (tokenAuth.opts.error || tokenVerify.opts.error) { + const tokenAuthOpts = tokenAuth[1]; + const tokenVerifyOpts = tokenVerify[1]; + + if (tokenAuthOpts.error || tokenVerifyOpts.error) { this.logout(); } - if (tokenAuth.opts.data) { - const user = tokenAuth.opts.data.tokenCreate.user; + if (tokenAuthOpts.data) { + const user = tokenAuthOpts.data.tokenCreate.user; // FIXME: Now we set state also when auth fails and returned user is // `null`, because the LoginView uses this `null` to display error. this.setState({ user }); if (user) { setAuthToken( - tokenAuth.opts.data.tokenCreate.token, + tokenAuthOpts.data.tokenCreate.token, this.state.persistToken ); } } else { - if (tokenVerify.opts.data && tokenVerify.opts.data.tokenVerify.user) { - const user = tokenVerify.opts.data.tokenVerify.user; + if (tokenVerifyOpts.data && tokenVerifyOpts.data.tokenVerify.user) { + const user = tokenVerifyOpts.data.tokenVerify.user; this.setState({ user }); } } @@ -90,17 +96,23 @@ class AuthProvider extends React.Component< componentDidMount() { const { user } = this.state; - const { tokenVerify } = this.props; const token = getAuthToken(); if (!!token && !user) { - tokenVerify.mutate({ token }); + this.verifyToken(token); } } login = (email: string, password: string, persistToken: boolean) => { const { tokenAuth } = this.props; + const [tokenAuthFn] = tokenAuth; + this.setState({ persistToken }); - tokenAuth.mutate({ email, password }); + tokenAuthFn({ variables: { email, password } }); + }; + + loginByToken = (token: string, user: User) => { + this.setState({ user }); + setAuthToken(token, this.state.persistToken); }; logout = () => { @@ -108,8 +120,17 @@ class AuthProvider extends React.Component< removeAuthToken(); }; + verifyToken = (token: string) => { + const { tokenVerify } = this.props; + const [tokenVerifyFn] = tokenVerify; + + return tokenVerifyFn({ variables: { token } }); + }; + render() { const { children, tokenAuth, tokenVerify } = this.props; + const tokenAuthOpts = tokenAuth[1]; + const tokenVerifyOpts = tokenVerify[1]; const { user } = this.state; const isAuthenticated = !!user; @@ -117,17 +138,18 @@ class AuthProvider extends React.Component< {children({ hasToken: !!getAuthToken(), isAuthenticated, - tokenAuthLoading: tokenAuth.opts.loading, - tokenVerifyLoading: tokenVerify.opts.loading, + tokenAuthLoading: tokenAuthOpts.loading, + tokenVerifyLoading: tokenVerifyOpts.loading, user })} diff --git a/src/auth/components/LoginPage/LoginPage.tsx b/src/auth/components/LoginPage/LoginPage.tsx index c94b72433..9f3345e54 100644 --- a/src/auth/components/LoginPage/LoginPage.tsx +++ b/src/auth/components/LoginPage/LoginPage.tsx @@ -9,7 +9,6 @@ import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import Layout from "../Layout"; import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; import Form from "@saleor/components/Form"; @@ -71,7 +70,7 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })( onSubmit={onSubmit} > {({ change: handleChange, data, submit: handleSubmit }) => ( - + <> {error && (
@@ -135,7 +134,7 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })( description="button" /> - + )} ); diff --git a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx index 2a75498f1..143ac17bf 100644 --- a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx +++ b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx @@ -7,7 +7,6 @@ import { FormattedMessage, useIntl } from "react-intl"; import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; -import Layout from "../Layout"; const useStyles = makeStyles( { @@ -47,7 +46,7 @@ const NewPasswordPage: React.FC = props => { data.password !== data.confirmPassword && data.password.length > 0; return ( - + <> @@ -106,7 +105,7 @@ const NewPasswordPage: React.FC = props => { description="button" /> - + ); }} diff --git a/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx b/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx index 64a0c9ec4..eddb94c11 100644 --- a/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx +++ b/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx @@ -8,7 +8,6 @@ import { FormattedMessage, useIntl } from "react-intl"; import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; import { commonMessages } from "@saleor/intl"; -import Layout from "../Layout"; const useStyles = makeStyles( { @@ -38,7 +37,7 @@ const ResetPasswordPage: React.FC = props => { return (
{({ change: handleChange, data, submit: handleSubmit }) => ( - + <> @@ -70,7 +69,7 @@ const ResetPasswordPage: React.FC = props => { description="password reset, button" /> - + )}
); diff --git a/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx b/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx index b7b1bb694..ec3ef1fc2 100644 --- a/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx +++ b/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx @@ -5,7 +5,6 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import FormSpacer from "@saleor/components/FormSpacer"; -import Layout from "../Layout"; const useStyles = makeStyles( { @@ -33,7 +32,7 @@ const ResetPasswordSuccessPage: React.FC< const classes = useStyles(props); return ( - + <> @@ -47,7 +46,7 @@ const ResetPasswordSuccessPage: React.FC< > - + ); }; diff --git a/src/auth/index.tsx b/src/auth/index.tsx index 80021af2f..74fb61b65 100644 --- a/src/auth/index.tsx +++ b/src/auth/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; +import Layout from "./components/Layout"; import { User } from "./types/User"; import { newPasswordPath, @@ -12,10 +13,9 @@ import NewPassword from "./views/NewPassword"; import ResetPassword from "./views/ResetPassword"; import ResetPasswordSuccess from "./views/ResetPasswordSuccess"; -const TOKEN_STORAGE_KEY = "dashboardAuth"; - interface UserContext { login: (username: string, password: string, persist: boolean) => void; + loginByToken: (token: string, user: User) => void; logout: () => void; tokenAuthLoading: boolean; tokenVerifyLoading: boolean; @@ -24,33 +24,24 @@ interface UserContext { export const UserContext = React.createContext({ login: undefined, + loginByToken: undefined, logout: undefined, tokenAuthLoading: false, tokenVerifyLoading: false }); -export const getAuthToken = () => - localStorage.getItem(TOKEN_STORAGE_KEY) || - sessionStorage.getItem(TOKEN_STORAGE_KEY); - -export const setAuthToken = (token: string, persist: boolean) => - persist - ? localStorage.setItem(TOKEN_STORAGE_KEY, token) - : sessionStorage.setItem(TOKEN_STORAGE_KEY, token); - -export const removeAuthToken = () => { - localStorage.removeItem(TOKEN_STORAGE_KEY); - sessionStorage.removeItem(TOKEN_STORAGE_KEY); -}; - const AuthRouter: React.FC = () => ( - - - - - - + + + + + + + + ); AuthRouter.displayName = "AuthRouter"; export default AuthRouter; + +export * from "./utils"; diff --git a/src/auth/mutations.ts b/src/auth/mutations.ts index 3f6509be7..6791e4b0d 100644 --- a/src/auth/mutations.ts +++ b/src/auth/mutations.ts @@ -5,6 +5,7 @@ import { RequestPasswordReset, RequestPasswordResetVariables } from "./types/RequestPasswordReset"; +import { SetPassword, SetPasswordVariables } from "./types/SetPassword"; import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; @@ -78,3 +79,23 @@ export const RequestPasswordResetMutation = TypedMutation< RequestPasswordReset, RequestPasswordResetVariables >(requestPasswordReset); + +export const setPassword = gql` + ${fragmentUser} + mutation SetPassword($email: String!, $password: String!, $token: String!) { + setPassword(email: $email, password: $password, token: $token) { + token + errors { + field + message + } + user { + ...User + } + } + } +`; +export const SetPasswordMutation = TypedMutation< + SetPassword, + SetPasswordVariables +>(setPassword); diff --git a/src/auth/types/SetPassword.ts b/src/auth/types/SetPassword.ts new file mode 100644 index 000000000..708560696 --- /dev/null +++ b/src/auth/types/SetPassword.ts @@ -0,0 +1,55 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PermissionEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: SetPassword +// ==================================================== + +export interface SetPassword_setPassword_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface SetPassword_setPassword_user_permissions { + __typename: "PermissionDisplay"; + code: PermissionEnum; + name: string; +} + +export interface SetPassword_setPassword_user_avatar { + __typename: "Image"; + url: string; +} + +export interface SetPassword_setPassword_user { + __typename: "User"; + id: string; + email: string; + firstName: string; + lastName: string; + isStaff: boolean; + note: string | null; + permissions: (SetPassword_setPassword_user_permissions | null)[] | null; + avatar: SetPassword_setPassword_user_avatar | null; +} + +export interface SetPassword_setPassword { + __typename: "SetPassword"; + token: string | null; + errors: (SetPassword_setPassword_errors | null)[]; + user: SetPassword_setPassword_user | null; +} + +export interface SetPassword { + setPassword: SetPassword_setPassword | null; +} + +export interface SetPasswordVariables { + email: string; + password: string; + token: string; +} diff --git a/src/auth/urls.ts b/src/auth/urls.ts index 62556a0e9..c0ac7c68b 100644 --- a/src/auth/urls.ts +++ b/src/auth/urls.ts @@ -1,8 +1,5 @@ import { stringify as stringifyQs } from "qs"; -export const loginPath = "/login/"; -export const loginUrl = loginPath; - export const passwordResetPath = "/reset-password/"; export const passwordResetUrl = passwordResetPath; @@ -11,6 +8,7 @@ export const passwordResetSuccessUrl = passwordResetSuccessPath; export const newPasswordPath = "/new-password/"; export interface NewPasswordUrlQueryParams { + email: string; token: string; } export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) => diff --git a/src/auth/utils.ts b/src/auth/utils.ts new file mode 100644 index 000000000..f2ed57e85 --- /dev/null +++ b/src/auth/utils.ts @@ -0,0 +1,15 @@ +const TOKEN_STORAGE_KEY = "dashboardAuth"; + +export const getAuthToken = () => + localStorage.getItem(TOKEN_STORAGE_KEY) || + sessionStorage.getItem(TOKEN_STORAGE_KEY); + +export const setAuthToken = (token: string, persist: boolean) => + persist + ? localStorage.setItem(TOKEN_STORAGE_KEY, token) + : sessionStorage.setItem(TOKEN_STORAGE_KEY, token); + +export const removeAuthToken = () => { + localStorage.removeItem(TOKEN_STORAGE_KEY); + sessionStorage.removeItem(TOKEN_STORAGE_KEY); +}; diff --git a/src/auth/views/NewPassword.tsx b/src/auth/views/NewPassword.tsx index e69de29bb..c667f92d0 100644 --- a/src/auth/views/NewPassword.tsx +++ b/src/auth/views/NewPassword.tsx @@ -0,0 +1,51 @@ +import { parse as parseQs } from "qs"; +import React from "react"; +import { RouteComponentProps } from "react-router"; + +import useNavigator from "@saleor/hooks/useNavigator"; +import useUser from "@saleor/hooks/useUser"; +import NewPasswordPage, { + NewPasswordPageFormData +} from "../components/NewPasswordPage"; +import { SetPasswordMutation } from "../mutations"; +import { SetPassword } from "../types/SetPassword"; +import { NewPasswordUrlQueryParams } from "../urls"; + +const NewPassword: React.FC = ({ location }) => { + const navigate = useNavigator(); + const { loginByToken } = useUser(); + + const params: NewPasswordUrlQueryParams = parseQs(location.search.substr(1)); + + const handleSetPassword = async (data: SetPassword) => { + if (data.setPassword.errors.length === 0) { + loginByToken(data.setPassword.token, data.setPassword.user); + navigate("/", true); + } + }; + + return ( + + {(setPassword, setPasswordOpts) => { + const handleSubmit = (data: NewPasswordPageFormData) => + setPassword({ + variables: { + email: params.email, + password: data.password, + token: params.token + } + }); + + return ( + + ); + }} + + ); +}; + +NewPassword.displayName = "NewPassword"; +export default NewPassword; diff --git a/src/auth/views/ResetPassword.tsx b/src/auth/views/ResetPassword.tsx index 6ac2dab5c..3ba7fffa4 100644 --- a/src/auth/views/ResetPassword.tsx +++ b/src/auth/views/ResetPassword.tsx @@ -1,8 +1,8 @@ import React from "react"; import urlJoin from "url-join"; +import { APP_MOUNT_URI } from "@saleor/config"; import useNavigator from "@saleor/hooks/useNavigator"; -import useShop from "@saleor/hooks/useShop"; import ResetPasswordPage, { ResetPasswordPageFormData } from "../components/ResetPasswordPage"; @@ -12,7 +12,6 @@ import { newPasswordUrl, passwordResetSuccessUrl } from "../urls"; const ResetPasswordView: React.FC = () => { const navigate = useNavigator(); - const shop = useShop(); const handleRequestPasswordReset = (data: RequestPasswordReset) => { if (data.requestPasswordReset.errors.length === 0) { @@ -27,7 +26,11 @@ const ResetPasswordView: React.FC = () => { requestPasswordReset({ variables: { email: data.email, - redirectUrl: urlJoin(shop.domain.url, newPasswordUrl()) + redirectUrl: urlJoin( + window.location.origin, + APP_MOUNT_URI, + newPasswordUrl().replace(/\?/, "") + ) } }); diff --git a/src/auth/views/ResetPasswordSuccess.tsx b/src/auth/views/ResetPasswordSuccess.tsx index 58f48de94..650a22cd4 100644 --- a/src/auth/views/ResetPasswordSuccess.tsx +++ b/src/auth/views/ResetPasswordSuccess.tsx @@ -2,12 +2,11 @@ import React from "react"; import useNavigator from "@saleor/hooks/useNavigator"; import ResetPasswordSuccessPage from "../components/ResetPasswordSuccessPage"; -import { loginUrl } from "../urls"; const ResetPasswordSuccessView: React.FC = () => { const navigate = useNavigator(); - return navigate(loginUrl)} />; + return navigate("/")} />; }; ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView"; export default ResetPasswordSuccessView; diff --git a/src/index.tsx b/src/index.tsx index 1782f96ee..bf8e504e8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -63,6 +63,7 @@ const invalidTokenLink = onError((error: ResponseError) => { const authLink = setContext((_, context) => { const authToken = getAuthToken(); + return { ...context, headers: {