Make password reset work
This commit is contained in:
parent
48ca3b9e15
commit
2cb559b537
14 changed files with 216 additions and 64 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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, you’ll 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = () => (
|
||||
<Switch>
|
||||
<Route path={passwordResetPath} component={ResetPassword} />
|
||||
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
|
||||
<Route path={newPasswordPath} component={NewPassword} />
|
||||
<Route component={LoginView} />
|
||||
</Switch>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<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";
|
||||
|
|
|
@ -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);
|
||||
|
|
55
src/auth/types/SetPassword.ts
Normal file
55
src/auth/types/SetPassword.ts
Normal 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;
|
||||
}
|
|
@ -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
15
src/auth/utils.ts
Normal 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);
|
||||
};
|
|
@ -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;
|
|
@ -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(/\?/, "")
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -63,6 +63,7 @@ const invalidTokenLink = onError((error: ResponseError) => {
|
|||
|
||||
const authLink = setContext((_, context) => {
|
||||
const authToken = getAuthToken();
|
||||
|
||||
return {
|
||||
...context,
|
||||
headers: {
|
||||
|
|
Loading…
Reference in a new issue