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