From 36fd991360be7fe815e0fd037774b8ea4ede7b32 Mon Sep 17 00:00:00 2001 From: Lansana DIOMANDE <ldiomande@takima.fr> Date: Wed, 18 May 2022 15:56:57 +0200 Subject: [PATCH] refactor: improve api service --- .../src/core/api.service.ts | 124 ++++++++++++++++++ .../src/core/callApi.service.ts | 102 -------------- .../deadlock-extension/src/core/controller.ts | 6 +- 3 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 deadlock-plugins/deadlock-extension/src/core/api.service.ts delete mode 100644 deadlock-plugins/deadlock-extension/src/core/callApi.service.ts diff --git a/deadlock-plugins/deadlock-extension/src/core/api.service.ts b/deadlock-plugins/deadlock-extension/src/core/api.service.ts new file mode 100644 index 00000000..2552c328 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/api.service.ts @@ -0,0 +1,124 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { API_QUERY_REFERER, API_URL } from '../config'; +import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; +import { SshKeyPair } from '../model/sshKeyPair.model'; +import { User } from '../model/user.model'; +import Controller from './controller'; +import ExtensionStore from './extensionStore'; +import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; + +export default class ApiService { + private axiosInstance: AxiosInstance; + constructor( + private keycloackConnection: KeycloakOAuth2DeviceFlowConnection, + private extensionStore: ExtensionStore, + private controller: Controller, + ) { + this.axiosInstance = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + referer: API_QUERY_REFERER, + }, + }); + + this.initApiInterceptor(); + } + + initApiInterceptor() { + this.initRequestInterceptor(); + this.initResponseInterceptor(); + } + + private initRequestInterceptor() { + this.axiosInstance.interceptors.request.use( + async (config) => { + const accessToken = await this.extensionStore.getAccessToken(); + if (accessToken && config.headers) { + config.headers['authorization'] = `BEARER ${accessToken}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + }, + ); + } + + private initResponseInterceptor() { + this.axiosInstance.interceptors.response.use( + (res) => { + return res; + }, + async (err) => { + this.handleResponseError(err); + }, + ); + } + + private async handleResponseError(err: { + config: AxiosRequestConfig & { _retry: boolean }; + response: { status: number; data: any }; + }) { + const originalConfig = err.config; + if (err.response) { + // Access Token was expired + if (err.response.status === 401) { + if (!originalConfig._retry) { + originalConfig._retry = true; + return this.onRetry(originalConfig); + } else if (originalConfig._retry) { + // IF REFRESH TOKEN NOT WORK, REQUEST NEW CONNECTION IN USER BROWSER + return this.onInvalidRefreshToken(originalConfig); + } + } + + if (err.response.status === 403 && err.response.data) { + return Promise.reject(err.response.data); + } + } + return Promise.reject(err); + } + + private async onRetry(originalConfig) { + try { + const storedRefreshToken = await this.extensionStore.getRefreshToken(); + const { accessToken, refreshToken } = await this.keycloackConnection.getToken({ + refreshToken: storedRefreshToken, + openLink: Controller.openBrowserWithUrl, + }); + + await this.extensionStore.setAccessToken(accessToken); + await this.extensionStore.setRefreshToken(refreshToken); + + this.axiosInstance.defaults.headers.common['authorization'] = `BEARER ${accessToken}`; + return this.axiosInstance(originalConfig); + } catch (_error: any) { + if (_error.response && _error.response.data) { + return Promise.reject(_error.response.data); + } + return Promise.reject(_error); + } + } + + private async onInvalidRefreshToken(originalConfig) { + try { + await this.controller.authenticate(); + return this.axiosInstance(originalConfig); + } catch (_error) { + return Promise.reject(_error); + } + } + + getGiteaPublicProperties(): Promise<GiteaPublicProperties> { + return this.axiosInstance.get<GiteaPublicProperties>(`gitea`).then((res) => res.data); + } + + getUserSshKey(): Promise<SshKeyPair> { + return this.axiosInstance.put<SshKeyPair>(`users/gitea/keypair`).then((res) => res.data); + } + + getUser(): Promise<User> { + return this.axiosInstance.get<User>(`auth`).then((res) => res.data); + } +} diff --git a/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts b/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts deleted file mode 100644 index da086311..00000000 --- a/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import axios, { AxiosInstance } from 'axios'; -import { API_QUERY_REFERER, API_URL } from '../config'; -import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; -import { SshKeyPair } from '../model/sshKeyPair.model'; -import { User } from '../model/user.model'; -import Controller from './controller'; -import ExtensionStore from './extensionStore'; -import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; - -export default class CallApiService { - private callApi: AxiosInstance; - constructor( - private keycloackConnection: KeycloakOAuth2DeviceFlowConnection, - private extensionStore: ExtensionStore, - private controller: Controller, - ) { - this.callApi = axios.create({ - baseURL: API_URL, - headers: { - 'Content-Type': 'application/json', - referer: API_QUERY_REFERER, - }, - }); - - this.initApiInterceptor(); - } - - initApiInterceptor() { - this.callApi.interceptors.request.use( - async (config) => { - let accessToken = await this.extensionStore.getAccessToken(); - if (accessToken && config.headers) { - config.headers['authorization'] = `BEARER ${accessToken}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - }, - ); - - this.callApi.interceptors.response.use( - (res) => { - return res; - }, - async (err) => { - const originalConfig = err.config; - if (err.response) { - // Access Token was expired - if (err.response.status === 401) { - if (!originalConfig._retry) { - originalConfig._retry = true; - try { - const storedRefreshToken = await this.extensionStore.getRefreshToken(); - const { accessToken, refreshToken } = await this.keycloackConnection.getToken({ - refreshToken: storedRefreshToken, - openLink: Controller.openBrowserWithUrl, - }); - - await this.extensionStore.setAccessToken(accessToken); - await this.extensionStore.setRefreshToken(refreshToken); - - this.callApi.defaults.headers.common['authorization'] = `BEARER ${accessToken}`; - return this.callApi(originalConfig); - } catch (_error: any) { - if (_error.response && _error.response.data) { - return Promise.reject(_error.response.data); - } - return Promise.reject(_error); - } - } else if (originalConfig._retry) { - // IF REFRESH TOKEN NOT WORK, REQUEST NEW CONNECTION IN USER BROWSER - try { - await this.controller.authenticate(); - return this.callApi(originalConfig); - } catch (_error) { - return Promise.reject(_error); - } - } - } - - if (err.response.status === 403 && err.response.data) { - return Promise.reject(err.response.data); - } - } - return Promise.reject(err); - }, - ); - } - - getGiteaPublicProperties(): Promise<GiteaPublicProperties> { - return this.callApi.get<GiteaPublicProperties>(`gitea`).then((res) => res.data); - } - - getUserSshKey(): Promise<SshKeyPair> { - return this.callApi.put<SshKeyPair>(`users/gitea/keypair`).then((res) => res.data); - } - - getUser(): Promise<User> { - return this.callApi.get<User>(`auth`).then((res) => res.data); - } -} diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index e2435100..951ce840 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -14,7 +14,7 @@ import { CHOOSE_MISSION_WORKDIR_COMMAND, CommandHandler, OPEN_URL_IN_BROWSER_COM import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; import Mission from './mission'; -import CallApiService from './callApi.service'; +import ApiService from './api.service'; import KeycloakOAuth2DeviceFlowConnectionVSCodeImpl from './keycloakOAuth2DeviceFlowConnectionVSCodeImpl'; import SshKeyManager from './sshKeyManager'; import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; @@ -26,7 +26,7 @@ export default class Controller { private briefingView: BriefingView; private quickSetupView: QuickSetupView; private extensionStore: ExtensionStore; - private callApiService: CallApiService; + private callApiService: ApiService; private sshKeyManager: SshKeyManager; constructor(private context: vscode.ExtensionContext) { @@ -40,7 +40,7 @@ export default class Controller { KEYCLOAK_USER_INFO_URL, ); - this.callApiService = new CallApiService(this.connection, this.extensionStore, this); + this.callApiService = new ApiService(this.connection, this.extensionStore, this); this.sshKeyManager = new SshKeyManager(); this.init(); -- GitLab