fix(rates): серверный прокси /api/rates-proxy и бейдж «Курсы не загружены»
- Добавлен /api/rates-proxy для загрузки курсов без CORS - Фронтенд загружает курсы через прокси, а не напрямую - convertWithProviderRate возвращает source=no-rates при отсутствии ratesData - ConvertedAmount показывает «Курсы не загружены» (жёлтый) вместо ложного «Глобальный курс» Made-with: Cursor
This commit is contained in:
@@ -18,6 +18,7 @@ import settingsRouter from './routes/settings.js'
|
|||||||
import syncRouter from './routes/sync.js'
|
import syncRouter from './routes/sync.js'
|
||||||
import projectsRouter from './routes/projects.js'
|
import projectsRouter from './routes/projects.js'
|
||||||
import backupRouter from './routes/backup.js'
|
import backupRouter from './routes/backup.js'
|
||||||
|
import ratesProxyRouter from './routes/rates-proxy.js'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const PORT = process.env.PORT || 3001
|
const PORT = process.env.PORT || 3001
|
||||||
@@ -39,6 +40,7 @@ app.use(express.json({ limit: '50mb' }))
|
|||||||
app.use('/api/sync', syncRouter)
|
app.use('/api/sync', syncRouter)
|
||||||
app.use('/api/projects', projectsRouter)
|
app.use('/api/projects', projectsRouter)
|
||||||
app.use('/api/backup', backupRouter)
|
app.use('/api/backup', backupRouter)
|
||||||
|
app.use('/api/rates-proxy', ratesProxyRouter)
|
||||||
|
|
||||||
if (existsSync(distPath)) {
|
if (existsSync(distPath)) {
|
||||||
app.use(express.static(distPath))
|
app.use(express.static(distPath))
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
const url = req.query.url
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({ error: 'Missing url parameter' })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { signal: AbortSignal.timeout(10_000) })
|
||||||
|
if (!response.ok) {
|
||||||
|
return res.status(502).json({ error: `Upstream returned ${response.status}` })
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
res.json(data)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(502).json({ error: err.message || 'Failed to fetch rates' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
+2
-1
@@ -56,7 +56,8 @@ function App() {
|
|||||||
if (!settings?.ratesUrl) {
|
if (!settings?.ratesUrl) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetch(settings.ratesUrl)
|
const proxyUrl = `/api/rates-proxy?url=${encodeURIComponent(settings.ratesUrl)}`
|
||||||
|
fetch(proxyUrl)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Не удалось получить курсы валют')
|
throw new Error('Не удалось получить курсы валют')
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ function sourceMeta(source) {
|
|||||||
if (source === 'global') {
|
if (source === 'global') {
|
||||||
return { label: 'Глобальный курс', className: 'bg-blue-lt text-blue' }
|
return { label: 'Глобальный курс', className: 'bg-blue-lt text-blue' }
|
||||||
}
|
}
|
||||||
|
if (source === 'no-rates') {
|
||||||
|
return { label: 'Курсы не загружены', className: 'bg-yellow-lt text-yellow' }
|
||||||
|
}
|
||||||
return { label: 'Без конвертации', className: 'bg-secondary-lt text-secondary' }
|
return { label: 'Без конвертации', className: 'bg-secondary-lt text-secondary' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConvertedAmount({ amount, currency, provider, settings, ratesData }) {
|
export function ConvertedAmount({ amount, currency, provider, settings, ratesData }) {
|
||||||
const result = convertWithProviderRate(amount, currency, provider, settings, ratesData)
|
const result = convertWithProviderRate(amount, currency, provider, settings, ratesData)
|
||||||
const meta = sourceMeta(result.source)
|
const meta = sourceMeta(result.source)
|
||||||
|
const showOriginal = result.source !== 'native' && result.source !== 'no-rates'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -20,7 +24,9 @@ export function ConvertedAmount({ amount, currency, provider, settings, ratesDat
|
|||||||
<span>{formatCurrency(result.value, result.currency)}</span>
|
<span>{formatCurrency(result.value, result.currency)}</span>
|
||||||
<span className={`badge ${meta.className}`}>{meta.label}</span>
|
<span className={`badge ${meta.className}`}>{meta.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-secondary small">{formatCurrency(amount, currency)}</div>
|
{showOriginal ? (
|
||||||
|
<div className="text-secondary small">{formatCurrency(amount, currency)}</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -337,10 +337,16 @@ export function convertWithProviderRate(amount, currency, provider, appSettings,
|
|||||||
return { value: safeAmount * eurRate, currency: appBase, source: 'provider' }
|
return { value: safeAmount * eurRate, currency: appBase, source: 'provider' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ratesData || !ratesData.rates || !ratesData.base) {
|
||||||
|
return { value: safeAmount, currency: fromCurrency, source: 'no-rates' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const converted = convertCurrency(safeAmount, fromCurrency, appBase, ratesData)
|
||||||
|
const didConvert = Math.abs(converted - safeAmount) > 0.0001 || fromCurrency === appBase
|
||||||
return {
|
return {
|
||||||
value: convertCurrency(safeAmount, fromCurrency, appBase, ratesData),
|
value: converted,
|
||||||
currency: appBase,
|
currency: appBase,
|
||||||
source: 'global',
|
source: didConvert ? 'global' : 'no-rates',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user