Handle session expiration
This commit is contained in:
parent
3f8c5b0c17
commit
a5e72c456e
11 changed files with 130 additions and 9 deletions
|
@ -7,11 +7,16 @@ import {
|
||||||
} from "@saleor/utils/credentialsManagement";
|
} from "@saleor/utils/credentialsManagement";
|
||||||
import { MutationFunction, MutationResult } from "react-apollo";
|
import { MutationFunction, MutationResult } from "react-apollo";
|
||||||
import { maybe } from "@saleor/misc";
|
import { maybe } from "@saleor/misc";
|
||||||
import { TypedTokenAuthMutation, TypedVerifyTokenMutation } from "./mutations";
|
import {
|
||||||
|
TypedTokenAuthMutation,
|
||||||
|
TypedVerifyTokenMutation,
|
||||||
|
TokenRefreshMutation
|
||||||
|
} from "./mutations";
|
||||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||||
import { User } from "./types/User";
|
import { User } from "./types/User";
|
||||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||||
import { getAuthToken, removeAuthToken, setAuthToken } from "./utils";
|
import { getAuthToken, removeAuthToken, setAuthToken } from "./utils";
|
||||||
|
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
|
||||||
import { UserContext } from "./";
|
import { UserContext } from "./";
|
||||||
|
|
||||||
interface AuthProviderOperationsProps {
|
interface AuthProviderOperationsProps {
|
||||||
|
@ -30,9 +35,17 @@ const AuthProviderOperations: React.FC<AuthProviderOperationsProps> = ({
|
||||||
{(...tokenAuth) => (
|
{(...tokenAuth) => (
|
||||||
<TypedVerifyTokenMutation>
|
<TypedVerifyTokenMutation>
|
||||||
{(...tokenVerify) => (
|
{(...tokenVerify) => (
|
||||||
<AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
|
<TokenRefreshMutation>
|
||||||
{children}
|
{(...tokenRefresh) => (
|
||||||
</AuthProvider>
|
<AuthProvider
|
||||||
|
tokenAuth={tokenAuth}
|
||||||
|
tokenVerify={tokenVerify}
|
||||||
|
tokenRefresh={tokenRefresh}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthProvider>
|
||||||
|
)}
|
||||||
|
</TokenRefreshMutation>
|
||||||
)}
|
)}
|
||||||
</TypedVerifyTokenMutation>
|
</TypedVerifyTokenMutation>
|
||||||
)}
|
)}
|
||||||
|
@ -55,6 +68,10 @@ interface AuthProviderProps {
|
||||||
MutationFunction<VerifyToken, VerifyTokenVariables>,
|
MutationFunction<VerifyToken, VerifyTokenVariables>,
|
||||||
MutationResult<VerifyToken>
|
MutationResult<VerifyToken>
|
||||||
];
|
];
|
||||||
|
tokenRefresh: [
|
||||||
|
MutationFunction<RefreshToken, RefreshTokenVariables>,
|
||||||
|
MutationResult<RefreshToken>
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthProviderState {
|
interface AuthProviderState {
|
||||||
|
@ -143,6 +160,16 @@ class AuthProvider extends React.Component<
|
||||||
return tokenVerifyFn({ variables: { token } });
|
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() {
|
render() {
|
||||||
const { children, tokenAuth, tokenVerify } = this.props;
|
const { children, tokenAuth, tokenVerify } = this.props;
|
||||||
const tokenAuthOpts = tokenAuth[1];
|
const tokenAuthOpts = tokenAuth[1];
|
||||||
|
@ -157,6 +184,7 @@ class AuthProvider extends React.Component<
|
||||||
loginByToken: this.loginByToken,
|
loginByToken: this.loginByToken,
|
||||||
logout: this.logout,
|
logout: this.logout,
|
||||||
tokenAuthLoading: tokenAuthOpts.loading,
|
tokenAuthLoading: tokenAuthOpts.loading,
|
||||||
|
tokenRefresh: this.refreshToken,
|
||||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||||
user
|
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;
|
loginByToken: (token: string, user: User) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
tokenAuthLoading: boolean;
|
tokenAuthLoading: boolean;
|
||||||
|
tokenRefresh: () => Promise<void>;
|
||||||
tokenVerifyLoading: boolean;
|
tokenVerifyLoading: boolean;
|
||||||
user?: User;
|
user?: User;
|
||||||
}
|
}
|
||||||
|
@ -28,6 +29,7 @@ export const UserContext = React.createContext<UserContext>({
|
||||||
loginByToken: undefined,
|
loginByToken: undefined,
|
||||||
logout: undefined,
|
logout: undefined,
|
||||||
tokenAuthLoading: false,
|
tokenAuthLoading: false,
|
||||||
|
tokenRefresh: undefined,
|
||||||
tokenVerifyLoading: false
|
tokenVerifyLoading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
|
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
|
||||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||||
|
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
|
||||||
|
|
||||||
export const fragmentUser = gql`
|
export const fragmentUser = gql`
|
||||||
fragment User on User {
|
fragment User on User {
|
||||||
|
@ -98,3 +99,16 @@ export const SetPasswordMutation = TypedMutation<
|
||||||
SetPassword,
|
SetPassword,
|
||||||
SetPasswordVariables
|
SetPasswordVariables
|
||||||
>(setPassword);
|
>(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 { commonMessages } from "@saleor/intl";
|
||||||
import { maybe, getMutationStatus } from "@saleor/misc";
|
import { maybe, getMutationStatus } from "@saleor/misc";
|
||||||
import { MutationResultAdditionalProps } from "@saleor/types";
|
import { MutationResultAdditionalProps } from "@saleor/types";
|
||||||
|
import { isJwtError } from "@saleor/auth/errors";
|
||||||
import useNotifier from "./useNotifier";
|
import useNotifier from "./useNotifier";
|
||||||
|
import useUser from "./useUser";
|
||||||
|
|
||||||
export type UseMutation<TData, TVariables> = [
|
export type UseMutation<TData, TVariables> = [
|
||||||
MutationFunction<TData, TVariables>,
|
MutationFunction<TData, TVariables>,
|
||||||
|
@ -33,6 +35,8 @@ function makeMutation<TData, TVariables>(
|
||||||
}: UseMutationCbs<TData>): UseMutation<TData, TVariables> {
|
}: UseMutationCbs<TData>): UseMutation<TData, TVariables> {
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
const [mutateFn, result] = useBaseMutation(mutation, {
|
const [mutateFn, result] = useBaseMutation(mutation, {
|
||||||
onCompleted,
|
onCompleted,
|
||||||
onError: (err: ApolloError) => {
|
onError: (err: ApolloError) => {
|
||||||
|
@ -46,6 +50,11 @@ function makeMutation<TData, TVariables>(
|
||||||
notify({
|
notify({
|
||||||
text: intl.formatMessage(commonMessages.readOnly)
|
text: intl.formatMessage(commonMessages.readOnly)
|
||||||
});
|
});
|
||||||
|
} else if (err.graphQLErrors.every(isJwtError)) {
|
||||||
|
user.logout();
|
||||||
|
notify({
|
||||||
|
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notify({
|
notify({
|
||||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||||
|
|
|
@ -6,8 +6,10 @@ import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { commonMessages } from "@saleor/intl";
|
import { commonMessages } from "@saleor/intl";
|
||||||
import { maybe, RequireAtLeastOne } from "@saleor/misc";
|
import { maybe, RequireAtLeastOne } from "@saleor/misc";
|
||||||
|
import { isJwtError } from "@saleor/auth/errors";
|
||||||
import useAppState from "./useAppState";
|
import useAppState from "./useAppState";
|
||||||
import useNotifier from "./useNotifier";
|
import useNotifier from "./useNotifier";
|
||||||
|
import useUser from "./useUser";
|
||||||
|
|
||||||
export interface LoadMore<TData, TVariables> {
|
export interface LoadMore<TData, TVariables> {
|
||||||
loadMore: (
|
loadMore: (
|
||||||
|
@ -38,6 +40,8 @@ function makeQuery<TData, TVariables>(
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [, dispatchAppState] = useAppState();
|
const [, dispatchAppState] = useAppState();
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
const queryData = useBaseQuery(query, {
|
const queryData = useBaseQuery(query, {
|
||||||
context: {
|
context: {
|
||||||
useBatching: true
|
useBatching: true
|
||||||
|
@ -60,7 +64,12 @@ function makeQuery<TData, TVariables>(
|
||||||
}, [queryData.loading]);
|
}, [queryData.loading]);
|
||||||
|
|
||||||
if (queryData.error) {
|
if (queryData.error) {
|
||||||
if (
|
if (queryData.error.graphQLErrors.every(isJwtError)) {
|
||||||
|
user.logout();
|
||||||
|
notify({
|
||||||
|
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
!queryData.error.graphQLErrors.every(
|
!queryData.error.graphQLErrors.every(
|
||||||
err =>
|
err =>
|
||||||
maybe(() => err.extensions.exception.code) === "PermissionDenied"
|
maybe(() => err.extensions.exception.code) === "PermissionDenied"
|
||||||
|
|
|
@ -17,6 +17,7 @@ import useAppState from "@saleor/hooks/useAppState";
|
||||||
import AttributeSection from "./attributes";
|
import AttributeSection from "./attributes";
|
||||||
import { attributeSection } from "./attributes/urls";
|
import { attributeSection } from "./attributes/urls";
|
||||||
import Auth, { getAuthToken, removeAuthToken } from "./auth";
|
import Auth, { getAuthToken, removeAuthToken } from "./auth";
|
||||||
|
import { isJwtError } from "./auth/errors";
|
||||||
import AuthProvider from "./auth/AuthProvider";
|
import AuthProvider from "./auth/AuthProvider";
|
||||||
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
|
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
|
||||||
import SectionRoute from "./auth/components/SectionRoute";
|
import SectionRoute from "./auth/components/SectionRoute";
|
||||||
|
@ -66,7 +67,10 @@ interface ResponseError extends ErrorResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalidTokenLink = onError((error: ResponseError) => {
|
const invalidTokenLink = onError((error: ResponseError) => {
|
||||||
if (error.networkError && error.networkError.statusCode === 401) {
|
if (
|
||||||
|
(error.networkError && error.networkError.statusCode === 401) ||
|
||||||
|
error.graphQLErrors?.some(isJwtError)
|
||||||
|
) {
|
||||||
removeAuthToken();
|
removeAuthToken();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,6 +56,9 @@ export const commonMessages = defineMessages({
|
||||||
savedChanges: {
|
savedChanges: {
|
||||||
defaultMessage: "Saved changes"
|
defaultMessage: "Saved changes"
|
||||||
},
|
},
|
||||||
|
sessionExpired: {
|
||||||
|
defaultMessage: "Your session has expired. Please log in again to continue."
|
||||||
|
},
|
||||||
somethingWentWrong: {
|
somethingWentWrong: {
|
||||||
defaultMessage: "Saleor ran into an unexpected problem"
|
defaultMessage: "Saleor ran into an unexpected problem"
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,8 @@ import useNotifier from "./hooks/useNotifier";
|
||||||
import { commonMessages } from "./intl";
|
import { commonMessages } from "./intl";
|
||||||
import { maybe, getMutationStatus } from "./misc";
|
import { maybe, getMutationStatus } from "./misc";
|
||||||
import { MutationResultAdditionalProps } from "./types";
|
import { MutationResultAdditionalProps } from "./types";
|
||||||
|
import { isJwtError } from "./auth/errors";
|
||||||
|
import useUser from "./hooks/useUser";
|
||||||
|
|
||||||
export interface TypedMutationInnerProps<TData, TVariables> {
|
export interface TypedMutationInnerProps<TData, TVariables> {
|
||||||
children: (
|
children: (
|
||||||
|
@ -27,6 +29,7 @@ export function TypedMutation<TData, TVariables>(
|
||||||
return (props: TypedMutationInnerProps<TData, TVariables>) => {
|
return (props: TypedMutationInnerProps<TData, TVariables>) => {
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const user = useUser();
|
||||||
const { children, onCompleted, onError, variables } = props;
|
const { children, onCompleted, onError, variables } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -44,6 +47,11 @@ export function TypedMutation<TData, TVariables>(
|
||||||
notify({
|
notify({
|
||||||
text: intl.formatMessage(commonMessages.readOnly)
|
text: intl.formatMessage(commonMessages.readOnly)
|
||||||
});
|
});
|
||||||
|
} else if (err.graphQLErrors.every(isJwtError)) {
|
||||||
|
user.logout();
|
||||||
|
notify({
|
||||||
|
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notify({
|
notify({
|
||||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||||
|
|
|
@ -9,6 +9,8 @@ import useAppState from "./hooks/useAppState";
|
||||||
import useNotifier from "./hooks/useNotifier";
|
import useNotifier from "./hooks/useNotifier";
|
||||||
import { commonMessages } from "./intl";
|
import { commonMessages } from "./intl";
|
||||||
import { maybe, RequireAtLeastOne } from "./misc";
|
import { maybe, RequireAtLeastOne } from "./misc";
|
||||||
|
import { isJwtError } from "./auth/errors";
|
||||||
|
import useUser from "./hooks/useUser";
|
||||||
|
|
||||||
export interface LoadMore<TData, TVariables> {
|
export interface LoadMore<TData, TVariables> {
|
||||||
loadMore: (
|
loadMore: (
|
||||||
|
@ -65,9 +67,10 @@ export function TypedQuery<TData, TVariables>(
|
||||||
query: DocumentNode
|
query: DocumentNode
|
||||||
): React.FC<TypedQueryInnerProps<TData, TVariables>> {
|
): React.FC<TypedQueryInnerProps<TData, TVariables>> {
|
||||||
return ({ children, displayLoader, skip, variables }) => {
|
return ({ children, displayLoader, skip, variables }) => {
|
||||||
const pushMessage = useNotifier();
|
const notify = useNotifier();
|
||||||
const [, dispatchAppState] = useAppState();
|
const [, dispatchAppState] = useAppState();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Query
|
<Query
|
||||||
|
@ -80,14 +83,19 @@ export function TypedQuery<TData, TVariables>(
|
||||||
>
|
>
|
||||||
{(queryData: QueryResult<TData, TVariables>) => {
|
{(queryData: QueryResult<TData, TVariables>) => {
|
||||||
if (queryData.error) {
|
if (queryData.error) {
|
||||||
if (
|
if (queryData.error.graphQLErrors.every(isJwtError)) {
|
||||||
|
user.logout();
|
||||||
|
notify({
|
||||||
|
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
!queryData.error.graphQLErrors.every(
|
!queryData.error.graphQLErrors.every(
|
||||||
err =>
|
err =>
|
||||||
maybe(() => err.extensions.exception.code) ===
|
maybe(() => err.extensions.exception.code) ===
|
||||||
"PermissionDenied"
|
"PermissionDenied"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
pushMessage({
|
notify({
|
||||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue