Merge pull request #147 from mirumee/add/reset-password
Password reset flow
This commit is contained in:
commit
0f4bdabc19
34 changed files with 1935 additions and 685 deletions
|
@ -16,3 +16,4 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Fix table cell padding - #143 by @dominik-zeglen
|
||||
- Add fallback locale - #153 by @dominik-zeglen
|
||||
- Replace checkbox with switch component in "product type has variants" - #152 by @dominik-zeglen
|
||||
- Add password reset flow - #147 by @dominik-zeglen
|
||||
|
|
|
@ -37,7 +37,8 @@ module.exports = api => {
|
|||
extractFromFormatMessageCall: true,
|
||||
messagesDir: "build/locale/"
|
||||
}
|
||||
]
|
||||
],
|
||||
"macros"
|
||||
];
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2019-09-04T11:51:21.421Z\n"
|
||||
"POT-Creation-Date: 2019-09-06T13:56:36.897Z\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -1487,6 +1487,14 @@ msgctxt "button"
|
|||
msgid "Back to home"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.json
|
||||
#. [src.auth.components.ResetPasswordSuccessPage.2245157344] - button
|
||||
#. defaultMessage is:
|
||||
#. Back to login
|
||||
msgctxt "button"
|
||||
msgid "Back to login"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/categories/components/CategoryBackground/CategoryBackground.json
|
||||
#. [src.categories.components.CategoryBackground.1849089820] - section header
|
||||
#. defaultMessage is:
|
||||
|
@ -1943,6 +1951,14 @@ msgctxt "button"
|
|||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
|
||||
#. [src.auth.components.NewPasswordPage.2799926859]
|
||||
#. defaultMessage is:
|
||||
#. Confirm Password
|
||||
msgctxt "description"
|
||||
msgid "Confirm Password"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json
|
||||
#. [src.orders.components.OrderCustomer.1111991638] - subheader
|
||||
#. defaultMessage is:
|
||||
|
@ -3363,6 +3379,14 @@ msgctxt "tax rate"
|
|||
msgid "Foodstuffs"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/ResetPasswordPage/ResetPasswordPage.json
|
||||
#. [src.auth.components.ResetPasswordPage.2370700732]
|
||||
#. defaultMessage is:
|
||||
#. Forgot your password? Don't worry, we'll reset it for you.
|
||||
msgctxt "description"
|
||||
msgid "Forgot your password? Don't worry, we'll reset it for you."
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/discounts/components/VoucherTypes/VoucherTypes.json
|
||||
#. [src.discounts.components.VoucherTypes.3289659291] - voucher discount type
|
||||
#. defaultMessage is:
|
||||
|
@ -4179,6 +4203,14 @@ msgctxt "navigation section name"
|
|||
msgid "Navigation"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
|
||||
#. [src.auth.components.NewPasswordPage.1254879564]
|
||||
#. defaultMessage is:
|
||||
#. New Password
|
||||
msgctxt "description"
|
||||
msgid "New Password"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/products/views/ProductCreate.json
|
||||
#. [src.products.views.1591632382] - page header
|
||||
#. defaultMessage is:
|
||||
|
@ -5111,6 +5143,14 @@ msgctxt "description"
|
|||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
|
||||
#. [src.auth.components.NewPasswordPage.4253911811]
|
||||
#. defaultMessage is:
|
||||
#. Passwords do not match
|
||||
msgctxt "description"
|
||||
msgid "Passwords do not match"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/orders/components/OrderList/OrderList.json
|
||||
#. [src.orders.components.OrderList.2743232155] - payment status
|
||||
#. defaultMessage is:
|
||||
|
@ -5259,6 +5299,14 @@ msgctxt "product type"
|
|||
msgid "Physical"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
|
||||
#. [src.auth.components.NewPasswordPage.1915811227]
|
||||
#. defaultMessage is:
|
||||
#. Please set up a new password.
|
||||
msgctxt "description"
|
||||
msgid "Please set up a new password."
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/plugins/components/PluginInfo/PluginInfo.json
|
||||
#. [src.plugins.components.PluginInfo.3425535100] - section header
|
||||
#. defaultMessage is:
|
||||
|
@ -5675,6 +5723,14 @@ msgctxt "tax rate"
|
|||
msgid "Property renovations"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/views/ResetPassword.json
|
||||
#. [src.auth.views.2388238158]
|
||||
#. defaultMessage is:
|
||||
#. Provided email address does not exist in our database.
|
||||
msgctxt "description"
|
||||
msgid "Provided email address does not exist in our database."
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/collections/views/CollectionList.json
|
||||
#. [src.collections.views.1547167026] - publish collections
|
||||
#. defaultMessage is:
|
||||
|
@ -6031,6 +6087,14 @@ msgctxt "button"
|
|||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/LoginPage/LoginPage.json
|
||||
#. [src.auth.components.LoginPage.4028609483] - button
|
||||
#. defaultMessage is:
|
||||
#. Reset your password
|
||||
msgctxt "button"
|
||||
msgid "Reset your password"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json
|
||||
#. [src.shipping.components.ShippingZoneCountriesAssignDialog.1003092716]
|
||||
#. defaultMessage is:
|
||||
|
@ -6107,6 +6171,14 @@ msgctxt "description"
|
|||
msgid "Sale Name"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/intl.json
|
||||
#. [src.somethingWentWrong]
|
||||
#. defaultMessage is:
|
||||
#. Saleor ran into an unexpected problem
|
||||
msgctxt "description"
|
||||
msgid "Saleor ran into an unexpected problem"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/intl.json
|
||||
#. [src.sales] - sales section name
|
||||
#. defaultMessage is:
|
||||
|
@ -6447,6 +6519,14 @@ msgctxt "add order note, button"
|
|||
msgid "Send"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/ResetPasswordPage/ResetPasswordPage.json
|
||||
#. [src.auth.components.ResetPasswordPage.3663311080] - password reset, button
|
||||
#. defaultMessage is:
|
||||
#. Send Instructions
|
||||
msgctxt "password reset, button"
|
||||
msgid "Send Instructions"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.json
|
||||
#. [src.staff.components.StaffAddMemberDialog.449055697] - button
|
||||
#. defaultMessage is:
|
||||
|
@ -6479,6 +6559,14 @@ msgctxt "voucher end date, switch button"
|
|||
msgid "Set end date"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
|
||||
#. [src.auth.components.NewPasswordPage.2342634351] - button
|
||||
#. defaultMessage is:
|
||||
#. Set new password
|
||||
msgctxt "button"
|
||||
msgid "Set new password"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/plugins/components/PluginInfo/PluginInfo.json
|
||||
#. [src.plugins.components.PluginInfo.4013064767]
|
||||
#. defaultMessage is:
|
||||
|
@ -6683,18 +6771,6 @@ msgctxt "description"
|
|||
msgid "Some products require shipping, but no method provided"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/mutations.json
|
||||
#. [src.1395232177] - error message
|
||||
#. defaultMessage is:
|
||||
#. Something went wrong. {errorMessage}
|
||||
#: build/locale/src/queries.json
|
||||
#. [src.1395232177] - error message
|
||||
#. defaultMessage is:
|
||||
#. Something went wrong. {errorMessage}
|
||||
msgctxt "error message"
|
||||
msgid "Something went wrong. {errorMessage}"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/components/NotFoundPage/NotFoundPage.json
|
||||
#. [src.components.NotFoundPage.4036415297]
|
||||
#. defaultMessage is:
|
||||
|
@ -6943,6 +7019,14 @@ msgctxt "plugin success message"
|
|||
msgid "Succesfully updated plugin settings"
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.json
|
||||
#. [src.auth.components.ResetPasswordSuccessPage.4066297200]
|
||||
#. defaultMessage is:
|
||||
#. Success! In a few minutes you’ll receive a message with instructions on how to reset your password.
|
||||
msgctxt "description"
|
||||
msgid "Success! In a few minutes you’ll receive a message with instructions on how to reset your password."
|
||||
msgstr ""
|
||||
|
||||
#: build/locale/src/attributes/views/AttributeCreate/AttributeCreate.json
|
||||
#. [src.attributes.views.AttributeCreate.11941964]
|
||||
#. defaultMessage is:
|
||||
|
|
50
package-lock.json
generated
50
package-lock.json
generated
|
@ -9409,8 +9409,7 @@
|
|||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -9428,13 +9427,11 @@
|
|||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -9447,18 +9444,15 @@
|
|||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -9561,8 +9555,7 @@
|
|||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -9572,7 +9565,6 @@
|
|||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -9585,20 +9577,17 @@
|
|||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -9615,7 +9604,6 @@
|
|||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -9688,8 +9676,7 @@
|
|||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -9699,7 +9686,6 @@
|
|||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -9775,8 +9761,7 @@
|
|||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -9806,7 +9791,6 @@
|
|||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -9824,7 +9808,6 @@
|
|||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -9863,13 +9846,11 @@
|
|||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -16962,6 +16943,15 @@
|
|||
"tough-cookie": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"require-context.macro": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-context.macro/-/require-context.macro-1.1.1.tgz",
|
||||
"integrity": "sha512-l1XH5HruDyG+Iwo5pz39EGbOFVtoYQt8cl7mJ6KJFWzARNJnpb+XUui+jBQUGlJ8SJOdx+QDh784e8b42PxLXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/webpack-env": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
"react-intl-po": "^2.2.2",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"regenerator-runtime": "^0.11.1",
|
||||
"require-context.macro": "^1.1.1",
|
||||
"rimraf": "^2.7.0",
|
||||
"testcafe": "^1.3.3",
|
||||
"ts-jest": "^23.10.5",
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
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: {
|
||||
hasToken: boolean;
|
||||
isAuthenticated: boolean;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenVerifyLoading: boolean;
|
||||
user: User;
|
||||
}
|
||||
) => React.ReactNode;
|
||||
children: (props: {
|
||||
hasToken: boolean;
|
||||
isAuthenticated: boolean;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenVerifyLoading: boolean;
|
||||
user: User;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
const AuthProviderOperations: React.StatelessComponent<
|
||||
AuthProviderOperationsProps
|
||||
|
@ -27,10 +25,7 @@ const AuthProviderOperations: React.StatelessComponent<
|
|||
{(...tokenAuth) => (
|
||||
<TypedVerifyTokenMutation>
|
||||
{(...tokenVerify) => (
|
||||
<AuthProvider
|
||||
tokenAuth={getMutationProviderData(...tokenAuth)}
|
||||
tokenVerify={getMutationProviderData(...tokenVerify)}
|
||||
>
|
||||
<AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
)}
|
||||
|
@ -41,17 +36,21 @@ const AuthProviderOperations: React.StatelessComponent<
|
|||
};
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: (
|
||||
props: {
|
||||
hasToken: boolean;
|
||||
isAuthenticated: boolean;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenVerifyLoading: boolean;
|
||||
user: User;
|
||||
}
|
||||
) => React.ReactNode;
|
||||
tokenAuth: PartialMutationProviderOutput<TokenAuth, TokenAuthVariables>;
|
||||
tokenVerify: PartialMutationProviderOutput<VerifyToken, VerifyTokenVariables>;
|
||||
children: (props: {
|
||||
hasToken: boolean;
|
||||
isAuthenticated: boolean;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenVerifyLoading: boolean;
|
||||
user: User;
|
||||
}) => React.ReactNode;
|
||||
tokenAuth: [
|
||||
MutationFunction<TokenAuth, TokenAuthVariables>,
|
||||
MutationResult<TokenAuth>
|
||||
];
|
||||
tokenVerify: [
|
||||
MutationFunction<VerifyToken, VerifyTokenVariables>,
|
||||
MutationResult<VerifyToken>
|
||||
];
|
||||
}
|
||||
|
||||
interface AuthProviderState {
|
||||
|
@ -70,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 });
|
||||
}
|
||||
}
|
||||
|
@ -94,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 = () => {
|
||||
|
@ -112,19 +120,36 @@ 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;
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{ user, login: this.login, logout: this.logout }}
|
||||
value={{
|
||||
login: this.login,
|
||||
loginByToken: this.loginByToken,
|
||||
logout: this.logout,
|
||||
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>
|
||||
|
|
100
src/auth/components/Layout.tsx
Normal file
100
src/auth/components/Layout.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { Theme } from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import SVG from "react-inlinesvg";
|
||||
|
||||
import backgroundArt from "@assets/images/login-background.svg";
|
||||
import saleorDarkLogo from "@assets/images/logo-dark.svg";
|
||||
import saleorLightLogo from "@assets/images/logo-light.svg";
|
||||
import useTheme from "@saleor/hooks/useTheme";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
logo: {
|
||||
"& svg": {
|
||||
display: "block",
|
||||
height: 40,
|
||||
marginBottom: theme.spacing.unit * 4
|
||||
}
|
||||
},
|
||||
mainPanel: {
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
padding: theme.spacing.unit * 2
|
||||
},
|
||||
background: theme.palette.background.paper,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
padding: theme.spacing.unit * 6,
|
||||
width: "100%"
|
||||
},
|
||||
mainPanelContent: {
|
||||
[theme.breakpoints.up("xs")]: {
|
||||
width: "100%"
|
||||
},
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
width: 328
|
||||
},
|
||||
"@media (min-width: 1440px)": {
|
||||
width: 464
|
||||
},
|
||||
margin: "auto",
|
||||
width: "100%"
|
||||
},
|
||||
root: {
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
gridTemplateColumns: "376px 1fr"
|
||||
},
|
||||
"@media (min-width: 1440px)": {
|
||||
gridTemplateColumns: "520px 1fr"
|
||||
},
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
width: "100vw"
|
||||
},
|
||||
sidebar: {
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
display: "block"
|
||||
},
|
||||
display: "none"
|
||||
},
|
||||
sidebarArt: {
|
||||
"& svg": {
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "Layout"
|
||||
}
|
||||
);
|
||||
|
||||
const Layout: React.FC = props => {
|
||||
const { children } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const { isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.sidebar}>
|
||||
<SVG className={classes.sidebarArt} src={backgroundArt} />
|
||||
</div>
|
||||
<div className={classes.mainPanel}>
|
||||
<div className={classes.mainPanelContent}>
|
||||
<SVG
|
||||
className={classes.logo}
|
||||
src={isDark ? saleorDarkLogo : saleorLightLogo}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = "Layout";
|
||||
export default Layout;
|
|
@ -8,16 +8,11 @@ import {
|
|||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import SVG from "react-inlinesvg";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import backgroundArt from "@assets/images/login-background.svg";
|
||||
import saleorDarkLogo from "@assets/images/logo-dark.svg";
|
||||
import saleorLightLogo from "@assets/images/logo-light.svg";
|
||||
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
|
||||
import Form from "@saleor/components/Form";
|
||||
import { FormSpacer } from "@saleor/components/FormSpacer";
|
||||
import useTheme from "@saleor/hooks/useTheme";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
|
||||
export interface FormData {
|
||||
|
@ -41,38 +36,6 @@ const styles = (theme: Theme) =>
|
|||
loginButton: {
|
||||
width: 140
|
||||
},
|
||||
logo: {
|
||||
"& svg": {
|
||||
display: "block",
|
||||
height: 40,
|
||||
marginBottom: theme.spacing.unit * 4
|
||||
}
|
||||
},
|
||||
mainPanel: {
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
padding: theme.spacing.unit * 2
|
||||
},
|
||||
background: theme.palette.background.paper,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
padding: theme.spacing.unit * 6,
|
||||
width: "100%"
|
||||
},
|
||||
mainPanelContent: {
|
||||
[theme.breakpoints.up("xs")]: {
|
||||
width: "100%"
|
||||
},
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
width: 328
|
||||
},
|
||||
"@media (min-width: 1440px)": {
|
||||
width: 464
|
||||
},
|
||||
margin: "auto",
|
||||
width: "100%"
|
||||
},
|
||||
panel: {
|
||||
"& span": {
|
||||
color: theme.palette.error.contrastText
|
||||
|
@ -81,30 +44,6 @@ const styles = (theme: Theme) =>
|
|||
borderRadius: theme.spacing.unit,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
padding: theme.spacing.unit * 1.5
|
||||
},
|
||||
root: {
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
gridTemplateColumns: "376px 1fr"
|
||||
},
|
||||
"@media (min-width: 1440px)": {
|
||||
gridTemplateColumns: "520px 1fr"
|
||||
},
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
width: "100vw"
|
||||
},
|
||||
sidebar: {
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
display: "block"
|
||||
},
|
||||
display: "none"
|
||||
},
|
||||
sidebarArt: {
|
||||
"& svg": {
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -116,8 +55,13 @@ export interface LoginCardProps extends WithStyles<typeof styles> {
|
|||
}
|
||||
|
||||
const LoginCard = withStyles(styles, { name: "LoginCard" })(
|
||||
({ classes, error, disableLoginButton, onSubmit }: LoginCardProps) => {
|
||||
const { isDark } = useTheme();
|
||||
({
|
||||
classes,
|
||||
error,
|
||||
disableLoginButton,
|
||||
onPasswordRecovery,
|
||||
onSubmit
|
||||
}: LoginCardProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
|
@ -126,82 +70,71 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
|
|||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ change: handleChange, data, submit: handleSubmit }) => (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.sidebar}>
|
||||
<SVG className={classes.sidebarArt} src={backgroundArt} />
|
||||
</div>
|
||||
<div className={classes.mainPanel}>
|
||||
<div className={classes.mainPanelContent}>
|
||||
<SVG
|
||||
className={classes.logo}
|
||||
src={isDark ? saleorDarkLogo : saleorLightLogo}
|
||||
/>
|
||||
{error && (
|
||||
<div className={classes.panel}>
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="Sorry, your username and/or password are incorrect. Please try again." />
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
autoComplete="username"
|
||||
label={intl.formatMessage(commonMessages.email)}
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={data.email}
|
||||
inputProps={{
|
||||
"data-tc": "email"
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
fullWidth
|
||||
autoComplete="current-password"
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Password"
|
||||
})}
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
type="password"
|
||||
value={data.password}
|
||||
data-tc="password"
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.buttonContainer}>
|
||||
<ControlledCheckbox
|
||||
checked={data.rememberMe}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Remember me",
|
||||
description: "login"
|
||||
})}
|
||||
name="rememberMe"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
className={classes.loginButton}
|
||||
color="primary"
|
||||
disabled={disableLoginButton}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
data-tc="submit"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Login"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{/* <FormSpacer />
|
||||
<Typography className={classes.link}>
|
||||
{i18n.t("Reset your password")}
|
||||
</Typography> */}
|
||||
<>
|
||||
{error && (
|
||||
<div className={classes.panel}>
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="Sorry, your username and/or password are incorrect. Please try again." />
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
autoComplete="username"
|
||||
label={intl.formatMessage(commonMessages.email)}
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={data.email}
|
||||
inputProps={{
|
||||
"data-tc": "email"
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
fullWidth
|
||||
autoComplete="current-password"
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Password"
|
||||
})}
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
type="password"
|
||||
value={data.password}
|
||||
data-tc="password"
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.buttonContainer}>
|
||||
<ControlledCheckbox
|
||||
checked={data.rememberMe}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Remember me",
|
||||
description: "login"
|
||||
})}
|
||||
name="rememberMe"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
className={classes.loginButton}
|
||||
color="primary"
|
||||
disabled={disableLoginButton}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
data-tc="submit"
|
||||
>
|
||||
<FormattedMessage defaultMessage="Login" description="button" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<FormSpacer />
|
||||
<Typography className={classes.link} onClick={onPasswordRecovery}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Reset your password"
|
||||
description="button"
|
||||
/>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import CardDecorator from "@saleor/storybook//CardDecorator";
|
||||
import Decorator from "@saleor/storybook//Decorator";
|
||||
import NewPasswordPage from "./NewPasswordPage";
|
||||
|
||||
storiesOf("Views / Authentication / Set up a new password", module)
|
||||
.addDecorator(CardDecorator)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => (
|
||||
<NewPasswordPage disabled={false} onSubmit={() => undefined} />
|
||||
))
|
||||
.add("loading", () => (
|
||||
<NewPasswordPage disabled={true} onSubmit={() => undefined} />
|
||||
));
|
115
src/auth/components/NewPasswordPage/NewPasswordPage.tsx
Normal file
115
src/auth/components/NewPasswordPage/NewPasswordPage.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import Form from "@saleor/components/Form";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
submit: {
|
||||
width: "100%"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "NewPasswordPage"
|
||||
}
|
||||
);
|
||||
|
||||
export interface NewPasswordPageFormData {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
export interface NewPasswordPageProps {
|
||||
disabled: boolean;
|
||||
onSubmit: (data: NewPasswordPageFormData) => void;
|
||||
}
|
||||
|
||||
const initialForm: NewPasswordPageFormData = {
|
||||
confirmPassword: "",
|
||||
password: ""
|
||||
};
|
||||
|
||||
const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
|
||||
const { disabled, onSubmit } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Form initial={initialForm} onSubmit={onSubmit}>
|
||||
{({ change: handleChange, data, submit: handleSubmit }) => {
|
||||
const passwordError =
|
||||
data.password !== data.confirmPassword && data.password.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="Please set up a new password." />
|
||||
</Typography>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
autoComplete="none"
|
||||
disabled={disabled}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "New Password"
|
||||
})}
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
type="password"
|
||||
value={data.password}
|
||||
inputProps={{
|
||||
"data-tc": "password"
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
fullWidth
|
||||
error={passwordError}
|
||||
autoComplete="none"
|
||||
disabled={disabled}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Confirm Password"
|
||||
})}
|
||||
name="confirmPassword"
|
||||
onChange={handleChange}
|
||||
type="password"
|
||||
value={data.confirmPassword}
|
||||
helperText={
|
||||
passwordError &&
|
||||
intl.formatMessage({
|
||||
defaultMessage: "Passwords do not match"
|
||||
})
|
||||
}
|
||||
inputProps={{
|
||||
"data-tc": "confirm-password"
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
className={classes.submit}
|
||||
color="primary"
|
||||
disabled={(passwordError && data.password.length > 0) || disabled}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Set new password"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
NewPasswordPage.displayName = "NewPasswordPage";
|
||||
export default NewPasswordPage;
|
2
src/auth/components/NewPasswordPage/index.ts
Normal file
2
src/auth/components/NewPasswordPage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./NewPasswordPage";
|
||||
export * from "./NewPasswordPage";
|
|
@ -0,0 +1,21 @@
|
|||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { formError } from "@saleor/storybook/misc";
|
||||
import ResetPasswordPage, { ResetPasswordPageProps } from "./ResetPasswordPage";
|
||||
|
||||
const props: ResetPasswordPageProps = {
|
||||
disabled: false,
|
||||
error: undefined,
|
||||
onSubmit: () => undefined
|
||||
};
|
||||
storiesOf("Views / Authentication / Reset password", module)
|
||||
.addDecorator(CardDecorator)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <ResetPasswordPage {...props} />)
|
||||
.add("loading", () => <ResetPasswordPage {...props} disabled={true} />)
|
||||
.add("error", () => (
|
||||
<ResetPasswordPage {...props} error={formError("").message} />
|
||||
));
|
95
src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx
Normal file
95
src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import Form from "@saleor/components/Form";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme: Theme) => ({
|
||||
panel: {
|
||||
"& span": {
|
||||
color: theme.palette.error.contrastText
|
||||
},
|
||||
background: theme.palette.error.main,
|
||||
borderRadius: theme.spacing.unit,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
padding: theme.spacing.unit * 1.5
|
||||
},
|
||||
submit: {
|
||||
width: "100%"
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "ResetPasswordPage"
|
||||
}
|
||||
);
|
||||
|
||||
export interface ResetPasswordPageFormData {
|
||||
email: string;
|
||||
}
|
||||
export interface ResetPasswordPageProps {
|
||||
disabled: boolean;
|
||||
error: string;
|
||||
onSubmit: (data: ResetPasswordPageFormData) => void;
|
||||
}
|
||||
|
||||
const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
|
||||
const { disabled, error, onSubmit } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Form initial={{ email: "" }} onSubmit={onSubmit}>
|
||||
{({ change: handleChange, data, submit: handleSubmit }) => (
|
||||
<>
|
||||
{!!error && (
|
||||
<div className={classes.panel}>
|
||||
<Typography variant="caption">{error}</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="Forgot your password? Don't worry, we'll reset it for you." />
|
||||
</Typography>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
autoComplete="username"
|
||||
label={intl.formatMessage(commonMessages.email)}
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={data.email}
|
||||
inputProps={{
|
||||
"data-tc": "email"
|
||||
}}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
className={classes.submit}
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Send Instructions"
|
||||
description="password reset, button"
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPasswordPage.displayName = "ResetPasswordPage";
|
||||
export default ResetPasswordPage;
|
2
src/auth/components/ResetPasswordPage/index.ts
Normal file
2
src/auth/components/ResetPasswordPage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./ResetPasswordPage";
|
||||
export * from "./ResetPasswordPage";
|
|
@ -0,0 +1,11 @@
|
|||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import ResetPasswordSuccessPage from "./ResetPasswordSuccessPage";
|
||||
|
||||
storiesOf("Views / Authentication / Reset password success", module)
|
||||
.addDecorator(CardDecorator)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <ResetPasswordSuccessPage onBack={() => undefined} />);
|
|
@ -0,0 +1,54 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
submit: {
|
||||
width: "100%"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "ResetPasswordSuccessPage"
|
||||
}
|
||||
);
|
||||
|
||||
export interface ResetPasswordSuccessPageFormData {
|
||||
email: string;
|
||||
}
|
||||
export interface ResetPasswordSuccessPageProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const ResetPasswordSuccessPage: React.FC<
|
||||
ResetPasswordSuccessPageProps
|
||||
> = props => {
|
||||
const { onBack } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
<FormattedMessage defaultMessage="Success! In a few minutes you’ll receive a message with instructions on how to reset your password." />
|
||||
</Typography>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
className={classes.submit}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={onBack}
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage defaultMessage="Back to login" description="button" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPasswordSuccessPage.displayName = "ResetPasswordSuccessPage";
|
||||
export default ResetPasswordSuccessPage;
|
2
src/auth/components/ResetPasswordSuccessPage/index.ts
Normal file
2
src/auth/components/ResetPasswordSuccessPage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./ResetPasswordSuccessPage";
|
||||
export * from "./ResetPasswordSuccessPage";
|
|
@ -1,30 +1,47 @@
|
|||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import { User } from "./types/User";
|
||||
|
||||
const TOKEN_STORAGE_KEY = "dashboardAuth";
|
||||
import {
|
||||
newPasswordPath,
|
||||
passwordResetPath,
|
||||
passwordResetSuccessPath
|
||||
} from "./urls";
|
||||
import LoginView from "./views/Login";
|
||||
import NewPassword from "./views/NewPassword";
|
||||
import ResetPassword from "./views/ResetPassword";
|
||||
import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
|
||||
|
||||
interface UserContext {
|
||||
login: (username: string, password: string, persist: boolean) => void;
|
||||
loginByToken: (token: string, user: User) => void;
|
||||
logout: () => void;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export const UserContext = React.createContext<UserContext>({
|
||||
login: undefined,
|
||||
logout: undefined
|
||||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenVerifyLoading: false
|
||||
});
|
||||
|
||||
export const getAuthToken = () =>
|
||||
localStorage.getItem(TOKEN_STORAGE_KEY) ||
|
||||
sessionStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
const AuthRouter: React.FC = () => (
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
|
||||
<Route path={passwordResetPath} component={ResetPassword} />
|
||||
<Route path={newPasswordPath} component={NewPassword} />
|
||||
<Route component={LoginView} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export const setAuthToken = (token: string, persist: boolean) =>
|
||||
persist
|
||||
? localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
: sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
AuthRouter.displayName = "AuthRouter";
|
||||
export default AuthRouter;
|
||||
|
||||
export const removeAuthToken = () => {
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
};
|
||||
export * from "./utils";
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import gql from "graphql-tag";
|
||||
|
||||
import { TypedMutation } from "../mutations";
|
||||
import {
|
||||
RequestPasswordReset,
|
||||
RequestPasswordResetVariables
|
||||
} from "./types/RequestPasswordReset";
|
||||
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
|
||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||
|
||||
|
@ -59,3 +64,38 @@ export const TypedVerifyTokenMutation = TypedMutation<
|
|||
VerifyToken,
|
||||
VerifyTokenVariables
|
||||
>(tokenVerifyMutation);
|
||||
|
||||
export const requestPasswordReset = gql`
|
||||
mutation RequestPasswordReset($email: String!, $redirectUrl: String!) {
|
||||
requestPasswordReset(email: $email, redirectUrl: $redirectUrl) {
|
||||
errors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
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);
|
||||
|
|
27
src/auth/types/RequestPasswordReset.ts
Normal file
27
src/auth/types/RequestPasswordReset.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: RequestPasswordReset
|
||||
// ====================================================
|
||||
|
||||
export interface RequestPasswordReset_requestPasswordReset_errors {
|
||||
__typename: "Error";
|
||||
field: string | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface RequestPasswordReset_requestPasswordReset {
|
||||
__typename: "RequestPasswordReset";
|
||||
errors: RequestPasswordReset_requestPasswordReset_errors[] | null;
|
||||
}
|
||||
|
||||
export interface RequestPasswordReset {
|
||||
requestPasswordReset: RequestPasswordReset_requestPasswordReset | null;
|
||||
}
|
||||
|
||||
export interface RequestPasswordResetVariables {
|
||||
email: string;
|
||||
redirectUrl: string;
|
||||
}
|
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;
|
||||
}
|
15
src/auth/urls.ts
Normal file
15
src/auth/urls.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { stringify as stringifyQs } from "qs";
|
||||
|
||||
export const passwordResetPath = "/reset-password/";
|
||||
export const passwordResetUrl = passwordResetPath;
|
||||
|
||||
export const passwordResetSuccessPath = "/reset-password/success/";
|
||||
export const passwordResetSuccessUrl = passwordResetSuccessPath;
|
||||
|
||||
export const newPasswordPath = "/new-password/";
|
||||
export interface NewPasswordUrlQueryParams {
|
||||
email: string;
|
||||
token: string;
|
||||
}
|
||||
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>
|
||||
newPasswordPath + "?" + stringifyQs(params);
|
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);
|
||||
};
|
|
@ -1,27 +1,25 @@
|
|||
import React from "react";
|
||||
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import LoginPage, { FormData } from "../components/LoginPage";
|
||||
import { UserContext } from "../index";
|
||||
import { passwordResetUrl } from "../urls";
|
||||
|
||||
interface LoginViewProps {
|
||||
loading: boolean;
|
||||
}
|
||||
const LoginView: React.FC = () => {
|
||||
const navigate = useNavigator();
|
||||
const { login, user, tokenAuthLoading } = useUser();
|
||||
|
||||
const LoginView: React.StatelessComponent<LoginViewProps> = ({ loading }) => (
|
||||
<UserContext.Consumer>
|
||||
{({ login, user }) => {
|
||||
const handleSubmit = (data: FormData) =>
|
||||
login(data.email, data.password, data.rememberMe);
|
||||
return (
|
||||
<LoginPage
|
||||
error={user === null}
|
||||
disableLoginButton={loading}
|
||||
onPasswordRecovery={undefined}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UserContext.Consumer>
|
||||
);
|
||||
const handleSubmit = (data: FormData) =>
|
||||
login(data.email, data.password, data.rememberMe);
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
error={user === null}
|
||||
disableLoginButton={tokenAuthLoading}
|
||||
onPasswordRecovery={() => navigate(passwordResetUrl)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
LoginView.displayName = "LoginView";
|
||||
export default LoginView;
|
||||
|
|
51
src/auth/views/NewPassword.tsx
Normal file
51
src/auth/views/NewPassword.tsx
Normal 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;
|
64
src/auth/views/ResetPassword.tsx
Normal file
64
src/auth/views/ResetPassword.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import urlJoin from "url-join";
|
||||
|
||||
import { APP_MOUNT_URI } from "@saleor/config";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import ResetPasswordPage, {
|
||||
ResetPasswordPageFormData
|
||||
} from "../components/ResetPasswordPage";
|
||||
import { RequestPasswordResetMutation } from "../mutations";
|
||||
import { RequestPasswordReset } from "../types/RequestPasswordReset";
|
||||
import { newPasswordUrl, passwordResetSuccessUrl } from "../urls";
|
||||
|
||||
const ResetPasswordView: React.FC = () => {
|
||||
const [error, setError] = React.useState<string>();
|
||||
const navigate = useNavigator();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleRequestPasswordReset = (data: RequestPasswordReset) => {
|
||||
if (data.requestPasswordReset.errors.length === 0) {
|
||||
navigate(passwordResetSuccessUrl);
|
||||
} else {
|
||||
if (data.requestPasswordReset.errors.find(err => err.field === "email")) {
|
||||
setError(
|
||||
intl.formatMessage({
|
||||
defaultMessage:
|
||||
"Provided email address does not exist in our database."
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setError(intl.formatMessage(commonMessages.somethingWentWrong));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RequestPasswordResetMutation onCompleted={handleRequestPasswordReset}>
|
||||
{(requestPasswordReset, requestPasswordResetOpts) => {
|
||||
const handleSubmit = (data: ResetPasswordPageFormData) =>
|
||||
requestPasswordReset({
|
||||
variables: {
|
||||
email: data.email,
|
||||
redirectUrl: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === "/" ? "" : APP_MOUNT_URI,
|
||||
newPasswordUrl().replace(/\?/, "")
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ResetPasswordPage
|
||||
disabled={requestPasswordResetOpts.loading}
|
||||
error={error}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RequestPasswordResetMutation>
|
||||
);
|
||||
};
|
||||
ResetPasswordView.displayName = "ResetPasswordView";
|
||||
export default ResetPasswordView;
|
12
src/auth/views/ResetPasswordSuccess.tsx
Normal file
12
src/auth/views/ResetPasswordSuccess.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import ResetPasswordSuccessPage from "../components/ResetPasswordSuccessPage";
|
||||
|
||||
const ResetPasswordSuccessView: React.FC = () => {
|
||||
const navigate = useNavigator();
|
||||
|
||||
return <ResetPasswordSuccessPage onBack={() => navigate("/")} />;
|
||||
};
|
||||
ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView";
|
||||
export default ResetPasswordSuccessView;
|
|
@ -13,12 +13,11 @@ import { BrowserRouter, Route, Switch } from "react-router-dom";
|
|||
|
||||
import AttributeSection from "./attributes";
|
||||
import { attributeSection } from "./attributes/urls";
|
||||
import { getAuthToken, removeAuthToken } from "./auth";
|
||||
import Auth, { getAuthToken, removeAuthToken } from "./auth";
|
||||
import AuthProvider from "./auth/AuthProvider";
|
||||
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
|
||||
import SectionRoute from "./auth/components/SectionRoute";
|
||||
import { hasPermission } from "./auth/misc";
|
||||
import Login from "./auth/views/Login";
|
||||
import CategorySection from "./categories";
|
||||
import CollectionSection from "./collections";
|
||||
import { AppProgressProvider } from "./components/AppProgress";
|
||||
|
@ -64,6 +63,7 @@ const invalidTokenLink = onError((error: ResponseError) => {
|
|||
|
||||
const authLink = setContext((_, context) => {
|
||||
const authToken = getAuthToken();
|
||||
|
||||
return {
|
||||
...context,
|
||||
headers: {
|
||||
|
@ -241,7 +241,7 @@ const Routes: React.FC = () => {
|
|||
) : hasToken && tokenVerifyLoading ? (
|
||||
<LoginLoading />
|
||||
) : (
|
||||
<Login loading={tokenAuthLoading} />
|
||||
<Auth />
|
||||
)
|
||||
}
|
||||
</AuthProvider>
|
||||
|
|
|
@ -50,6 +50,9 @@ export const commonMessages = defineMessages({
|
|||
savedChanges: {
|
||||
defaultMessage: "Saved changes"
|
||||
},
|
||||
somethingWentWrong: {
|
||||
defaultMessage: "Saleor ran into an unexpected problem"
|
||||
},
|
||||
startDate: {
|
||||
defaultMessage: "Start Date"
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Mutation, MutationFunction, MutationResult } from "react-apollo";
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import useNotifier from "./hooks/useNotifier";
|
||||
import { commonMessages } from "./intl";
|
||||
|
||||
export interface TypedMutationInnerProps<TData, TVariables> {
|
||||
children: (
|
||||
|
@ -30,17 +31,10 @@ export function TypedMutation<TData, TVariables>(
|
|||
<Mutation
|
||||
mutation={mutation}
|
||||
onCompleted={onCompleted}
|
||||
onError={err => {
|
||||
const msg = intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Something went wrong. {errorMessage}",
|
||||
description: "error message"
|
||||
},
|
||||
{
|
||||
errorMessage: err.message
|
||||
}
|
||||
);
|
||||
notify({ text: msg });
|
||||
onError={(err: ApolloError) => {
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
});
|
||||
if (onError) {
|
||||
onError(err);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import AppProgress from "./components/AppProgress";
|
|||
import ErrorPage from "./components/ErrorPage/ErrorPage";
|
||||
import useNavigator from "./hooks/useNavigator";
|
||||
import useNotifier from "./hooks/useNotifier";
|
||||
import { commonMessages } from "./intl";
|
||||
import { RequireAtLeastOne } from "./misc";
|
||||
|
||||
export interface LoadMore<TData, TVariables> {
|
||||
|
@ -83,16 +84,9 @@ export function TypedQuery<TData, TVariables>(
|
|||
>
|
||||
{queryData => {
|
||||
if (queryData.error) {
|
||||
const msg = intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Something went wrong. {errorMessage}",
|
||||
description: "error message"
|
||||
},
|
||||
{
|
||||
message: queryData.error.message
|
||||
}
|
||||
);
|
||||
pushMessage({ text: msg });
|
||||
pushMessage({
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
});
|
||||
}
|
||||
|
||||
const loadMore = (
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,13 @@
|
|||
/* eslint-disable */
|
||||
const { configure } = require("@storybook/react");
|
||||
import requireContext from "require-context.macro";
|
||||
import { configure } from "@storybook/react";
|
||||
|
||||
const req = requireContext("../", true, /.stories.tsx$/);
|
||||
|
||||
function loadStories() {
|
||||
// Story autodiscovery
|
||||
req.keys().forEach(filename => req(filename));
|
||||
|
||||
// Components
|
||||
require("./stories/components/ActionDialog");
|
||||
require("./stories/components/AddressEdit");
|
||||
|
|
|
@ -3,6 +3,7 @@ import { storiesOf } from "@storybook/react";
|
|||
import React from "react";
|
||||
|
||||
import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage";
|
||||
import CardDecorator from "../../CardDecorator";
|
||||
import Decorator from "../../Decorator";
|
||||
|
||||
const props: Omit<LoginCardProps, "classes"> = {
|
||||
|
@ -13,6 +14,7 @@ const props: Omit<LoginCardProps, "classes"> = {
|
|||
};
|
||||
|
||||
storiesOf("Views / Authentication / Log in", module)
|
||||
.addDecorator(CardDecorator)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <LoginPage {...props} />)
|
||||
.add("error", () => <LoginPage {...props} error={true} />)
|
||||
|
|
Loading…
Reference in a new issue