Files
X-Financial/web/src/services/api.js

308 lines
7.1 KiB
JavaScript
Raw Normal View History

const API_BASE_STORAGE_KEY = 'x-financial-api-base-url'
const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user'
function isHeaderValueSafe(value) {
const normalized = String(value || '').trim()
if (!normalized) {
return false
}
for (let index = 0; index < normalized.length; index += 1) {
const codePoint = normalized.charCodeAt(index)
if (codePoint > 255 || codePoint === 10 || codePoint === 13 || codePoint === 0) {
return false
}
}
return true
}
export function pickSafeHeaderValue(value, fallback = '') {
const normalized = String(value || '').trim()
if (isHeaderValueSafe(normalized)) {
return normalized
}
const fallbackValue = String(fallback || '').trim()
if (isHeaderValueSafe(fallbackValue)) {
return fallbackValue
}
return ''
}
function readCurrentUserHeaders() {
if (typeof window === 'undefined') {
return {}
}
const raw = window.sessionStorage.getItem(AUTH_USER_STORAGE_KEY)
if (!raw) {
return {}
}
try {
const payload = JSON.parse(raw)
const username = String(payload?.username || '').trim()
const name = String(payload?.name || username).trim()
const roleCodes = Array.isArray(payload?.roleCodes) ? payload.roleCodes.filter(Boolean) : []
const isAdmin = Boolean(payload?.isAdmin)
const safeUsername = pickSafeHeaderValue(username)
const safeName = pickSafeHeaderValue(name)
if (!safeUsername && !safeName) {
return {}
}
const headers = {
'x-auth-role-codes': roleCodes.join(','),
'x-auth-is-admin': String(isAdmin)
}
if (safeUsername) {
headers['x-auth-username'] = safeUsername
}
if (safeName) {
headers['x-auth-name'] = safeName
}
return headers
} catch {
return {}
}
}
function normalizeApiBaseUrl(value) {
return String(value || '/api/v1').replace(/\/$/, '')
}
function isLoopbackHost(hostname) {
const normalized = String(hostname || '').trim().toLowerCase()
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '0.0.0.0' || normalized === '::1'
}
function resolveBrowserReachableApiBaseUrl(value) {
const normalized = normalizeApiBaseUrl(value)
if (typeof window === 'undefined') {
return normalized
}
try {
const apiUrl = new URL(normalized)
const browserHost = window.location.hostname
if (isLoopbackHost(apiUrl.hostname) && browserHost && !isLoopbackHost(browserHost)) {
apiUrl.hostname = browserHost
return normalizeApiBaseUrl(apiUrl.toString())
}
} catch {
return normalized
}
return normalized
}
function readStoredApiBaseUrl() {
if (typeof window === 'undefined') {
return ''
}
return resolveBrowserReachableApiBaseUrl(window.localStorage.getItem(API_BASE_STORAGE_KEY) || '')
}
let runtimeApiBaseUrl = normalizeApiBaseUrl('/api/v1')
if (typeof window !== 'undefined') {
window.localStorage.removeItem(API_BASE_STORAGE_KEY)
}
export function setRuntimeApiBaseUrl(value) {
runtimeApiBaseUrl = resolveBrowserReachableApiBaseUrl(value)
if (typeof window !== 'undefined') {
window.localStorage.setItem(API_BASE_STORAGE_KEY, runtimeApiBaseUrl)
}
}
export function getRuntimeApiBaseUrl() {
return runtimeApiBaseUrl
}
function buildUrl(path) {
if (!path.startsWith('/')) {
return `${runtimeApiBaseUrl}/${path}`
}
return `${runtimeApiBaseUrl}${path}`
}
function formatValidationLocation(loc) {
if (!Array.isArray(loc)) {
return ''
}
return loc
.filter((item) => item !== 'body')
.map((item) => String(item || '').trim())
.filter(Boolean)
.join('.')
}
function resolveErrorMessage(payload, fallback = '接口请求失败,请稍后重试。') {
const detail = payload?.detail
if (typeof detail === 'string' && detail.trim()) {
return detail.trim()
}
if (Array.isArray(detail) && detail.length) {
const messages = detail
.map((item) => {
if (typeof item === 'string' && item.trim()) {
return item.trim()
}
if (!item || typeof item !== 'object') {
return ''
}
const message = String(item.msg || item.message || '').trim()
const location = formatValidationLocation(item.loc)
if (location && message) {
return `${location}: ${message}`
}
return message
})
.filter(Boolean)
if (messages.length) {
return messages.join('')
}
}
if (detail && typeof detail === 'object') {
const message = String(detail.message || detail.msg || '').trim()
if (message) {
return message
}
}
if (typeof payload?.message === 'string' && payload.message.trim()) {
return payload.message.trim()
}
return fallback
}
function sanitizeHeaders(headers) {
const nextHeaders = {}
Object.entries(headers || {}).forEach(([key, value]) => {
if (typeof value === 'undefined' || value === null) {
return
}
const normalizedValue = String(value).trim()
if (!normalizedValue) {
return
}
if (!isHeaderValueSafe(normalizedValue)) {
return
}
nextHeaders[key] = normalizedValue
})
return nextHeaders
}
export async function apiRequest(path, options = {}) {
const {
contentType = 'application/json',
responseType = 'json',
headers: customHeaders,
timeoutMs = 0,
timeoutMessage = '',
...fetchOptions
} = options
const headers = sanitizeHeaders({
...readCurrentUserHeaders(),
...(customHeaders || {})
})
if (contentType !== null && typeof headers['Content-Type'] === 'undefined') {
headers['Content-Type'] = contentType
}
let response
let timeoutId = 0
let requestOptions = {
...fetchOptions,
headers
}
if (!fetchOptions.signal && Number(timeoutMs) > 0 && typeof AbortController !== 'undefined') {
const controller = new AbortController()
timeoutId = globalThis.setTimeout(() => controller.abort(), Number(timeoutMs))
requestOptions = {
...requestOptions,
signal: controller.signal
}
}
try {
response = await fetch(buildUrl(path), requestOptions)
} catch (error) {
if (timeoutId) {
globalThis.clearTimeout(timeoutId)
}
if (error?.name === 'AbortError') {
throw new Error(String(timeoutMessage || '').trim() || '接口请求超时,请稍后重试。')
}
if (String(error?.message || '').includes('ByteString')) {
throw new Error('当前登录用户信息包含浏览器不支持的请求头字符,请重新登录后重试。')
}
throw new Error('无法连接 FastAPI 后端服务,请确认后端已启动且浏览器可访问后端端口。')
}
if (timeoutId) {
globalThis.clearTimeout(timeoutId)
}
if (responseType === 'blob') {
if (!response.ok) {
let payload = null
try {
payload = await response.json()
} catch {
payload = null
}
throw new Error(resolveErrorMessage(payload))
}
return response.blob()
}
let payload = null
try {
payload = await response.json()
} catch {
payload = null
}
if (!response.ok) {
throw new Error(resolveErrorMessage(payload))
}
return payload
}