// eslint-disable-next-line @typescript-eslint/no-unused-vars import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios' import { Request, Response } from 'express' import { inject, injectable } from 'inversify' import { Logger } from 'winston' import TYPES from '../../Bootstrap/Types' import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface' import { HttpServiceInterface } from './HttpServiceInterface' @injectable() export class HttpService implements HttpServiceInterface { constructor( @inject(TYPES.HTTPClient) private httpClient: AxiosInstance, @inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string, @inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string, @inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string, @inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string, @inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string, @inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number, @inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface, @inject(TYPES.Logger) private logger: Logger, ) {} async callSyncingServer( request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { await this.callServer(this.syncingServerJsUrl, request, response, endpoint, payload) } async callLegacySyncingServer( request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { await this.callServerWithLegacyFormat(this.syncingServerJsUrl, request, response, endpoint, payload) } async callAuthServer( request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { await this.callServer(this.authServerUrl, request, response, endpoint, payload) } async callWorkspaceServer( request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { await this.callServer(this.workspaceServerUrl, request, response, endpoint, payload) } async callPaymentsServer( request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { if (!this.paymentsServerUrl) { this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.') return } await this.callServerWithLegacyFormat(this.paymentsServerUrl, request, response, endpoint, payload) } async callAuthServerWithLegacyFormat( request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { await this.callServerWithLegacyFormat(this.authServerUrl, request, response, endpoint, payload) } private async getServerResponse( serverUrl: string, request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { try { const headers: Record = {} for (const headerName of Object.keys(request.headers)) { headers[headerName] = request.headers[headerName] as string } delete headers.host delete headers['content-length'] if (response.locals.authToken) { headers['X-Auth-Token'] = response.locals.authToken } if (response.locals.offlineAuthToken) { headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken } this.logger.debug(`Calling [${request.method}] ${serverUrl}/${endpoint}, headers: ${JSON.stringify(headers)}, query: ${JSON.stringify(request.query)}, payload: ${JSON.stringify(payload)}`) const serviceResponse = await this.httpClient.request({ method: request.method as Method, headers, url: `${serverUrl}/${endpoint}`, data: this.getRequestData(payload), maxContentLength: Infinity, maxBodyLength: Infinity, params: request.query, timeout: this.httpCallTimeout, validateStatus: (status: number) => { return status >= 200 && status < 500 }, }) if (serviceResponse.headers['x-invalidate-cache']) { const userUuid = serviceResponse.headers['x-invalidate-cache'] await this.crossServiceTokenCache.invalidate(userUuid) } return serviceResponse } catch (error) { const errorMessage = (error as AxiosError).isAxiosError ? JSON.stringify((error as AxiosError).response?.data) : (error as Error).message this.logger.error(`Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${errorMessage}`) this.logger.debug('Response error: %O', (error as AxiosError).response ?? error) if ((error as AxiosError).response?.headers['content-type']) { response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string) } const errorCode = (error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string)) ? +((error as AxiosError).code as string) : 500 response.status(errorCode).send(errorMessage) } return } private async callServer( serverUrl: string, request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload) this.logger.debug(`Response from underlying server: ${JSON.stringify(serviceResponse?.data)}, headers: ${JSON.stringify(serviceResponse?.headers)}`) if (!serviceResponse) { return } this.applyResponseHeaders(serviceResponse, response) if (this.responseShouldNotBeDecorated(serviceResponse)) { response.status(serviceResponse.status).send(serviceResponse.data) return } response.status(serviceResponse.status).send({ meta: { auth: { userUuid: response.locals.userUuid, roles: response.locals.roles, }, server: { filesServerUrl: this.filesServerUrl, }, }, data: serviceResponse.data, }) } private async callServerWithLegacyFormat( serverUrl: string, request: Request, response: Response, endpoint: string, payload?: Record | string, ): Promise { const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload) if (!serviceResponse) { return } this.applyResponseHeaders(serviceResponse, response) if (serviceResponse.request._redirectable._redirectCount > 0) { response.status(302).redirect(serviceResponse.request.res.responseUrl) } else { response.status(serviceResponse.status).send(serviceResponse.data) } } private getRequestData( payload: Record | string | undefined, ): Record | string | undefined { if ( payload === '' || payload === null || payload === undefined || (typeof payload === 'object' && Object.keys(payload).length === 0) ) { return undefined } return payload } private responseShouldNotBeDecorated(serviceResponse: AxiosResponse): boolean { return ( serviceResponse.headers['content-type'] !== undefined && serviceResponse.headers['content-type'].toLowerCase().includes('text/html') ) } private applyResponseHeaders(serviceResponse: AxiosResponse, response: Response): void { const returnedHeadersFromUnderlyingService = [ 'access-control-allow-methods', 'access-control-allow-origin', 'access-control-expose-headers', 'authorization', 'content-type', 'x-ssjs-version', 'x-auth-version', ] returnedHeadersFromUnderlyingService.map((headerName) => { const headerValue = serviceResponse.headers[headerName] if (headerValue) { response.setHeader(headerName, headerValue) } }) } }