Created
November 6, 2019 15:36
-
-
Save lancegliser/a1c7394c91f9dbddc540eae8c5dc7515 to your computer and use it in GitHub Desktop.
A module providing an axios based service that uses interceptors for handling authentication, logging, and errors in calls
This file contains hidden or 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, AxiosRequestConfig, AxiosResponse} from "axios"; | |
| import {Constants} from "./Constants"; | |
| import AxiosUtilities from "./Utilities/AxiosUtilities"; | |
| import { | |
| GetServiceCredentials, | |
| IServicesCredentials, | |
| } from "./IServiceTypes"; | |
| /** | |
| * Provides constants and functionality common to and api service. | |
| */ | |
| export default class Service { | |
| protected static LOCAL_SERVER = 'https://local.example.com/'; | |
| protected static TEST_SERVER = 'https://test.example.com/'; | |
| protected static PRODUCTION_SERVER = 'https://example.com/'; | |
| protected static ENDPOINT_V1 = 'v1'; | |
| // https://example.com/auth/v1 | |
| public static ENDPOINT_AUTH = 'auth/' + Service.ENDPOINT_V1; | |
| protected static ANONYMOUS_ENDPOINTS = [ | |
| Service.ENDPOINT_AUTH, | |
| ]; | |
| protected static DEFAULT_TIMEOUT = Constants.DEFAULT_TIMEOUT; | |
| protected env!: string; | |
| protected client!: AxiosInstance; | |
| protected getAccessToken: GetServiceCredentials | undefined; | |
| protected credentials: IServicesCredentials | undefined; | |
| public static getServiceUrl(environment: string) { | |
| switch (environment) { | |
| case Constants.ENVIRONMENT_LOCAL: | |
| return Service.LOCAL_SERVER; | |
| case Constants.ENVIRONMENT_TEST: | |
| case Constants.ENVIRONMENT_UAT: | |
| return Service.TEST_SERVER; | |
| case Constants.ENVIRONMENT_PRODUCTION: | |
| return Service.PRODUCTION_SERVER; | |
| default: | |
| throw new Error('Unhandled environment: ' + environment); | |
| } | |
| } | |
| /** | |
| * @param environment | |
| */ | |
| constructor(environment: string) { | |
| this.setEnvironment(environment); | |
| } | |
| /** | |
| * @param environment | |
| */ | |
| setEnvironment(environment: string) { | |
| this.credentials = undefined; | |
| this.client = axios.create({ | |
| baseURL: Service.getServiceUrl(environment), | |
| timeout: Service.DEFAULT_TIMEOUT, | |
| }); | |
| // Bind to incoming requests | |
| this.client.interceptors.request.use(this.onRequest.bind(this)); | |
| // Bind to responses | |
| this.client.interceptors.response.use(this.onResponseSuccess.bind(this), this.onResponseError.bind(this)); | |
| this.env = environment; | |
| return this; | |
| } | |
| /** | |
| * Modifies the original request adding authorization if required. | |
| */ | |
| protected onRequest(originalRequest: AxiosRequestConfig): Promise<AxiosRequestConfig> { | |
| // Return a promise as some requests may need to try auth | |
| return new Promise((resolve, reject) => { | |
| this.addAuthorizationHeader(originalRequest) | |
| .then(resolve); | |
| }); | |
| } | |
| /** | |
| * Provides success response handling | |
| */ | |
| protected onResponseSuccess(response: AxiosResponse): Promise<AxiosResponse> { | |
| // Return a promise as some requests may need to retry auth | |
| return new Promise((resolve, reject) => { | |
| resolve(response); | |
| }); | |
| } | |
| /** | |
| * Provides failure response handling | |
| */ | |
| protected onResponseError(error: AxiosError) { | |
| // Return a promise as some requests may need to retry auth | |
| return new Promise((resolve, reject) => { | |
| if ( | |
| error.response && | |
| error.response.status === 403 | |
| ) { | |
| // TODO clear the existing authentication and try again | |
| } | |
| AxiosUtilities.logResponseError(error); | |
| reject(error); | |
| }); | |
| } | |
| /** | |
| * @param func | |
| * @return this | |
| */ | |
| setGetAccessToken(func: GetServiceCredentials) { | |
| this.getAccessToken = func; | |
| return this; | |
| } | |
| /** | |
| * Middleware | |
| */ | |
| addAuthorizationHeader(originalRequest: AxiosRequestConfig): Promise<AxiosRequestConfig> { | |
| // Ignore anonymous endpoint requests | |
| if (!originalRequest.url || Service.ANONYMOUS_ENDPOINTS.indexOf(originalRequest.url) >= 0) { | |
| // console.debug('Skipping authorization: ' + originalRequest.url); | |
| return Promise.resolve(originalRequest); | |
| } | |
| return this.addAuthorization(originalRequest); | |
| } | |
| protected addAuthorization(originalRequest: AxiosRequestConfig) { | |
| const _this = this; | |
| // TODO remove the credentials if this.credentials.ValidTill has passed | |
| if (!this.credentials) { | |
| return this.getCredentials() | |
| .then((v1Credentials) => { | |
| this.credentials = v1Credentials; | |
| attachAuthorization(); | |
| return Promise.resolve(originalRequest); | |
| }); | |
| } | |
| if (this.credentials) { | |
| attachAuthorization(); | |
| } | |
| return Promise.resolve(originalRequest); | |
| function attachAuthorization() { | |
| if (!_this.credentials) { | |
| throw new Error('Credentials were not available'); | |
| } | |
| originalRequest.headers.Authorization = `Bearer ${_this.credentials.Token}`; | |
| } | |
| } | |
| /** | |
| * Gets the credential for the service using intermediary auth function providing during initialization | |
| */ | |
| async getCredentials(): Promise<IServicesCredentials> { | |
| if (!this.getAccessToken) { | |
| return new Promise((resolve, reject) => { | |
| reject("getAccessToken is not defined"); | |
| }); | |
| } | |
| return this.getAccessToken(); | |
| } | |
| } |
This file contains hidden or 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 {AxiosError} from "axios"; | |
| export default class AxiosUtilities { | |
| static logResponseError(error: AxiosError): void { | |
| console.error('Response error:', error.message, error.config.method, error.config.url); | |
| if (error.response) { | |
| // The request was made and the server responded with a status code | |
| // that falls out of the range of 2xx | |
| console.debug('Status: ', error.response.status); | |
| console.debug('Headers:', error.response.headers); | |
| console.debug('Data:', error.response.data); | |
| } else { | |
| if (error.request) { | |
| // The request was made but no response was received | |
| // `error.request` is an instance of XMLHttpRequest in the browser and an instance of | |
| // http.ClientRequest in node.js | |
| console.debug('Request:', error.request); | |
| } | |
| } | |
| console.debug('Configuration:', error.config); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment