diff --git a/web/src/services/agentAssets.js b/web/src/services/agentAssets.js index 96dc7de..c5a5622 100644 --- a/web/src/services/agentAssets.js +++ b/web/src/services/agentAssets.js @@ -1,4 +1,4 @@ -import { apiRequest } from './api.js' +import { apiRequest, pickSafeHeaderValue } from './api.js' const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user' @@ -14,7 +14,11 @@ function readActorName() { try { const payload = JSON.parse(raw) - return String(payload?.name || payload?.username || 'system').trim() || 'system' + return ( + pickSafeHeaderValue(payload?.name, payload?.username) || + pickSafeHeaderValue(payload?.username, 'system') || + 'system' + ) } catch { return 'system' } diff --git a/web/src/services/api.js b/web/src/services/api.js index 106dcd6..e8cdbf0 100644 --- a/web/src/services/api.js +++ b/web/src/services/api.js @@ -1,37 +1,77 @@ -const API_BASE_STORAGE_KEY = 'x-financial-api-base-url' -const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user' - -function readCurrentUserHeaders() { - if (typeof window === 'undefined') { - return {} - } +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) - - if (!username && !name) { - return {} - } - - return { - 'x-auth-username': username, - 'x-auth-name': name, - 'x-auth-role-codes': roleCodes.join(','), - 'x-auth-is-admin': String(isAdmin) - } - } catch { - 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(/\/$/, '') @@ -158,33 +198,60 @@ function resolveErrorMessage(payload, fallback = '接口请求失败,请稍后 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, + headers: customHeaders, ...fetchOptions } = options - const headers = { - ...readCurrentUserHeaders(), - ...(customHeaders || {}) - } - - if (contentType !== null && typeof headers['Content-Type'] === 'undefined') { - headers['Content-Type'] = contentType - } + const headers = sanitizeHeaders({ + ...readCurrentUserHeaders(), + ...(customHeaders || {}) + }) + + if (contentType !== null && typeof headers['Content-Type'] === 'undefined') { + headers['Content-Type'] = contentType + } let response - try { - response = await fetch(buildUrl(path), { - ...fetchOptions, - headers - }) - } catch { - throw new Error('无法连接 FastAPI 后端服务,请确认后端已启动且浏览器可访问后端端口。') - } + try { + response = await fetch(buildUrl(path), { + ...fetchOptions, + headers + }) + } catch (error) { + if (String(error?.message || '').includes('ByteString')) { + throw new Error('当前登录用户信息包含浏览器不支持的请求头字符,请重新登录后重试。') + } + + throw new Error('无法连接 FastAPI 后端服务,请确认后端已启动且浏览器可访问后端端口。') + } if (responseType === 'blob') { if (!response.ok) { diff --git a/web/src/services/employees.js b/web/src/services/employees.js index 93c5f3d..33c9380 100644 --- a/web/src/services/employees.js +++ b/web/src/services/employees.js @@ -35,3 +35,9 @@ export function disableEmployee(employeeId) { method: 'POST' }) } + +export function enableEmployee(employeeId) { + return apiRequest(`/employees/${employeeId}/enable`, { + method: 'POST' + }) +}