Merge branch 'master' of github.com:mirumee/saleor-dashboard into feature/unconfirmed-order-line-manipulation
This commit is contained in:
commit
4f3fea10ff
37 changed files with 1474 additions and 399 deletions
2
.github/workflows/test-env-deploy.yml
vendored
2
.github/workflows/test-env-deploy.yml
vendored
|
@ -88,7 +88,7 @@ jobs:
|
|||
aws s3 sync ./build/storybook s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }}/storybook
|
||||
|
||||
- name: Invalidate cache
|
||||
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ env.domain }}/*"
|
||||
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ steps.set-domain.outputs.domain }}/*"
|
||||
|
||||
- name: Update deployment status
|
||||
uses: bobheadxi/deployments@v0.4.2
|
||||
|
|
|
@ -18,6 +18,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Add product reference attributes - #948 by @orzechdev
|
||||
- Drop descriptionJson and contentJson fields - #950 by @jwm0
|
||||
- Add error tracking with Sentry adapter - #956 by @jwm0
|
||||
- Add OAuth2 login with OpenID support - #963 by @orzechdev
|
||||
|
||||
# 2.11.1
|
||||
|
||||
|
|
|
@ -1016,12 +1016,23 @@
|
|||
"src_dot_auth_dot_components_dot_LoginPage_dot_2237029987": {
|
||||
"string": "Password"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_2981302356": {
|
||||
"context": "link",
|
||||
"string": "Use this link to recover it"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_3476994590": {
|
||||
"string": "Sorry, your username and/or password are incorrect. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_4028609483": {
|
||||
"context": "button",
|
||||
"string": "Reset your password"
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_3762459576": {
|
||||
"context": "description",
|
||||
"string": "or login using"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_534894384": {
|
||||
"string": "Sorry, login went wrong. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_599516345": {
|
||||
"context": "description",
|
||||
"string": "Forgot password? {resetPasswordLink}"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_NewPasswordPage_dot_1254879564": {
|
||||
"string": "New Password"
|
||||
|
|
|
@ -1922,6 +1922,49 @@ enum ExportScope {
|
|||
FILTER
|
||||
}
|
||||
|
||||
type ExternalAuthentication {
|
||||
id: String!
|
||||
name: String
|
||||
}
|
||||
|
||||
type ExternalAuthenticationUrl {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
authenticationData: JSONString
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalLogout {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
logoutData: JSONString
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalObtainAccessTokens {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
token: String
|
||||
refreshToken: String
|
||||
csrfToken: String
|
||||
user: User
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalRefresh {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
token: String
|
||||
refreshToken: String
|
||||
csrfToken: String
|
||||
user: User
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalVerify {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
user: User
|
||||
isValid: Boolean!
|
||||
verifyData: JSONString
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type File {
|
||||
url: String!
|
||||
contentType: String
|
||||
|
@ -2744,6 +2787,11 @@ type Mutation {
|
|||
tokenRefresh(csrfToken: String, refreshToken: String): RefreshToken
|
||||
tokenVerify(token: String!): VerifyToken
|
||||
tokensDeactivateAll: DeactivateAllUserTokens
|
||||
externalAuthenticationUrl(input: JSONString!, pluginId: String!): ExternalAuthenticationUrl
|
||||
externalObtainAccessTokens(input: JSONString!, pluginId: String!): ExternalObtainAccessTokens
|
||||
externalRefresh(input: JSONString!, pluginId: String!): ExternalRefresh
|
||||
externalLogout(input: JSONString!, pluginId: String!): ExternalLogout
|
||||
externalVerify(input: JSONString!, pluginId: String!): ExternalVerify
|
||||
requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset
|
||||
confirmAccount(email: String!, token: String!): ConfirmAccount
|
||||
setPassword(email: String!, password: String!, token: String!): SetPassword
|
||||
|
@ -4906,6 +4954,7 @@ input ShippingZoneUpdateInput {
|
|||
|
||||
type Shop {
|
||||
availablePaymentGateways(currency: String): [PaymentGateway!]!
|
||||
availableExternalAuthentications: [ExternalAuthentication!]!
|
||||
availableShippingMethods(channel: String!, address: AddressInput): [ShippingMethod]
|
||||
geolocalization: Geolocalization
|
||||
countries(languageCode: LanguageCodeEnum): [CountryDisplay!]!
|
||||
|
|
|
@ -79,6 +79,7 @@ export const appDeleteFailedInstallationMutation = gql`
|
|||
`;
|
||||
|
||||
export const appFetchMutation = gql`
|
||||
${appErrorFragment}
|
||||
mutation AppFetch($manifestUrl: String!) {
|
||||
appFetchManifest(manifestUrl: $manifestUrl) {
|
||||
manifest {
|
||||
|
|
|
@ -2,7 +2,7 @@ import setupApi from "@test/api";
|
|||
import { act, renderHook } from "@testing-library/react-hooks";
|
||||
import ApolloClient from "apollo-client";
|
||||
|
||||
import { useAuthProvider } from "./AuthProvider";
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
import { getTokens, setAuthToken } from "./utils";
|
||||
|
||||
const apolloClient = setupApi();
|
||||
|
@ -14,7 +14,7 @@ function renderAuthProvider(apolloClient: ApolloClient<any>) {
|
|||
const notify = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAuthProvider(intl as any, notify, apolloClient)
|
||||
useAuthProvider({ apolloClient, intl: intl as any, notify })
|
||||
);
|
||||
|
||||
return result;
|
||||
|
@ -43,7 +43,7 @@ describe("User", () => {
|
|||
await act(() =>
|
||||
hook.current.login(adminCredentials.email, adminCredentials.password)
|
||||
);
|
||||
expect(hook.current.userContext.email).toBe(adminCredentials.email);
|
||||
expect(hook.current.user.email).toBe(adminCredentials.email);
|
||||
adminCredentials.token = getTokens().auth;
|
||||
|
||||
done();
|
||||
|
@ -55,7 +55,7 @@ describe("User", () => {
|
|||
await act(() =>
|
||||
hook.current.login(adminCredentials.email, "NotAValidPassword123!")
|
||||
);
|
||||
expect(hook.current.userContext).toBe(null);
|
||||
expect(hook.current.user).toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -69,7 +69,7 @@ describe("User", () => {
|
|||
nonStaffUserCredentials.password
|
||||
)
|
||||
);
|
||||
expect(hook.current.userContext).toBe(undefined);
|
||||
expect(hook.current.user).toBe(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -79,7 +79,7 @@ describe("User", () => {
|
|||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() => hook.current.autologinPromise.current);
|
||||
expect(hook.current.userContext.email).toBe(adminCredentials.email);
|
||||
expect(hook.current.user.email).toBe(adminCredentials.email);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -89,7 +89,7 @@ describe("User", () => {
|
|||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() => hook.current.autologinPromise.current);
|
||||
expect(hook.current.userContext).toBe(undefined);
|
||||
expect(hook.current.user).toBe(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
@ -1,197 +1,11 @@
|
|||
import { IMessageContext } from "@saleor/components/messages";
|
||||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import {
|
||||
isSupported as isCredentialsManagementAPISupported,
|
||||
login as loginWithCredentialsManagementAPI,
|
||||
saveCredentials
|
||||
} from "@saleor/utils/credentialsManagement";
|
||||
import ApolloClient from "apollo-client";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useApolloClient, useMutation } from "react-apollo";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
import React, { useContext } from "react";
|
||||
import { useApolloClient } from "react-apollo";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { UserContext } from "./";
|
||||
import {
|
||||
tokenAuthMutation,
|
||||
tokenRefreshMutation,
|
||||
tokenVerifyMutation
|
||||
} from "./mutations";
|
||||
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
|
||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||
import {
|
||||
displayDemoMessage,
|
||||
getTokens,
|
||||
removeTokens,
|
||||
setAuthToken,
|
||||
setTokens
|
||||
} from "./utils";
|
||||
|
||||
const persistToken = false;
|
||||
|
||||
export function useAuthProvider(
|
||||
intl: IntlShape,
|
||||
notify: IMessageContext,
|
||||
apolloClient: ApolloClient<any>
|
||||
) {
|
||||
const [userContext, setUserContext] = useState<undefined | User>(undefined);
|
||||
const autologinPromise = useRef<Promise<any>>();
|
||||
const refreshPromise = useRef<Promise<boolean>>();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getTokens().auth;
|
||||
if (!!token && !userContext) {
|
||||
autologinPromise.current = tokenVerify({ variables: { token } });
|
||||
} else {
|
||||
autologinPromise.current = loginWithCredentialsManagementAPI(login);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userContext) {
|
||||
const { id, email, firstName, lastName } = userContext;
|
||||
errorTracker.setUserData({
|
||||
email,
|
||||
id,
|
||||
username: `${firstName} ${lastName}`
|
||||
});
|
||||
|
||||
if (!userContext.isStaff) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
|
||||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
const logout = () => {
|
||||
setUserContext(undefined);
|
||||
if (isCredentialsManagementAPISupported) {
|
||||
navigator.credentials.preventSilentAccess();
|
||||
}
|
||||
removeTokens();
|
||||
};
|
||||
|
||||
const [tokenAuth, tokenAuthResult] = useMutation<
|
||||
TokenAuth,
|
||||
TokenAuthVariables
|
||||
>(tokenAuthMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: ({ tokenCreate }) => {
|
||||
if (tokenCreate.errors.length > 0) {
|
||||
logout();
|
||||
}
|
||||
|
||||
const user = 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.
|
||||
setUserContext(user);
|
||||
if (user) {
|
||||
setTokens(tokenCreate.token, tokenCreate.csrfToken, persistToken);
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
const [tokenRefresh] = useMutation<RefreshToken, RefreshTokenVariables>(
|
||||
tokenRefreshMutation,
|
||||
{
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
}
|
||||
);
|
||||
const [tokenVerify, tokenVerifyResult] = useMutation<
|
||||
VerifyToken,
|
||||
VerifyTokenVariables
|
||||
>(tokenVerifyMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: result => {
|
||||
if (result.tokenVerify === null) {
|
||||
logout();
|
||||
} else {
|
||||
const user = result.tokenVerify?.user;
|
||||
|
||||
if (!!user) {
|
||||
setUserContext(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
|
||||
const tokenAuthOpts = {
|
||||
...tokenAuthResult,
|
||||
status: getMutationStatus(tokenAuthResult)
|
||||
};
|
||||
const tokenVerifyOpts = {
|
||||
...tokenVerifyResult,
|
||||
status: getMutationStatus(tokenVerifyResult)
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const result = await tokenAuth({ variables: { email, password } });
|
||||
|
||||
if (result && !result.data.tokenCreate.errors.length) {
|
||||
if (!!onLogin) {
|
||||
onLogin();
|
||||
}
|
||||
saveCredentials(result.data.tokenCreate.user, password);
|
||||
|
||||
return result.data.tokenCreate.user;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const loginByToken = (auth: string, refresh: string, user: User) => {
|
||||
setUserContext(user);
|
||||
setTokens(auth, refresh, persistToken);
|
||||
};
|
||||
|
||||
const refreshToken = (): Promise<boolean> => {
|
||||
if (!!refreshPromise.current) {
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const token = getTokens().refresh;
|
||||
|
||||
return tokenRefresh({ variables: { token } }).then(refreshData => {
|
||||
if (!!refreshData.data.tokenRefresh?.token) {
|
||||
setAuthToken(refreshData.data.tokenRefresh.token, persistToken);
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
return resolve(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
autologinPromise,
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
refreshToken,
|
||||
tokenAuthOpts,
|
||||
tokenVerifyOpts,
|
||||
userContext
|
||||
};
|
||||
}
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
import { getTokens } from "./utils";
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -202,30 +16,10 @@ const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||
const intl = useIntl();
|
||||
const notify = useNotifier();
|
||||
|
||||
const {
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
tokenAuthOpts,
|
||||
refreshToken,
|
||||
tokenVerifyOpts,
|
||||
userContext
|
||||
} = useAuthProvider(intl, notify, apolloClient);
|
||||
const authProvider = useAuthProvider({ apolloClient, intl, notify });
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
tokenAuthLoading: tokenAuthOpts.loading,
|
||||
tokenRefresh: refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user: userContext
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
<UserContext.Provider value={authProvider}>{children}</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import LoginLoading from "../../../auth/components/LoginLoading";
|
||||
import Decorator from "../../Decorator";
|
||||
import LoginLoading from ".";
|
||||
|
||||
storiesOf("Views / Authentication / Verifying remembered user", module)
|
||||
.addDecorator(Decorator)
|
|
@ -1,14 +1,24 @@
|
|||
import { Omit } from "@material-ui/core";
|
||||
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
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"> = {
|
||||
disableLoginButton: true,
|
||||
disabled: false,
|
||||
error: false,
|
||||
externalAuthentications: [
|
||||
{
|
||||
__typename: "ExternalAuthentication",
|
||||
id: "auth.plugin.example",
|
||||
name: "Example auth plugin"
|
||||
}
|
||||
],
|
||||
externalError: false,
|
||||
loading: false,
|
||||
onExternalAuthentication: () => undefined,
|
||||
onPasswordRecovery: undefined,
|
||||
onSubmit: () => undefined
|
||||
};
|
||||
|
@ -18,4 +28,5 @@ storiesOf("Views / Authentication / Log in", module)
|
|||
.addDecorator(Decorator)
|
||||
.add("default", () => <LoginPage {...props} />)
|
||||
.add("error", () => <LoginPage {...props} error={true} />)
|
||||
.add("loading", () => <LoginPage {...props} disableLoginButton={true} />);
|
||||
.add("disabled", () => <LoginPage {...props} disabled={true} />)
|
||||
.add("loading", () => <LoginPage {...props} loading={true} />);
|
|
@ -1,18 +1,17 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Form from "@saleor/components/Form";
|
||||
import { AvailableExternalAuthentications_shop_availableExternalAuthentications } from "@saleor/auth/types/AvailableExternalAuthentications";
|
||||
import { FormSpacer } from "@saleor/components/FormSpacer";
|
||||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { SubmitPromise } from "@saleor/hooks/useForm";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
export interface FormData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
import LoginForm, { LoginFormData } from "./form";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
|
@ -23,7 +22,13 @@ const useStyles = makeStyles(
|
|||
link: {
|
||||
color: theme.palette.primary.main,
|
||||
cursor: "pointer",
|
||||
textAlign: "center"
|
||||
textDecoration: "underline"
|
||||
},
|
||||
loading: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
justifyContent: "center"
|
||||
},
|
||||
loginButton: {
|
||||
width: 140
|
||||
|
@ -43,27 +48,40 @@ const useStyles = makeStyles(
|
|||
|
||||
export interface LoginCardProps {
|
||||
error: boolean;
|
||||
disableLoginButton: boolean;
|
||||
externalError: boolean;
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
externalAuthentications?: AvailableExternalAuthentications_shop_availableExternalAuthentications[];
|
||||
onExternalAuthentication: (pluginId: string) => void;
|
||||
onPasswordRecovery: () => void;
|
||||
onSubmit?(event: FormData);
|
||||
onSubmit?: (event: LoginFormData) => SubmitPromise;
|
||||
}
|
||||
|
||||
const LoginCard: React.FC<LoginCardProps> = props => {
|
||||
const { error, disableLoginButton, onPasswordRecovery, onSubmit } = props;
|
||||
const {
|
||||
error,
|
||||
externalError,
|
||||
disabled,
|
||||
loading,
|
||||
externalAuthentications = [],
|
||||
onExternalAuthentication,
|
||||
onPasswordRecovery,
|
||||
onSubmit
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
let initialFormData = { email: "", password: "" };
|
||||
if (DEMO_MODE) {
|
||||
initialFormData = {
|
||||
email: "admin@example.com",
|
||||
password: "admin"
|
||||
};
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress size={128} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form initial={initialFormData} onSubmit={onSubmit}>
|
||||
<LoginForm onSubmit={onSubmit}>
|
||||
{({ change: handleChange, data, submit: handleSubmit }) => (
|
||||
<>
|
||||
{error && (
|
||||
|
@ -73,6 +91,13 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{externalError && (
|
||||
<div className={classes.panel} data-test="loginErrorMessage">
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="Sorry, login went wrong. Please try again." />
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
|
@ -84,6 +109,7 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
inputProps={{
|
||||
"data-test": "email"
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
|
@ -99,13 +125,14 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
inputProps={{
|
||||
"data-test": "password"
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button
|
||||
className={classes.loginButton}
|
||||
color="primary"
|
||||
disabled={disableLoginButton}
|
||||
disabled={disabled}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
|
@ -115,15 +142,56 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
</Button>
|
||||
</div>
|
||||
<FormSpacer />
|
||||
<Typography className={classes.link} onClick={onPasswordRecovery}>
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
defaultMessage="Reset your password"
|
||||
description="button"
|
||||
defaultMessage="Forgot password? {resetPasswordLink}"
|
||||
description="description"
|
||||
values={{
|
||||
resetPasswordLink: (
|
||||
<a className={classes.link} onClick={onPasswordRecovery}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use this link to recover it"
|
||||
description="link"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
{externalAuthentications.length > 0 && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Divider />
|
||||
<FormSpacer />
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
defaultMessage="or login using"
|
||||
description="description"
|
||||
/>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{externalAuthentications.map(externalAuthentication => (
|
||||
<React.Fragment key={externalAuthentication.id}>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() =>
|
||||
onExternalAuthentication(externalAuthentication.id)
|
||||
}
|
||||
data-test="external-authentication"
|
||||
disabled={disabled}
|
||||
>
|
||||
{externalAuthentication.name}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</LoginForm>
|
||||
);
|
||||
};
|
||||
LoginCard.displayName = "LoginCard";
|
||||
|
|
73
src/auth/components/LoginPage/form.tsx
Normal file
73
src/auth/components/LoginPage/form.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
|
||||
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
|
||||
import React from "react";
|
||||
|
||||
export interface LoginFormData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UseLoginFormResult {
|
||||
change: FormChange;
|
||||
data: LoginFormData;
|
||||
hasChanged: boolean;
|
||||
submit: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface LoginFormProps {
|
||||
children: (props: UseLoginFormResult) => React.ReactNode;
|
||||
onSubmit: (data: LoginFormData) => SubmitPromise;
|
||||
}
|
||||
|
||||
const getLoginFormData = () => {
|
||||
if (DEMO_MODE) {
|
||||
return {
|
||||
email: "admin@example.com",
|
||||
password: "admin"
|
||||
};
|
||||
}
|
||||
return { email: "", password: "" };
|
||||
};
|
||||
|
||||
function useLoginForm(
|
||||
onSubmit: (data: LoginFormData) => SubmitPromise
|
||||
): UseLoginFormResult {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const triggerChange = () => setChanged(true);
|
||||
|
||||
const form = useForm(getLoginFormData());
|
||||
|
||||
const handleChange: FormChange = (event, cb) => {
|
||||
form.change(event, cb);
|
||||
triggerChange();
|
||||
};
|
||||
|
||||
const data: LoginFormData = {
|
||||
...form.data
|
||||
};
|
||||
|
||||
const handleSubmit = async (data: LoginFormData) => {
|
||||
const errors = await onSubmit(data);
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const submit = async () => handleFormSubmit(data, handleSubmit, setChanged);
|
||||
|
||||
return {
|
||||
change: handleChange,
|
||||
data,
|
||||
hasChanged: changed,
|
||||
submit
|
||||
};
|
||||
}
|
||||
|
||||
const LoginForm: React.FC<LoginFormProps> = ({ children, onSubmit }) => {
|
||||
const props = useLoginForm(onSubmit);
|
||||
|
||||
return <form onSubmit={props.submit}>{children(props)}</form>;
|
||||
};
|
||||
|
||||
LoginForm.displayName = "LoginForm";
|
||||
export default LoginForm;
|
58
src/auth/hooks/useAuthProvider.ts
Normal file
58
src/auth/hooks/useAuthProvider.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { IMessageContext } from "@saleor/components/messages";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import ApolloClient from "apollo-client";
|
||||
import { MutableRefObject } from "react";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { useExternalAuthProvider } from "./useExternalAuthProvider";
|
||||
import { useSaleorAuthProvider } from "./useSaleorAuthProvider";
|
||||
|
||||
export interface UseAuthProvider {
|
||||
logout: () => void;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenRefresh: () => Promise<boolean>;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
autologinPromise?: MutableRefObject<Promise<any>>;
|
||||
}
|
||||
export interface UseAuthProviderOpts {
|
||||
intl: IntlShape;
|
||||
notify: IMessageContext;
|
||||
apolloClient: ApolloClient<any>;
|
||||
}
|
||||
|
||||
export function useAuthProvider(opts: UseAuthProviderOpts) {
|
||||
const [authPlugin, setAuthPlugin] = useLocalStorage("authPlugin", undefined);
|
||||
|
||||
const saleorAuth = useSaleorAuthProvider({
|
||||
authPlugin,
|
||||
setAuthPlugin,
|
||||
...opts
|
||||
});
|
||||
|
||||
const externalAuth = useExternalAuthProvider({
|
||||
authPlugin,
|
||||
setAuthPlugin,
|
||||
...opts
|
||||
});
|
||||
|
||||
const loginAuth = {
|
||||
login: saleorAuth.login,
|
||||
loginByExternalPlugin: externalAuth.loginByExternalPlugin,
|
||||
loginByToken: saleorAuth.loginByToken,
|
||||
requestLoginByExternalPlugin: externalAuth.requestLoginByExternalPlugin
|
||||
};
|
||||
|
||||
if (authPlugin) {
|
||||
return {
|
||||
...externalAuth,
|
||||
...loginAuth
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...saleorAuth,
|
||||
...loginAuth
|
||||
};
|
||||
}
|
262
src/auth/hooks/useExternalAuthProvider.ts
Normal file
262
src/auth/hooks/useExternalAuthProvider.ts
Normal file
|
@ -0,0 +1,262 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import { SetLocalStorage } from "@saleor/hooks/useLocalStorage";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
|
||||
import {
|
||||
externalAuthenticationUrlMutation,
|
||||
externalObtainAccessTokensMutation,
|
||||
externalTokenRefreshMutation,
|
||||
externalTokenVerifyMutation
|
||||
} from "../mutations";
|
||||
import {
|
||||
ExternalAuthenticationUrl,
|
||||
ExternalAuthenticationUrlVariables
|
||||
} from "../types/ExternalAuthenticationUrl";
|
||||
import {
|
||||
ExternalObtainAccessTokens,
|
||||
ExternalObtainAccessTokens_externalObtainAccessTokens,
|
||||
ExternalObtainAccessTokensVariables
|
||||
} from "../types/ExternalObtainAccessTokens";
|
||||
import {
|
||||
ExternalRefreshToken,
|
||||
ExternalRefreshTokenVariables
|
||||
} from "../types/ExternalRefreshToken";
|
||||
import {
|
||||
ExternalVerifyToken,
|
||||
ExternalVerifyTokenVariables
|
||||
} from "../types/ExternalVerifyToken";
|
||||
import {
|
||||
displayDemoMessage,
|
||||
getTokens,
|
||||
removeTokens,
|
||||
setAuthToken,
|
||||
setTokens
|
||||
} from "../utils";
|
||||
import { UseAuthProvider, UseAuthProviderOpts } from "./useAuthProvider";
|
||||
|
||||
export interface RequestExternalLoginInput {
|
||||
redirectUri: string;
|
||||
}
|
||||
export interface ExternalLoginInput {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface UseExternalAuthProvider extends UseAuthProvider {
|
||||
requestLoginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => Promise<void>;
|
||||
loginByExternalPlugin: (
|
||||
input: ExternalLoginInput
|
||||
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
|
||||
}
|
||||
export interface UseExternalAuthProviderOpts extends UseAuthProviderOpts {
|
||||
setAuthPlugin: SetLocalStorage<any>;
|
||||
authPlugin: string;
|
||||
}
|
||||
|
||||
const persistToken = false;
|
||||
|
||||
export function useExternalAuthProvider({
|
||||
apolloClient,
|
||||
authPlugin,
|
||||
intl,
|
||||
notify,
|
||||
setAuthPlugin
|
||||
}: UseExternalAuthProviderOpts): UseExternalAuthProvider {
|
||||
const [userContext, setUserContext] = useState<undefined | User>(undefined);
|
||||
const autologinPromise = useRef<Promise<any>>();
|
||||
const refreshPromise = useRef<Promise<boolean>>();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getTokens().auth;
|
||||
if (authPlugin && !!token && !userContext) {
|
||||
const input = JSON.stringify({
|
||||
token
|
||||
});
|
||||
autologinPromise.current = tokenVerify({
|
||||
variables: { input, pluginId: authPlugin }
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authPlugin && userContext) {
|
||||
const { id, email, firstName, lastName } = userContext;
|
||||
errorTracker.setUserData({
|
||||
email,
|
||||
id,
|
||||
username: `${firstName} ${lastName}`
|
||||
});
|
||||
|
||||
if (!userContext.isStaff) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
|
||||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
const logout = () => {
|
||||
setUserContext(undefined);
|
||||
setAuthPlugin(undefined);
|
||||
removeTokens();
|
||||
};
|
||||
|
||||
const [externalAuthenticationUrl] = useMutation<
|
||||
ExternalAuthenticationUrl,
|
||||
ExternalAuthenticationUrlVariables
|
||||
>(externalAuthenticationUrlMutation, {
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
});
|
||||
const [obtainAccessTokens, obtainAccessTokensResult] = useMutation<
|
||||
ExternalObtainAccessTokens,
|
||||
ExternalObtainAccessTokensVariables
|
||||
>(externalObtainAccessTokensMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: ({ externalObtainAccessTokens }) => {
|
||||
if (externalObtainAccessTokens.errors.length > 0) {
|
||||
logout();
|
||||
}
|
||||
|
||||
const user = externalObtainAccessTokens.user;
|
||||
|
||||
setUserContext(user);
|
||||
if (user) {
|
||||
setTokens(
|
||||
externalObtainAccessTokens.token,
|
||||
externalObtainAccessTokens.csrfToken,
|
||||
persistToken
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
const [tokenRefresh] = useMutation<
|
||||
ExternalRefreshToken,
|
||||
ExternalRefreshTokenVariables
|
||||
>(externalTokenRefreshMutation, {
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
});
|
||||
const [tokenVerify, tokenVerifyResult] = useMutation<
|
||||
ExternalVerifyToken,
|
||||
ExternalVerifyTokenVariables
|
||||
>(externalTokenVerifyMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: result => {
|
||||
if (result.externalVerify === null) {
|
||||
logout();
|
||||
} else {
|
||||
const user = result.externalVerify?.user;
|
||||
|
||||
if (!!user) {
|
||||
setUserContext(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
|
||||
const obtainAccessTokensOpts = {
|
||||
...obtainAccessTokensResult,
|
||||
status: getMutationStatus(obtainAccessTokensResult)
|
||||
};
|
||||
const tokenVerifyOpts = {
|
||||
...tokenVerifyResult,
|
||||
status: getMutationStatus(tokenVerifyResult)
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
};
|
||||
|
||||
const requestLoginByExternalPlugin = async (
|
||||
pluginId: string,
|
||||
pluginInput: RequestExternalLoginInput
|
||||
) => {
|
||||
const input = JSON.stringify(pluginInput);
|
||||
const result = await externalAuthenticationUrl({
|
||||
variables: {
|
||||
input,
|
||||
pluginId
|
||||
}
|
||||
});
|
||||
|
||||
if (result && !result.data.externalAuthenticationUrl.errors.length) {
|
||||
setAuthPlugin(pluginId);
|
||||
|
||||
const authenticationData = JSON.parse(
|
||||
result.data.externalAuthenticationUrl.authenticationData
|
||||
);
|
||||
|
||||
location.href = authenticationData.authorizationUrl;
|
||||
} else {
|
||||
setAuthPlugin(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const loginByExternalPlugin = async (loginInput: ExternalLoginInput) => {
|
||||
const input = JSON.stringify(loginInput);
|
||||
const result = await obtainAccessTokens({
|
||||
variables: { input, pluginId: authPlugin }
|
||||
});
|
||||
|
||||
if (result && !result.data?.externalObtainAccessTokens?.errors?.length) {
|
||||
if (!!onLogin) {
|
||||
onLogin();
|
||||
}
|
||||
} else {
|
||||
setAuthPlugin(undefined);
|
||||
}
|
||||
|
||||
return result?.data?.externalObtainAccessTokens;
|
||||
};
|
||||
|
||||
const refreshToken = (): Promise<boolean> => {
|
||||
if (!!refreshPromise.current) {
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const token = getTokens().refresh;
|
||||
const input = JSON.stringify({
|
||||
refreshToken: token
|
||||
});
|
||||
|
||||
return tokenRefresh({ variables: { input, pluginId: authPlugin } }).then(
|
||||
refreshData => {
|
||||
if (!!refreshData.data.externalRefresh?.token) {
|
||||
setAuthToken(refreshData.data.externalRefresh.token, persistToken);
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
return resolve(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
autologinPromise,
|
||||
loginByExternalPlugin,
|
||||
logout,
|
||||
requestLoginByExternalPlugin,
|
||||
tokenAuthLoading: obtainAccessTokensOpts.loading,
|
||||
tokenRefresh: refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user: userContext
|
||||
};
|
||||
}
|
204
src/auth/hooks/useSaleorAuthProvider.ts
Normal file
204
src/auth/hooks/useSaleorAuthProvider.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import { SetLocalStorage } from "@saleor/hooks/useLocalStorage";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import {
|
||||
isSupported as isCredentialsManagementAPISupported,
|
||||
login as loginWithCredentialsManagementAPI,
|
||||
saveCredentials
|
||||
} from "@saleor/utils/credentialsManagement";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
|
||||
import {
|
||||
tokenAuthMutation,
|
||||
tokenRefreshMutation,
|
||||
tokenVerifyMutation
|
||||
} from "../mutations";
|
||||
import { RefreshToken, RefreshTokenVariables } from "../types/RefreshToken";
|
||||
import {
|
||||
TokenAuth,
|
||||
TokenAuth_tokenCreate,
|
||||
TokenAuthVariables
|
||||
} from "../types/TokenAuth";
|
||||
import { VerifyToken, VerifyTokenVariables } from "../types/VerifyToken";
|
||||
import {
|
||||
displayDemoMessage,
|
||||
getTokens,
|
||||
removeTokens,
|
||||
setAuthToken,
|
||||
setTokens
|
||||
} from "../utils";
|
||||
import { UseAuthProvider, UseAuthProviderOpts } from "./useAuthProvider";
|
||||
|
||||
export interface UseSaleorAuthProvider extends UseAuthProvider {
|
||||
login: (username: string, password: string) => Promise<TokenAuth_tokenCreate>;
|
||||
loginByToken: (auth: string, csrf: string, user: User) => void;
|
||||
}
|
||||
export interface UseSaleorAuthProviderOpts extends UseAuthProviderOpts {
|
||||
setAuthPlugin: SetLocalStorage<any>;
|
||||
authPlugin: string;
|
||||
}
|
||||
|
||||
const persistToken = false;
|
||||
|
||||
export function useSaleorAuthProvider({
|
||||
apolloClient,
|
||||
authPlugin,
|
||||
intl,
|
||||
notify,
|
||||
setAuthPlugin
|
||||
}: UseSaleorAuthProviderOpts): UseSaleorAuthProvider {
|
||||
const [userContext, setUserContext] = useState<undefined | User>(undefined);
|
||||
const autologinPromise = useRef<Promise<any>>();
|
||||
const refreshPromise = useRef<Promise<boolean>>();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getTokens().auth;
|
||||
if (!authPlugin && !!token && !userContext) {
|
||||
autologinPromise.current = tokenVerify({ variables: { token } });
|
||||
} else if (!authPlugin) {
|
||||
autologinPromise.current = loginWithCredentialsManagementAPI(login);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authPlugin && userContext) {
|
||||
const { id, email, firstName, lastName } = userContext;
|
||||
errorTracker.setUserData({
|
||||
email,
|
||||
id,
|
||||
username: `${firstName} ${lastName}`
|
||||
});
|
||||
|
||||
if (!userContext.isStaff) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
|
||||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
const logout = () => {
|
||||
setUserContext(undefined);
|
||||
if (isCredentialsManagementAPISupported) {
|
||||
navigator.credentials.preventSilentAccess();
|
||||
}
|
||||
removeTokens();
|
||||
};
|
||||
|
||||
const [tokenAuth, tokenAuthResult] = useMutation<
|
||||
TokenAuth,
|
||||
TokenAuthVariables
|
||||
>(tokenAuthMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: ({ tokenCreate }) => {
|
||||
if (tokenCreate.errors.length > 0) {
|
||||
logout();
|
||||
}
|
||||
|
||||
const user = tokenCreate.user;
|
||||
|
||||
setUserContext(user);
|
||||
if (user) {
|
||||
setTokens(tokenCreate.token, tokenCreate.csrfToken, persistToken);
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
const [tokenRefresh] = useMutation<RefreshToken, RefreshTokenVariables>(
|
||||
tokenRefreshMutation,
|
||||
{
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
}
|
||||
);
|
||||
const [tokenVerify, tokenVerifyResult] = useMutation<
|
||||
VerifyToken,
|
||||
VerifyTokenVariables
|
||||
>(tokenVerifyMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: result => {
|
||||
if (result.tokenVerify === null) {
|
||||
logout();
|
||||
} else {
|
||||
const user = result.tokenVerify?.user;
|
||||
|
||||
if (!!user) {
|
||||
setUserContext(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
|
||||
const tokenAuthOpts = {
|
||||
...tokenAuthResult,
|
||||
status: getMutationStatus(tokenAuthResult)
|
||||
};
|
||||
const tokenVerifyOpts = {
|
||||
...tokenVerifyResult,
|
||||
status: getMutationStatus(tokenVerifyResult)
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
setAuthPlugin(undefined);
|
||||
const result = await tokenAuth({ variables: { email, password } });
|
||||
|
||||
if (result && !result.data.tokenCreate.errors.length) {
|
||||
if (!!onLogin) {
|
||||
onLogin();
|
||||
}
|
||||
saveCredentials(result.data.tokenCreate.user, password);
|
||||
}
|
||||
|
||||
return result.data.tokenCreate;
|
||||
};
|
||||
|
||||
const loginByToken = (auth: string, refresh: string, user: User) => {
|
||||
setAuthPlugin(undefined);
|
||||
setUserContext(user);
|
||||
setTokens(auth, refresh, persistToken);
|
||||
};
|
||||
|
||||
const refreshToken = (): Promise<boolean> => {
|
||||
if (!!refreshPromise.current) {
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const token = getTokens().refresh;
|
||||
|
||||
return tokenRefresh({ variables: { token } }).then(refreshData => {
|
||||
if (!!refreshData.data.tokenRefresh?.token) {
|
||||
setAuthToken(refreshData.data.tokenRefresh.token, persistToken);
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
return resolve(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
autologinPromise,
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
tokenAuthLoading: tokenAuthOpts.loading,
|
||||
tokenRefresh: refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user: userContext
|
||||
};
|
||||
}
|
|
@ -1,32 +1,57 @@
|
|||
import { User } from "@saleor/fragments/types/User";
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { parse as parseQs } from "qs";
|
||||
import React, { MutableRefObject } from "react";
|
||||
import { Route, RouteComponentProps, Switch } from "react-router-dom";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import {
|
||||
ExternalLoginInput,
|
||||
RequestExternalLoginInput
|
||||
} from "./hooks/useExternalAuthProvider";
|
||||
import { ExternalObtainAccessTokens_externalObtainAccessTokens } from "./types/ExternalObtainAccessTokens";
|
||||
import { TokenAuth_tokenCreate } from "./types/TokenAuth";
|
||||
import {
|
||||
LoginUrlQueryParams,
|
||||
newPasswordPath,
|
||||
passwordResetPath,
|
||||
passwordResetSuccessPath
|
||||
} from "./urls";
|
||||
import LoginView from "./views/Login";
|
||||
import LoginViewComponent from "./views/Login";
|
||||
import NewPassword from "./views/NewPassword";
|
||||
import ResetPassword from "./views/ResetPassword";
|
||||
import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
|
||||
|
||||
const LoginView: React.FC<RouteComponentProps<any>> = () => {
|
||||
const qs = parseQs(location.search.substr(1));
|
||||
const params: LoginUrlQueryParams = qs;
|
||||
|
||||
return <LoginViewComponent params={params} />;
|
||||
};
|
||||
|
||||
interface UserContext {
|
||||
login: (username: string, password: string) => void;
|
||||
login: (username: string, password: string) => Promise<TokenAuth_tokenCreate>;
|
||||
loginByExternalPlugin: (
|
||||
input: ExternalLoginInput
|
||||
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
|
||||
loginByToken: (auth: string, csrf: string, user: User) => void;
|
||||
logout: () => void;
|
||||
requestLoginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => Promise<void>;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenRefresh: () => Promise<boolean>;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
autologinPromise?: MutableRefObject<Promise<any>>;
|
||||
}
|
||||
|
||||
export const UserContext = React.createContext<UserContext>({
|
||||
login: undefined,
|
||||
loginByExternalPlugin: undefined,
|
||||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
requestLoginByExternalPlugin: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenRefresh: undefined,
|
||||
tokenVerifyLoading: false
|
||||
|
|
|
@ -82,3 +82,52 @@ export const SetPasswordMutation = TypedMutation<
|
|||
SetPassword,
|
||||
SetPasswordVariables
|
||||
>(setPassword);
|
||||
|
||||
export const externalAuthenticationUrlMutation = gql`
|
||||
${accountErrorFragment}
|
||||
mutation ExternalAuthenticationUrl($pluginId: String!, $input: JSONString!) {
|
||||
externalAuthenticationUrl(pluginId: $pluginId, input: $input) {
|
||||
authenticationData
|
||||
errors: accountErrors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalObtainAccessTokensMutation = gql`
|
||||
${accountErrorFragment}
|
||||
${fragmentUser}
|
||||
mutation ExternalObtainAccessTokens($pluginId: String!, $input: JSONString!) {
|
||||
externalObtainAccessTokens(pluginId: $pluginId, input: $input) {
|
||||
token
|
||||
csrfToken
|
||||
user {
|
||||
...User
|
||||
}
|
||||
errors: accountErrors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalTokenRefreshMutation = gql`
|
||||
mutation ExternalRefreshToken($pluginId: String!, $input: JSONString!) {
|
||||
externalRefresh(pluginId: $pluginId, input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalTokenVerifyMutation = gql`
|
||||
${fragmentUser}
|
||||
mutation ExternalVerifyToken($pluginId: String!, $input: JSONString!) {
|
||||
externalVerify(pluginId: $pluginId, input: $input) {
|
||||
verifyData
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
12
src/auth/queries.ts
Normal file
12
src/auth/queries.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import gql from "graphql-tag";
|
||||
|
||||
export const availableExternalAuthentications = gql`
|
||||
query AvailableExternalAuthentications {
|
||||
shop {
|
||||
availableExternalAuthentications {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
22
src/auth/types/AvailableExternalAuthentications.ts
Normal file
22
src/auth/types/AvailableExternalAuthentications.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: AvailableExternalAuthentications
|
||||
// ====================================================
|
||||
|
||||
export interface AvailableExternalAuthentications_shop_availableExternalAuthentications {
|
||||
__typename: "ExternalAuthentication";
|
||||
id: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface AvailableExternalAuthentications_shop {
|
||||
__typename: "Shop";
|
||||
availableExternalAuthentications: AvailableExternalAuthentications_shop_availableExternalAuthentications[];
|
||||
}
|
||||
|
||||
export interface AvailableExternalAuthentications {
|
||||
shop: AvailableExternalAuthentications_shop;
|
||||
}
|
30
src/auth/types/ExternalAuthenticationUrl.ts
Normal file
30
src/auth/types/ExternalAuthenticationUrl.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AccountErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: ExternalAuthenticationUrl
|
||||
// ====================================================
|
||||
|
||||
export interface ExternalAuthenticationUrl_externalAuthenticationUrl_errors {
|
||||
__typename: "AccountError";
|
||||
code: AccountErrorCode;
|
||||
field: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalAuthenticationUrl_externalAuthenticationUrl {
|
||||
__typename: "ExternalAuthenticationUrl";
|
||||
authenticationData: any | null;
|
||||
errors: ExternalAuthenticationUrl_externalAuthenticationUrl_errors[];
|
||||
}
|
||||
|
||||
export interface ExternalAuthenticationUrl {
|
||||
externalAuthenticationUrl: ExternalAuthenticationUrl_externalAuthenticationUrl | null;
|
||||
}
|
||||
|
||||
export interface ExternalAuthenticationUrlVariables {
|
||||
pluginId: string;
|
||||
input: any;
|
||||
}
|
54
src/auth/types/ExternalObtainAccessTokens.ts
Normal file
54
src/auth/types/ExternalObtainAccessTokens.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { PermissionEnum, AccountErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: ExternalObtainAccessTokens
|
||||
// ====================================================
|
||||
|
||||
export interface ExternalObtainAccessTokens_externalObtainAccessTokens_user_userPermissions {
|
||||
__typename: "UserPermission";
|
||||
code: PermissionEnum;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ExternalObtainAccessTokens_externalObtainAccessTokens_user_avatar {
|
||||
__typename: "Image";
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ExternalObtainAccessTokens_externalObtainAccessTokens_user {
|
||||
__typename: "User";
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isStaff: boolean;
|
||||
userPermissions: (ExternalObtainAccessTokens_externalObtainAccessTokens_user_userPermissions | null)[] | null;
|
||||
avatar: ExternalObtainAccessTokens_externalObtainAccessTokens_user_avatar | null;
|
||||
}
|
||||
|
||||
export interface ExternalObtainAccessTokens_externalObtainAccessTokens_errors {
|
||||
__typename: "AccountError";
|
||||
code: AccountErrorCode;
|
||||
field: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalObtainAccessTokens_externalObtainAccessTokens {
|
||||
__typename: "ExternalObtainAccessTokens";
|
||||
token: string | null;
|
||||
csrfToken: string | null;
|
||||
user: ExternalObtainAccessTokens_externalObtainAccessTokens_user | null;
|
||||
errors: ExternalObtainAccessTokens_externalObtainAccessTokens_errors[];
|
||||
}
|
||||
|
||||
export interface ExternalObtainAccessTokens {
|
||||
externalObtainAccessTokens: ExternalObtainAccessTokens_externalObtainAccessTokens | null;
|
||||
}
|
||||
|
||||
export interface ExternalObtainAccessTokensVariables {
|
||||
pluginId: string;
|
||||
input: any;
|
||||
}
|
21
src/auth/types/ExternalRefreshToken.ts
Normal file
21
src/auth/types/ExternalRefreshToken.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: ExternalRefreshToken
|
||||
// ====================================================
|
||||
|
||||
export interface ExternalRefreshToken_externalRefresh {
|
||||
__typename: "ExternalRefresh";
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalRefreshToken {
|
||||
externalRefresh: ExternalRefreshToken_externalRefresh | null;
|
||||
}
|
||||
|
||||
export interface ExternalRefreshTokenVariables {
|
||||
pluginId: string;
|
||||
input: any;
|
||||
}
|
46
src/auth/types/ExternalVerifyToken.ts
Normal file
46
src/auth/types/ExternalVerifyToken.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { PermissionEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: ExternalVerifyToken
|
||||
// ====================================================
|
||||
|
||||
export interface ExternalVerifyToken_externalVerify_user_userPermissions {
|
||||
__typename: "UserPermission";
|
||||
code: PermissionEnum;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ExternalVerifyToken_externalVerify_user_avatar {
|
||||
__typename: "Image";
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ExternalVerifyToken_externalVerify_user {
|
||||
__typename: "User";
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isStaff: boolean;
|
||||
userPermissions: (ExternalVerifyToken_externalVerify_user_userPermissions | null)[] | null;
|
||||
avatar: ExternalVerifyToken_externalVerify_user_avatar | null;
|
||||
}
|
||||
|
||||
export interface ExternalVerifyToken_externalVerify {
|
||||
__typename: "ExternalVerify";
|
||||
verifyData: any | null;
|
||||
user: ExternalVerifyToken_externalVerify_user | null;
|
||||
}
|
||||
|
||||
export interface ExternalVerifyToken {
|
||||
externalVerify: ExternalVerifyToken_externalVerify | null;
|
||||
}
|
||||
|
||||
export interface ExternalVerifyTokenVariables {
|
||||
pluginId: string;
|
||||
input: any;
|
||||
}
|
|
@ -7,9 +7,18 @@ export const passwordResetSuccessPath = "/reset-password/success/";
|
|||
export const passwordResetSuccessUrl = passwordResetSuccessPath;
|
||||
|
||||
export const newPasswordPath = "/new-password/";
|
||||
|
||||
export const loginCallbackPath = "/login/callback/";
|
||||
|
||||
export interface NewPasswordUrlQueryParams {
|
||||
email: string;
|
||||
token: string;
|
||||
}
|
||||
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>
|
||||
newPasswordPath + "?" + stringifyQs(params);
|
||||
|
||||
export interface LoginOpenidconnectUrlQueryParams {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
export type LoginUrlQueryParams = LoginOpenidconnectUrlQueryParams;
|
||||
|
|
|
@ -1,20 +1,93 @@
|
|||
import { APP_DEFAULT_URI, APP_MOUNT_URI } from "@saleor/config";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useQuery } from "react-apollo";
|
||||
import urlJoin from "url-join";
|
||||
import useRouter from "use-react-router";
|
||||
|
||||
import LoginPage, { FormData } from "../components/LoginPage";
|
||||
import { passwordResetUrl } from "../urls";
|
||||
import LoginPage from "../components/LoginPage";
|
||||
import { LoginFormData } from "../components/LoginPage/form";
|
||||
import { availableExternalAuthentications } from "../queries";
|
||||
import { AvailableExternalAuthentications } from "../types/AvailableExternalAuthentications";
|
||||
import {
|
||||
loginCallbackPath,
|
||||
LoginUrlQueryParams,
|
||||
passwordResetUrl
|
||||
} from "../urls";
|
||||
|
||||
const LoginView: React.FC = () => {
|
||||
interface LoginViewProps {
|
||||
params: LoginUrlQueryParams;
|
||||
}
|
||||
|
||||
const LoginView: React.FC<LoginViewProps> = ({ params }) => {
|
||||
const navigate = useNavigator();
|
||||
const { login, user, tokenAuthLoading } = useUser();
|
||||
const { location } = useRouter();
|
||||
const {
|
||||
login,
|
||||
requestLoginByExternalPlugin,
|
||||
loginByExternalPlugin,
|
||||
tokenAuthLoading
|
||||
} = useUser();
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isExternalError, setIsExternalError] = useState(false);
|
||||
const {
|
||||
data: externalAuthentications,
|
||||
loading: externalAuthenticationsLoading
|
||||
} = useQuery<AvailableExternalAuthentications>(
|
||||
availableExternalAuthentications
|
||||
);
|
||||
|
||||
const handleSubmit = (data: FormData) => login(data.email, data.password);
|
||||
const handleSubmit = async (data: LoginFormData) => {
|
||||
const result = await login(data.email, data.password);
|
||||
const errors = result?.errors || [];
|
||||
|
||||
setIsExternalError(false);
|
||||
setIsError(!result || errors?.length > 0);
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleRequestExternalAuthentication = (pluginId: string) =>
|
||||
requestLoginByExternalPlugin(pluginId, {
|
||||
redirectUri: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === APP_DEFAULT_URI ? "" : APP_MOUNT_URI,
|
||||
loginCallbackPath
|
||||
)
|
||||
});
|
||||
|
||||
const handleExternalAuthentication = async (code: string, state: string) => {
|
||||
const result = await loginByExternalPlugin({ code, state });
|
||||
const errors = result?.errors || [];
|
||||
|
||||
setIsError(false);
|
||||
if (!result || errors?.length > 0) {
|
||||
setIsExternalError(true);
|
||||
} else {
|
||||
navigate(APP_DEFAULT_URI);
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { code, state } = params;
|
||||
const isCallbackPath = location.pathname.includes(loginCallbackPath);
|
||||
|
||||
if (code && state && isCallbackPath) {
|
||||
handleExternalAuthentication(code, state);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
error={user === null}
|
||||
disableLoginButton={tokenAuthLoading}
|
||||
error={isError}
|
||||
externalError={isExternalError}
|
||||
disabled={tokenAuthLoading}
|
||||
externalAuthentications={
|
||||
externalAuthentications?.shop?.availableExternalAuthentications
|
||||
}
|
||||
loading={externalAuthenticationsLoading}
|
||||
onExternalAuthentication={handleRequestExternalAuthentication}
|
||||
onPasswordRecovery={() => navigate(passwordResetUrl)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
import { channelDetailsFragment } from "@saleor/fragments/channels";
|
||||
import {
|
||||
channelDetailsFragment,
|
||||
channelFragment
|
||||
} from "@saleor/fragments/channels";
|
||||
import makeQuery from "@saleor/hooks/makeQuery";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { BaseChannels } from "./types/BaseChannels";
|
||||
import { Channel, ChannelVariables } from "./types/Channel";
|
||||
import { Channels } from "./types/Channels";
|
||||
|
||||
export const channelsListBase = gql`
|
||||
${channelFragment}
|
||||
query BaseChannels {
|
||||
channels {
|
||||
...ChannelFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const channelsList = gql`
|
||||
${channelDetailsFragment}
|
||||
query Channels {
|
||||
|
@ -23,6 +36,9 @@ export const channelDetails = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export const useBaseChannelsList = makeQuery<BaseChannels, {}>(
|
||||
channelsListBase
|
||||
);
|
||||
export const useChannelsList = makeQuery<Channels, {}>(channelsList);
|
||||
export const useChannelDetails = makeQuery<Channel, ChannelVariables>(
|
||||
channelDetails
|
||||
|
|
20
src/channels/types/BaseChannels.ts
Normal file
20
src/channels/types/BaseChannels.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: BaseChannels
|
||||
// ====================================================
|
||||
|
||||
export interface BaseChannels_channels {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface BaseChannels {
|
||||
channels: BaseChannels_channels[] | null;
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import { useAuth } from "@saleor/auth/AuthProvider";
|
||||
import { useChannelsList } from "@saleor/channels/queries";
|
||||
import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment";
|
||||
import { useBaseChannelsList } from "@saleor/channels/queries";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import React from "react";
|
||||
|
||||
interface UseAppChannel {
|
||||
availableChannels: ChannelDetailsFragment[];
|
||||
channel: ChannelDetailsFragment;
|
||||
availableChannels: ChannelFragment[];
|
||||
channel: ChannelFragment;
|
||||
isPickerActive: boolean;
|
||||
refreshChannels: () => void;
|
||||
setChannel: (id: string) => void;
|
||||
|
@ -27,7 +27,7 @@ const AppChannelContext = React.createContext<AppChannelContextData>({
|
|||
export const AppChannelProvider: React.FC = ({ children }) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [selectedChannel, setSelectedChannel] = useLocalStorage("channel", "");
|
||||
const { data: channelData, refetch } = useChannelsList({
|
||||
const { data: channelData, refetch } = useBaseChannelsList({
|
||||
skip: !isAuthenticated
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import { ChannelProps } from "@saleor/types";
|
||||
import { mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import React from "react";
|
||||
|
@ -22,7 +22,7 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
export interface AppChannelSelectProps extends ChannelProps {
|
||||
channels: ChannelDetailsFragment[];
|
||||
channels: ChannelFragment[];
|
||||
disabled: boolean;
|
||||
onChannelSelect: (id: string) => void;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { SearchVariables } from "./hooks/makeSearch";
|
|||
import { ListSettings, ListViews, Pagination } from "./types";
|
||||
|
||||
export const APP_MOUNT_URI = process.env.APP_MOUNT_URI;
|
||||
export const APP_DEFAULT_URI = "/";
|
||||
export const API_URI = process.env.API_URI;
|
||||
|
||||
export const DEFAULT_INITIAL_SEARCH_DATA: SearchVariables = {
|
||||
|
|
|
@ -8,13 +8,21 @@ export const channelErrorFragment = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export const channelDetailsFragment = gql`
|
||||
fragment ChannelDetailsFragment on Channel {
|
||||
export const channelFragment = gql`
|
||||
fragment ChannelFragment on Channel {
|
||||
id
|
||||
isActive
|
||||
name
|
||||
slug
|
||||
currencyCode
|
||||
}
|
||||
`;
|
||||
|
||||
export const channelDetailsFragment = gql`
|
||||
${channelFragment}
|
||||
|
||||
fragment ChannelDetailsFragment on Channel {
|
||||
...ChannelFragment
|
||||
hasOrders
|
||||
}
|
||||
`;
|
||||
|
|
16
src/fragments/types/ChannelFragment.ts
Normal file
16
src/fragments/types/ChannelFragment.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: ChannelFragment
|
||||
// ====================================================
|
||||
|
||||
export interface ChannelFragment {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
currencyCode: string;
|
||||
}
|
|
@ -4,12 +4,12 @@ import DialogActions from "@material-ui/core/DialogActions";
|
|||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Channels_channels } from "@saleor/channels/types/Channels";
|
||||
import ConfirmButton, {
|
||||
ConfirmButtonTransitionState
|
||||
} from "@saleor/components/ConfirmButton";
|
||||
import makeCreatorSteps, { Step } from "@saleor/components/CreatorSteps";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import { ExportErrorFragment } from "@saleor/fragments/types/ExportErrorFragment";
|
||||
import useForm, { FormChange } from "@saleor/hooks/useForm";
|
||||
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
|
||||
|
@ -79,7 +79,7 @@ const ProductExportSteps = makeCreatorSteps<ProductExportStep>();
|
|||
|
||||
export interface ProductExportDialogProps extends DialogProps, FetchMoreProps {
|
||||
attributes: SearchAttributes_search_edges_node[];
|
||||
channels: Channels_channels[];
|
||||
channels: ChannelFragment[];
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
errors: ExportErrorFragment[];
|
||||
productQuantity: ProductQuantity;
|
||||
|
@ -146,7 +146,7 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const handleChannelSelect = (option: Channels_channels) => {
|
||||
const handleChannelSelect = (option: ChannelFragment) => {
|
||||
change({
|
||||
target: {
|
||||
name: "exportInfo",
|
||||
|
@ -168,7 +168,7 @@ const ProductExportDialog: React.FC<ProductExportDialogProps> = ({
|
|||
};
|
||||
|
||||
const handleToggleAllChannels = (
|
||||
items: Channels_channels[],
|
||||
items: ChannelFragment[],
|
||||
selected: number
|
||||
) => {
|
||||
setSelectedChannels(selected === items.length ? [] : channels);
|
||||
|
|
|
@ -4,13 +4,13 @@ import FormControlLabel from "@material-ui/core/FormControlLabel";
|
|||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Channels_channels } from "@saleor/channels/types/Channels";
|
||||
import Accordion, { AccordionProps } from "@saleor/components/Accordion";
|
||||
import ChannelsAvailabilityContent from "@saleor/components/ChannelsAvailabilityContent";
|
||||
import Checkbox from "@saleor/components/Checkbox";
|
||||
import Chip from "@saleor/components/Chip";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
|
||||
import useSearchQuery from "@saleor/hooks/useSearchQuery";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
|
@ -207,8 +207,8 @@ const FieldAccordion: React.FC<AccordionProps & {
|
|||
|
||||
export interface ProductExportDialogInfoProps extends FetchMoreProps {
|
||||
attributes: MultiAutocompleteChoiceType[];
|
||||
channels: Channels_channels[];
|
||||
selectedChannels: Channels_channels[];
|
||||
channels: ChannelFragment[];
|
||||
selectedChannels: ChannelFragment[];
|
||||
warehouses: MultiAutocompleteChoiceType[];
|
||||
data: ExportProductsInput;
|
||||
selectedAttributes: MultiAutocompleteChoiceType[];
|
||||
|
@ -217,8 +217,8 @@ export interface ProductExportDialogInfoProps extends FetchMoreProps {
|
|||
onChange: FormChange;
|
||||
onFetch: (query: string) => void;
|
||||
onSelectAllWarehouses: FormChange;
|
||||
onSelectAllChannels: (items: Channels_channels[], selected: number) => void;
|
||||
onChannelSelect: (option: Channels_channels) => void;
|
||||
onSelectAllChannels: (items: ChannelFragment[], selected: number) => void;
|
||||
onChannelSelect: (option: ChannelFragment) => void;
|
||||
}
|
||||
|
||||
const ProductExportDialogInfo: React.FC<ProductExportDialogInfoProps> = ({
|
||||
|
|
|
@ -224,8 +224,9 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
);
|
||||
|
||||
const paginationState = createPaginationState(settings.rowNumber, params);
|
||||
const filter = !noChannel ? getFilterVariables(params, channel.slug) : null;
|
||||
const sort = !noChannel ? getSortQueryVariables(params, channel.slug) : null;
|
||||
const channelSlug = noChannel ? null : channel.slug;
|
||||
const filter = getFilterVariables(params, channelSlug);
|
||||
const sort = getSortQueryVariables(params, channelSlug);
|
||||
const queryVariables = React.useMemo<ProductListVariables>(
|
||||
() => ({
|
||||
...paginationState,
|
||||
|
|
|
@ -6,8 +6,10 @@ export const UserDecorator = (user: User) => storyFn => (
|
|||
<UserContext.Provider
|
||||
value={{
|
||||
login: undefined,
|
||||
loginByExternalPlugin: undefined,
|
||||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
requestLoginByExternalPlugin: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenRefresh: undefined,
|
||||
tokenVerifyLoading: false,
|
||||
|
|
|
@ -36858,6 +36858,165 @@ exports[`Storyshots Views / Authentication / Log in default 1`] = `
|
|||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="LoginCard-buttonContainer-id"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id LoginCard-loginButton-id MuiButton-containedPrimary-id"
|
||||
data-test="submit"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Login
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
Forgot password?
|
||||
<a
|
||||
class="LoginCard-link-id"
|
||||
>
|
||||
Use this link to recover it
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<hr
|
||||
class="MuiDivider-root-id"
|
||||
/>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
or login using
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-outlinedSizeLarge-id MuiButton-sizeLarge-id MuiButton-fullWidth-id"
|
||||
data-test="external-authentication"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Example auth plugin
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Views / Authentication / Log in disabled 1`] = `
|
||||
<div
|
||||
style="padding:24px"
|
||||
>
|
||||
<div
|
||||
class="MuiPaper-root-id MuiPaper-elevation0-id MuiCard-root-id MuiPaper-rounded-id"
|
||||
style="margin:auto;overflow:visible;position:relative;width:400px"
|
||||
>
|
||||
<div
|
||||
class="MuiCardContent-root-id"
|
||||
>
|
||||
<form>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id"
|
||||
data-shrink="false"
|
||||
>
|
||||
E-mail Address
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root-id MuiOutlinedInput-root-id MuiInputBase-disabled-id MuiOutlinedInput-disabled-id MuiInputBase-fullWidth-id MuiInputBase-formControl-id"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
autocomplete="username"
|
||||
autofocus=""
|
||||
class="MuiInputBase-input-id MuiOutlinedInput-input-id MuiInputBase-disabled-id MuiOutlinedInput-disabled-id"
|
||||
data-test="email"
|
||||
disabled=""
|
||||
name="email"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-id MuiOutlinedInput-notchedOutline-id"
|
||||
style="padding-left:8px"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legend-id"
|
||||
style="width:0.01px"
|
||||
>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id"
|
||||
data-shrink="false"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root-id MuiOutlinedInput-root-id MuiInputBase-disabled-id MuiOutlinedInput-disabled-id MuiInputBase-fullWidth-id MuiInputBase-formControl-id"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
autocomplete="password"
|
||||
class="MuiInputBase-input-id MuiOutlinedInput-input-id MuiInputBase-disabled-id MuiOutlinedInput-disabled-id"
|
||||
data-test="password"
|
||||
disabled=""
|
||||
name="password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-id MuiOutlinedInput-notchedOutline-id"
|
||||
style="padding-left:8px"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legend-id"
|
||||
style="width:0.01px"
|
||||
>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="LoginCard-buttonContainer-id"
|
||||
>
|
||||
|
@ -36879,10 +37038,45 @@ exports[`Storyshots Views / Authentication / Log in default 1`] = `
|
|||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id LoginCard-link-id MuiTypography-body1-id"
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
Reset your password
|
||||
Forgot password?
|
||||
<a
|
||||
class="LoginCard-link-id"
|
||||
>
|
||||
Use this link to recover it
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<hr
|
||||
class="MuiDivider-root-id"
|
||||
/>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
or login using
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-outlinedSizeLarge-id MuiButton-sizeLarge-id MuiButton-disabled-id MuiButton-fullWidth-id MuiButtonBase-disabled-id"
|
||||
data-test="external-authentication"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Example auth plugin
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36996,10 +37190,9 @@ exports[`Storyshots Views / Authentication / Log in error 1`] = `
|
|||
class="LoginCard-buttonContainer-id"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id LoginCard-loginButton-id MuiButton-containedPrimary-id MuiButton-disabled-id MuiButtonBase-disabled-id"
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id LoginCard-loginButton-id MuiButton-containedPrimary-id"
|
||||
data-test="submit"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
|
@ -37013,10 +37206,44 @@ exports[`Storyshots Views / Authentication / Log in error 1`] = `
|
|||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id LoginCard-link-id MuiTypography-body1-id"
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
Reset your password
|
||||
Forgot password?
|
||||
<a
|
||||
class="LoginCard-link-id"
|
||||
>
|
||||
Use this link to recover it
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<hr
|
||||
class="MuiDivider-root-id"
|
||||
/>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
or login using
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-outlinedSizeLarge-id MuiButton-sizeLarge-id MuiButton-fullWidth-id"
|
||||
data-test="external-authentication"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Example auth plugin
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37034,114 +37261,29 @@ exports[`Storyshots Views / Authentication / Log in loading 1`] = `
|
|||
<div
|
||||
class="MuiCardContent-root-id"
|
||||
>
|
||||
<form>
|
||||
<div
|
||||
class="LoginCard-loading-id"
|
||||
>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
class="MuiCircularProgress-root-id MuiCircularProgress-colorPrimary-id MuiCircularProgress-indeterminate-id"
|
||||
role="progressbar"
|
||||
style="width:128px;height:128px"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id"
|
||||
data-shrink="false"
|
||||
<svg
|
||||
class="MuiCircularProgress-svg-id"
|
||||
viewBox="22 22 44 44"
|
||||
>
|
||||
E-mail Address
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root-id MuiOutlinedInput-root-id MuiInputBase-fullWidth-id MuiInputBase-formControl-id"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
autocomplete="username"
|
||||
autofocus=""
|
||||
class="MuiInputBase-input-id MuiOutlinedInput-input-id"
|
||||
data-test="email"
|
||||
name="email"
|
||||
type="text"
|
||||
value=""
|
||||
<circle
|
||||
class="MuiCircularProgress-circle-id MuiCircularProgress-circleIndeterminate-id"
|
||||
cx="44"
|
||||
cy="44"
|
||||
fill="none"
|
||||
r="20.2"
|
||||
stroke-width="3.6"
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-id MuiOutlinedInput-notchedOutline-id"
|
||||
style="padding-left:8px"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legend-id"
|
||||
style="width:0.01px"
|
||||
>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id"
|
||||
data-shrink="false"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root-id MuiOutlinedInput-root-id MuiInputBase-fullWidth-id MuiInputBase-formControl-id"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
autocomplete="password"
|
||||
class="MuiInputBase-input-id MuiOutlinedInput-input-id"
|
||||
data-test="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-id MuiOutlinedInput-notchedOutline-id"
|
||||
style="padding-left:8px"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legend-id"
|
||||
style="width:0.01px"
|
||||
>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="LoginCard-buttonContainer-id"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id LoginCard-loginButton-id MuiButton-containedPrimary-id MuiButton-disabled-id MuiButtonBase-disabled-id"
|
||||
data-test="submit"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Login
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id LoginCard-link-id MuiTypography-body1-id"
|
||||
>
|
||||
Reset your password
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,10 +44,6 @@ function loadStories() {
|
|||
require("./stories/components/WeightRange");
|
||||
require("./stories/components/messages");
|
||||
|
||||
// Authentication
|
||||
require("./stories/auth/LoginPage");
|
||||
require("./stories/auth/LoginLoading");
|
||||
|
||||
// Attributes
|
||||
require("./stories/attributes/AttributeBulkDeleteDialog");
|
||||
require("./stories/attributes/AttributeDeleteDialog");
|
||||
|
|
Loading…
Reference in a new issue