forked from Yara724/api
fanavaran duplication request problems fixed.
This commit is contained in:
@@ -6,7 +6,9 @@ import {
|
||||
Logger,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import { isAxiosError } from "axios";
|
||||
import { Model } from "mongoose";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import {
|
||||
getFanavaranClientProfile,
|
||||
@@ -19,6 +21,10 @@ import {
|
||||
FanavaranAuditStatus,
|
||||
FanavaranAuditStep,
|
||||
} from "./schema/fanavaran-audit-log.schema";
|
||||
import {
|
||||
FanavaranAuthToken,
|
||||
FanavaranAuthTokenDocument,
|
||||
} from "./schema/fanavaran-auth-token.schema";
|
||||
|
||||
interface CachedFanavaranAuth {
|
||||
authenticationToken: string;
|
||||
@@ -63,6 +69,8 @@ export class FanavaranAuthService {
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly fanavaranAuditService: FanavaranAuditService,
|
||||
@InjectModel(FanavaranAuthToken.name)
|
||||
private readonly authTokenModel: Model<FanavaranAuthTokenDocument>,
|
||||
) {}
|
||||
|
||||
/** True when Fanavaran asked us to wait (Persian “try again later” / tracking-code 500). */
|
||||
@@ -162,6 +170,12 @@ export class FanavaranAuthService {
|
||||
|
||||
invalidateToken(clientKey: FanavaranClientKey): void {
|
||||
this.tokenCache.delete(clientKey);
|
||||
void this.authTokenModel.deleteOne({ clientKey }).exec().catch((error) => {
|
||||
this.logger.warn(
|
||||
`[${clientKey}] Failed to clear persisted Fanavaran auth token`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
assertNotInBackoff(clientKey: FanavaranClientKey): void {
|
||||
@@ -183,9 +197,13 @@ export class FanavaranAuthService {
|
||||
this.assertNotInBackoff(clientKey);
|
||||
|
||||
if (!options?.forceRefresh) {
|
||||
const cached = this.tokenCache.get(clientKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.authenticationToken;
|
||||
const memoryHit = this.readMemoryCache(clientKey);
|
||||
if (memoryHit) {
|
||||
return memoryHit;
|
||||
}
|
||||
const persisted = await this.readPersistedCache(clientKey);
|
||||
if (persisted) {
|
||||
return persisted;
|
||||
}
|
||||
} else {
|
||||
this.invalidateToken(clientKey);
|
||||
@@ -243,10 +261,7 @@ export class FanavaranAuthService {
|
||||
);
|
||||
|
||||
const expiresAt = FanavaranAuthService.getNextMidnightExpiryMs();
|
||||
this.tokenCache.set(clientKey, {
|
||||
authenticationToken,
|
||||
expiresAt,
|
||||
});
|
||||
await this.persistToken(clientKey, authenticationToken, expiresAt);
|
||||
this.clearBackoff(clientKey);
|
||||
this.logger.log(
|
||||
`[${clientKey}] Cached Fanavaran authenticationToken until ${new Date(
|
||||
@@ -256,6 +271,76 @@ export class FanavaranAuthService {
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
private readMemoryCache(clientKey: FanavaranClientKey): string | null {
|
||||
const cached = this.tokenCache.get(clientKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.authenticationToken;
|
||||
}
|
||||
if (cached) {
|
||||
this.tokenCache.delete(clientKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async readPersistedCache(
|
||||
clientKey: FanavaranClientKey,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const doc = await this.authTokenModel.findOne({ clientKey }).lean().exec();
|
||||
if (!doc?.authenticationToken || !doc.expiresAt) {
|
||||
return null;
|
||||
}
|
||||
const expiresAt = new Date(doc.expiresAt).getTime();
|
||||
if (!(expiresAt > Date.now())) {
|
||||
await this.authTokenModel.deleteOne({ clientKey }).exec();
|
||||
return null;
|
||||
}
|
||||
this.tokenCache.set(clientKey, {
|
||||
authenticationToken: doc.authenticationToken,
|
||||
expiresAt,
|
||||
});
|
||||
this.logger.log(
|
||||
`[${clientKey}] Reused persisted Fanavaran authenticationToken until ${new Date(
|
||||
expiresAt,
|
||||
).toISOString()}`,
|
||||
);
|
||||
return doc.authenticationToken;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`[${clientKey}] Failed to read persisted Fanavaran auth token`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async persistToken(
|
||||
clientKey: FanavaranClientKey,
|
||||
authenticationToken: string,
|
||||
expiresAt: number,
|
||||
): Promise<void> {
|
||||
this.tokenCache.set(clientKey, { authenticationToken, expiresAt });
|
||||
try {
|
||||
await this.authTokenModel
|
||||
.findOneAndUpdate(
|
||||
{ clientKey },
|
||||
{
|
||||
$set: {
|
||||
authenticationToken,
|
||||
expiresAt: new Date(expiresAt),
|
||||
},
|
||||
},
|
||||
{ upsert: true, new: true },
|
||||
)
|
||||
.exec();
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`[${clientKey}] Failed to persist Fanavaran auth token (memory cache still active)`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private emptyBodyTransformRequest() {
|
||||
return [
|
||||
(_data: unknown, headers?: Record<string, unknown>) => {
|
||||
@@ -273,12 +358,20 @@ export class FanavaranAuthService {
|
||||
auditSession?: FanavaranAuditSession,
|
||||
): Promise<string> {
|
||||
const startedAt = Date.now();
|
||||
const requestHeaders = {
|
||||
appname: config.appName,
|
||||
secret: config.secret,
|
||||
"Content-Length": "0",
|
||||
};
|
||||
if (auditSession) {
|
||||
await this.fanavaranAuditService.recordStep({
|
||||
session: auditSession,
|
||||
step: FanavaranAuditStep.GET_APP_TOKEN,
|
||||
status: FanavaranAuditStatus.STARTED,
|
||||
requestUrl: this.getAppTokenUrl,
|
||||
requestMethod: "POST",
|
||||
requestHeaders,
|
||||
requestBody: "",
|
||||
requestMeta: { appName: config.appName, cached: false },
|
||||
});
|
||||
}
|
||||
@@ -286,11 +379,7 @@ export class FanavaranAuthService {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(this.getAppTokenUrl, "", {
|
||||
headers: {
|
||||
appname: config.appName,
|
||||
secret: config.secret,
|
||||
"Content-Length": "0",
|
||||
},
|
||||
headers: requestHeaders,
|
||||
transformRequest: this.emptyBodyTransformRequest(),
|
||||
}),
|
||||
);
|
||||
@@ -306,13 +395,23 @@ export class FanavaranAuthService {
|
||||
}
|
||||
|
||||
if (auditSession) {
|
||||
const exchange =
|
||||
this.fanavaranAuditService.captureAxiosExchange(
|
||||
response,
|
||||
requestHeaders,
|
||||
);
|
||||
await this.fanavaranAuditService.recordStep({
|
||||
session: auditSession,
|
||||
step: FanavaranAuditStep.GET_APP_TOKEN,
|
||||
status: FanavaranAuditStatus.SUCCESS,
|
||||
requestUrl: this.getAppTokenUrl,
|
||||
requestMethod: "POST",
|
||||
httpStatus: response.status,
|
||||
responseMeta: { hasAppToken: true },
|
||||
requestHeaders: exchange.requestHeaders,
|
||||
requestBody: "",
|
||||
responseHeaders: exchange.responseHeaders,
|
||||
responseBody: exchange.responseBody,
|
||||
responseMeta: { hasAppToken: true, cached: false },
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
}
|
||||
@@ -320,12 +419,21 @@ export class FanavaranAuthService {
|
||||
return appToken;
|
||||
} catch (error) {
|
||||
if (auditSession) {
|
||||
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||
error,
|
||||
requestHeaders,
|
||||
);
|
||||
await this.fanavaranAuditService.recordStep({
|
||||
session: auditSession,
|
||||
step: FanavaranAuditStep.GET_APP_TOKEN,
|
||||
status: FanavaranAuditStatus.FAILURE,
|
||||
requestUrl: this.getAppTokenUrl,
|
||||
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
|
||||
requestMethod: "POST",
|
||||
httpStatus: exchange.httpStatus,
|
||||
requestHeaders: exchange.requestHeaders,
|
||||
requestBody: "",
|
||||
responseHeaders: exchange.responseHeaders,
|
||||
responseBody: exchange.responseBody,
|
||||
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||
durationMs: Date.now() - startedAt,
|
||||
@@ -341,12 +449,21 @@ export class FanavaranAuthService {
|
||||
auditSession?: FanavaranAuditSession,
|
||||
): Promise<string> {
|
||||
const startedAt = Date.now();
|
||||
const requestHeaders = {
|
||||
appToken,
|
||||
userName: config.username,
|
||||
password: config.password,
|
||||
"Content-Length": "0",
|
||||
};
|
||||
if (auditSession) {
|
||||
await this.fanavaranAuditService.recordStep({
|
||||
session: auditSession,
|
||||
step: FanavaranAuditStep.LOGIN,
|
||||
status: FanavaranAuditStatus.STARTED,
|
||||
requestUrl: this.loginUrl,
|
||||
requestMethod: "POST",
|
||||
requestHeaders,
|
||||
requestBody: "",
|
||||
requestMeta: { userName: config.username, cached: false },
|
||||
});
|
||||
}
|
||||
@@ -354,12 +471,7 @@ export class FanavaranAuthService {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(this.loginUrl, "", {
|
||||
headers: {
|
||||
appToken,
|
||||
userName: config.username,
|
||||
password: config.password,
|
||||
"Content-Length": "0",
|
||||
},
|
||||
headers: requestHeaders,
|
||||
transformRequest: this.emptyBodyTransformRequest(),
|
||||
}),
|
||||
);
|
||||
@@ -380,13 +492,26 @@ export class FanavaranAuthService {
|
||||
}
|
||||
|
||||
if (auditSession) {
|
||||
const exchange =
|
||||
this.fanavaranAuditService.captureAxiosExchange(
|
||||
response,
|
||||
requestHeaders,
|
||||
);
|
||||
await this.fanavaranAuditService.recordStep({
|
||||
session: auditSession,
|
||||
step: FanavaranAuditStep.LOGIN,
|
||||
status: FanavaranAuditStatus.SUCCESS,
|
||||
requestUrl: this.loginUrl,
|
||||
requestMethod: "POST",
|
||||
httpStatus: response.status,
|
||||
responseMeta: { hasAuthenticationToken: true },
|
||||
requestHeaders: exchange.requestHeaders,
|
||||
requestBody: "",
|
||||
responseHeaders: exchange.responseHeaders,
|
||||
responseBody: exchange.responseBody,
|
||||
responseMeta: {
|
||||
hasAuthenticationToken: true,
|
||||
cached: false,
|
||||
},
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
}
|
||||
@@ -394,12 +519,21 @@ export class FanavaranAuthService {
|
||||
return authenticationToken;
|
||||
} catch (error) {
|
||||
if (auditSession) {
|
||||
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||
error,
|
||||
requestHeaders,
|
||||
);
|
||||
await this.fanavaranAuditService.recordStep({
|
||||
session: auditSession,
|
||||
step: FanavaranAuditStep.LOGIN,
|
||||
status: FanavaranAuditStatus.FAILURE,
|
||||
requestUrl: this.loginUrl,
|
||||
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
|
||||
requestMethod: "POST",
|
||||
httpStatus: exchange.httpStatus,
|
||||
requestHeaders: exchange.requestHeaders,
|
||||
requestBody: "",
|
||||
responseHeaders: exchange.responseHeaders,
|
||||
responseBody: exchange.responseBody,
|
||||
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||
durationMs: Date.now() - startedAt,
|
||||
|
||||
Reference in New Issue
Block a user