UNPKG

@vulog/aima-client

Version:

```bash npm i @vulog/aima-client ```

273 lines (239 loc) 10.1 kB
import axios, { AxiosError } from 'axios'; import { isEqual, trimEnd } from 'es-toolkit'; import { LRUCache } from 'lru-cache'; import { CurlHelper } from './CurlHelper'; import { Client, ClientError, ClientOptions, Store, Token } from './types'; type RefreshSubscriber = (token?: string, error?: any) => void; const clientCache = new LRUCache< string, { options: ClientOptions; client: Client; } >({ max: 100 }); const tokenCache = new LRUCache<string, Token>({ max: 100 }); const getMemoryStore = (options: ClientOptions): Store => ({ getToken: async (): Promise<Token | undefined> => { // eslint-disable-next-line no-console const log = options.onLog ?? console.log; log('getMemoryStore.getToken', options.name ?? options.fleetId); if (tokenCache.has(options.name ?? options.fleetId)) { log('getMemoryStore.getToken', tokenCache.get(options.name ?? options.fleetId)); return tokenCache.get(options.name ?? options.fleetId)!; } return undefined; }, setToken: async (token: Token): Promise<void> => { // eslint-disable-next-line no-console const log = options.onLog ?? console.log; log('getMemoryStore.setToken', options.name ?? options.fleetId, token); tokenCache.set(options.name ?? options.fleetId, token); }, }); const formatError = (error: any): ClientError => { if (error instanceof AxiosError) { return { originalError: error.toJSON(), formattedError: { status: error.response?.status ?? error.status, data: error.response?.data, message: error.message, }, }; } return { formattedError: {}, originalError: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))), }; }; const getClient = (options: ClientOptions): Client => { if (clientCache.has(options.name ?? options.fleetId)) { const { options: cachedOptions, client } = clientCache.get(options.fleetId)!; if (isEqual(cachedOptions, options)) { return client; } } const client = axios.create({ baseURL: trimEnd(options.baseUrl, '/'), timeout: 30000, headers: { 'Cache-Control': 'no-cache', 'Content-Type': 'application/json', 'X-Api-Key': options.apiKey, }, withCredentials: false, }) as Client; client.clientOptions = options; const clientCredentialsAuthentification = async (): Promise<string> => { const params = new URLSearchParams(); params.append('client_id', options.clientId); params.append('client_secret', options.clientSecret); params.append('securityOptions', 'SSL_OP_NO_SSLv3'); params.append('grant_type', 'client_credentials'); const { data: token } = await axios.post( `${trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`, params, { timeout: 30000, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, withCredentials: false, } ); const store = options.store ?? getMemoryStore(options); await store.setToken({ accessToken: token.access_token, refreshToken: token.refresh_token, }); return token.access_token; }; const refreshTokenAuthentification = async (): Promise<string> => { const store = options.store ?? getMemoryStore(options); const oldToken = await store.getToken(); if (!oldToken?.refreshToken) { throw new Error('No refresh token available'); } const params = new URLSearchParams(); params.append('client_id', options.clientId); params.append('client_secret', options.clientSecret); params.append('securityOptions', 'SSL_OP_NO_SSLv3'); params.append('grant_type', 'refresh_token'); params.append('refresh_token', oldToken.refreshToken); const { data: token } = await axios.post( `${trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`, params, { timeout: 30000, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, withCredentials: false, } ); await store.setToken({ accessToken: token.access_token, refreshToken: token.refresh_token, }); return token.access_token; }; client.signInWithPassword = async (username: string, password: string): Promise<Token> => { if (!options.secure) { throw new Error('Not secure'); } const params = new URLSearchParams(); params.append('client_id', options.clientId); params.append('client_secret', options.clientSecret); params.append('securityOptions', 'SSL_OP_NO_SSLv3'); params.append('grant_type', 'password'); params.append('username', username); params.append('password', password); const { data: token } = await axios.post( `${trimEnd(options.baseUrl, '/')}/auth/realms/${options.fleetMaster ?? options.fleetId}/protocol/openid-connect/token`, params, { timeout: 30000, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, withCredentials: false, } ); const newToken: Token = { accessToken: token.access_token, refreshToken: token.refresh_token, }; const store = options.store ?? getMemoryStore(options); await store.setToken(newToken); return newToken; }; client.interceptors.request.use(async (request) => { const newRequest = request; const store = options.store ?? getMemoryStore(options); const token = await store.getToken(); if (token?.accessToken) { newRequest.headers.Authorization = `Bearer ${token.accessToken}`; } if (options.logCurl) { const curl = new CurlHelper(newRequest).generateCommand(); if (options.onLog) options.onLog({ curl, message: 'getClient > Curl command' }); // eslint-disable-next-line no-console else console.log({ curl, message: 'getClient > Curl command' }); } return newRequest; }); let isRefreshing = false; let refreshSubscribers: RefreshSubscriber[] = []; const executorRefresh = (config: any) => { return new Promise((resolve, reject) => { refreshSubscribers.push((token, error) => { if (error) { reject(formatError(error)); return; } resolve(client.request(config)); }); }); }; client.interceptors.response.use( (response) => { if (options.logResponse) { const finalUrl = new CurlHelper(response.config).getBuiltURL(); const { data, headers } = response; const dataLog = response.config.responseType !== 'arraybuffer' && response.config.responseType !== 'blob' ? data : 'ArrayBuffer or blob'; if (options.onLog) options.onLog({ finalUrl, data: dataLog, headers, message: 'getClient > Response' }); // eslint-disable-next-line no-console else console.log({ finalUrl, data: dataLog, headers, message: 'getClient > Response' }); } return response; }, (error) => { const { config, response: { status } = { status: 500 } } = error; const originalRequest = config; if (originalRequest.attemptCount === undefined) { originalRequest.attemptCount = 0; } if (originalRequest.attemptCount === 5) { return Promise.reject(formatError(error)); } if (status === 401) { originalRequest.attemptCount += 1; if (!isRefreshing) { isRefreshing = true; let authentification: () => Promise<string>; if (options.secure) { authentification = refreshTokenAuthentification; } else { authentification = async () => { const store = options.store ?? getMemoryStore(options); const token = await store.getToken(); if (!token?.refreshToken) { return clientCredentialsAuthentification(); } return refreshTokenAuthentification(); }; } authentification() .then((accessToken) => { refreshSubscribers.forEach((cb) => cb(accessToken)); }) .catch((errorAuth) => { refreshSubscribers.forEach((cb) => cb(undefined, errorAuth)); }) .finally(() => { isRefreshing = false; refreshSubscribers = []; }); } return executorRefresh(originalRequest); } return Promise.reject(formatError(error)); } ); clientCache.set(options.name ?? options.fleetId, { options, client }); return client; }; export default getClient;