import { Injectable, Inject, InternalServerErrorException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import { getErrorMessage } from "@bff/core/utils/error.util"; import { FreebitOperationException } from "@bff/core/exceptions/domain-exceptions"; import type { AuthRequest as FreebitAuthRequest, AuthResponse as FreebitAuthResponse, } from "@customer-portal/domain/sim/providers/freebit"; import { Freebit as FreebitProvider } from "@customer-portal/domain/sim/providers/freebit"; import { FreebitError } from "./freebit-error.service"; interface FreebitConfig { baseUrl: string; oemId: string; oemKey: string; timeout: number; retryAttempts: number; detailsEndpoint?: string; } @Injectable() export class FreebitAuthService { private readonly config: FreebitConfig; private authKeyCache: { token: string; expiresAt: number } | null = null; constructor( private readonly configService: ConfigService, @Inject(Logger) private readonly logger: Logger ) { this.config = { baseUrl: this.configService.get("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api", oemId: this.configService.get("FREEBIT_OEM_ID") || "PASI", oemKey: this.configService.get("FREEBIT_OEM_KEY") || "", timeout: this.configService.get("FREEBIT_TIMEOUT") || 30000, retryAttempts: this.configService.get("FREEBIT_RETRY_ATTEMPTS") || 3, detailsEndpoint: this.configService.get("FREEBIT_DETAILS_ENDPOINT") || "/master/getAcnt/", }; if (!this.config.oemKey) { this.logger.warn("FREEBIT_OEM_KEY is not configured. SIM management features will not work."); } this.logger.debug("Freebit auth service initialized", { baseUrl: this.config.baseUrl, oemId: this.config.oemId, hasOemKey: !!this.config.oemKey, }); } /** * Get the current configuration */ getConfig(): FreebitConfig { return this.config; } /** * Get authentication key (cached or fetch new one) */ async getAuthKey(): Promise { if (this.authKeyCache && this.authKeyCache.expiresAt > Date.now()) { return this.authKeyCache.token; } try { if (!this.config.oemKey) { throw new FreebitOperationException( "Freebit API not configured: FREEBIT_OEM_KEY is missing", { operation: "authenticate", } ); } const request: FreebitAuthRequest = FreebitProvider.schemas.auth.parse({ oemId: this.config.oemId, oemKey: this.config.oemKey, }); const response = await fetch(`${this.config.baseUrl}/authOem/`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `json=${JSON.stringify(request)}`, }); if (!response.ok) { throw new FreebitOperationException(`HTTP ${response.status}: ${response.statusText}`, { operation: "authenticate", status: response.status, }); } const json: unknown = await response.json(); const data: FreebitAuthResponse = FreebitProvider.mapper.transformFreebitAuthResponse(json); if (data.resultCode !== "100" || !data.authKey) { throw new FreebitError( `Authentication failed: ${data.status?.message ?? "Unknown error"}`, data.resultCode, data.status?.statusCode, data.status?.message ); } this.authKeyCache = { token: data.authKey, expiresAt: Date.now() + 50 * 60 * 1000 }; this.logger.log("Successfully authenticated with Freebit API"); return data.authKey; } catch (error: unknown) { const message = getErrorMessage(error); this.logger.error("Failed to authenticate with Freebit API", { error: message }); throw new InternalServerErrorException("Failed to authenticate with Freebit API"); } } /** * Clear cached authentication key */ clearAuthCache(): void { this.authKeyCache = null; this.logger.debug("Cleared Freebit auth cache"); } /** * Check if we have a valid cached auth key */ hasValidAuthCache(): boolean { return !!(this.authKeyCache && this.authKeyCache.expiresAt > Date.now()); } }