Files
pfm/PostFundManagement.PartnerUI/src/services/userSyncService.ts
T

298 lines
8.2 KiB
TypeScript

import { apiClient } from './httpClient';
export const LOCAL_STORAGE_USER_SYNC_KEY = 'pfm_user_sync_record';
export const LOCAL_STORAGE_ACTIVATION_PENDING_KEY = 'pfm_user_pending_activation';
export const LOCAL_STORAGE_ACTIVATION_REASON_KEY = 'pfm_user_pending_activation_reason';
export interface UserSyncRecord {
sub: string;
syncedAt: number;
syncedEndpoint: string;
tokenSnippet: string;
status: 'synced';
userData?: any;
}
export interface UserSyncProfile {
id?: string;
sub?: string;
email?: string | null;
name?: string | null;
role?: string | null;
tenant?: string | null;
tenant_id?: number | string | null;
organisationId?: number | string | null;
organisationName?: string | null;
}
export interface UserSyncResult {
success: boolean;
isUnauthorized?: boolean;
isActivationPending?: boolean;
endpoint?: string;
data?: any;
errorMessage?: string;
}
let inFlightSyncPromise: Promise<UserSyncResult> | null = null;
let inMemorySyncRecord: UserSyncRecord | null = null;
let inMemoryActivationPending: boolean | null = null;
let inMemoryActivationReason: string | null = null;
export function isActivationPending(): boolean {
if (inMemoryActivationPending !== null) {
return inMemoryActivationPending;
}
try {
const val = localStorage.getItem(LOCAL_STORAGE_ACTIVATION_PENDING_KEY) === 'true';
inMemoryActivationPending = val;
return val;
} catch {
return false;
}
}
export function getActivationPendingReason(): string | null {
if (inMemoryActivationReason !== null) {
return inMemoryActivationReason;
}
try {
const reason = localStorage.getItem(LOCAL_STORAGE_ACTIVATION_REASON_KEY);
inMemoryActivationReason = reason;
return reason;
} catch {
return null;
}
}
export function setActivationPending(pending: boolean, reason?: string): void {
inMemoryActivationPending = pending;
inMemoryActivationReason = reason || null;
try {
if (pending) {
localStorage.setItem(LOCAL_STORAGE_ACTIVATION_PENDING_KEY, 'true');
if (reason) {
localStorage.setItem(LOCAL_STORAGE_ACTIVATION_REASON_KEY, reason);
}
} else {
localStorage.removeItem(LOCAL_STORAGE_ACTIVATION_PENDING_KEY);
localStorage.removeItem(LOCAL_STORAGE_ACTIVATION_REASON_KEY);
}
} catch (err) {
console.warn('[UserSync] Failed to update activation flag:', err);
}
}
export function isUserSyncedInBrowser(currentSub?: string, currentToken?: string | null): boolean {
try {
if (isActivationPending()) {
return false;
}
if (!inMemorySyncRecord) {
const raw = localStorage.getItem(LOCAL_STORAGE_USER_SYNC_KEY);
if (!raw) return false;
const parsed: UserSyncRecord = JSON.parse(raw);
if (!parsed || parsed.status !== 'synced') return false;
inMemorySyncRecord = parsed;
}
const record = inMemorySyncRecord;
if (!record || record.status !== 'synced') return false;
if (currentSub && record.sub && record.sub !== currentSub) {
return false;
}
if (currentToken && record.tokenSnippet) {
const snippet = currentToken.slice(-16);
if (record.tokenSnippet !== snippet) {
return false;
}
}
return true;
} catch {
return false;
}
}
export function saveUserSyncInBrowser(
sub: string,
token: string | null | undefined,
endpoint: string,
userData?: any
): void {
try {
const record: UserSyncRecord = {
sub: sub || 'authenticated_user',
syncedAt: Date.now(),
syncedEndpoint: endpoint,
tokenSnippet: token ? token.slice(-16) : '',
status: 'synced',
userData: userData || undefined,
};
inMemorySyncRecord = record;
localStorage.setItem(LOCAL_STORAGE_USER_SYNC_KEY, JSON.stringify(record));
} catch (err) {
console.warn('[UserSync] Failed to write sync record to localStorage:', err);
}
}
export function clearUserSyncInBrowser(): void {
inMemorySyncRecord = null;
inMemoryActivationPending = null;
inMemoryActivationReason = null;
try {
localStorage.removeItem(LOCAL_STORAGE_USER_SYNC_KEY);
localStorage.removeItem(LOCAL_STORAGE_ACTIVATION_PENDING_KEY);
localStorage.removeItem(LOCAL_STORAGE_ACTIVATION_REASON_KEY);
} catch {}
}
export function clearSuccessfulSyncRecord(): void {
inMemorySyncRecord = null;
try {
localStorage.removeItem(LOCAL_STORAGE_USER_SYNC_KEY);
} catch {}
}
export function getUserSyncRecord(): UserSyncRecord | null {
try {
const raw = localStorage.getItem(LOCAL_STORAGE_USER_SYNC_KEY);
if (!raw) return null;
return JSON.parse(raw) as UserSyncRecord;
} catch {
return null;
}
}
export const USER_SYNC_CANDIDATE_ENDPOINTS = [
'/v1/api/integration/sync',
'/v1/api/users/sync',
'/v1/api/user/sync',
];
export function isUserSyncEndpoint(url?: string): boolean {
if (!url) return false;
const clean = url.toLowerCase().split('?')[0];
return (
clean.includes('/integration/sync') ||
clean.includes('/users/sync') ||
clean.includes('/user/sync') ||
clean.endsWith('/sync') ||
clean === '/sync'
);
}
async function executeBackendSync(
userSub?: string,
profile?: UserSyncProfile
): Promise<{ endpoint: string; data?: any }> {
const endpoint = '/v1/api/integration/sync';
const payload = {
id: userSub || profile?.id || profile?.sub,
sub: userSub || profile?.sub,
email: profile?.email || undefined,
name: profile?.name || undefined,
role: profile?.role || undefined,
tenant: profile?.tenant || undefined,
tenant_id: profile?.tenant_id,
organisationId: profile?.organisationId,
};
try {
const res = await apiClient.post(endpoint, payload);
return { endpoint, data: res?.data };
} catch (err: any) {
const status = err?.response?.status;
if (status === 400 || status === 415 || status === 422) {
try {
const resFallback = await apiClient.post(endpoint, {});
return { endpoint, data: resFallback?.data };
} catch (fallbackErr) {
throw err;
}
}
throw err;
}
}
export async function syncUserAccount(
userSub?: string,
token?: string | null,
profile?: UserSyncProfile,
force = false
): Promise<UserSyncResult> {
const effectiveToken = token || (typeof window !== 'undefined' ? localStorage.getItem('pfm_access_token') : null);
const effectiveSub = userSub || (typeof window !== 'undefined' ? localStorage.getItem('pfm_user_sub') || '' : '');
if (!force && isUserSyncedInBrowser(effectiveSub, effectiveToken) && !isActivationPending()) {
return {
success: true,
endpoint: getUserSyncRecord()?.syncedEndpoint || '/v1/api/users/sync',
data: getUserSyncRecord()?.userData,
};
}
if (inFlightSyncPromise) {
return inFlightSyncPromise;
}
inFlightSyncPromise = (async (): Promise<UserSyncResult> => {
try {
const result = await executeBackendSync(effectiveSub, profile);
setActivationPending(false);
saveUserSyncInBrowser(effectiveSub, effectiveToken, result.endpoint, result.data);
return {
success: true,
endpoint: result.endpoint,
data: result.data,
};
} catch (err: any) {
const status = err?.response?.status;
if (status === 401) {
clearUserSyncInBrowser();
setActivationPending(false);
return {
success: false,
isUnauthorized: true,
errorMessage: 'Authentication session expired or unauthorized (HTTP 401).',
};
}
clearSuccessfulSyncRecord();
const serverMsg =
err?.response?.data?.detail ||
err?.response?.data?.message ||
err?.response?.data?.error ||
err?.message ||
'Account is not yet enabled by the administrator.';
setActivationPending(true, serverMsg);
return {
success: false,
isUnauthorized: false,
isActivationPending: true,
errorMessage: serverMsg,
};
} finally {
inFlightSyncPromise = null;
}
})();
return inFlightSyncPromise;
}
export async function ensureUserSynced(
userSub?: string,
token?: string | null,
profile?: UserSyncProfile,
force = false
): Promise<boolean> {
const result = await syncUserAccount(userSub, token, profile, force);
return result.success;
}