diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8d8bd133..179d1e69e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json
index 9f56ff639..f1d38d88a 100644
--- a/locale/defaultMessages.json
+++ b/locale/defaultMessages.json
@@ -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 here."
- },
- "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"
diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx
index fdb809cd8..a99fc03e1 100644
--- a/src/auth/AuthProvider.tsx
+++ b/src/auth/AuthProvider.tsx
@@ -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 = ({
{(...tokenAuth) => (
{(...tokenVerify) => (
-
- {children}
-
+
+ {(...tokenRefresh) => (
+
+ {children}
+
+ )}
+
)}
)}
@@ -55,6 +68,10 @@ interface AuthProviderProps {
MutationFunction,
MutationResult
];
+ tokenRefresh: [
+ MutationFunction,
+ MutationResult
+ ];
}
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
}}
diff --git a/src/auth/errors.ts b/src/auth/errors.ts
new file mode 100644
index 000000000..61808e3b2
--- /dev/null
+++ b/src/auth/errors.ts
@@ -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;
+}
diff --git a/src/auth/index.tsx b/src/auth/index.tsx
index e57a0159b..7eecf89d1 100644
--- a/src/auth/index.tsx
+++ b/src/auth/index.tsx
@@ -19,6 +19,7 @@ interface UserContext {
loginByToken: (token: string, user: User) => void;
logout: () => void;
tokenAuthLoading: boolean;
+ tokenRefresh: () => Promise;
tokenVerifyLoading: boolean;
user?: User;
}
@@ -28,6 +29,7 @@ export const UserContext = React.createContext({
loginByToken: undefined,
logout: undefined,
tokenAuthLoading: false,
+ tokenRefresh: undefined,
tokenVerifyLoading: false
});
diff --git a/src/auth/mutations.ts b/src/auth/mutations.ts
index e5480427b..ef6152076 100644
--- a/src/auth/mutations.ts
+++ b/src/auth/mutations.ts
@@ -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);
diff --git a/src/auth/types/RefreshToken.ts b/src/auth/types/RefreshToken.ts
new file mode 100644
index 000000000..cbd3679b9
--- /dev/null
+++ b/src/auth/types/RefreshToken.ts
@@ -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;
+}
diff --git a/src/hooks/makeMutation.ts b/src/hooks/makeMutation.ts
index 52f75eeb4..1bda2b926 100644
--- a/src/hooks/makeMutation.ts
+++ b/src/hooks/makeMutation.ts
@@ -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 = [
MutationFunction,
@@ -33,6 +35,8 @@ function makeMutation(
}: UseMutationCbs): UseMutation {
const notify = useNotifier();
const intl = useIntl();
+ const user = useUser();
+
const [mutateFn, result] = useBaseMutation(mutation, {
onCompleted,
onError: (err: ApolloError) => {
@@ -46,6 +50,11 @@ function makeMutation(
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)
diff --git a/src/hooks/makeQuery.ts b/src/hooks/makeQuery.ts
index 3cc28a19b..e5ff1703c 100644
--- a/src/hooks/makeQuery.ts
+++ b/src/hooks/makeQuery.ts
@@ -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 {
loadMore: (
@@ -38,6 +40,8 @@ function makeQuery(
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(
}, [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"
diff --git a/src/index.tsx b/src/index.tsx
index 12279e63b..2a60c75cb 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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();
}
});
diff --git a/src/intl.ts b/src/intl.ts
index 458961851..479b630dc 100644
--- a/src/intl.ts
+++ b/src/intl.ts
@@ -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"
},
diff --git a/src/mutations.tsx b/src/mutations.tsx
index c829e8410..727a9d8e0 100644
--- a/src/mutations.tsx
+++ b/src/mutations.tsx
@@ -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 {
children: (
@@ -27,6 +29,7 @@ export function TypedMutation(
return (props: TypedMutationInnerProps) => {
const notify = useNotifier();
const intl = useIntl();
+ const user = useUser();
const { children, onCompleted, onError, variables } = props;
return (
@@ -44,6 +47,11 @@ export function TypedMutation(
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)
diff --git a/src/queries.tsx b/src/queries.tsx
index abe3e8bc0..952c943d8 100644
--- a/src/queries.tsx
+++ b/src/queries.tsx
@@ -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 {
loadMore: (
@@ -65,9 +67,10 @@ export function TypedQuery(
query: DocumentNode
): React.FC> {
return ({ children, displayLoader, skip, variables }) => {
- const pushMessage = useNotifier();
+ const notify = useNotifier();
const [, dispatchAppState] = useAppState();
const intl = useIntl();
+ const user = useUser();
return (
(
>
{(queryData: QueryResult) => {
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)
});
}