2025-09-25 15:11:28 +09:00
|
|
|
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";
|
2025-10-28 13:43:45 +09:00
|
|
|
import { FreebitOperationException } from "@bff/core/exceptions/domain-exceptions";
|
2025-09-25 17:42:36 +09:00
|
|
|
import type {
|
2025-10-03 17:13:02 +09:00
|
|
|
AuthRequest as FreebitAuthRequest,
|
|
|
|
|
AuthResponse as FreebitAuthResponse,
|
2025-10-03 17:08:42 +09:00
|
|
|
} from "@customer-portal/domain/sim/providers/freebit";
|
2025-10-08 10:33:33 +09:00
|
|
|
import { Freebit as FreebitProvider } from "@customer-portal/domain/sim/providers/freebit";
|
2025-09-25 15:11:28 +09:00
|
|
|
import { FreebitError } from "./freebit-error.service";
|
|
|
|
|
|
2025-10-03 17:13:02 +09:00
|
|
|
interface FreebitConfig {
|
|
|
|
|
baseUrl: string;
|
|
|
|
|
oemId: string;
|
|
|
|
|
oemKey: string;
|
|
|
|
|
timeout: number;
|
|
|
|
|
retryAttempts: number;
|
|
|
|
|
detailsEndpoint?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 15:11:28 +09:00
|
|
|
@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<string>("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api",
|
|
|
|
|
oemId: this.configService.get<string>("FREEBIT_OEM_ID") || "PASI",
|
|
|
|
|
oemKey: this.configService.get<string>("FREEBIT_OEM_KEY") || "",
|
|
|
|
|
timeout: this.configService.get<number>("FREEBIT_TIMEOUT") || 30000,
|
|
|
|
|
retryAttempts: this.configService.get<number>("FREEBIT_RETRY_ATTEMPTS") || 3,
|
|
|
|
|
detailsEndpoint:
|
|
|
|
|
this.configService.get<string>("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<string> {
|
|
|
|
|
if (this.authKeyCache && this.authKeyCache.expiresAt > Date.now()) {
|
|
|
|
|
return this.authKeyCache.token;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!this.config.oemKey) {
|
2025-10-29 13:29:28 +09:00
|
|
|
throw new FreebitOperationException(
|
|
|
|
|
"Freebit API not configured: FREEBIT_OEM_KEY is missing",
|
|
|
|
|
{
|
|
|
|
|
operation: "authenticate",
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-09-25 15:11:28 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-22 10:23:56 +09:00
|
|
|
const request: FreebitAuthRequest = FreebitProvider.schemas.auth.parse({
|
2025-09-25 15:11:28 +09:00
|
|
|
oemId: this.config.oemId,
|
|
|
|
|
oemKey: this.config.oemKey,
|
2025-10-03 17:13:02 +09:00
|
|
|
});
|
2025-09-25 15:11:28 +09:00
|
|
|
|
|
|
|
|
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) {
|
2025-10-28 13:43:45 +09:00
|
|
|
throw new FreebitOperationException(`HTTP ${response.status}: ${response.statusText}`, {
|
|
|
|
|
operation: "authenticate",
|
|
|
|
|
status: response.status,
|
|
|
|
|
});
|
2025-09-25 15:11:28 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-22 10:23:56 +09:00
|
|
|
const json: unknown = await response.json();
|
2025-10-22 10:58:16 +09:00
|
|
|
const data: FreebitAuthResponse = FreebitProvider.mapper.transformFreebitAuthResponse(json);
|
2025-10-03 17:13:02 +09:00
|
|
|
|
|
|
|
|
if (data.resultCode !== "100" || !data.authKey) {
|
2025-09-25 15:11:28 +09:00
|
|
|
throw new FreebitError(
|
2025-10-03 17:13:02 +09:00
|
|
|
`Authentication failed: ${data.status?.message ?? "Unknown error"}`,
|
2025-09-25 15:11:28 +09:00
|
|
|
data.resultCode,
|
2025-10-03 17:13:02 +09:00
|
|
|
data.status?.statusCode,
|
|
|
|
|
data.status?.message
|
2025-09-25 15:11:28 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|