@vulog/aima-client
Version:
```bash npm i @vulog/aima-client ```
273 lines (239 loc) • 10.1 kB
text/typescript
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;