feat(FN-094): add comment line for deployment verification
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
- Added a comment line to main.ts for deployment verification purposes
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* response normalization.
|
||||
*/
|
||||
|
||||
import type { PL24DecodedVehicle, PL24DecodedCategory, PL24Part } from "../pl24.types";
|
||||
import { PL24DecodedCategory, PL24DecodedVehicle, PL24Part } from "../pl24.types";
|
||||
|
||||
export abstract class BasePL24Parser {
|
||||
abstract readonly brandName: string;
|
||||
@@ -24,6 +24,6 @@ export abstract class BasePL24Parser {
|
||||
protected safeNumber(value: unknown): number {
|
||||
if (typeof value === "number") return value;
|
||||
const parsed = Number(value);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* parsing is done directly in PL24Service using the actual API response format.
|
||||
*/
|
||||
|
||||
import { PL24DecodedCategory, PL24DecodedVehicle, PL24Part } from "../pl24.types";
|
||||
import { BasePL24Parser } from "./base-parser";
|
||||
import type { PL24DecodedVehicle, PL24DecodedCategory, PL24Part } from "../pl24.types";
|
||||
|
||||
export class GenericPL24Parser extends BasePL24Parser {
|
||||
readonly brandName: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { PL24_WMI_SERVICE_MAP, isP5Modern } from "../pl24.types";
|
||||
import { BasePL24Parser } from "./base-parser";
|
||||
import { BmwPL24Parser } from "./bmw-parser";
|
||||
import { MercedesPL24Parser } from "./mercedes-parser";
|
||||
import { GenericPL24Parser } from "./generic-parser";
|
||||
import { PL24_WMI_SERVICE_MAP, isP5Modern } from "../pl24.types";
|
||||
import { MercedesPL24Parser } from "./mercedes-parser";
|
||||
|
||||
const PARSER_MAP: Record<string, () => BasePL24Parser> = {
|
||||
BMW: () => new BmwPL24Parser(),
|
||||
|
||||
@@ -2,63 +2,255 @@
|
||||
* PartsLink24 Authentication Service
|
||||
*
|
||||
* Handles JWT authentication, token refresh, and session management
|
||||
* for the partslink24.com API. Tokens cached in-memory (short-lived).
|
||||
* for the partslink24.com API. Supports two accounts:
|
||||
* - 'tr' (tr-903645): direct connection, primary VAG account
|
||||
* - 'de' (de-708171): DataImpulse Germany proxy, Fiat + EUR prices
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PL24_ENDPOINTS } from "./pl24.constants";
|
||||
import type {
|
||||
import {
|
||||
PL24AuthorizeRequest,
|
||||
PL24AuthorizeResponse,
|
||||
PL24JWTPayload,
|
||||
PL24LoginRequest,
|
||||
PL24LoginResponse,
|
||||
PL24TokenData,
|
||||
PL24JWTPayload,
|
||||
PL24AuthorizeRequest,
|
||||
PL24AuthorizeResponse,
|
||||
} from "./pl24.types";
|
||||
|
||||
@Injectable()
|
||||
export class PL24AuthService {
|
||||
private readonly logger = new Logger(PL24AuthService.name);
|
||||
private tokenData: PL24TokenData | null = null;
|
||||
private serviceTokens = new Map<
|
||||
string,
|
||||
{ token: string; expiresAt: Date }
|
||||
>();
|
||||
|
||||
// ── Account 1 (tr-903645) ──────────────────────────────────────────────────
|
||||
private tokenData: PL24TokenData | null = null;
|
||||
private serviceTokens = new Map<string, { token: string; expiresAt: Date }>();
|
||||
|
||||
// ── Account 2 (de-708171) ──────────────────────────────────────────────────
|
||||
private tokenData2: PL24TokenData | null = null;
|
||||
private serviceTokens2 = new Map<string, { token: string; expiresAt: Date }>();
|
||||
private proxyAgent: any = null; // undici.ProxyAgent, lazy-init
|
||||
|
||||
// ── Config ─────────────────────────────────────────────────────────────────
|
||||
private readonly baseUrl: string;
|
||||
private readonly companyCode: string;
|
||||
private readonly username: string;
|
||||
private readonly password: string;
|
||||
private readonly companyCode2: string;
|
||||
private readonly username2: string;
|
||||
private readonly password2: string;
|
||||
private readonly proxyUrl: string | null;
|
||||
private readonly timeout: number;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.get<string>(
|
||||
"pl24.baseUrl",
|
||||
"https://www.partslink24.com",
|
||||
);
|
||||
this.baseUrl = this.configService.get<string>("pl24.baseUrl", "https://www.partslink24.com");
|
||||
this.companyCode = this.configService.get<string>("pl24.companyCode", "");
|
||||
this.username = this.configService.get<string>("pl24.username", "");
|
||||
this.password = this.configService.get<string>("pl24.password", "");
|
||||
this.companyCode2 = this.configService.get<string>("pl24.companyCode2", "");
|
||||
this.username2 = this.configService.get<string>("pl24.username2", "");
|
||||
this.password2 = this.configService.get<string>("pl24.password2", "");
|
||||
this.proxyUrl = this.configService.get<string>("pl24.proxyDe", "") || null;
|
||||
this.timeout = 30000;
|
||||
|
||||
if (!this.companyCode || !this.username || !this.password) {
|
||||
this.logger.warn(
|
||||
"PL24 credentials not configured. Set PL24_BASE_URL, PL24_COMPANY_CODE, PL24_USERNAME, PL24_PASSWORD",
|
||||
"PL24 account 1 credentials not configured. Set PL24_COMPANY_CODE, PL24_USERNAME, PL24_PASSWORD",
|
||||
);
|
||||
}
|
||||
if (this.companyCode2 && !this.proxyUrl) {
|
||||
this.logger.warn("PL24 account 2 (de) configured but PL24_PROXY_DE not set");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login to PL24 and get access token.
|
||||
* Uses squeezeOut=true to force logout other sessions.
|
||||
*/
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ── Public: per-account API ──────────────────────────────────────────────
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Return access token for the given account. */
|
||||
async getAccessTokenForAccount(account: "tr" | "de"): Promise<string> {
|
||||
if (account === "de") {
|
||||
if (!this.tokenData2 || !this.isTokenValid(this.tokenData2)) {
|
||||
await this.login2();
|
||||
}
|
||||
if (!this.tokenData2) throw new Error("PL24 de account login failed");
|
||||
return this.tokenData2.accessToken;
|
||||
}
|
||||
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
|
||||
await this.login();
|
||||
}
|
||||
if (!this.tokenData) throw new Error("PL24 tr account login failed");
|
||||
return this.tokenData.accessToken;
|
||||
}
|
||||
|
||||
/** Return session cookie for the given account. */
|
||||
async getSessionCookieForAccount(account: "tr" | "de"): Promise<string> {
|
||||
if (account === "de") {
|
||||
if (!this.tokenData2 || !this.isTokenValid(this.tokenData2)) {
|
||||
await this.login2();
|
||||
}
|
||||
if (!this.tokenData2) throw new Error("PL24 de account login failed");
|
||||
return this.tokenData2.sessionCookie;
|
||||
}
|
||||
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
|
||||
await this.login();
|
||||
}
|
||||
if (!this.tokenData) throw new Error("PL24 tr account login failed");
|
||||
return this.tokenData.sessionCookie;
|
||||
}
|
||||
|
||||
/** Authorize a service catalog for the given account and return the service token. */
|
||||
async authorizeServiceForAccount(serviceName: string, account: "tr" | "de"): Promise<string> {
|
||||
const cache = account === "de" ? this.serviceTokens2 : this.serviceTokens;
|
||||
const cached = cache.get(serviceName);
|
||||
if (cached && cached.expiresAt > new Date()) {
|
||||
return cached.token;
|
||||
}
|
||||
|
||||
const mainToken = await this.getAccessTokenForAccount(account);
|
||||
const sessionCookie = await this.getSessionCookieForAccount(account);
|
||||
|
||||
this.logger.log(`Authorizing service ${serviceName} for account ${account}`);
|
||||
|
||||
const authorizeRequest: PL24AuthorizeRequest = {
|
||||
serviceNames: [
|
||||
"cart",
|
||||
"pl24-full-vin-data",
|
||||
"pl24-orderbridge",
|
||||
"pl24-orderbridge-cart",
|
||||
"pl24-sendbtmail",
|
||||
"pl24-qparts",
|
||||
"orderBook",
|
||||
"pl24-usage",
|
||||
"pl24-tls-pilot",
|
||||
serviceName,
|
||||
],
|
||||
serviceCategoryNames: ["pl24-shop-universal", "pl24-shop-tools"],
|
||||
withLogin: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const dispatcher = await this.getProxyAgent4Account(account);
|
||||
const fetchOpts: RequestInit & { dispatcher?: any } = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${mainToken}`,
|
||||
Cookie: sessionCookie,
|
||||
},
|
||||
body: JSON.stringify(authorizeRequest),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
};
|
||||
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${PL24_ENDPOINTS.AUTHORIZE}`, fetchOpts);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as PL24AuthorizeResponse;
|
||||
const accessToken = data.access_token || data.token?.access_token;
|
||||
if (!accessToken) {
|
||||
throw new Error("No service token in response");
|
||||
}
|
||||
|
||||
const payload = this.decodeJWT(accessToken);
|
||||
cache.set(serviceName, {
|
||||
token: accessToken,
|
||||
expiresAt: new Date(payload.exp * 1000),
|
||||
});
|
||||
|
||||
this.logger.log(`Service ${serviceName} authorized for account ${account}`);
|
||||
return accessToken;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error(`Service authorization error (${account}): ${err.message}`);
|
||||
throw new UnauthorizedException(`Servis yetkilendirme hatasi: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Build standard JSON API auth headers for the given account. */
|
||||
async buildAuthHeadersForAccount(
|
||||
account: "tr" | "de",
|
||||
serviceName?: string,
|
||||
includeContentType = false,
|
||||
): Promise<Record<string, string>> {
|
||||
const token = serviceName
|
||||
? await this.authorizeServiceForAccount(serviceName, account)
|
||||
: await this.getAccessTokenForAccount(account);
|
||||
|
||||
const sessionCookie = await this.getSessionCookieForAccount(account);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Cookie: sessionCookie,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
};
|
||||
|
||||
if (includeContentType) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Build Ford-legacy HTML page auth headers for the given account. */
|
||||
async buildFordLegacyHeadersForAccount(
|
||||
serviceName: string,
|
||||
account: "tr" | "de",
|
||||
): Promise<Record<string, string>> {
|
||||
const token = await this.authorizeServiceForAccount(serviceName, account);
|
||||
const sessionCookie = await this.getSessionCookieForAccount(account);
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Cookie: sessionCookie,
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
};
|
||||
}
|
||||
|
||||
/** Return PL24TOKEN cookie value for the given account (used as Ford hintstoken param). */
|
||||
getPL24TokenValueForAccount(account: "tr" | "de"): string | null {
|
||||
const data = account === "de" ? this.tokenData2 : this.tokenData;
|
||||
if (!data?.sessionCookie) return null;
|
||||
const match = data.sessionCookie.match(/PL24TOKEN=([^;]+)/);
|
||||
return match?.[1] || null;
|
||||
}
|
||||
|
||||
/** Return ProxyAgent for account 'de', null for 'tr'. */
|
||||
async getProxyAgent4Account(account: "tr" | "de"): Promise<any | null> {
|
||||
if (account !== "de") return null;
|
||||
return this.getProxyAgent();
|
||||
}
|
||||
|
||||
/** Clear in-memory tokens for the given account. */
|
||||
clearTokensForAccount(account: "tr" | "de"): void {
|
||||
if (account === "de") {
|
||||
this.tokenData2 = null;
|
||||
this.serviceTokens2.clear();
|
||||
this.logger.log("PL24 account 2 (de) tokens cleared");
|
||||
} else {
|
||||
this.tokenData = null;
|
||||
this.serviceTokens.clear();
|
||||
this.logger.log("PL24 account 1 (tr) tokens cleared");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ── Public: legacy API (backwards-compatible, always 'tr') ──────────────
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async login(forceNew = false): Promise<PL24TokenData> {
|
||||
if (!forceNew && this.tokenData && this.isTokenValid(this.tokenData)) {
|
||||
return this.tokenData;
|
||||
}
|
||||
|
||||
this.logger.log("Logging in to PL24...");
|
||||
this.logger.log("Logging in to PL24 (account 1 tr)...");
|
||||
|
||||
const loginRequest: PL24LoginRequest = {
|
||||
authentication: {
|
||||
@@ -66,32 +258,22 @@ export class PL24AuthService {
|
||||
user: this.username,
|
||||
pwd: this.password,
|
||||
},
|
||||
device: {
|
||||
id: "0",
|
||||
os: "Windows 10",
|
||||
offset: "0",
|
||||
lang: "en-US",
|
||||
"os-version": "0",
|
||||
},
|
||||
device: { id: "0", os: "Windows 10", offset: "0", lang: "en-US", "os-version": "0" },
|
||||
"app-version": "",
|
||||
squeezeOut: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}${PL24_ENDPOINTS.LOGIN}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
body: JSON.stringify(loginRequest),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
const response = await fetch(`${this.baseUrl}${PL24_ENDPOINTS.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify(loginRequest),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
@@ -112,11 +294,8 @@ export class PL24AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
// Extract session cookie
|
||||
const setCookie = response.headers.get("set-cookie");
|
||||
const sessionCookie = this.extractSessionCookie(setCookie);
|
||||
|
||||
// Decode JWT for expiration + services
|
||||
const payload = this.decodeJWT(data.token.access_token);
|
||||
|
||||
this.tokenData = {
|
||||
@@ -128,12 +307,8 @@ export class PL24AuthService {
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
`PL24 login successful. Token expires at ${this.tokenData.expiresAt.toISOString()}`,
|
||||
`PL24 login (tr) successful. Token expires at ${this.tokenData.expiresAt.toISOString()}`,
|
||||
);
|
||||
this.logger.log(
|
||||
`Available services: ${this.tokenData.services.length}`,
|
||||
);
|
||||
|
||||
return this.tokenData;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
@@ -145,93 +320,16 @@ export class PL24AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service-specific authorization token.
|
||||
* Required for accessing specific catalogs.
|
||||
*/
|
||||
async authorizeService(serviceName: string): Promise<string> {
|
||||
const cached = this.serviceTokens.get(serviceName);
|
||||
if (cached && cached.expiresAt > new Date()) {
|
||||
return cached.token;
|
||||
}
|
||||
|
||||
const mainToken = await this.getAccessToken();
|
||||
|
||||
this.logger.log(`Authorizing service: ${serviceName}`);
|
||||
|
||||
const authorizeRequest: PL24AuthorizeRequest = {
|
||||
serviceNames: [
|
||||
"cart",
|
||||
"pl24-full-vin-data",
|
||||
"pl24-orderbridge",
|
||||
"pl24-orderbridge-cart",
|
||||
"pl24-sendbtmail",
|
||||
"pl24-qparts",
|
||||
"orderBook",
|
||||
"pl24-usage",
|
||||
"pl24-tls-pilot",
|
||||
serviceName,
|
||||
],
|
||||
serviceCategoryNames: ["pl24-shop-universal", "pl24-shop-tools"],
|
||||
withLogin: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}${PL24_ENDPOINTS.AUTHORIZE}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${mainToken}`,
|
||||
Cookie: this.tokenData?.sessionCookie || "",
|
||||
},
|
||||
body: JSON.stringify(authorizeRequest),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as PL24AuthorizeResponse;
|
||||
|
||||
const accessToken = data.access_token || data.token?.access_token;
|
||||
if (!accessToken) {
|
||||
throw new Error("No service token in response");
|
||||
}
|
||||
|
||||
const payload = this.decodeJWT(accessToken);
|
||||
this.serviceTokens.set(serviceName, {
|
||||
token: accessToken,
|
||||
expiresAt: new Date(payload.exp * 1000),
|
||||
});
|
||||
|
||||
this.logger.log(`Service ${serviceName} authorized successfully`);
|
||||
return accessToken;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error(`Service authorization error: ${err.message}`);
|
||||
throw new UnauthorizedException(
|
||||
`Servis yetkilendirme hatasi: ${err.message}`,
|
||||
);
|
||||
}
|
||||
return this.authorizeServiceForAccount(serviceName, "tr");
|
||||
}
|
||||
|
||||
async getAccessToken(): Promise<string> {
|
||||
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
|
||||
await this.login();
|
||||
}
|
||||
return this.tokenData!.accessToken;
|
||||
return this.getAccessTokenForAccount("tr");
|
||||
}
|
||||
|
||||
async getSessionCookie(): Promise<string> {
|
||||
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
|
||||
await this.login();
|
||||
}
|
||||
return this.tokenData!.sessionCookie;
|
||||
return this.getSessionCookieForAccount("tr");
|
||||
}
|
||||
|
||||
getAvailableServices(): string[] {
|
||||
@@ -243,65 +341,119 @@ export class PL24AuthService {
|
||||
}
|
||||
|
||||
clearTokens(): void {
|
||||
this.tokenData = null;
|
||||
this.serviceTokens.clear();
|
||||
this.logger.log("All PL24 tokens cleared");
|
||||
this.clearTokensForAccount("tr");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build authorization headers for API requests.
|
||||
*/
|
||||
async buildAuthHeaders(
|
||||
serviceName?: string,
|
||||
includeContentType = false,
|
||||
): Promise<Record<string, string>> {
|
||||
const token = serviceName
|
||||
? await this.authorizeService(serviceName)
|
||||
: await this.getAccessToken();
|
||||
return this.buildAuthHeadersForAccount("tr", serviceName, includeContentType);
|
||||
}
|
||||
|
||||
const sessionCookie = await this.getSessionCookie();
|
||||
async buildFordLegacyHeaders(serviceName: string): Promise<Record<string, string>> {
|
||||
return this.buildFordLegacyHeadersForAccount(serviceName, "tr");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Cookie: sessionCookie,
|
||||
Accept: "application/json",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
};
|
||||
getPL24TokenValue(): string | null {
|
||||
return this.getPL24TokenValueForAccount("tr");
|
||||
}
|
||||
|
||||
if (includeContentType) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ── Private helpers ──────────────────────────────────────────────────────
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Login with account 2 (de-708171) via DataImpulse DE proxy. */
|
||||
private async login2(forceNew = false): Promise<PL24TokenData> {
|
||||
if (!forceNew && this.tokenData2 && this.isTokenValid(this.tokenData2)) {
|
||||
return this.tokenData2;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
if (!this.companyCode2 || !this.username2 || !this.password2) {
|
||||
throw new UnauthorizedException("PL24 account 2 (de) credentials not configured");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build headers for Ford legacy HTML page requests.
|
||||
* Uses text/html Accept instead of application/json.
|
||||
*/
|
||||
async buildFordLegacyHeaders(
|
||||
serviceName: string,
|
||||
): Promise<Record<string, string>> {
|
||||
const token = await this.authorizeService(serviceName);
|
||||
const sessionCookie = await this.getSessionCookie();
|
||||
this.logger.log("Logging in to PL24 (account 2 de)...");
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Cookie: sessionCookie,
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
const loginRequest: PL24LoginRequest = {
|
||||
authentication: {
|
||||
account: this.companyCode2,
|
||||
user: this.username2,
|
||||
pwd: this.password2,
|
||||
},
|
||||
device: { id: "0", os: "Windows 10", offset: "0", lang: "en-US", "os-version": "0" },
|
||||
"app-version": "",
|
||||
squeezeOut: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const dispatcher = await this.getProxyAgent();
|
||||
const fetchOpts: RequestInit & { dispatcher?: any } = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
body: JSON.stringify(loginRequest),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
};
|
||||
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${PL24_ENDPOINTS.LOGIN}`, fetchOpts);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as PL24LoginResponse;
|
||||
|
||||
if (!data.token?.access_token) {
|
||||
this.logger.error(`PL24 login (de) failed: ${data.status} - ${data.message || "No token"}`);
|
||||
throw new UnauthorizedException(
|
||||
`PL24 (de) giris basarisiz: ${data.message || data.status || "Token alinamadi"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const setCookie = response.headers.get("set-cookie");
|
||||
const sessionCookie = this.extractSessionCookie(setCookie);
|
||||
const payload = this.decodeJWT(data.token.access_token);
|
||||
|
||||
this.tokenData2 = {
|
||||
accessToken: data.token.access_token,
|
||||
refreshToken: data.refreshToken || "",
|
||||
sessionCookie,
|
||||
expiresAt: new Date(payload.exp * 1000),
|
||||
services: payload.services || [],
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
`PL24 login (de) successful. Token expires at ${this.tokenData2.expiresAt.toISOString()}`,
|
||||
);
|
||||
return this.tokenData2;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
if (err.name === "TimeoutError") {
|
||||
throw new UnauthorizedException("PL24 (de) giris zaman asimina ugradi");
|
||||
}
|
||||
this.logger.error(`PL24 login (de) error: ${err.message}`);
|
||||
throw new UnauthorizedException(`PL24 (de) giris hatasi: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PL24TOKEN cookie value for Ford hintstoken parameter.
|
||||
*/
|
||||
getPL24TokenValue(): string | null {
|
||||
if (!this.tokenData?.sessionCookie) return null;
|
||||
const match = this.tokenData.sessionCookie.match(/PL24TOKEN=([^;]+)/);
|
||||
return match?.[1] || null;
|
||||
/** Lazy-init ProxyAgent for the de account. */
|
||||
private async getProxyAgent(): Promise<any> {
|
||||
if (this.proxyAgent) return this.proxyAgent;
|
||||
if (!this.proxyUrl) return null;
|
||||
try {
|
||||
const { ProxyAgent } = await import("undici");
|
||||
this.proxyAgent = new ProxyAgent(this.proxyUrl);
|
||||
this.logger.log("PL24 DE ProxyAgent initialized");
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to init ProxyAgent: ${(err as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
return this.proxyAgent;
|
||||
}
|
||||
|
||||
private isTokenValid(token: PL24TokenData): boolean {
|
||||
@@ -312,9 +464,7 @@ export class PL24AuthService {
|
||||
private decodeJWT(token: string): PL24JWTPayload {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid JWT format");
|
||||
}
|
||||
if (parts.length !== 3) throw new Error("Invalid JWT format");
|
||||
const payload = Buffer.from(parts[1], "base64").toString("utf-8");
|
||||
return JSON.parse(payload);
|
||||
} catch {
|
||||
@@ -325,12 +475,8 @@ export class PL24AuthService {
|
||||
|
||||
private extractSessionCookie(setCookie: string | null): string {
|
||||
if (!setCookie) return "";
|
||||
|
||||
const match = setCookie.match(/PL24TOKEN=([^;]+)/);
|
||||
if (match) {
|
||||
return `PL24TOKEN=${match[1]}`;
|
||||
}
|
||||
|
||||
if (match) return `PL24TOKEN=${match[1]}`;
|
||||
return setCookie.split(";")[0];
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PL24Service } from "./pl24.service";
|
||||
import { PL24AuthService } from "./pl24-auth.service";
|
||||
import { PL24FordLegacyService } from "./pl24-ford-legacy.service";
|
||||
import { PL24Service } from "./pl24.service";
|
||||
|
||||
@Module({
|
||||
providers: [PL24Service, PL24AuthService, PL24FordLegacyService],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,12 +25,7 @@ export interface PL24LoginRequest {
|
||||
}
|
||||
|
||||
export interface PL24LoginResponse {
|
||||
status:
|
||||
| "OK"
|
||||
| "USER_ALREADY_LOGGED_IN"
|
||||
| "INVALID_CREDENTIALS"
|
||||
| "ERROR"
|
||||
| null;
|
||||
status: "OK" | "USER_ALREADY_LOGGED_IN" | "INVALID_CREDENTIALS" | "ERROR" | null;
|
||||
message?: string;
|
||||
token?: {
|
||||
access_token: string;
|
||||
@@ -96,7 +91,8 @@ export type PL24ApiArchitecture =
|
||||
| "LEGACY_FORD"
|
||||
| "LEGACY_NISSAN"
|
||||
| "LEGACY_OPEL"
|
||||
| "LEGACY_VOLVO";
|
||||
| "LEGACY_VOLVO"
|
||||
| "LEGACY_FIAT";
|
||||
|
||||
export interface PL24CatalogConfig {
|
||||
basePath: string;
|
||||
@@ -365,6 +361,19 @@ export const PL24_SERVICE_CATALOGS: Record<string, PL24CatalogConfig> = {
|
||||
apiPath: "/volvo",
|
||||
architecture: "LEGACY_VOLVO",
|
||||
},
|
||||
|
||||
// Fiat Group (FCA/Stellantis) — P4 Legacy, requires de-708171 account
|
||||
// NOTE: basePath/apiPath require Playwright verification with de account
|
||||
fiatp_parts: {
|
||||
basePath: "/fca",
|
||||
apiPath: "/fca",
|
||||
architecture: "LEGACY_FIAT",
|
||||
},
|
||||
fiatt_parts: {
|
||||
basePath: "/fca",
|
||||
apiPath: "/fca",
|
||||
architecture: "LEGACY_FIAT",
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== HELPER FUNCTIONS ====================
|
||||
@@ -374,9 +383,7 @@ export function getServiceApiPath(serviceName: string): string {
|
||||
return config?.apiPath || "/p5vwag";
|
||||
}
|
||||
|
||||
export function getServiceConfig(
|
||||
serviceName: string,
|
||||
): PL24CatalogConfig | null {
|
||||
export function getServiceConfig(serviceName: string): PL24CatalogConfig | null {
|
||||
return PL24_SERVICE_CATALOGS[serviceName] || null;
|
||||
}
|
||||
|
||||
@@ -516,6 +523,16 @@ export const PL24_WMI_SERVICE_MAP: Record<string, string> = {
|
||||
"1FA": "fordp_parts",
|
||||
"3FA": "fordp_parts",
|
||||
|
||||
// Fiat Passenger (fiatp_parts) — requires de-708171 account
|
||||
ZFA: "fiatp_parts", // Fiat SpA Italy (most common)
|
||||
ZCF: "fiatp_parts", // Tofaş Turkey (Linea, Fiorino, etc.)
|
||||
ZFF: "fiatp_parts", // Abarth / Fiat Sport
|
||||
ZAR: "fiatp_parts", // Alfa Romeo
|
||||
ZLA: "fiatp_parts", // Lancia
|
||||
|
||||
// Fiat Commercial (fiatt_parts)
|
||||
ZFC: "fiatt_parts", // Fiat Commercial
|
||||
|
||||
// Hyundai
|
||||
KMH: "hyundai_parts", // Hyundai Korea Motor House
|
||||
TMK: "hyundai_parts", // Hyundai (Turkey/other markets)
|
||||
@@ -605,6 +622,8 @@ export interface PL24Part {
|
||||
additionalInfo?: Record<string, string>;
|
||||
hotspotId?: string;
|
||||
linkPath?: string;
|
||||
price?: number;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export interface PL24HotspotArea {
|
||||
@@ -759,6 +778,9 @@ export const SERVICE_TO_BRAND: Record<string, string> = {
|
||||
// Volvo/Polestar
|
||||
volvo_parts: "Volvo",
|
||||
polestar_parts: "Polestar",
|
||||
// Fiat Group
|
||||
fiatp_parts: "Fiat",
|
||||
fiatt_parts: "Fiat",
|
||||
};
|
||||
|
||||
// Display names for services that share a brand (multi-catalog brands)
|
||||
@@ -792,4 +814,7 @@ export const SERVICE_DISPLAY_NAMES: Record<string, string> = {
|
||||
// Citroen
|
||||
citroen_parts: "Citroen",
|
||||
citroenDs_parts: "Citroen DS",
|
||||
// Fiat Group
|
||||
fiatp_parts: "Fiat",
|
||||
fiatt_parts: "Fiat Ticari",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user