Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

147 changes: 117 additions & 30 deletions services/http-service/src/constants/axiosRestController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import HttpRestController from '../restController';
import { ITokenData, ITokenPayload } from '../token.types';
import { httpStatus } from '../httpStatus';

const DEFAULT_REQUEST_TIMEOUT = 60000;

Expand All @@ -8,6 +10,7 @@ export class HttpRestControllerAxios extends HttpRestController {

constructor() {
super();

this.axiosInstance = axios.create({
headers: {
'X-Requested-With': 'XMLHttpRequest',
Expand All @@ -17,59 +20,143 @@ export class HttpRestControllerAxios extends HttpRestController {
},
timeout: DEFAULT_REQUEST_TIMEOUT
});

this.setupInterceptors();
}

private tokenData: ITokenData | null = null;

private refreshTokenRequest: Promise<string> | null = null;

private isTokenExpired(): boolean {
const MILLISECONDS_PER_SECOND = 1000;
const expireTime = this.tokenData == null ? null : this.tokenData.expireTime();

if (!expireTime) return false;

return Math.round(new Date().getTime() / MILLISECONDS_PER_SECOND) > expireTime;
}

private async refreshTokens(): Promise<string> {
if (this.refreshTokenRequest) {
return this.refreshTokenRequest;
}

if (!this.tokenData?.refreshToken) {
throw new Error('No refresh token available');
}

try {
this.refreshTokenRequest = this
.post<ITokenPayload>(this.tokenData.tokenRefreshUrl, { refreshToken: this.tokenData.refreshToken })
.then(response => {
this.tokenData?.setNewTokenPayload(response.data);

return response.data.accessToken;
})

return this.refreshTokenRequest;
} catch (error) {
this.tokenData?.clearTokens();
window.location.href = '/login';

throw new Error(`Token refresh failed: ${error}`);
} finally {
this.refreshTokenRequest = null;
}
}

private setupInterceptors(): void {
this.axiosInstance.interceptors.request.use(async request => {
const accessToken = this.tokenData?.accessToken();
const isExpired = this.isTokenExpired();
const isNotRefreshUrl = request.url !== this.tokenData?.tokenRefreshUrl;

if (accessToken && !isExpired) {
request.headers['Authorization'] = `Bearer ${accessToken}`;
}

if (isExpired && isNotRefreshUrl) {
try {
const newAccessToken = await this.refreshTokens();
request.headers['Authorization'] = `Bearer ${newAccessToken}`;
} catch (refreshError) {
console.error('Session expired');

return Promise.reject(refreshError);
}
}

return request;
});

this.axiosInstance.interceptors.response.use(
response => response,
async (error: AxiosError) => {
const requestConfig = error.config;
const isNotRefreshUrl = error.config?.url !== this.tokenData?.tokenRefreshUrl;

if (error.response?.status === (httpStatus.UNAUTHORIZED as number) && isNotRefreshUrl) {
try {
const newAccessToken = await this.refreshTokens();

if (requestConfig) {
requestConfig.headers['Authorization'] = `Bearer ${newAccessToken}`;
}

return this.axiosInstance(requestConfig ?? {});
} catch (refreshError) {
console.error('Session expired');

return Promise.reject(refreshError);
}
}

throw error;
}
);
}

setTokenData(tokenData: ITokenData): void {
this.tokenData = tokenData;
};

setHeader = (key: string, value: string | null): void => {
this.axiosInstance.defaults.headers[key] = value;
};

get = async <T, R = AxiosResponse<T>>(url: string, params: object = {}, options: object = {}): Promise<R> => {
return this.axiosInstance.get(url, {
params,
...options
});
async get<T, R = AxiosResponse<T>>(url: string, params: object = {}, options: object = {}): Promise<R> {
return this.axiosInstance.get(url, { params, ...options });
};

post = async <T, R = AxiosResponse<T>>(
async post<T, R = AxiosResponse<T>>(
url: string,
body?: object,
params: object = {},
options: object = {}
): Promise<R> => {
return this.axiosInstance.post(url, body, {
params,
...options
});
): Promise<R> {
return this.axiosInstance.post(url, body, { params, ...options });
};

patch = async <T, R = AxiosResponse<T>>(
async patch<T, R = AxiosResponse<T>>(
url: string,
body?: object,
params: object = {},
options: object = {}
): Promise<R> => {
return this.axiosInstance.patch(url, body, {
params,
...options
});
): Promise<R> {
return this.axiosInstance.patch(url, body, { params, ...options });
};

put = async <T, R = AxiosResponse<T>>(
async put<T, R = AxiosResponse<T>>(
url: string,
body?: object,
params: object = {},
options: object = {}
): Promise<R> => {
return this.axiosInstance.put(url, body, {
params,
...options
});
};
): Promise<R> {
return this.axiosInstance.put(url, body, { params, ...options });
}

delete = async <T, R = AxiosResponse<T>>(url: string, params: object = {}, options: object = {}): Promise<R> => {
return this.axiosInstance.delete(url, {
params,
...options
});
async delete<T, R = AxiosResponse<T>>(url: string, params: object = {}, options: object = {}): Promise<R> {
return this.axiosInstance.delete(url, { params, ...options });
};
}
6 changes: 4 additions & 2 deletions services/http-service/src/httpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,27 @@ class HttpService<RestController extends HttpRestController> {
public put: RestController['put'];
public delete: RestController['delete'];
public setHeader: RestController['setHeader'];
public setTokenData: RestController['setTokenData'];

constructor(options: IHttpServiceOptions<RestController>) {
if (options.restController) {
const restController = options.restController;
this.restController = restController;
this.restController = options.restController;

this.get = this.restController.get;
this.post = this.restController.post;
this.patch = this.restController.patch;
this.put = this.restController.put;
this.delete = this.restController.delete;
this.setHeader = this.restController.setHeader;
this.setTokenData = this.restController.setTokenData;
} else {
this.get = () => Promise.reject('get handler was not specified');
this.post = () => Promise.reject('post handler was not specified');
this.patch = () => Promise.reject('patch handler was not specified');
this.put = () => Promise.reject('put handler was not specified');
this.delete = () => Promise.reject('delete handler was not specified');
this.setHeader = () => undefined;
this.setTokenData = () => undefined;
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions services/http-service/src/httpStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export declare enum httpStatus {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLYHINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
AMBIGUOUS = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
PAYLOAD_TOO_LARGE = 413,
URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
I_AM_A_TEAPOT = 418,
MISDIRECTED = 421,
UNPROCESSABLE_ENTITY = 422,
FAILED_DEPENDENCY = 424,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505
}
7 changes: 5 additions & 2 deletions services/http-service/src/restController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { ITokenData } from './token.types';

abstract class HttpRestController {
abstract get<R>(...args: any[]): Promise<R>;
abstract post<R>(...args: any[]): Promise<R>;
abstract patch<R>(...args: any[]): Promise<R>;
abstract put<R>(...args: any[]): Promise<R>;
abstract delete<R>(...args: any[]): Promise<R>;
public setHeader: (...args: any[]) => void;
abstract setTokenData(arg: ITokenData): void;
setHeader: (...args: any[]) => void;

constructor() {
protected constructor() {
this.setHeader = () => {
console.error('setHeader for HttpRestController is undefined');
};
Expand Down
22 changes: 22 additions & 0 deletions services/http-service/src/token.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type TValueOrGetter<T> = T extends Function ? never : T | (() => T);
export type TGetter<T> = () => T;

export interface ITokenPayload {
accessToken: string;
refreshToken: string;
expiresIn: number;
}

export type TAccessTokenGetter = TGetter<string | null>;
export type TRefreshTokenGetter = TGetter<string | null>;
export type TTokenExpireTimeGetter = TGetter<number | null>;
export type TTokenRefreshUrlGetter = string;

export interface ITokenData {
accessToken: TAccessTokenGetter;
refreshToken: TRefreshTokenGetter;
expireTime: TTokenExpireTimeGetter;
tokenRefreshUrl: TTokenRefreshUrlGetter;
setNewTokenPayload: (arg: ITokenPayload) => void;
clearTokens: () => void;
}