Created
September 18, 2020 12:30
-
-
Save kugimiya/5125ffe85d74c71915c33bb5834ce332 to your computer and use it in GitHub Desktop.
Token refreshing + some dependencies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; | |
import { API_BASENAME, ENDPOINTS } from '../constants/api'; | |
import interceptorInjector from './interceptor'; | |
import { AuthData, AuthStoreInstance } from '../stores/auth'; | |
import { LOCAL_STORAGE } from '../constants/hardcode'; | |
type RequestConfig = { | |
endpoint: string; | |
method: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE'; | |
data: Record<string, string | number | null>; | |
skipHeaders?: boolean; | |
}; | |
class ApiRequest { | |
apiInstance: AxiosInstance; | |
constructor() { | |
this.apiInstance = axios.create({ | |
baseURL: API_BASENAME, | |
}); | |
// Tricky fix! Handle multiply requests now | |
interceptorInjector<AuthData['tokens']>(this.apiInstance, { | |
shouldIntercept: (error: AxiosError) => { | |
try { | |
return error?.response?.status === 403; | |
} catch (e) { | |
return false; | |
} | |
}, | |
setTokenData: (authData) => { | |
localStorage.setItem(LOCAL_STORAGE.ACCESS, authData.accessToken); | |
localStorage.setItem(LOCAL_STORAGE.REFRESH, authData.refreshToken); | |
if (AuthStoreInstance.authData) { | |
AuthStoreInstance.authData.tokens = authData; | |
} | |
}, | |
attachTokenToRequest: (request) => { | |
request.headers.bearer = localStorage.getItem(LOCAL_STORAGE.ACCESS) || ''; | |
}, | |
handleTokenRefresh: () => { | |
return new Promise<AuthData['tokens']>((res, rej) => { | |
this.request<AuthData['tokens']>({ | |
endpoint: ENDPOINTS.REFRESH, | |
method: 'POST', | |
data: { | |
accessToken: localStorage.getItem(LOCAL_STORAGE.ACCESS), | |
refreshToken: localStorage.getItem(LOCAL_STORAGE.REFRESH), | |
}, | |
skipHeaders: true, | |
}) | |
.then((response) => { | |
res(response.data); | |
}) | |
.catch((err) => { | |
if (err.response.status === 401) { | |
AuthStoreInstance.clearAuth(); | |
} | |
rej(err); | |
}); | |
}); | |
}, | |
}); | |
} | |
// eslint-disable-next-line class-methods-use-this | |
request<Response>({ data = {}, method = 'GET', endpoint = '', skipHeaders = false }: RequestConfig): Promise<AxiosResponse<Response>> { | |
const requestData = method === 'GET' || method === 'DELETE' ? { params: data } : { data }; | |
const headers = skipHeaders ? {} : { bearer: localStorage.getItem(LOCAL_STORAGE.ACCESS) || '' }; | |
return this.apiInstance.request<Response>({ | |
method, | |
baseURL: API_BASENAME, | |
url: endpoint, | |
...requestData, | |
headers, | |
}); | |
} | |
} | |
export default new ApiRequest(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createContext } from 'react'; | |
import { observable, computed, action } from 'mobx'; | |
import axios, { AxiosError } from 'axios'; | |
import { API_BASENAME, ENDPOINTS } from '../constants/api'; | |
import { LOCAL_STORAGE } from '../constants/hardcode'; | |
export interface AuthData { | |
tokens: { | |
accessToken: string; | |
refreshToken: string; | |
}; | |
tokenData: { | |
userId: string; | |
humansProfileId: string; | |
humansLocale: string; | |
humansRoles: string; | |
humansTimezone: string; | |
}; | |
} | |
export interface Credentials { | |
login: string; | |
password: string; | |
} | |
class AuthStore { | |
constructor() { | |
const authData = localStorage.getItem(LOCAL_STORAGE.AUTH); | |
if (authData !== null) { | |
this.authData = JSON.parse(authData); | |
} | |
} | |
// eslint-disable-next-line @typescript-eslint/unbound-method | |
@action.bound | |
async authenticate(credentials: Credentials): Promise<void> { | |
try { | |
this.pending = true; | |
const { data } = await axios.post<AuthData>(`${API_BASENAME}${ENDPOINTS.AUTH}`, credentials); | |
localStorage.setItem(LOCAL_STORAGE.ACCESS, data.tokens.accessToken); | |
localStorage.setItem(LOCAL_STORAGE.REFRESH, data.tokens.refreshToken); | |
localStorage.setItem(LOCAL_STORAGE.AUTH, JSON.stringify(data)); | |
this.authData = data; | |
} catch (error) { | |
this.error = error as AxiosError; | |
} finally { | |
this.pending = false; | |
} | |
} | |
// eslint-disable-next-line @typescript-eslint/unbound-method | |
@action.bound | |
clearAuth(): void { | |
localStorage.removeItem(LOCAL_STORAGE.AUTH); | |
localStorage.removeItem(LOCAL_STORAGE.ACCESS); | |
localStorage.removeItem(LOCAL_STORAGE.REFRESH); | |
this.authData = undefined; | |
} | |
@observable | |
pending = false; | |
@observable | |
error: AxiosError; | |
@observable | |
authData: AuthData | undefined; | |
@computed | |
get isAuthenticated(): boolean { | |
return this.authData !== undefined; | |
} | |
} | |
export const AuthStoreInstance = new AuthStore(); | |
export default createContext(AuthStoreInstance); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable */ | |
// @ts-nocheck | |
// I disable file checking, cause its third-party solution written in JS (some mind-blow) | |
// Btw, i remove some unnecessary code | |
// thanks to: https://gist.github.com/Godofbrowser/bf118322301af3fc334437c683887c5f | |
import { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; | |
export interface InjectorOptions<T> { | |
shouldIntercept?: (error: AxiosError) => boolean; | |
setTokenData?: (authData: T) => void; | |
handleTokenRefresh?: () => Promise<T>; | |
attachTokenToRequest?: (request: AxiosRequestConfig) => void; | |
} | |
export default <T>(axiosClient: AxiosInstance, customOptions: InjectorOptions<T>): void => { | |
let isRefreshing = false; | |
let failedQueue = []; | |
const options = customOptions; | |
const processQueue = (error: AxiosError, token = null) => { | |
failedQueue.forEach((prom) => { | |
if (error) { | |
prom.reject(error); | |
} else { | |
prom.resolve(token); | |
} | |
}); | |
failedQueue = []; | |
}; | |
const interceptor = (error: AxiosError) => { | |
if (!options.shouldIntercept(error)) { | |
return Promise.reject(error); | |
} | |
if (error.config._retry || error.config._queued) { | |
return Promise.reject(error); | |
} | |
const originalRequest = error.config; | |
if (isRefreshing) { | |
return new Promise((resolve, reject) => { | |
failedQueue.push({ resolve, reject }); | |
}) | |
.then((token) => { | |
originalRequest._queued = true; | |
options.attachTokenToRequest(originalRequest, token); | |
return axiosClient.request(originalRequest); | |
}) | |
.catch((err) => { | |
return Promise.reject(error); // Ignore refresh token request's "err" and return actual "error" for the original request | |
}); | |
} | |
originalRequest._retry = true; | |
isRefreshing = true; | |
return new Promise((resolve, reject) => { | |
options.handleTokenRefresh | |
.call(options.handleTokenRefresh) | |
.then((tokenData) => { | |
options.setTokenData(tokenData, axiosClient); | |
options.attachTokenToRequest(originalRequest, tokenData.idToken); | |
processQueue(null, tokenData.idToken); | |
resolve(axiosClient.request(originalRequest)); | |
}) | |
.catch((err) => { | |
processQueue(err, null); | |
reject(err); | |
}) | |
.finally(() => { | |
isRefreshing = false; | |
}); | |
}); | |
}; | |
axiosClient.interceptors.response.use(undefined, interceptor); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment