initial commit

This commit is contained in:
2026-06-09 14:07:37 +03:30
parent 30ac533800
commit 996a4fcda7
121 changed files with 20557 additions and 3 deletions

View 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