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 { 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) => (
<TypedVerifyTokenMutation>
{(...tokenVerify) => (
<AuthProvider
tokenAuth={getMutationProviderData(...tokenAuth)}
tokenVerify={getMutationProviderData(...tokenVerify)}
>
<AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
{children}
</AuthProvider>
)}
@ -46,8 +43,14 @@ interface AuthProviderProps {
tokenVerifyLoading: boolean;
user: User;
}) => React.ReactNode;
tokenAuth: PartialMutationProviderOutput<TokenAuth, TokenAuthVariables>;
tokenVerify: PartialMutationProviderOutput<VerifyToken, VerifyTokenVariables>;
tokenAuth: [
MutationFunction<TokenAuth, TokenAuthVariables>,
MutationResult<TokenAuth>
];
tokenVerify: [
MutationFunction<VerifyToken, VerifyTokenVariables>,
MutationResult<VerifyToken>
];
}
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<
<UserContext.Provider
value={{
login: this.login,
loginByToken: this.loginByToken,
logout: this.logout,
tokenAuthLoading: tokenAuth.opts.loading,
tokenVerifyLoading: tokenVerify.opts.loading,
tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerifyOpts.loading,
user
}}
>
{children({
hasToken: !!getAuthToken(),
isAuthenticated,
tokenAuthLoading: tokenAuth.opts.loading,
tokenVerifyLoading: tokenVerify.opts.loading,
tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerifyOpts.loading,
user
})}
</UserContext.Provider>

View file

@ -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 }) => (
<Layout>
<>
{error && (
<div className={classes.panel}>
<Typography variant="caption">
@ -135,7 +134,7 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
description="button"
/>
</Typography>
</Layout>
</>
)}
</Form>
);

View file

@ -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<NewPasswordPageProps> = props => {
data.password !== data.confirmPassword && data.password.length > 0;
return (
<Layout>
<>
<Typography>
<FormattedMessage defaultMessage="Please set up a new password." />
</Typography>
@ -106,7 +105,7 @@ const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
description="button"
/>
</Button>
</Layout>
</>
);
}}
</Form>

View file

@ -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<ResetPasswordPageProps> = props => {
return (
<Form initial={{ email: "" }} onSubmit={onSubmit}>
{({ change: handleChange, data, submit: handleSubmit }) => (
<Layout>
<>
<Typography>
<FormattedMessage defaultMessage="Forgot your password? Don't worry, we'll reset it for you." />
</Typography>
@ -70,7 +69,7 @@ const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
description="password reset, button"
/>
</Button>
</Layout>
</>
)}
</Form>
);

View file

@ -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 (
<Layout>
<>
<Typography>
<FormattedMessage defaultMessage="Success! If we have your e-mail, youll receive a message with instructions on how to reset your password." />
</Typography>
@ -47,7 +46,7 @@ const ResetPasswordSuccessPage: React.FC<
>
<FormattedMessage defaultMessage="Back to login" description="button" />
</Button>
</Layout>
</>
);
};

View file

@ -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<UserContext>({
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 = () => (
<Layout>
<Switch>
<Route path={passwordResetPath} component={ResetPassword} />
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
<Route path={passwordResetPath} component={ResetPassword} />
<Route path={newPasswordPath} component={NewPassword} />
<Route component={LoginView} />
</Switch>
</Layout>
);
AuthRouter.displayName = "AuthRouter";
export default AuthRouter;
export * from "./utils";

View file

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

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";
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) =>

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 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(/\?/, "")
)
}
});

View file

@ -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 <ResetPasswordSuccessPage onBack={() => navigate(loginUrl)} />;
return <ResetPasswordSuccessPage onBack={() => navigate("/")} />;
};
ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView";
export default ResetPasswordSuccessView;

View file

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