perf(api): Redis-back PL24 account tokens (survive restarts, shared api+worker)

PL24 account JWTs were in-memory only, so every (re)deploy forced a fresh login
on the next decode, and api + worker each logged in independently — fighting over
PL24's one-session-per-account (squeezeOut). Persist account tokens to Redis
(keyed by account, TTL = JWT exp) and hydrate from Redis before logging in.
Best-effort: any Redis error falls through to a normal login, so auth never
depends on Redis. Service tokens stay in-memory (warmed by the boot pre-warm).
Complements the OnModuleInit pre-warm — now even the base login survives restarts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 01:47:36 +03:00
parent 59e40d6ccd
commit f9cbcfb620

View File

@@ -9,6 +9,7 @@
import { Injectable, Logger, type OnModuleInit, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { RedisService } from "../../redis/redis.service";
import { PL24_ENDPOINTS } from "./pl24.constants";
import {
PL24AuthorizeRequest,
@@ -43,7 +44,10 @@ export class PL24AuthService implements OnModuleInit {
private readonly proxyUrl: string | null;
private readonly timeout: number;
constructor(private configService: ConfigService) {
constructor(
private configService: ConfigService,
private readonly redis: RedisService,
) {
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", "");
@@ -304,6 +308,13 @@ export class PL24AuthService implements OnModuleInit {
if (!forceNew && this.tokenData && this.isTokenValid(this.tokenData)) {
return this.tokenData;
}
if (!forceNew) {
const cached = await this.hydrateAccountFromRedis("tr");
if (cached) {
this.tokenData = cached;
return this.tokenData;
}
}
this.logger.log("Logging in to PL24 (account 1 tr)...");
@@ -360,6 +371,7 @@ export class PL24AuthService implements OnModuleInit {
expiresAt: new Date(payload.exp * 1000),
services: payload.services || [],
};
await this.persistAccountToRedis("tr", this.tokenData);
this.logger.log(
`PL24 login (tr) successful. Token expires at ${this.tokenData.expiresAt.toISOString()}`,
@@ -423,6 +435,13 @@ export class PL24AuthService implements OnModuleInit {
if (!forceNew && this.tokenData2 && this.isTokenValid(this.tokenData2)) {
return this.tokenData2;
}
if (!forceNew) {
const cached = await this.hydrateAccountFromRedis("de");
if (cached) {
this.tokenData2 = cached;
return this.tokenData2;
}
}
if (!this.companyCode2 || !this.username2 || !this.password2) {
throw new UnauthorizedException("PL24 account 2 (de) credentials not configured");
@@ -481,6 +500,7 @@ export class PL24AuthService implements OnModuleInit {
expiresAt: new Date(payload.exp * 1000),
services: payload.services || [],
};
await this.persistAccountToRedis("de", this.tokenData2);
this.logger.log(
`PL24 login (de) successful. Token expires at ${this.tokenData2.expiresAt.toISOString()}`,
@@ -511,6 +531,33 @@ export class PL24AuthService implements OnModuleInit {
return this.proxyAgent;
}
// ── Redis-backed account-token cache (survives restarts; shared api+worker) ──
// Best-effort: any Redis error falls through to a normal login, so PL24 auth
// never depends on Redis being up. Sharing the token across processes also
// avoids the squeezeOut session-fight (PL24 allows one session per account).
private acctKey(account: "tr" | "de"): string {
return `pl24:auth:acct:${account}`;
}
private async hydrateAccountFromRedis(account: "tr" | "de"): Promise<PL24TokenData | null> {
try {
const raw = await this.redis.getJson<PL24TokenData>(this.acctKey(account));
if (!raw) return null;
const data: PL24TokenData = { ...raw, expiresAt: new Date(raw.expiresAt) };
if (!this.isTokenValid(data)) return null;
this.logger.log(`PL24 ${account} token hydrated from Redis (login skipped)`);
return data;
} catch {
return null;
}
}
private async persistAccountToRedis(account: "tr" | "de", data: PL24TokenData): Promise<void> {
const ttlSeconds = Math.floor((data.expiresAt.getTime() - Date.now()) / 1000);
if (ttlSeconds <= 0) return;
await this.redis.setJson(this.acctKey(account), data, ttlSeconds).catch(() => {});
}
private isTokenValid(token: PL24TokenData): boolean {
const bufferMs = 60 * 1000;
return token.expiresAt.getTime() - bufferMs > Date.now();