This commit is contained in:
dominik-zeglen 2019-09-02 21:23:37 +02:00
parent b582a7f51a
commit 48ca3b9e15
18 changed files with 225 additions and 50 deletions

View file

@ -9,15 +9,13 @@ import { User } from "./types/User";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
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
@ -41,15 +39,13 @@ 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;
}
) => React.ReactNode;
tokenAuth: PartialMutationProviderOutput<TokenAuth, TokenAuthVariables>; tokenAuth: PartialMutationProviderOutput<TokenAuth, TokenAuthVariables>;
tokenVerify: PartialMutationProviderOutput<VerifyToken, VerifyTokenVariables>; tokenVerify: PartialMutationProviderOutput<VerifyToken, VerifyTokenVariables>;
} }
@ -116,9 +112,16 @@ class AuthProvider extends React.Component<
const { children, tokenAuth, tokenVerify } = this.props; const { children, tokenAuth, tokenVerify } = this.props;
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,
logout: this.logout,
tokenAuthLoading: tokenAuth.opts.loading,
tokenVerifyLoading: tokenVerify.opts.loading,
user
}}
> >
{children({ {children({
hasToken: !!getAuthToken(), hasToken: !!getAuthToken(),

View file

@ -56,7 +56,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) => { ({
classes,
error,
disableLoginButton,
onPasswordRecovery,
onSubmit
}: LoginCardProps) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@ -123,7 +129,7 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
</Button> </Button>
</div> </div>
<FormSpacer /> <FormSpacer />
<Typography className={classes.link}> <Typography className={classes.link} onClick={onPasswordRecovery}>
<FormattedMessage <FormattedMessage
defaultMessage="Reset your password" defaultMessage="Reset your password"
description="button" description="button"

View file

@ -6,4 +6,9 @@ import NewPasswordPage from "./NewPasswordPage";
storiesOf("Views / Authentication / Set up a new password", module) storiesOf("Views / Authentication / Set up a new password", module)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("default", () => <NewPasswordPage onSubmit={() => undefined} />); .add("default", () => (
<NewPasswordPage disabled={false} onSubmit={() => undefined} />
))
.add("loading", () => (
<NewPasswordPage disabled={true} onSubmit={() => undefined} />
));

View file

@ -25,6 +25,7 @@ export interface NewPasswordPageFormData {
confirmPassword: string; confirmPassword: string;
} }
export interface NewPasswordPageProps { export interface NewPasswordPageProps {
disabled: boolean;
onSubmit: (data: NewPasswordPageFormData) => void; onSubmit: (data: NewPasswordPageFormData) => void;
} }
@ -34,7 +35,7 @@ const initialForm: NewPasswordPageFormData = {
}; };
const NewPasswordPage: React.FC<NewPasswordPageProps> = props => { const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
const { onSubmit } = props; const { disabled, onSubmit } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
@ -55,6 +56,7 @@ const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
autoFocus autoFocus
fullWidth fullWidth
autoComplete="none" autoComplete="none"
disabled={disabled}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "New Password" defaultMessage: "New Password"
})} })}
@ -72,6 +74,7 @@ const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
fullWidth fullWidth
error={passwordError} error={passwordError}
autoComplete="none" autoComplete="none"
disabled={disabled}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Confirm Password" defaultMessage: "Confirm Password"
})} })}
@ -93,7 +96,7 @@ const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
<Button <Button
className={classes.submit} className={classes.submit}
color="primary" color="primary"
disabled={passwordError && data.password.length > 0} disabled={(passwordError && data.password.length > 0) || disabled}
variant="contained" variant="contained"
onClick={handleSubmit} onClick={handleSubmit}
type="submit" type="submit"

View file

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

View file

@ -6,4 +6,9 @@ import ResetPasswordPage from "./ResetPasswordPage";
storiesOf("Views / Authentication / Reset password", module) storiesOf("Views / Authentication / Reset password", module)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("default", () => <ResetPasswordPage onSubmit={() => undefined} />); .add("default", () => (
<ResetPasswordPage disabled={false} onSubmit={() => undefined} />
))
.add("loading", () => (
<ResetPasswordPage disabled={true} onSubmit={() => undefined} />
));

View file

@ -25,11 +25,12 @@ export interface ResetPasswordPageFormData {
email: string; email: string;
} }
export interface ResetPasswordPageProps { export interface ResetPasswordPageProps {
disabled: boolean;
onSubmit: (data: ResetPasswordPageFormData) => void; onSubmit: (data: ResetPasswordPageFormData) => void;
} }
const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => { const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
const { onSubmit } = props; const { disabled, onSubmit } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
@ -44,6 +45,7 @@ const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
<FormSpacer /> <FormSpacer />
<TextField <TextField
autoFocus autoFocus
disabled={disabled}
fullWidth fullWidth
autoComplete="username" autoComplete="username"
label={intl.formatMessage(commonMessages.email)} label={intl.formatMessage(commonMessages.email)}
@ -58,6 +60,7 @@ const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
<Button <Button
className={classes.submit} className={classes.submit}
color="primary" color="primary"
disabled={disabled}
variant="contained" variant="contained"
onClick={handleSubmit} onClick={handleSubmit}
type="submit" type="submit"

View file

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

View file

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

View file

@ -1,18 +1,32 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom";
import { User } from "./types/User"; import { User } from "./types/User";
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";
const TOKEN_STORAGE_KEY = "dashboardAuth"; const TOKEN_STORAGE_KEY = "dashboardAuth";
interface UserContext { interface UserContext {
login: (username: string, password: string, persist: boolean) => void; login: (username: string, password: string, persist: boolean) => void;
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 logout: undefined,
tokenAuthLoading: false,
tokenVerifyLoading: false
}); });
export const getAuthToken = () => export const getAuthToken = () =>
@ -28,3 +42,15 @@ export const removeAuthToken = () => {
localStorage.removeItem(TOKEN_STORAGE_KEY); localStorage.removeItem(TOKEN_STORAGE_KEY);
sessionStorage.removeItem(TOKEN_STORAGE_KEY); sessionStorage.removeItem(TOKEN_STORAGE_KEY);
}; };
const AuthRouter: React.FC = () => (
<Switch>
<Route path={passwordResetPath} component={ResetPassword} />
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
<Route path={newPasswordPath} component={NewPassword} />
<Route component={LoginView} />
</Switch>
);
AuthRouter.displayName = "AuthRouter";
export default AuthRouter;

View file

@ -1,6 +1,10 @@
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 { 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 +63,18 @@ 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);

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

17
src/auth/urls.ts Normal file
View file

@ -0,0 +1,17 @@
import { stringify as stringifyQs } from "qs";
export const loginPath = "/login/";
export const loginUrl = loginPath;
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 {
token: string;
}
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>
newPasswordPath + "?" + stringifyQs(params);

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

View file

@ -0,0 +1,45 @@
import React from "react";
import urlJoin from "url-join";
import useNavigator from "@saleor/hooks/useNavigator";
import useShop from "@saleor/hooks/useShop";
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 navigate = useNavigator();
const shop = useShop();
const handleRequestPasswordReset = (data: RequestPasswordReset) => {
if (data.requestPasswordReset.errors.length === 0) {
navigate(passwordResetSuccessUrl);
}
};
return (
<RequestPasswordResetMutation onCompleted={handleRequestPasswordReset}>
{(requestPasswordReset, requestPasswordResetOpts) => {
const handleSubmit = (data: ResetPasswordPageFormData) =>
requestPasswordReset({
variables: {
email: data.email,
redirectUrl: urlJoin(shop.domain.url, newPasswordUrl())
}
});
return (
<ResetPasswordPage
disabled={requestPasswordResetOpts.loading}
onSubmit={handleSubmit}
/>
);
}}
</RequestPasswordResetMutation>
);
};
ResetPasswordView.displayName = "ResetPasswordView";
export default ResetPasswordView;

View file

@ -0,0 +1,13 @@
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import ResetPasswordSuccessPage from "../components/ResetPasswordSuccessPage";
import { loginUrl } from "../urls";
const ResetPasswordSuccessView: React.FC = () => {
const navigate = useNavigator();
return <ResetPasswordSuccessPage onBack={() => navigate(loginUrl)} />;
};
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";
@ -241,7 +240,7 @@ const Routes: React.FC = () => {
) : hasToken && tokenVerifyLoading ? ( ) : hasToken && tokenVerifyLoading ? (
<LoginLoading /> <LoginLoading />
) : ( ) : (
<Login loading={tokenAuthLoading} /> <Auth />
) )
} }
</AuthProvider> </AuthProvider>