Fix improper double attempt to get external access token (#2504)
* Fix improper double attempt to get external access token * Update messages * Add auth error types
This commit is contained in:
parent
1fa9213871
commit
2e45f04802
12 changed files with 110 additions and 41 deletions
|
@ -1162,6 +1162,10 @@
|
|||
"context": "product field",
|
||||
"string": "Export Product Weight"
|
||||
},
|
||||
"7Jg9ec": {
|
||||
"context": "error message",
|
||||
"string": "You don't have permission to login."
|
||||
},
|
||||
"7KRGqz": {
|
||||
"context": "Payment card title",
|
||||
"string": "Payment balance"
|
||||
|
@ -2215,6 +2219,10 @@
|
|||
"Fn3bE0": {
|
||||
"string": "Order line updated"
|
||||
},
|
||||
"FopBSj": {
|
||||
"context": "error message",
|
||||
"string": "Your username and/or password are incorrect. Please try again."
|
||||
},
|
||||
"FpIcp9": {
|
||||
"string": "No customers found"
|
||||
},
|
||||
|
@ -3051,10 +3059,6 @@
|
|||
"context": "collection",
|
||||
"string": "Not Published"
|
||||
},
|
||||
"M4q0Ye": {
|
||||
"context": "error message",
|
||||
"string": "Sorry, login went wrong. Please try again."
|
||||
},
|
||||
"M6s/9e": {
|
||||
"context": "unassign country, dialog header",
|
||||
"string": "Remove from Shipping Zone"
|
||||
|
@ -4515,6 +4519,10 @@
|
|||
"context": "title",
|
||||
"string": "There’s a problem with app."
|
||||
},
|
||||
"Wr5UOV": {
|
||||
"context": "error message",
|
||||
"string": "Login went wrong. Please try again."
|
||||
},
|
||||
"Ww69SE": {
|
||||
"context": "search input placeholder",
|
||||
"string": "Search tax classes"
|
||||
|
@ -7197,10 +7205,6 @@
|
|||
"tR+UuE": {
|
||||
"string": "User doesn't exist. Please check your email in URL"
|
||||
},
|
||||
"tTtoKd": {
|
||||
"context": "error message",
|
||||
"string": "Sorry, your username and/or password are incorrect. Please try again."
|
||||
},
|
||||
"tTuCYj": {
|
||||
"context": "all gift cards label",
|
||||
"string": "All Gift Cards"
|
||||
|
|
|
@ -15,6 +15,7 @@ const props: Omit<LoginCardProps, "classes"> = {
|
|||
},
|
||||
],
|
||||
loading: false,
|
||||
errors: [],
|
||||
onExternalAuthentication: () => undefined,
|
||||
onSubmit: () => undefined,
|
||||
};
|
||||
|
@ -23,9 +24,9 @@ storiesOf("Views / Authentication / Log in", module)
|
|||
.addDecorator(CardDecorator)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <LoginPage {...props} />)
|
||||
.add("error login", () => <LoginPage {...props} error={"loginError"} />)
|
||||
.add("error login", () => <LoginPage {...props} errors={["loginError"]} />)
|
||||
.add("error external login", () => (
|
||||
<LoginPage {...props} error={"externalLoginError"} />
|
||||
<LoginPage {...props} errors={["externalLoginError"]} />
|
||||
))
|
||||
.add("disabled", () => <LoginPage {...props} disabled={true} />)
|
||||
.add("loading", () => <LoginPage {...props} loading={true} />);
|
||||
|
|
|
@ -21,7 +21,7 @@ import LoginForm, { LoginFormData } from "./form";
|
|||
import { getErrorMessage } from "./messages";
|
||||
|
||||
export interface LoginCardProps {
|
||||
error?: UserContextError;
|
||||
errors: UserContextError[];
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
externalAuthentications?: AvailableExternalAuthenticationsQuery["shop"]["availableExternalAuthentications"];
|
||||
|
@ -31,7 +31,7 @@ export interface LoginCardProps {
|
|||
|
||||
const LoginCard: React.FC<LoginCardProps> = props => {
|
||||
const {
|
||||
error,
|
||||
errors,
|
||||
disabled,
|
||||
loading,
|
||||
externalAuthentications = [],
|
||||
|
@ -62,11 +62,15 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
description="card header"
|
||||
/>
|
||||
</Typography>
|
||||
{error && (
|
||||
<div className={classes.panel} data-test-id="login-error-message">
|
||||
{errors.map(error => (
|
||||
<div
|
||||
className={classes.panel}
|
||||
key={error}
|
||||
data-test-id="login-error-message"
|
||||
>
|
||||
{getErrorMessage(error, intl)}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
|
|
|
@ -3,14 +3,14 @@ import { defineMessages, IntlShape } from "react-intl";
|
|||
|
||||
export const errorMessages = defineMessages({
|
||||
loginError: {
|
||||
id: "tTtoKd",
|
||||
id: "FopBSj",
|
||||
defaultMessage:
|
||||
"Sorry, your username and/or password are incorrect. Please try again.",
|
||||
"Your username and/or password are incorrect. Please try again.",
|
||||
description: "error message",
|
||||
},
|
||||
externalLoginError: {
|
||||
id: "M4q0Ye",
|
||||
defaultMessage: "Sorry, login went wrong. Please try again.",
|
||||
unknownLoginError: {
|
||||
id: "Wr5UOV",
|
||||
defaultMessage: "Login went wrong. Please try again.",
|
||||
description: "error message",
|
||||
},
|
||||
serverError: {
|
||||
|
@ -19,6 +19,11 @@ export const errorMessages = defineMessages({
|
|||
"Saleor is unavailable, please check your network connection and try again.",
|
||||
description: "error message",
|
||||
},
|
||||
noPermissionsError: {
|
||||
id: "7Jg9ec",
|
||||
defaultMessage: "You don't have permission to login.",
|
||||
description: "error message",
|
||||
},
|
||||
});
|
||||
|
||||
export function getErrorMessage(
|
||||
|
@ -29,8 +34,12 @@ export function getErrorMessage(
|
|||
case "loginError":
|
||||
return intl.formatMessage(errorMessages.loginError);
|
||||
case "externalLoginError":
|
||||
return intl.formatMessage(errorMessages.externalLoginError);
|
||||
return intl.formatMessage(errorMessages.unknownLoginError);
|
||||
case "unknownLoginError":
|
||||
return intl.formatMessage(errorMessages.unknownLoginError);
|
||||
case "serverError":
|
||||
return intl.formatMessage(errorMessages.serverError);
|
||||
case "noPermissionsError":
|
||||
return intl.formatMessage(errorMessages.noPermissionsError);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
import { ApolloError } from "@apollo/client";
|
||||
import { findValueInEnum } from "@saleor/misc";
|
||||
import { GraphQLError } from "graphql";
|
||||
|
||||
import { UserContextError } from "./types";
|
||||
|
||||
export enum JWTError {
|
||||
invalid = "InvalidTokenError",
|
||||
invalidSignature = "InvalidSignatureError",
|
||||
expired = "ExpiredSignatureError",
|
||||
}
|
||||
|
||||
export const AuthError = {
|
||||
PermissionDenied: "PermissionDenied",
|
||||
OAuthError: "OAuthError",
|
||||
} as const;
|
||||
|
||||
export type AuthError = typeof AuthError[keyof typeof AuthError];
|
||||
|
||||
export function isJwtError(error: GraphQLError): boolean {
|
||||
let jwtError: boolean;
|
||||
try {
|
||||
|
@ -21,3 +31,22 @@ export function isJwtError(error: GraphQLError): boolean {
|
|||
export function isTokenExpired(error: GraphQLError): boolean {
|
||||
return error.extensions.exception.code === JWTError.expired;
|
||||
}
|
||||
|
||||
export function getAuthErrorType(graphQLError: GraphQLError): UserContextError {
|
||||
switch (graphQLError.extensions?.exception?.code as AuthError) {
|
||||
case AuthError.PermissionDenied:
|
||||
return UserContextError.noPermissionsError;
|
||||
case AuthError.OAuthError:
|
||||
return UserContextError.externalLoginError;
|
||||
default:
|
||||
return UserContextError.unknownLoginError;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAuthError(authError: ApolloError): UserContextError[] {
|
||||
return (
|
||||
authError?.graphQLErrors?.map(graphQLError =>
|
||||
getAuthErrorType(graphQLError),
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ApolloClient } from "@apollo/client";
|
||||
import { ApolloClient, ApolloError } from "@apollo/client";
|
||||
import { IMessageContext } from "@saleor/components/messages";
|
||||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { useUserDetailsQuery } from "@saleor/graphql";
|
||||
|
@ -21,6 +21,7 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { IntlShape } from "react-intl";
|
||||
import urlJoin from "url-join";
|
||||
|
||||
import { parseAuthError } from "../errors";
|
||||
import {
|
||||
ExternalLoginInput,
|
||||
RequestExternalLoginInput,
|
||||
|
@ -53,12 +54,12 @@ export function useAuthProvider({
|
|||
"requestedExternalPluginId",
|
||||
null,
|
||||
);
|
||||
const [error, setError] = useState<UserContextError>();
|
||||
const [errors, setErrors] = useState<UserContextError[]>([]);
|
||||
const permitCredentialsAPI = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticating && error) {
|
||||
setError(undefined);
|
||||
if (authenticating && errors.length) {
|
||||
setErrors([]);
|
||||
}
|
||||
}, [authenticating]);
|
||||
|
||||
|
@ -88,6 +89,16 @@ export function useAuthProvider({
|
|||
fetchPolicy: "cache-and-network",
|
||||
});
|
||||
|
||||
const handleLoginError = (error: ApolloError) => {
|
||||
const parsedErrors = parseAuthError(error);
|
||||
|
||||
if (parsedErrors.length) {
|
||||
setErrors(parsedErrors);
|
||||
} else {
|
||||
setErrors(["unknownLoginError"]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
const returnTo = urlJoin(
|
||||
window.location.origin,
|
||||
|
@ -139,14 +150,14 @@ export function useAuthProvider({
|
|||
}
|
||||
saveCredentials(result.data.tokenCreate.user, password);
|
||||
} else {
|
||||
setError("loginError");
|
||||
setErrors(["loginError"]);
|
||||
}
|
||||
|
||||
await logoutNonStaffUser(result.data.tokenCreate);
|
||||
|
||||
return result.data.tokenCreate;
|
||||
} catch (error) {
|
||||
setError("serverError");
|
||||
handleLoginError(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -177,14 +188,14 @@ export function useAuthProvider({
|
|||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
} else {
|
||||
setError("externalLoginError");
|
||||
setErrors(["externalLoginError"]);
|
||||
}
|
||||
|
||||
await logoutNonStaffUser(result.data.externalObtainAccessTokens);
|
||||
|
||||
return result?.data?.externalObtainAccessTokens;
|
||||
} catch (error) {
|
||||
setError("serverError");
|
||||
handleLoginError(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -206,9 +217,9 @@ export function useAuthProvider({
|
|||
requestLoginByExternalPlugin: handleRequestExternalLogin,
|
||||
loginByExternalPlugin: handleExternalLogin,
|
||||
logout: handleLogout,
|
||||
authenticating: authenticating && !error,
|
||||
authenticating: authenticating && !errors.length,
|
||||
authenticated: authenticated && user?.isStaff,
|
||||
user: userDetails.data?.me,
|
||||
error,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export const UserContext = React.createContext<Context>({
|
|||
requestLoginByExternalPlugin: undefined,
|
||||
authenticating: false,
|
||||
authenticated: false,
|
||||
errors: [],
|
||||
});
|
||||
|
||||
const AuthRouter: React.FC = () => (
|
||||
|
|
|
@ -18,10 +18,15 @@ export interface RequestExternalLogoutInput {
|
|||
returnTo: string;
|
||||
}
|
||||
|
||||
export type UserContextError =
|
||||
| "loginError"
|
||||
| "externalLoginError"
|
||||
| "serverError";
|
||||
export const UserContextError = {
|
||||
loginError: "loginError",
|
||||
serverError: "serverError",
|
||||
noPermissionsError: "noPermissionsError",
|
||||
externalLoginError: "externalLoginError",
|
||||
unknownLoginError: "unknownLoginError",
|
||||
} as const;
|
||||
|
||||
export type UserContextError = typeof UserContextError[keyof typeof UserContextError];
|
||||
|
||||
export interface UserContext {
|
||||
login: (username: string, password: string) => Promise<LoginData>;
|
||||
|
@ -37,5 +42,5 @@ export interface UserContext {
|
|||
user?: UserFragment;
|
||||
authenticating: boolean;
|
||||
authenticated: boolean;
|
||||
error?: UserContextError;
|
||||
errors: UserContextError[];
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const LoginView: React.FC<LoginViewProps> = ({ params }) => {
|
|||
requestLoginByExternalPlugin,
|
||||
loginByExternalPlugin,
|
||||
authenticating,
|
||||
error,
|
||||
errors,
|
||||
} = useUser();
|
||||
const {
|
||||
data: externalAuthentications,
|
||||
|
@ -79,14 +79,17 @@ const LoginView: React.FC<LoginViewProps> = ({ params }) => {
|
|||
const { code, state } = params;
|
||||
const isCallbackPath = location.pathname.includes(loginCallbackPath);
|
||||
|
||||
if (code && state && isCallbackPath) {
|
||||
const externalAuthParamsExist = code && state && isCallbackPath;
|
||||
const externalAuthNotPerformed = !authenticating && !errors.length;
|
||||
|
||||
if (externalAuthParamsExist && externalAuthNotPerformed) {
|
||||
handleExternalAuthentication(code, state);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
error={error}
|
||||
errors={errors}
|
||||
disabled={authenticating}
|
||||
externalAuthentications={
|
||||
externalAuthentications?.shop?.availableExternalAuthentications
|
||||
|
|
|
@ -12,6 +12,7 @@ export const UserDecorator = (user: UserFragment) => storyFn => (
|
|||
user,
|
||||
authenticated: false,
|
||||
authenticating: false,
|
||||
errors: [],
|
||||
}}
|
||||
>
|
||||
{storyFn()}
|
||||
|
|
|
@ -37127,7 +37127,7 @@ exports[`Storyshots Views / Authentication / Log in error external login 1`] = `
|
|||
class="Login-panel-id"
|
||||
data-test-id="login-error-message"
|
||||
>
|
||||
Sorry, login went wrong. Please try again.
|
||||
Login went wrong. Please try again.
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
|
@ -37311,7 +37311,7 @@ exports[`Storyshots Views / Authentication / Log in error login 1`] = `
|
|||
class="Login-panel-id"
|
||||
data-test-id="login-error-message"
|
||||
>
|
||||
Sorry, your username and/or password are incorrect. Please try again.
|
||||
Your username and/or password are incorrect. Please try again.
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
|
|
|
@ -24,6 +24,7 @@ export const MockedUserProvider: React.FC<{
|
|||
avatar: null,
|
||||
__typename: "User",
|
||||
},
|
||||
errors: [],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
Loading…
Reference in a new issue