Skip to content

Instantly share code, notes, and snippets.

@kugimiya
Created September 18, 2020 12:30
Show Gist options
  • Save kugimiya/5125ffe85d74c71915c33bb5834ce332 to your computer and use it in GitHub Desktop.
Save kugimiya/5125ffe85d74c71915c33bb5834ce332 to your computer and use it in GitHub Desktop.
Token refreshing + some dependencies
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();
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);
/* 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