forked from Shared/esg
initial commit
This commit is contained in:
240
src/providers/implementations/amitis.provider.ts
Normal file
240
src/providers/implementations/amitis.provider.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||
import { AmitisAuthServiceConfig } from '../../config/configuration';
|
||||
import { GeneralTokenDocument } from '../schemas/general-token.schema';
|
||||
import { GeneralTokenService } from '../services/general-token.service';
|
||||
|
||||
interface CentInsurTokenResponse {
|
||||
Token?: string;
|
||||
token?: string;
|
||||
AccessToken?: string;
|
||||
accessToken?: string;
|
||||
access_token?: string;
|
||||
RefreshToken?: string;
|
||||
refreshToken?: string;
|
||||
refresh_token?: string;
|
||||
TokenType?: string;
|
||||
tokenType?: string;
|
||||
token_type?: string;
|
||||
ExpiresIn?: number | string;
|
||||
expiresIn?: number | string;
|
||||
expires_in?: number | string;
|
||||
data?: CentInsurTokenResponse;
|
||||
}
|
||||
|
||||
interface AmitisAuthToken {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
tokenType: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AMITIS Authentication Service
|
||||
* Provides token-based authentication for Hamta and Moallem providers
|
||||
* This is NOT a provider itself, but an authentication helper service
|
||||
*/
|
||||
@Injectable()
|
||||
export class AmitisProvider {
|
||||
readonly name = ProviderName.AMITIS;
|
||||
private readonly logger = new Logger(AmitisProvider.name);
|
||||
private readonly httpClient: AxiosInstance;
|
||||
private readonly config: AmitisAuthServiceConfig;
|
||||
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
private readonly generalTokenService: GeneralTokenService,
|
||||
) {
|
||||
this.config = configService.get<AmitisAuthServiceConfig>('amitis')!;
|
||||
this.httpClient = axios.create({
|
||||
baseURL: this.config.baseUrl,
|
||||
timeout: this.config.timeout,
|
||||
});
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.config.enabled && Boolean(this.config.baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token for a specific provider and inquiry type
|
||||
* Handles token caching, refresh, and daily expiration
|
||||
*/
|
||||
async getAccessToken(
|
||||
providerName: ProviderName,
|
||||
inquiryType: InquiryType,
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<string> {
|
||||
const tokenKey = this.getTokenKey(providerName, inquiryType);
|
||||
const latestToken = await this.generalTokenService.getLatestToken(tokenKey);
|
||||
|
||||
// Use cached token if valid and from same day
|
||||
if (latestToken && !this.isExpired(latestToken) && this.isSameTokenDay(latestToken)) {
|
||||
return latestToken.accessToken;
|
||||
}
|
||||
|
||||
// Try to refresh if we have a refresh token and it's from the same day
|
||||
if (latestToken?.refreshToken && this.isSameTokenDay(latestToken)) {
|
||||
try {
|
||||
return await this.refreshAccessToken(latestToken, providerName, inquiryType);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`AMITIS token refresh failed for ${providerName}/${inquiryType}, requesting a new token: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Login to get new token
|
||||
return this.login(providerName, inquiryType, username, password);
|
||||
}
|
||||
|
||||
private async login(
|
||||
providerName: ProviderName,
|
||||
inquiryType: InquiryType,
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<string> {
|
||||
// Use URLSearchParams for application/x-www-form-urlencoded format
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post<CentInsurTokenResponse>(
|
||||
this.config.loginPath,
|
||||
formData.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = this.extractToken(response.data);
|
||||
|
||||
await this.saveToken(token, providerName, inquiryType, username, 'login');
|
||||
return token.accessToken;
|
||||
} catch (error) {
|
||||
throw this.toAuthError(`AMITIS login failed for ${providerName}/${inquiryType}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshAccessToken(
|
||||
latestToken: GeneralTokenDocument,
|
||||
providerName: ProviderName,
|
||||
inquiryType: InquiryType,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await this.httpClient.post<CentInsurTokenResponse>(this.config.refreshPath, {
|
||||
Token: latestToken.accessToken,
|
||||
RefreshToken: latestToken.refreshToken,
|
||||
});
|
||||
const token = this.extractToken(response.data, latestToken.refreshToken);
|
||||
|
||||
await this.saveToken(
|
||||
token,
|
||||
providerName,
|
||||
inquiryType,
|
||||
latestToken.username,
|
||||
'refresh',
|
||||
);
|
||||
return token.accessToken;
|
||||
} catch (error) {
|
||||
throw this.toAuthError(
|
||||
`AMITIS refresh failed for ${providerName}/${inquiryType}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveToken(
|
||||
token: AmitisAuthToken,
|
||||
providerName: ProviderName,
|
||||
inquiryType: InquiryType,
|
||||
username: string,
|
||||
scope: string,
|
||||
): Promise<void> {
|
||||
await this.generalTokenService.create({
|
||||
serviceProvider: this.getTokenKey(providerName, inquiryType),
|
||||
tokenType: token.tokenType,
|
||||
url: this.config.baseUrl,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
username,
|
||||
scope,
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken,
|
||||
expiresIn: token.expiresIn,
|
||||
expiresAt: new Date(Date.now() + token.expiresIn * 1000),
|
||||
});
|
||||
}
|
||||
|
||||
private extractToken(
|
||||
responseBody: CentInsurTokenResponse,
|
||||
fallbackRefreshToken?: string,
|
||||
): AmitisAuthToken {
|
||||
const body = responseBody.data ?? responseBody;
|
||||
const accessToken =
|
||||
body.Token ?? body.token ?? body.AccessToken ?? body.accessToken ?? body.access_token;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('AMITIS auth response did not include an access token');
|
||||
}
|
||||
|
||||
const expiresIn = Number(body.ExpiresIn ?? body.expiresIn ?? body.expires_in ?? 20 * 60);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken:
|
||||
body.RefreshToken ?? body.refreshToken ?? body.refresh_token ?? fallbackRefreshToken,
|
||||
tokenType: body.TokenType ?? body.tokenType ?? body.token_type ?? 'Bearer',
|
||||
expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 20 * 60,
|
||||
};
|
||||
}
|
||||
|
||||
private isExpired(token: GeneralTokenDocument): boolean {
|
||||
const refreshBufferMs = 60 * 1000;
|
||||
return !token.expiresAt || Date.now() + refreshBufferMs >= token.expiresAt.getTime();
|
||||
}
|
||||
|
||||
private isSameTokenDay(token: GeneralTokenDocument): boolean {
|
||||
if (!token.createdAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.getDateKey(token.createdAt) === this.getDateKey(new Date());
|
||||
}
|
||||
|
||||
private getDateKey(date: Date): string {
|
||||
return new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: this.config.tokenTimeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
private getTokenKey(providerName: ProviderName, inquiryType: InquiryType): string {
|
||||
return `${ProviderName.AMITIS}:${providerName}:${inquiryType}`;
|
||||
}
|
||||
|
||||
private toAuthError(message: string, error: unknown): Error {
|
||||
if (error instanceof AxiosError) {
|
||||
const status = error.response?.status ?? 'NETWORK_ERROR';
|
||||
return new Error(`${message}: ${status} ${error.message}`);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return new Error(`${message}: ${error.message}`);
|
||||
}
|
||||
|
||||
return new Error(`${message}: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
Reference in New Issue
Block a user