Handle session expiration

This commit is contained in:
dominik-zeglen 2020-05-07 13:04:15 +02:00
parent 3f8c5b0c17
commit a5e72c456e
11 changed files with 130 additions and 9 deletions

View file

@ -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,10 +35,18 @@ const AuthProviderOperations: React.FC<AuthProviderOperationsProps> = ({
{(...tokenAuth) => (
<TypedVerifyTokenMutation>
{(...tokenVerify) => (
<AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
<TokenRefreshMutation>
{(...tokenRefresh) => (
<AuthProvider
tokenAuth={tokenAuth}
tokenVerify={tokenVerify}
tokenRefresh={tokenRefresh}
>
{children}
</AuthProvider>
)}
</TokenRefreshMutation>
)}
</TypedVerifyTokenMutation>
)}
</TypedTokenAuthMutation>
@ -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
View 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;
}

View file

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

View file

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

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

View file

@ -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)

View file

@ -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"

View file

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

View file

@ -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"
},

View file

@ -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)

View file

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