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; } export interface UserSyncResult { success: boolean; isUnauthorized?: boolean; isActivationPending?: boolean; endpoint?: string; data?: any; errorMessage?: string; } let inFlightSyncPromise: Promise | null = null; export function isActivationPending(): boolean { try { return localStorage.getItem(LOCAL_STORAGE_ACTIVATION_PENDING_KEY) === 'true'; } catch { return false; } } export function getActivationPendingReason(): string | null { try { return localStorage.getItem(LOCAL_STORAGE_ACTIVATION_REASON_KEY); } catch { return null; } } export function setActivationPending(pending: boolean, reason?: string): void { 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 {} } export function isUserSyncedInBrowser(currentSub?: string, currentToken?: string | null): boolean { try { if (isActivationPending()) return false; const raw = localStorage.getItem(LOCAL_STORAGE_USER_SYNC_KEY); if (!raw) return false; const record: UserSyncRecord = JSON.parse(raw); if (!record || record.status !== 'synced') return false; if (currentSub && record.sub && record.sub !== currentSub) return false; if (currentToken && record.tokenSnippet && record.tokenSnippet !== currentToken.slice(-16)) 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, }; localStorage.setItem(LOCAL_STORAGE_USER_SYNC_KEY, JSON.stringify(record)); } catch {} } export function clearUserSyncInBrowser(): void { try { localStorage.removeItem(LOCAL_STORAGE_USER_SYNC_KEY); localStorage.removeItem(LOCAL_STORAGE_ACTIVATION_PENDING_KEY); localStorage.removeItem(LOCAL_STORAGE_ACTIVATION_REASON_KEY); } catch {} } export const USER_SYNC_CANDIDATE_ENDPOINTS = [ '/v1/api/integration/sync', '/v1/api/users/sync', '/v1/api/user/sync', ]; async function executeBackendSync(userSub?: string, profile?: UserSyncProfile): Promise<{ endpoint: string; data?: any }> { 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_id: profile?.tenant_id ? Number(profile.tenant_id) : undefined, organisationId: profile?.organisationId ? Number(profile.organisationId) : undefined, }; let lastError: any = null; for (const endpoint of USER_SYNC_CANDIDATE_ENDPOINTS) { try { const res = await apiClient.post(endpoint, payload); return { endpoint, data: res?.data }; } catch (err: any) { lastError = err; const status = err?.response?.status; if (status === 401 || status === 403 || status === 423) throw err; } } throw lastError || new Error('Failed to reach user sync endpoint'); } 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: '/v1/api/users/sync' }; } 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: 'Session expired (HTTP 401).' }; } const serverMsg = err?.response?.data?.detail || err?.response?.data?.message || 'Account activation pending.'; setActivationPending(true, serverMsg); return { success: false, isUnauthorized: false, isActivationPending: true, errorMessage: serverMsg }; } finally { inFlightSyncPromise = null; } })(); return inFlightSyncPromise; }