|
|
import axios from 'axios'; |
|
|
|
|
|
|
|
|
|
|
|
const envUrlRaw = (process.env.REACT_APP_API_URL || '').trim(); |
|
|
const envUrlNoTrailingSlash = envUrlRaw.replace(/\/$/, ''); |
|
|
const envBaseWithoutApi = envUrlNoTrailingSlash.replace(/\/api$/, ''); |
|
|
|
|
|
const isLocalhost = typeof window !== 'undefined' && /^(localhost|127\.0\.0\.1)$/i.test(window.location.hostname); |
|
|
|
|
|
const localCandidates = isLocalhost |
|
|
? ['http://localhost:5000', 'http://127.0.0.1:5000', 'http://localhost:7860', 'http://127.0.0.1:7860'] |
|
|
: []; |
|
|
|
|
|
const hfBackend = 'https://linguabot-transhub-backend.hf.space'; |
|
|
|
|
|
const candidateSet = new Set<string>([ |
|
|
envBaseWithoutApi, |
|
|
...localCandidates, |
|
|
hfBackend |
|
|
].filter(Boolean) as string[]); |
|
|
const candidates = Array.from(candidateSet); |
|
|
|
|
|
const storedBase = typeof window !== 'undefined' ? (localStorage.getItem('refinity_api_base') || '') : ''; |
|
|
let activeBase = storedBase && candidateSet.has(storedBase) ? storedBase : (candidates[0] || hfBackend); |
|
|
|
|
|
const api = axios.create({ |
|
|
baseURL: activeBase, |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
timeout: 10000, |
|
|
}); |
|
|
|
|
|
|
|
|
function setActiveBase(nextBase: string) { |
|
|
if (!nextBase) return; |
|
|
activeBase = nextBase.replace(/\/$/, ''); |
|
|
api.defaults.baseURL = activeBase; |
|
|
(api.defaults as any).baseURL = activeBase; |
|
|
try { localStorage.setItem('refinity_api_base', activeBase); } catch {} |
|
|
console.log('[Refinity] API base updated:', activeBase); |
|
|
} |
|
|
|
|
|
|
|
|
async function probeBases() { |
|
|
for (const base of candidates) { |
|
|
try { |
|
|
const controller = new AbortController(); |
|
|
const timer = setTimeout(() => controller.abort(), 2500); |
|
|
const resp = await fetch(`${base.replace(/\/$/,'')}/api/health`, { signal: controller.signal }); |
|
|
clearTimeout(timer); |
|
|
if (resp && resp.ok) { |
|
|
if (activeBase !== base) setActiveBase(base); |
|
|
return; |
|
|
} |
|
|
} catch { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') { |
|
|
|
|
|
setActiveBase(activeBase); |
|
|
|
|
|
if (process.env.NODE_ENV === 'production') { |
|
|
probeBases(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
console.log('π§ API CONFIGURATION DEBUG'); |
|
|
console.log('Environment variables:', { |
|
|
REACT_APP_API_URL: process.env.REACT_APP_API_URL, |
|
|
NODE_ENV: process.env.NODE_ENV |
|
|
}); |
|
|
console.log('Initial API Base URL:', activeBase); |
|
|
console.log('Build timestamp:', new Date().toISOString()); |
|
|
|
|
|
|
|
|
api.interceptors.request.use( |
|
|
(config) => { |
|
|
const token = localStorage.getItem('token'); |
|
|
if (token) { |
|
|
config.headers.Authorization = `Bearer ${token}`; |
|
|
} |
|
|
|
|
|
|
|
|
const user = localStorage.getItem('user'); |
|
|
const viewMode = (localStorage.getItem('viewMode') || 'auto'); |
|
|
if (user) { |
|
|
try { |
|
|
const userData = JSON.parse(user); |
|
|
|
|
|
const effectiveRole = viewMode === 'student' ? 'student' : (userData.role || 'visitor'); |
|
|
config.headers['user-role'] = effectiveRole; |
|
|
const derivedUsername = userData.username || userData.name || userData.displayName || (userData.email ? String(userData.email).split('@')[0] : undefined); |
|
|
config.headers['user-info'] = JSON.stringify({ |
|
|
_id: userData._id || userData.id, |
|
|
username: derivedUsername, |
|
|
name: userData.name, |
|
|
displayName: userData.displayName, |
|
|
email: userData.email, |
|
|
role: userData.role |
|
|
}); |
|
|
} catch (error) { |
|
|
config.headers['user-role'] = 'visitor'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
console.log('π Making API request to:', (config.baseURL || '') + (config.url || '')); |
|
|
console.log('π Auth token:', token ? 'Present' : 'Missing'); |
|
|
|
|
|
return config; |
|
|
}, |
|
|
(error) => { |
|
|
return Promise.reject(error); |
|
|
} |
|
|
); |
|
|
|
|
|
|
|
|
api.interceptors.response.use( |
|
|
(response) => { |
|
|
console.log('β
API response received:', response.config.url); |
|
|
return response; |
|
|
}, |
|
|
(error) => { |
|
|
console.error('β API request failed:', error.config?.url, error.message); |
|
|
|
|
|
|
|
|
const isNetworkOrCORS = !error.response || error.message?.includes('Network') || error.message?.includes('Failed to fetch'); |
|
|
if (isNetworkOrCORS && typeof window !== 'undefined') { |
|
|
const idx = candidates.indexOf(activeBase); |
|
|
const nextIdx = (idx + 1) % candidates.length; |
|
|
const nextBase = candidates[nextIdx]; |
|
|
if (nextBase && nextBase !== activeBase) { |
|
|
setActiveBase(nextBase); |
|
|
const cfg: any = error.config || {}; |
|
|
if (!cfg.__retriedWithNextBase) { |
|
|
cfg.__retriedWithNextBase = true; |
|
|
cfg.baseURL = activeBase; |
|
|
console.warn('[Refinity] Retrying request with base:', activeBase); |
|
|
return api.request(cfg); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (error.response?.status === 401 && !error.config?.url?.includes('/subtitles/update/')) { |
|
|
|
|
|
localStorage.removeItem('token'); |
|
|
localStorage.removeItem('user'); |
|
|
window.location.href = '/login'; |
|
|
} else if (error.response?.status === 429) { |
|
|
|
|
|
console.warn('Rate limit exceeded, retrying after delay...'); |
|
|
return new Promise(resolve => { |
|
|
setTimeout(() => { |
|
|
resolve(api.request(error.config)); |
|
|
}, 2000); |
|
|
}); |
|
|
} else if (error.response?.status === 503) { |
|
|
|
|
|
const cfg: any = error.config || {}; |
|
|
cfg.__retryCount = cfg.__retryCount || 0; |
|
|
if (cfg.__retryCount < 2) { |
|
|
cfg.__retryCount += 1; |
|
|
const delay = 500 * Math.pow(2, cfg.__retryCount); |
|
|
return new Promise(resolve => setTimeout(() => resolve(api.request(cfg)), delay)); |
|
|
} |
|
|
} else if (error.response?.status === 500) { |
|
|
console.error('Server error:', error.response.data); |
|
|
} else if (error.code === 'ECONNABORTED') { |
|
|
console.error('Request timeout'); |
|
|
} |
|
|
return Promise.reject(error); |
|
|
} |
|
|
); |
|
|
|
|
|
export { api }; |