added offline inquiry in system setting also fix some bugs

This commit is contained in:
2026-08-03 11:49:24 +03:30
parent ab4f667c8d
commit 7f672541ae
19 changed files with 965 additions and 26 deletions

View File

@@ -4,7 +4,12 @@ describe("FanavaranAuthService", () => {
const createAuthTokenModel = () => {
const store = new Map<
string,
{ clientKey: string; authenticationToken: string; expiresAt: Date }
{
clientKey: string;
authenticationToken: string;
expiresAt: Date;
authFingerprint?: string;
}
>();
return {
findOne: jest.fn((query: { clientKey: string }) => ({
@@ -15,13 +20,20 @@ describe("FanavaranAuthService", () => {
findOneAndUpdate: jest.fn(
(
query: { clientKey: string },
update: { $set: { authenticationToken: string; expiresAt: Date } },
update: {
$set: {
authenticationToken: string;
expiresAt: Date;
authFingerprint?: string;
};
},
) => ({
exec: async () => {
const next = {
clientKey: query.clientKey,
authenticationToken: update.$set.authenticationToken,
expiresAt: update.$set.expiresAt,
authFingerprint: update.$set.authFingerprint,
};
store.set(query.clientKey, next);
return next;

View File

@@ -30,6 +30,8 @@ interface CachedFanavaranAuth {
authenticationToken: string;
/** Epoch ms when the cached token should be refreshed. */
expiresAt: number;
/** Hash of auth fields used when this token was obtained. */
authFingerprint: string;
}
interface TenantBackoffState {
@@ -178,6 +180,22 @@ export class FanavaranAuthService {
});
}
/**
* Stable fingerprint of tenant auth. Any change (location, credentials, …)
* forces a fresh GetAppToken+Login even before Tehran midnight.
*/
static authFingerprint(auth: FanavaranAuthConfig): string {
return [
auth.appName,
auth.secret,
auth.username,
auth.password,
auth.corpId,
auth.contractId,
auth.location,
].join("|");
}
assertNotInBackoff(clientKey: FanavaranClientKey): void {
const remaining = this.getBackoffRemainingMs(clientKey);
if (remaining <= 0) return;
@@ -195,13 +213,16 @@ export class FanavaranAuthService {
},
): Promise<string> {
this.assertNotInBackoff(clientKey);
const fingerprint = FanavaranAuthService.authFingerprint(
getFanavaranClientProfile(clientKey).auth,
);
if (!options?.forceRefresh) {
const memoryHit = this.readMemoryCache(clientKey);
const memoryHit = this.readMemoryCache(clientKey, fingerprint);
if (memoryHit) {
return memoryHit;
}
const persisted = await this.readPersistedCache(clientKey);
const persisted = await this.readPersistedCache(clientKey, fingerprint);
if (persisted) {
return persisted;
}
@@ -253,6 +274,7 @@ export class FanavaranAuthService {
auditSession?: FanavaranAuditSession,
): Promise<string> {
const profile = getFanavaranClientProfile(clientKey);
const fingerprint = FanavaranAuthService.authFingerprint(profile.auth);
const appToken = await this.fetchAppToken(profile.auth, auditSession);
const authenticationToken = await this.fetchLoginToken(
appToken,
@@ -261,7 +283,12 @@ export class FanavaranAuthService {
);
const expiresAt = FanavaranAuthService.getNextMidnightExpiryMs();
await this.persistToken(clientKey, authenticationToken, expiresAt);
await this.persistToken(
clientKey,
authenticationToken,
expiresAt,
fingerprint,
);
this.clearBackoff(clientKey);
this.logger.log(
`[${clientKey}] Cached Fanavaran authenticationToken until ${new Date(
@@ -271,25 +298,44 @@ export class FanavaranAuthService {
return authenticationToken;
}
private readMemoryCache(clientKey: FanavaranClientKey): string | null {
private readMemoryCache(
clientKey: FanavaranClientKey,
fingerprint: string,
): string | null {
const cached = this.tokenCache.get(clientKey);
if (cached && cached.expiresAt > Date.now()) {
if (!cached) {
return null;
}
if (cached.authFingerprint !== fingerprint) {
this.logger.log(
`[${clientKey}] Auth config changed — discarding in-memory Fanavaran token`,
);
this.tokenCache.delete(clientKey);
return null;
}
if (cached.expiresAt > Date.now()) {
return cached.authenticationToken;
}
if (cached) {
this.tokenCache.delete(clientKey);
}
this.tokenCache.delete(clientKey);
return null;
}
private async readPersistedCache(
clientKey: FanavaranClientKey,
fingerprint: string,
): Promise<string | null> {
try {
const doc = await this.authTokenModel.findOne({ clientKey }).lean().exec();
if (!doc?.authenticationToken || !doc.expiresAt) {
return null;
}
if (!doc.authFingerprint || doc.authFingerprint !== fingerprint) {
this.logger.log(
`[${clientKey}] Auth config changed (or legacy token without fingerprint) — discarding persisted Fanavaran token`,
);
await this.authTokenModel.deleteOne({ clientKey }).exec();
return null;
}
const expiresAt = new Date(doc.expiresAt).getTime();
if (!(expiresAt > Date.now())) {
await this.authTokenModel.deleteOne({ clientKey }).exec();
@@ -298,6 +344,7 @@ export class FanavaranAuthService {
this.tokenCache.set(clientKey, {
authenticationToken: doc.authenticationToken,
expiresAt,
authFingerprint: doc.authFingerprint,
});
this.logger.log(
`[${clientKey}] Reused persisted Fanavaran authenticationToken until ${new Date(
@@ -318,8 +365,13 @@ export class FanavaranAuthService {
clientKey: FanavaranClientKey,
authenticationToken: string,
expiresAt: number,
authFingerprint: string,
): Promise<void> {
this.tokenCache.set(clientKey, { authenticationToken, expiresAt });
this.tokenCache.set(clientKey, {
authenticationToken,
expiresAt,
authFingerprint,
});
try {
await this.authTokenModel
.findOneAndUpdate(
@@ -328,6 +380,7 @@ export class FanavaranAuthService {
$set: {
authenticationToken,
expiresAt: new Date(expiresAt),
authFingerprint,
},
},
{ upsert: true, new: true },

View File

@@ -4,10 +4,12 @@ import { Model } from "mongoose";
import {
FANAVARAN_CLIENT_KEYS,
SEED_FANAVARAN_CLIENT_PROFILES,
getFanavaranClientProfile,
setFanavaranClientProfilesCache,
type FanavaranClientKey,
type FanavaranClientProfile,
} from "src/core/config/fanavaran-client.config";
import { FanavaranAuthService } from "./fanavaran-auth.service";
import {
FanavaranClientConfig,
FanavaranClientConfigDocument,
@@ -27,6 +29,7 @@ export class FanavaranClientConfigService implements OnModuleInit {
constructor(
@InjectModel(FanavaranClientConfig.name)
private readonly configModel: Model<FanavaranClientConfigDocument>,
private readonly fanavaranAuthService: FanavaranAuthService,
) {}
async onModuleInit(): Promise<void> {
@@ -56,7 +59,26 @@ export class FanavaranClientConfigService implements OnModuleInit {
}
}
/**
* Re-read Mongo into the runtime profile cache. If any tenant's auth block
* changed (location, credentials, …), drop cached Fanavaran tokens so the
* next call re-logins instead of waiting for Tehran midnight.
*/
async reloadCache(): Promise<void> {
const previousFingerprints = new Map<FanavaranClientKey, string>();
for (const key of FANAVARAN_CLIENT_KEYS) {
try {
previousFingerprints.set(
key,
FanavaranAuthService.authFingerprint(
getFanavaranClientProfile(key).auth,
),
);
} catch {
// First boot / empty cache — nothing to compare.
}
}
const docs = await this.configModel.find().lean().exec();
const cache: Partial<Record<FanavaranClientKey, FanavaranClientProfile>> =
{};
@@ -83,6 +105,18 @@ export class FanavaranClientConfigService implements OnModuleInit {
}
setFanavaranClientProfilesCache(cache);
for (const key of FANAVARAN_CLIENT_KEYS) {
const next = FanavaranAuthService.authFingerprint(cache[key]!.auth);
const prev = previousFingerprints.get(key);
if (prev && prev !== next) {
this.logger.warn(
`[${key}] Fanavaran auth config changed — invalidating cached authenticationToken`,
);
this.fanavaranAuthService.invalidateToken(key);
}
}
this.logger.log(
`Loaded ${docs.length} Fanavaran client config(s) from Mongo into runtime cache`,
);

View File

@@ -14,6 +14,13 @@ export class FanavaranAuthToken {
@Prop({ type: String, required: true })
authenticationToken: string;
/**
* Fingerprint of auth config used when this token was minted
* (appName/secret/user/pass/corp/contract/location). Mismatch → re-login.
*/
@Prop({ type: String, required: false })
authFingerprint?: string;
/** When this token should be refreshed (Asia/Tehran midnight). */
@Prop({ type: Date, required: true, index: true })
expiresAt: Date;