Merge pull request #520 from mirumee/fix/jwt-expire
Handle session expiration
This commit is contained in:
commit
873ed68916
13 changed files with 138 additions and 23 deletions
|
@ -51,6 +51,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Add Permission Groups section - #406 by @krzysztofwolski
|
||||
- Add warehouse management - #390 by @dominik-zeglen
|
||||
- Fix minor visual bugs - #521 by @dominik-zeglen
|
||||
- Handle session expiration - #520 by @dominik-zeglen
|
||||
|
||||
## 2.0.0
|
||||
|
||||
|
|
|
@ -3621,17 +3621,14 @@
|
|||
"context": "header",
|
||||
"string": "Quantity"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductStocks_dot_3143542809": {
|
||||
"string": "This product doesn't have any stock. You can add it <l>here</l>."
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductStocks_dot_3482985373": {
|
||||
"context": "button",
|
||||
"string": "Edit Warehouses"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductStocks_dot_3633706025": {
|
||||
"context": "product inventory, checkbox",
|
||||
"string": "Track Inventory"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductStocks_dot_3857433664": {
|
||||
"context": "button",
|
||||
"string": "Assign Warehouse"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductStocks_dot_849869830": {
|
||||
"string": "Active inventory tracking will automatically calculate changes of stock"
|
||||
},
|
||||
|
@ -3859,13 +3856,6 @@
|
|||
"context": "product variant name",
|
||||
"string": "Variant"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductWarehousesDialog_dot_3482985373": {
|
||||
"context": "dialog header",
|
||||
"string": "Edit Warehouses"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductWarehousesDialog_dot_4112406382": {
|
||||
"string": "Select warehouses that stock selected product"
|
||||
},
|
||||
"src_dot_products_dot_views_dot_1542417144": {
|
||||
"context": "window title",
|
||||
"string": "Create Product"
|
||||
|
@ -4147,6 +4137,9 @@
|
|||
"context": "window title",
|
||||
"string": "Create Service Account"
|
||||
},
|
||||
"src_dot_sessionExpired": {
|
||||
"string": "Your session has expired. Please log in again to continue."
|
||||
},
|
||||
"src_dot_shipping": {
|
||||
"context": "shipping section name",
|
||||
"string": "Shipping Methods"
|
||||
|
|
|
@ -7,11 +7,16 @@ import {
|
|||
} from "@saleor/utils/credentialsManagement";
|
||||
import { MutationFunction, MutationResult } from "react-apollo";
|
||||
import { maybe } from "@saleor/misc";
|
||||
import { TypedTokenAuthMutation, TypedVerifyTokenMutation } from "./mutations";
|
||||
import {
|
||||
TypedTokenAuthMutation,
|
||||
TypedVerifyTokenMutation,
|
||||
TokenRefreshMutation
|
||||
} from "./mutations";
|
||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||
import { User } from "./types/User";
|
||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||
import { getAuthToken, removeAuthToken, setAuthToken } from "./utils";
|
||||
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
|
||||
import { UserContext } from "./";
|
||||
|
||||
interface AuthProviderOperationsProps {
|
||||
|
@ -30,9 +35,17 @@ const AuthProviderOperations: React.FC<AuthProviderOperationsProps> = ({
|
|||
{(...tokenAuth) => (
|
||||
<TypedVerifyTokenMutation>
|
||||
{(...tokenVerify) => (
|
||||
<AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
<TokenRefreshMutation>
|
||||
{(...tokenRefresh) => (
|
||||
<AuthProvider
|
||||
tokenAuth={tokenAuth}
|
||||
tokenVerify={tokenVerify}
|
||||
tokenRefresh={tokenRefresh}
|
||||
>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
)}
|
||||
</TokenRefreshMutation>
|
||||
)}
|
||||
</TypedVerifyTokenMutation>
|
||||
)}
|
||||
|
@ -55,6 +68,10 @@ interface AuthProviderProps {
|
|||
MutationFunction<VerifyToken, VerifyTokenVariables>,
|
||||
MutationResult<VerifyToken>
|
||||
];
|
||||
tokenRefresh: [
|
||||
MutationFunction<RefreshToken, RefreshTokenVariables>,
|
||||
MutationResult<RefreshToken>
|
||||
];
|
||||
}
|
||||
|
||||
interface AuthProviderState {
|
||||
|
@ -143,6 +160,16 @@ class AuthProvider extends React.Component<
|
|||
return tokenVerifyFn({ variables: { token } });
|
||||
};
|
||||
|
||||
refreshToken = async () => {
|
||||
const { tokenRefresh } = this.props;
|
||||
const [tokenRefreshFn] = tokenRefresh;
|
||||
const token = getAuthToken();
|
||||
|
||||
const refreshData = await tokenRefreshFn({ variables: { token } });
|
||||
|
||||
setAuthToken(refreshData.data.tokenRefresh.token, this.state.persistToken);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, tokenAuth, tokenVerify } = this.props;
|
||||
const tokenAuthOpts = tokenAuth[1];
|
||||
|
@ -157,6 +184,7 @@ class AuthProvider extends React.Component<
|
|||
loginByToken: this.loginByToken,
|
||||
logout: this.logout,
|
||||
tokenAuthLoading: tokenAuthOpts.loading,
|
||||
tokenRefresh: this.refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user
|
||||
}}
|
||||
|
|
15
src/auth/errors.ts
Normal file
15
src/auth/errors.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { GraphQLError } from "graphql";
|
||||
import { findValueInEnum } from "@saleor/misc";
|
||||
|
||||
export enum JWTError {
|
||||
invalid = "JSONWebTokenError",
|
||||
expired = "JSONWebTokenExpired"
|
||||
}
|
||||
|
||||
export function isJwtError(error: GraphQLError): boolean {
|
||||
return !!findValueInEnum(error.extensions.exception.code, JWTError);
|
||||
}
|
||||
|
||||
export function isTokenExpired(error: GraphQLError): boolean {
|
||||
return error.extensions.exception.code === JWTError.expired;
|
||||
}
|
|
@ -19,6 +19,7 @@ interface UserContext {
|
|||
loginByToken: (token: string, user: User) => void;
|
||||
logout: () => void;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenRefresh: () => Promise<void>;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
}
|
||||
|
@ -28,6 +29,7 @@ export const UserContext = React.createContext<UserContext>({
|
|||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenRefresh: undefined,
|
||||
tokenVerifyLoading: false
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
|
||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
|
||||
|
||||
export const fragmentUser = gql`
|
||||
fragment User on User {
|
||||
|
@ -98,3 +99,16 @@ export const SetPasswordMutation = TypedMutation<
|
|||
SetPassword,
|
||||
SetPasswordVariables
|
||||
>(setPassword);
|
||||
|
||||
const refreshToken = gql`
|
||||
mutation RefreshToken($token: String!) {
|
||||
tokenRefresh(token: $token) {
|
||||
token
|
||||
payload
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const TokenRefreshMutation = TypedMutation<
|
||||
RefreshToken,
|
||||
RefreshTokenVariables
|
||||
>(refreshToken);
|
||||
|
|
21
src/auth/types/RefreshToken.ts
Normal file
21
src/auth/types/RefreshToken.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: RefreshToken
|
||||
// ====================================================
|
||||
|
||||
export interface RefreshToken_tokenRefresh {
|
||||
__typename: "RefreshToken";
|
||||
token: string | null;
|
||||
payload: any | null;
|
||||
}
|
||||
|
||||
export interface RefreshToken {
|
||||
tokenRefresh: RefreshToken_tokenRefresh | null;
|
||||
}
|
||||
|
||||
export interface RefreshTokenVariables {
|
||||
token: string;
|
||||
}
|
|
@ -10,7 +10,9 @@ import { useIntl } from "react-intl";
|
|||
import { commonMessages } from "@saleor/intl";
|
||||
import { maybe, getMutationStatus } from "@saleor/misc";
|
||||
import { MutationResultAdditionalProps } from "@saleor/types";
|
||||
import { isJwtError } from "@saleor/auth/errors";
|
||||
import useNotifier from "./useNotifier";
|
||||
import useUser from "./useUser";
|
||||
|
||||
export type UseMutation<TData, TVariables> = [
|
||||
MutationFunction<TData, TVariables>,
|
||||
|
@ -33,6 +35,8 @@ function makeMutation<TData, TVariables>(
|
|||
}: UseMutationCbs<TData>): UseMutation<TData, TVariables> {
|
||||
const notify = useNotifier();
|
||||
const intl = useIntl();
|
||||
const user = useUser();
|
||||
|
||||
const [mutateFn, result] = useBaseMutation(mutation, {
|
||||
onCompleted,
|
||||
onError: (err: ApolloError) => {
|
||||
|
@ -46,6 +50,11 @@ function makeMutation<TData, TVariables>(
|
|||
notify({
|
||||
text: intl.formatMessage(commonMessages.readOnly)
|
||||
});
|
||||
} else if (err.graphQLErrors.every(isJwtError)) {
|
||||
user.logout();
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else {
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
|
|
|
@ -6,8 +6,10 @@ import { useIntl } from "react-intl";
|
|||
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { maybe, RequireAtLeastOne } from "@saleor/misc";
|
||||
import { isJwtError } from "@saleor/auth/errors";
|
||||
import useAppState from "./useAppState";
|
||||
import useNotifier from "./useNotifier";
|
||||
import useUser from "./useUser";
|
||||
|
||||
export interface LoadMore<TData, TVariables> {
|
||||
loadMore: (
|
||||
|
@ -38,6 +40,8 @@ function makeQuery<TData, TVariables>(
|
|||
const notify = useNotifier();
|
||||
const intl = useIntl();
|
||||
const [, dispatchAppState] = useAppState();
|
||||
const user = useUser();
|
||||
|
||||
const queryData = useBaseQuery(query, {
|
||||
context: {
|
||||
useBatching: true
|
||||
|
@ -60,7 +64,12 @@ function makeQuery<TData, TVariables>(
|
|||
}, [queryData.loading]);
|
||||
|
||||
if (queryData.error) {
|
||||
if (
|
||||
if (queryData.error.graphQLErrors.every(isJwtError)) {
|
||||
user.logout();
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else if (
|
||||
!queryData.error.graphQLErrors.every(
|
||||
err =>
|
||||
maybe(() => err.extensions.exception.code) === "PermissionDenied"
|
||||
|
|
|
@ -17,6 +17,7 @@ import useAppState from "@saleor/hooks/useAppState";
|
|||
import AttributeSection from "./attributes";
|
||||
import { attributeSection } from "./attributes/urls";
|
||||
import Auth, { getAuthToken, removeAuthToken } from "./auth";
|
||||
import { isJwtError } from "./auth/errors";
|
||||
import AuthProvider from "./auth/AuthProvider";
|
||||
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
|
||||
import SectionRoute from "./auth/components/SectionRoute";
|
||||
|
@ -66,7 +67,10 @@ interface ResponseError extends ErrorResponse {
|
|||
}
|
||||
|
||||
const invalidTokenLink = onError((error: ResponseError) => {
|
||||
if (error.networkError && error.networkError.statusCode === 401) {
|
||||
if (
|
||||
(error.networkError && error.networkError.statusCode === 401) ||
|
||||
error.graphQLErrors?.some(isJwtError)
|
||||
) {
|
||||
removeAuthToken();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -56,6 +56,9 @@ export const commonMessages = defineMessages({
|
|||
savedChanges: {
|
||||
defaultMessage: "Saved changes"
|
||||
},
|
||||
sessionExpired: {
|
||||
defaultMessage: "Your session has expired. Please log in again to continue."
|
||||
},
|
||||
somethingWentWrong: {
|
||||
defaultMessage: "Saleor ran into an unexpected problem"
|
||||
},
|
||||
|
|
|
@ -8,6 +8,8 @@ import useNotifier from "./hooks/useNotifier";
|
|||
import { commonMessages } from "./intl";
|
||||
import { maybe, getMutationStatus } from "./misc";
|
||||
import { MutationResultAdditionalProps } from "./types";
|
||||
import { isJwtError } from "./auth/errors";
|
||||
import useUser from "./hooks/useUser";
|
||||
|
||||
export interface TypedMutationInnerProps<TData, TVariables> {
|
||||
children: (
|
||||
|
@ -27,6 +29,7 @@ export function TypedMutation<TData, TVariables>(
|
|||
return (props: TypedMutationInnerProps<TData, TVariables>) => {
|
||||
const notify = useNotifier();
|
||||
const intl = useIntl();
|
||||
const user = useUser();
|
||||
const { children, onCompleted, onError, variables } = props;
|
||||
|
||||
return (
|
||||
|
@ -44,6 +47,11 @@ export function TypedMutation<TData, TVariables>(
|
|||
notify({
|
||||
text: intl.formatMessage(commonMessages.readOnly)
|
||||
});
|
||||
} else if (err.graphQLErrors.every(isJwtError)) {
|
||||
user.logout();
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else {
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
|
|
|
@ -9,6 +9,8 @@ import useAppState from "./hooks/useAppState";
|
|||
import useNotifier from "./hooks/useNotifier";
|
||||
import { commonMessages } from "./intl";
|
||||
import { maybe, RequireAtLeastOne } from "./misc";
|
||||
import { isJwtError } from "./auth/errors";
|
||||
import useUser from "./hooks/useUser";
|
||||
|
||||
export interface LoadMore<TData, TVariables> {
|
||||
loadMore: (
|
||||
|
@ -65,9 +67,10 @@ export function TypedQuery<TData, TVariables>(
|
|||
query: DocumentNode
|
||||
): React.FC<TypedQueryInnerProps<TData, TVariables>> {
|
||||
return ({ children, displayLoader, skip, variables }) => {
|
||||
const pushMessage = useNotifier();
|
||||
const notify = useNotifier();
|
||||
const [, dispatchAppState] = useAppState();
|
||||
const intl = useIntl();
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<Query
|
||||
|
@ -80,14 +83,19 @@ export function TypedQuery<TData, TVariables>(
|
|||
>
|
||||
{(queryData: QueryResult<TData, TVariables>) => {
|
||||
if (queryData.error) {
|
||||
if (
|
||||
if (queryData.error.graphQLErrors.every(isJwtError)) {
|
||||
user.logout();
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else if (
|
||||
!queryData.error.graphQLErrors.every(
|
||||
err =>
|
||||
maybe(() => err.extensions.exception.code) ===
|
||||
"PermissionDenied"
|
||||
)
|
||||
) {
|
||||
pushMessage({
|
||||
notify({
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue