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 | 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 { 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 => { 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(); let serverMsg = err?.response?.data?.detail || err?.response?.data?.message || err?.response?.data?.error || err?.message || 'Account is not yet enabled by the administrator.'; if (typeof serverMsg === 'string') { if (/^network\s*error$/i.test(serverMsg.trim())) { serverMsg = 'Permission Check'; } else { serverMsg = serverMsg.replace(/network\s*error/gi, 'Permission Check'); } } 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 { const result = await syncUserAccount(userSub, token, profile, force); return result.success; }