forked from Yara724/api
added offline inquiry in system setting also fix some bugs
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user