Merge pull request #147 from mirumee/add/reset-password

Password reset flow
This commit is contained in:
Dominik Żegleń 2019-09-06 15:57:35 +02:00 committed by GitHub
commit 0f4bdabc19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1935 additions and 685 deletions

View file

@ -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 - Fix table cell padding - #143 by @dominik-zeglen
- Add fallback locale - #153 by @dominik-zeglen - Add fallback locale - #153 by @dominik-zeglen
- Replace checkbox with switch component in "product type has variants" - #152 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

View file

@ -37,7 +37,8 @@ module.exports = api => {
extractFromFormatMessageCall: true, extractFromFormatMessageCall: true,
messagesDir: "build/locale/" messagesDir: "build/locale/"
} }
] ],
"macros"
]; ];
return { return {

View file

@ -1,6 +1,6 @@
msgid "" msgid ""
msgstr "" 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-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -1487,6 +1487,14 @@ msgctxt "button"
msgid "Back to home" msgid "Back to home"
msgstr "" 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 #: build/locale/src/categories/components/CategoryBackground/CategoryBackground.json
#. [src.categories.components.CategoryBackground.1849089820] - section header #. [src.categories.components.CategoryBackground.1849089820] - section header
#. defaultMessage is: #. defaultMessage is:
@ -1943,6 +1951,14 @@ msgctxt "button"
msgid "Confirm" msgid "Confirm"
msgstr "" 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 #: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json
#. [src.orders.components.OrderCustomer.1111991638] - subheader #. [src.orders.components.OrderCustomer.1111991638] - subheader
#. defaultMessage is: #. defaultMessage is:
@ -3363,6 +3379,14 @@ msgctxt "tax rate"
msgid "Foodstuffs" msgid "Foodstuffs"
msgstr "" 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 #: build/locale/src/discounts/components/VoucherTypes/VoucherTypes.json
#. [src.discounts.components.VoucherTypes.3289659291] - voucher discount type #. [src.discounts.components.VoucherTypes.3289659291] - voucher discount type
#. defaultMessage is: #. defaultMessage is:
@ -4179,6 +4203,14 @@ msgctxt "navigation section name"
msgid "Navigation" msgid "Navigation"
msgstr "" 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 #: build/locale/src/products/views/ProductCreate.json
#. [src.products.views.1591632382] - page header #. [src.products.views.1591632382] - page header
#. defaultMessage is: #. defaultMessage is:
@ -5111,6 +5143,14 @@ msgctxt "description"
msgid "Password" msgid "Password"
msgstr "" 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 #: build/locale/src/orders/components/OrderList/OrderList.json
#. [src.orders.components.OrderList.2743232155] - payment status #. [src.orders.components.OrderList.2743232155] - payment status
#. defaultMessage is: #. defaultMessage is:
@ -5259,6 +5299,14 @@ msgctxt "product type"
msgid "Physical" msgid "Physical"
msgstr "" 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 #: build/locale/src/plugins/components/PluginInfo/PluginInfo.json
#. [src.plugins.components.PluginInfo.3425535100] - section header #. [src.plugins.components.PluginInfo.3425535100] - section header
#. defaultMessage is: #. defaultMessage is:
@ -5675,6 +5723,14 @@ msgctxt "tax rate"
msgid "Property renovations" msgid "Property renovations"
msgstr "" 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 #: build/locale/src/collections/views/CollectionList.json
#. [src.collections.views.1547167026] - publish collections #. [src.collections.views.1547167026] - publish collections
#. defaultMessage is: #. defaultMessage is:
@ -6031,6 +6087,14 @@ msgctxt "button"
msgid "Reset" msgid "Reset"
msgstr "" 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 #: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json
#. [src.shipping.components.ShippingZoneCountriesAssignDialog.1003092716] #. [src.shipping.components.ShippingZoneCountriesAssignDialog.1003092716]
#. defaultMessage is: #. defaultMessage is:
@ -6107,6 +6171,14 @@ msgctxt "description"
msgid "Sale Name" msgid "Sale Name"
msgstr "" 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 #: build/locale/src/intl.json
#. [src.sales] - sales section name #. [src.sales] - sales section name
#. defaultMessage is: #. defaultMessage is:
@ -6447,6 +6519,14 @@ msgctxt "add order note, button"
msgid "Send" msgid "Send"
msgstr "" 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 #: build/locale/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.json
#. [src.staff.components.StaffAddMemberDialog.449055697] - button #. [src.staff.components.StaffAddMemberDialog.449055697] - button
#. defaultMessage is: #. defaultMessage is:
@ -6479,6 +6559,14 @@ msgctxt "voucher end date, switch button"
msgid "Set end date" msgid "Set end date"
msgstr "" 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 #: build/locale/src/plugins/components/PluginInfo/PluginInfo.json
#. [src.plugins.components.PluginInfo.4013064767] #. [src.plugins.components.PluginInfo.4013064767]
#. defaultMessage is: #. defaultMessage is:
@ -6683,18 +6771,6 @@ msgctxt "description"
msgid "Some products require shipping, but no method provided" msgid "Some products require shipping, but no method provided"
msgstr "" 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 #: build/locale/src/components/NotFoundPage/NotFoundPage.json
#. [src.components.NotFoundPage.4036415297] #. [src.components.NotFoundPage.4036415297]
#. defaultMessage is: #. defaultMessage is:
@ -6943,6 +7019,14 @@ msgctxt "plugin success message"
msgid "Succesfully updated plugin settings" msgid "Succesfully updated plugin settings"
msgstr "" msgstr ""
#: build/locale/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.json
#. [src.auth.components.ResetPasswordSuccessPage.4066297200]
#. defaultMessage is:
#. Success! In a few minutes youll receive a message with instructions on how to reset your password.
msgctxt "description"
msgid "Success! In a few minutes youll receive a message with instructions on how to reset your password."
msgstr ""
#: build/locale/src/attributes/views/AttributeCreate/AttributeCreate.json #: build/locale/src/attributes/views/AttributeCreate/AttributeCreate.json
#. [src.attributes.views.AttributeCreate.11941964] #. [src.attributes.views.AttributeCreate.11941964]
#. defaultMessage is: #. defaultMessage is:

50
package-lock.json generated
View file

@ -9409,8 +9409,7 @@
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -9428,13 +9427,11 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -9447,18 +9444,15 @@
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -9561,8 +9555,7 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -9572,7 +9565,6 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -9585,20 +9577,17 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -9615,7 +9604,6 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -9688,8 +9676,7 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -9699,7 +9686,6 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -9775,8 +9761,7 @@
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -9806,7 +9791,6 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -9824,7 +9808,6 @@
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -9863,13 +9846,11 @@
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true
"optional": true
} }
} }
}, },
@ -16962,6 +16943,15 @@
"tough-cookie": "^2.3.3" "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": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View file

@ -120,6 +120,7 @@
"react-intl-po": "^2.2.2", "react-intl-po": "^2.2.2",
"react-test-renderer": "^16.8.6", "react-test-renderer": "^16.8.6",
"regenerator-runtime": "^0.11.1", "regenerator-runtime": "^0.11.1",
"require-context.macro": "^1.1.1",
"rimraf": "^2.7.0", "rimraf": "^2.7.0",
"testcafe": "^1.3.3", "testcafe": "^1.3.3",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",

View file

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

View file

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

View file

@ -8,16 +8,11 @@ import {
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage, useIntl } from "react-intl"; 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 { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import { FormSpacer } from "@saleor/components/FormSpacer"; import { FormSpacer } from "@saleor/components/FormSpacer";
import useTheme from "@saleor/hooks/useTheme";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
export interface FormData { export interface FormData {
@ -41,38 +36,6 @@ const styles = (theme: Theme) =>
loginButton: { loginButton: {
width: 140 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: { panel: {
"& span": { "& span": {
color: theme.palette.error.contrastText color: theme.palette.error.contrastText
@ -81,30 +44,6 @@ const styles = (theme: Theme) =>
borderRadius: theme.spacing.unit, borderRadius: theme.spacing.unit,
marginBottom: theme.spacing.unit * 3, marginBottom: theme.spacing.unit * 3,
padding: theme.spacing.unit * 1.5 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" })( const LoginCard = withStyles(styles, { name: "LoginCard" })(
({ classes, error, disableLoginButton, onSubmit }: LoginCardProps) => { ({
const { isDark } = useTheme(); classes,
error,
disableLoginButton,
onPasswordRecovery,
onSubmit
}: LoginCardProps) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@ -126,82 +70,71 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ change: handleChange, data, submit: handleSubmit }) => ( {({ change: handleChange, data, submit: handleSubmit }) => (
<div className={classes.root}> <>
<div className={classes.sidebar}> {error && (
<SVG className={classes.sidebarArt} src={backgroundArt} /> <div className={classes.panel}>
</div> <Typography variant="caption">
<div className={classes.mainPanel}> <FormattedMessage defaultMessage="Sorry, your username and/or password are incorrect. Please try again." />
<div className={classes.mainPanelContent}> </Typography>
<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> */}
</div> </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>
</div> <FormSpacer />
<Typography className={classes.link} onClick={onPasswordRecovery}>
<FormattedMessage
defaultMessage="Reset your password"
description="button"
/>
</Typography>
</>
)} )}
</Form> </Form>
); );

View file

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

View 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;

View file

@ -0,0 +1,2 @@
export { default } from "./NewPasswordPage";
export * from "./NewPasswordPage";

View file

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

View 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;

View file

@ -0,0 +1,2 @@
export { default } from "./ResetPasswordPage";
export * from "./ResetPasswordPage";

View file

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

View file

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

View file

@ -0,0 +1,2 @@
export { default } from "./ResetPasswordSuccessPage";
export * from "./ResetPasswordSuccessPage";

View file

@ -1,30 +1,47 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom";
import Layout from "./components/Layout";
import { User } from "./types/User"; import { User } from "./types/User";
import {
const TOKEN_STORAGE_KEY = "dashboardAuth"; 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 { interface UserContext {
login: (username: string, password: string, persist: boolean) => void; login: (username: string, password: string, persist: boolean) => void;
loginByToken: (token: string, user: User) => void;
logout: () => void; logout: () => void;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user?: User; user?: User;
} }
export const UserContext = React.createContext<UserContext>({ export const UserContext = React.createContext<UserContext>({
login: undefined, login: undefined,
logout: undefined loginByToken: undefined,
logout: undefined,
tokenAuthLoading: false,
tokenVerifyLoading: false
}); });
export const getAuthToken = () => const AuthRouter: React.FC = () => (
localStorage.getItem(TOKEN_STORAGE_KEY) || <Layout>
sessionStorage.getItem(TOKEN_STORAGE_KEY); <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) => AuthRouter.displayName = "AuthRouter";
persist export default AuthRouter;
? localStorage.setItem(TOKEN_STORAGE_KEY, token)
: sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
export const removeAuthToken = () => { export * from "./utils";
localStorage.removeItem(TOKEN_STORAGE_KEY);
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
};

View file

@ -1,6 +1,11 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedMutation } from "../mutations"; import { TypedMutation } from "../mutations";
import {
RequestPasswordReset,
RequestPasswordResetVariables
} from "./types/RequestPasswordReset";
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
@ -59,3 +64,38 @@ export const TypedVerifyTokenMutation = TypedMutation<
VerifyToken, VerifyToken,
VerifyTokenVariables VerifyTokenVariables
>(tokenVerifyMutation); >(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);

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

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

15
src/auth/urls.ts Normal file
View 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
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

@ -1,27 +1,25 @@
import React from "react"; import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import LoginPage, { FormData } from "../components/LoginPage"; import LoginPage, { FormData } from "../components/LoginPage";
import { UserContext } from "../index"; import { passwordResetUrl } from "../urls";
interface LoginViewProps { const LoginView: React.FC = () => {
loading: boolean; const navigate = useNavigator();
} const { login, user, tokenAuthLoading } = useUser();
const LoginView: React.StatelessComponent<LoginViewProps> = ({ loading }) => ( const handleSubmit = (data: FormData) =>
<UserContext.Consumer> login(data.email, data.password, data.rememberMe);
{({ login, user }) => {
const handleSubmit = (data: FormData) => return (
login(data.email, data.password, data.rememberMe); <LoginPage
return ( error={user === null}
<LoginPage disableLoginButton={tokenAuthLoading}
error={user === null} onPasswordRecovery={() => navigate(passwordResetUrl)}
disableLoginButton={loading} onSubmit={handleSubmit}
onPasswordRecovery={undefined} />
onSubmit={handleSubmit} );
/> };
);
}}
</UserContext.Consumer>
);
LoginView.displayName = "LoginView"; LoginView.displayName = "LoginView";
export default LoginView; export default LoginView;

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

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

View 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;

View file

@ -13,12 +13,11 @@ import { BrowserRouter, Route, Switch } from "react-router-dom";
import AttributeSection from "./attributes"; import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls"; import { attributeSection } from "./attributes/urls";
import { getAuthToken, removeAuthToken } from "./auth"; import Auth, { getAuthToken, removeAuthToken } from "./auth";
import AuthProvider from "./auth/AuthProvider"; import AuthProvider from "./auth/AuthProvider";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading"; import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import SectionRoute from "./auth/components/SectionRoute"; import SectionRoute from "./auth/components/SectionRoute";
import { hasPermission } from "./auth/misc"; import { hasPermission } from "./auth/misc";
import Login from "./auth/views/Login";
import CategorySection from "./categories"; import CategorySection from "./categories";
import CollectionSection from "./collections"; import CollectionSection from "./collections";
import { AppProgressProvider } from "./components/AppProgress"; import { AppProgressProvider } from "./components/AppProgress";
@ -64,6 +63,7 @@ const invalidTokenLink = onError((error: ResponseError) => {
const authLink = setContext((_, context) => { const authLink = setContext((_, context) => {
const authToken = getAuthToken(); const authToken = getAuthToken();
return { return {
...context, ...context,
headers: { headers: {
@ -241,7 +241,7 @@ const Routes: React.FC = () => {
) : hasToken && tokenVerifyLoading ? ( ) : hasToken && tokenVerifyLoading ? (
<LoginLoading /> <LoginLoading />
) : ( ) : (
<Login loading={tokenAuthLoading} /> <Auth />
) )
} }
</AuthProvider> </AuthProvider>

View file

@ -50,6 +50,9 @@ export const commonMessages = defineMessages({
savedChanges: { savedChanges: {
defaultMessage: "Saved changes" defaultMessage: "Saved changes"
}, },
somethingWentWrong: {
defaultMessage: "Saleor ran into an unexpected problem"
},
startDate: { startDate: {
defaultMessage: "Start Date" defaultMessage: "Start Date"
}, },

View file

@ -5,6 +5,7 @@ import { Mutation, MutationFunction, MutationResult } from "react-apollo";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import useNotifier from "./hooks/useNotifier"; import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl";
export interface TypedMutationInnerProps<TData, TVariables> { export interface TypedMutationInnerProps<TData, TVariables> {
children: ( children: (
@ -30,17 +31,10 @@ export function TypedMutation<TData, TVariables>(
<Mutation <Mutation
mutation={mutation} mutation={mutation}
onCompleted={onCompleted} onCompleted={onCompleted}
onError={err => { onError={(err: ApolloError) => {
const msg = intl.formatMessage( notify({
{ text: intl.formatMessage(commonMessages.somethingWentWrong)
defaultMessage: "Something went wrong. {errorMessage}", });
description: "error message"
},
{
errorMessage: err.message
}
);
notify({ text: msg });
if (onError) { if (onError) {
onError(err); onError(err);
} }

View file

@ -9,6 +9,7 @@ import AppProgress from "./components/AppProgress";
import ErrorPage from "./components/ErrorPage/ErrorPage"; import ErrorPage from "./components/ErrorPage/ErrorPage";
import useNavigator from "./hooks/useNavigator"; import useNavigator from "./hooks/useNavigator";
import useNotifier from "./hooks/useNotifier"; import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl";
import { RequireAtLeastOne } from "./misc"; import { RequireAtLeastOne } from "./misc";
export interface LoadMore<TData, TVariables> { export interface LoadMore<TData, TVariables> {
@ -83,16 +84,9 @@ export function TypedQuery<TData, TVariables>(
> >
{queryData => { {queryData => {
if (queryData.error) { if (queryData.error) {
const msg = intl.formatMessage( pushMessage({
{ text: intl.formatMessage(commonMessages.somethingWentWrong)
defaultMessage: "Something went wrong. {errorMessage}", });
description: "error message"
},
{
message: queryData.error.message
}
);
pushMessage({ text: msg });
} }
const loadMore = ( const loadMore = (

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,13 @@
/* eslint-disable */ /* 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() { function loadStories() {
// Story autodiscovery
req.keys().forEach(filename => req(filename));
// Components // Components
require("./stories/components/ActionDialog"); require("./stories/components/ActionDialog");
require("./stories/components/AddressEdit"); require("./stories/components/AddressEdit");

View file

@ -3,6 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage"; import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
const props: Omit<LoginCardProps, "classes"> = { const props: Omit<LoginCardProps, "classes"> = {
@ -13,6 +14,7 @@ const props: Omit<LoginCardProps, "classes"> = {
}; };
storiesOf("Views / Authentication / Log in", module) storiesOf("Views / Authentication / Log in", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("default", () => <LoginPage {...props} />) .add("default", () => <LoginPage {...props} />)
.add("error", () => <LoginPage {...props} error={true} />) .add("error", () => <LoginPage {...props} error={true} />)