Make password reset work

This commit is contained in:
dominik-zeglen 2019-09-03 15:42:15 +02:00
parent 48ca3b9e15
commit 2cb559b537
14 changed files with 216 additions and 64 deletions

View file

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import { getMutationProviderData } from "../misc"; import { MutationFunction, MutationResult } from "react-apollo";
import { PartialMutationProviderOutput } from "../types"; import { UserContext } from "./";
import { getAuthToken, removeAuthToken, setAuthToken, UserContext } from "./";
import { TypedTokenAuthMutation, TypedVerifyTokenMutation } from "./mutations"; import { TypedTokenAuthMutation, TypedVerifyTokenMutation } from "./mutations";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { User } from "./types/User"; import { User } from "./types/User";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
import { getAuthToken, removeAuthToken, setAuthToken } from "./utils";
interface AuthProviderOperationsProps { interface AuthProviderOperationsProps {
children: (props: { children: (props: {
@ -25,10 +25,7 @@ const AuthProviderOperations: React.StatelessComponent<
{(...tokenAuth) => ( {(...tokenAuth) => (
<TypedVerifyTokenMutation> <TypedVerifyTokenMutation>
{(...tokenVerify) => ( {(...tokenVerify) => (
<AuthProvider <AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
tokenAuth={getMutationProviderData(...tokenAuth)}
tokenVerify={getMutationProviderData(...tokenVerify)}
>
{children} {children}
</AuthProvider> </AuthProvider>
)} )}
@ -46,8 +43,14 @@ interface AuthProviderProps {
tokenVerifyLoading: boolean; tokenVerifyLoading: boolean;
user: User; user: User;
}) => React.ReactNode; }) => React.ReactNode;
tokenAuth: PartialMutationProviderOutput<TokenAuth, TokenAuthVariables>; tokenAuth: [
tokenVerify: PartialMutationProviderOutput<VerifyToken, VerifyTokenVariables>; MutationFunction<TokenAuth, TokenAuthVariables>,
MutationResult<TokenAuth>
];
tokenVerify: [
MutationFunction<VerifyToken, VerifyTokenVariables>,
MutationResult<VerifyToken>
];
} }
interface AuthProviderState { interface AuthProviderState {
@ -66,23 +69,26 @@ class AuthProvider extends React.Component<
componentWillReceiveProps(props: AuthProviderProps) { componentWillReceiveProps(props: AuthProviderProps) {
const { tokenAuth, tokenVerify } = props; 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(); this.logout();
} }
if (tokenAuth.opts.data) { if (tokenAuthOpts.data) {
const user = tokenAuth.opts.data.tokenCreate.user; const user = tokenAuthOpts.data.tokenCreate.user;
// FIXME: Now we set state also when auth fails and returned user is // FIXME: Now we set state also when auth fails and returned user is
// `null`, because the LoginView uses this `null` to display error. // `null`, because the LoginView uses this `null` to display error.
this.setState({ user }); this.setState({ user });
if (user) { if (user) {
setAuthToken( setAuthToken(
tokenAuth.opts.data.tokenCreate.token, tokenAuthOpts.data.tokenCreate.token,
this.state.persistToken this.state.persistToken
); );
} }
} else { } else {
if (tokenVerify.opts.data && tokenVerify.opts.data.tokenVerify.user) { if (tokenVerifyOpts.data && tokenVerifyOpts.data.tokenVerify.user) {
const user = tokenVerify.opts.data.tokenVerify.user; const user = tokenVerifyOpts.data.tokenVerify.user;
this.setState({ user }); this.setState({ user });
} }
} }
@ -90,17 +96,23 @@ class AuthProvider extends React.Component<
componentDidMount() { componentDidMount() {
const { user } = this.state; const { user } = this.state;
const { tokenVerify } = this.props;
const token = getAuthToken(); const token = getAuthToken();
if (!!token && !user) { if (!!token && !user) {
tokenVerify.mutate({ token }); this.verifyToken(token);
} }
} }
login = (email: string, password: string, persistToken: boolean) => { login = (email: string, password: string, persistToken: boolean) => {
const { tokenAuth } = this.props; const { tokenAuth } = this.props;
const [tokenAuthFn] = tokenAuth;
this.setState({ persistToken }); 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 = () => { logout = () => {
@ -108,8 +120,17 @@ class AuthProvider extends React.Component<
removeAuthToken(); removeAuthToken();
}; };
verifyToken = (token: string) => {
const { tokenVerify } = this.props;
const [tokenVerifyFn] = tokenVerify;
return tokenVerifyFn({ variables: { token } });
};
render() { render() {
const { children, tokenAuth, tokenVerify } = this.props; const { children, tokenAuth, tokenVerify } = this.props;
const tokenAuthOpts = tokenAuth[1];
const tokenVerifyOpts = tokenVerify[1];
const { user } = this.state; const { user } = this.state;
const isAuthenticated = !!user; const isAuthenticated = !!user;
@ -117,17 +138,18 @@ class AuthProvider extends React.Component<
<UserContext.Provider <UserContext.Provider
value={{ value={{
login: this.login, login: this.login,
loginByToken: this.loginByToken,
logout: this.logout, logout: this.logout,
tokenAuthLoading: tokenAuth.opts.loading, tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerify.opts.loading, tokenVerifyLoading: tokenVerifyOpts.loading,
user user
}} }}
> >
{children({ {children({
hasToken: !!getAuthToken(), hasToken: !!getAuthToken(),
isAuthenticated, isAuthenticated,
tokenAuthLoading: tokenAuth.opts.loading, tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerify.opts.loading, tokenVerifyLoading: tokenVerifyOpts.loading,
user user
})} })}
</UserContext.Provider> </UserContext.Provider>

View file

@ -9,7 +9,6 @@ import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import Layout from "../Layout";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
@ -71,7 +70,7 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ change: handleChange, data, submit: handleSubmit }) => ( {({ change: handleChange, data, submit: handleSubmit }) => (
<Layout> <>
{error && ( {error && (
<div className={classes.panel}> <div className={classes.panel}>
<Typography variant="caption"> <Typography variant="caption">
@ -135,7 +134,7 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
description="button" description="button"
/> />
</Typography> </Typography>
</Layout> </>
)} )}
</Form> </Form>
); );

View file

@ -7,7 +7,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import Layout from "../Layout";
const useStyles = makeStyles( const useStyles = makeStyles(
{ {
@ -47,7 +46,7 @@ const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
data.password !== data.confirmPassword && data.password.length > 0; data.password !== data.confirmPassword && data.password.length > 0;
return ( return (
<Layout> <>
<Typography> <Typography>
<FormattedMessage defaultMessage="Please set up a new password." /> <FormattedMessage defaultMessage="Please set up a new password." />
</Typography> </Typography>
@ -106,7 +105,7 @@ const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
description="button" description="button"
/> />
</Button> </Button>
</Layout> </>
); );
}} }}
</Form> </Form>

View file

@ -8,7 +8,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import Layout from "../Layout";
const useStyles = makeStyles( const useStyles = makeStyles(
{ {
@ -38,7 +37,7 @@ const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
return ( return (
<Form initial={{ email: "" }} onSubmit={onSubmit}> <Form initial={{ email: "" }} onSubmit={onSubmit}>
{({ change: handleChange, data, submit: handleSubmit }) => ( {({ change: handleChange, data, submit: handleSubmit }) => (
<Layout> <>
<Typography> <Typography>
<FormattedMessage defaultMessage="Forgot your password? Don't worry, we'll reset it for you." /> <FormattedMessage defaultMessage="Forgot your password? Don't worry, we'll reset it for you." />
</Typography> </Typography>
@ -70,7 +69,7 @@ const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
description="password reset, button" description="password reset, button"
/> />
</Button> </Button>
</Layout> </>
)} )}
</Form> </Form>
); );

View file

@ -5,7 +5,6 @@ import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import Layout from "../Layout";
const useStyles = makeStyles( const useStyles = makeStyles(
{ {
@ -33,7 +32,7 @@ const ResetPasswordSuccessPage: React.FC<
const classes = useStyles(props); const classes = useStyles(props);
return ( return (
<Layout> <>
<Typography> <Typography>
<FormattedMessage defaultMessage="Success! If we have your e-mail, youll receive a message with instructions on how to reset your password." /> <FormattedMessage defaultMessage="Success! If we have your e-mail, youll receive a message with instructions on how to reset your password." />
</Typography> </Typography>
@ -47,7 +46,7 @@ const ResetPasswordSuccessPage: React.FC<
> >
<FormattedMessage defaultMessage="Back to login" description="button" /> <FormattedMessage defaultMessage="Back to login" description="button" />
</Button> </Button>
</Layout> </>
); );
}; };

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import Layout from "./components/Layout";
import { User } from "./types/User"; import { User } from "./types/User";
import { import {
newPasswordPath, newPasswordPath,
@ -12,10 +13,9 @@ import NewPassword from "./views/NewPassword";
import ResetPassword from "./views/ResetPassword"; import ResetPassword from "./views/ResetPassword";
import ResetPasswordSuccess from "./views/ResetPasswordSuccess"; import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
const TOKEN_STORAGE_KEY = "dashboardAuth";
interface UserContext { interface UserContext {
login: (username: string, password: string, persist: boolean) => void; login: (username: string, password: string, persist: boolean) => void;
loginByToken: (token: string, user: User) => void;
logout: () => void; logout: () => void;
tokenAuthLoading: boolean; tokenAuthLoading: boolean;
tokenVerifyLoading: boolean; tokenVerifyLoading: boolean;
@ -24,33 +24,24 @@ interface UserContext {
export const UserContext = React.createContext<UserContext>({ export const UserContext = React.createContext<UserContext>({
login: undefined, login: undefined,
loginByToken: undefined,
logout: undefined, logout: undefined,
tokenAuthLoading: false, tokenAuthLoading: false,
tokenVerifyLoading: 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 = () => ( const AuthRouter: React.FC = () => (
<Switch> <Layout>
<Route path={passwordResetPath} component={ResetPassword} /> <Switch>
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} /> <Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
<Route path={newPasswordPath} component={NewPassword} /> <Route path={passwordResetPath} component={ResetPassword} />
<Route component={LoginView} /> <Route path={newPasswordPath} component={NewPassword} />
</Switch> <Route component={LoginView} />
</Switch>
</Layout>
); );
AuthRouter.displayName = "AuthRouter"; AuthRouter.displayName = "AuthRouter";
export default AuthRouter; export default AuthRouter;
export * from "./utils";

View file

@ -5,6 +5,7 @@ import {
RequestPasswordReset, RequestPasswordReset,
RequestPasswordResetVariables RequestPasswordResetVariables
} from "./types/RequestPasswordReset"; } from "./types/RequestPasswordReset";
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
@ -78,3 +79,23 @@ export const RequestPasswordResetMutation = TypedMutation<
RequestPasswordReset, RequestPasswordReset,
RequestPasswordResetVariables RequestPasswordResetVariables
>(requestPasswordReset); >(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);

View file

@ -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;
}

View file

@ -1,8 +1,5 @@
import { stringify as stringifyQs } from "qs"; import { stringify as stringifyQs } from "qs";
export const loginPath = "/login/";
export const loginUrl = loginPath;
export const passwordResetPath = "/reset-password/"; export const passwordResetPath = "/reset-password/";
export const passwordResetUrl = passwordResetPath; export const passwordResetUrl = passwordResetPath;
@ -11,6 +8,7 @@ export const passwordResetSuccessUrl = passwordResetSuccessPath;
export const newPasswordPath = "/new-password/"; export const newPasswordPath = "/new-password/";
export interface NewPasswordUrlQueryParams { export interface NewPasswordUrlQueryParams {
email: string;
token: string; token: string;
} }
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) => export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>

15
src/auth/utils.ts Normal file
View file

@ -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);
};

View file

@ -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<RouteComponentProps> = ({ 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 (
<SetPasswordMutation onCompleted={handleSetPassword}>
{(setPassword, setPasswordOpts) => {
const handleSubmit = (data: NewPasswordPageFormData) =>
setPassword({
variables: {
email: params.email,
password: data.password,
token: params.token
}
});
return (
<NewPasswordPage
disabled={setPasswordOpts.loading}
onSubmit={handleSubmit}
/>
);
}}
</SetPasswordMutation>
);
};
NewPassword.displayName = "NewPassword";
export default NewPassword;

View file

@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { APP_MOUNT_URI } from "@saleor/config";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useShop from "@saleor/hooks/useShop";
import ResetPasswordPage, { import ResetPasswordPage, {
ResetPasswordPageFormData ResetPasswordPageFormData
} from "../components/ResetPasswordPage"; } from "../components/ResetPasswordPage";
@ -12,7 +12,6 @@ import { newPasswordUrl, passwordResetSuccessUrl } from "../urls";
const ResetPasswordView: React.FC = () => { const ResetPasswordView: React.FC = () => {
const navigate = useNavigator(); const navigate = useNavigator();
const shop = useShop();
const handleRequestPasswordReset = (data: RequestPasswordReset) => { const handleRequestPasswordReset = (data: RequestPasswordReset) => {
if (data.requestPasswordReset.errors.length === 0) { if (data.requestPasswordReset.errors.length === 0) {
@ -27,7 +26,11 @@ const ResetPasswordView: React.FC = () => {
requestPasswordReset({ requestPasswordReset({
variables: { variables: {
email: data.email, email: data.email,
redirectUrl: urlJoin(shop.domain.url, newPasswordUrl()) redirectUrl: urlJoin(
window.location.origin,
APP_MOUNT_URI,
newPasswordUrl().replace(/\?/, "")
)
} }
}); });

View file

@ -2,12 +2,11 @@ import React from "react";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import ResetPasswordSuccessPage from "../components/ResetPasswordSuccessPage"; import ResetPasswordSuccessPage from "../components/ResetPasswordSuccessPage";
import { loginUrl } from "../urls";
const ResetPasswordSuccessView: React.FC = () => { const ResetPasswordSuccessView: React.FC = () => {
const navigate = useNavigator(); const navigate = useNavigator();
return <ResetPasswordSuccessPage onBack={() => navigate(loginUrl)} />; return <ResetPasswordSuccessPage onBack={() => navigate("/")} />;
}; };
ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView"; ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView";
export default ResetPasswordSuccessView; export default ResetPasswordSuccessView;

View file

@ -63,6 +63,7 @@ const invalidTokenLink = onError((error: ResponseError) => {
const authLink = setContext((_, context) => { const authLink = setContext((_, context) => {
const authToken = getAuthToken(); const authToken = getAuthToken();
return { return {
...context, ...context,
headers: { headers: {