import axios from 'axios'; import { appConfig } from '@/config'; const AES_KEY = 'xczx2026!@#secKe'; const ENCRYPTION_EXCLUDE_PATHS = ['/avatar/', '/f/', '/amr2mp3', '/appVersion/check']; const AUTH_STORAGE_KEY = 'qmt-web-im-app-auth-v1'; const AUTH_ERROR_CODE = 13; const REFRESH_ERROR_CODE = 20045; let importedCryptoKeyPromise = null; let refreshPromise = null; function storageScope() { return appConfig.appServer || ''; } function readAuthStore() { try { return JSON.parse(localStorage.getItem(AUTH_STORAGE_KEY) || '{}'); } catch (error) { return {}; } } function writeAuthStore(store) { localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(store)); } function normalizeAuthBundle(bundle = {}) { const accessToken = String(bundle.accessToken || '').trim(); const refreshToken = String(bundle.refreshToken || '').trim(); if (!accessToken || !refreshToken) { return null; } return { accessToken, refreshToken, tokenType: String(bundle.tokenType || 'Bearer').trim() || 'Bearer', accessTokenExpireIn: Number(bundle.accessTokenExpireIn || 0) || 0, accessTokenExpireAt: Number(bundle.accessTokenExpireAt || 0) || 0, refreshTokenExpireIn: Number(bundle.refreshTokenExpireIn || 0) || 0, refreshTokenExpireAt: Number(bundle.refreshTokenExpireAt || 0) || 0 }; } export function getStoredAppAuth() { const store = readAuthStore(); return normalizeAuthBundle(store[storageScope()]); } export function persistAppAuth(bundle) { const normalized = normalizeAuthBundle(bundle); const store = readAuthStore(); if (!normalized) { delete store[storageScope()]; writeAuthStore(store); return null; } store[storageScope()] = normalized; writeAuthStore(store); return normalized; } export function clearStoredAppAuth() { persistAppAuth(null); } function hasFormData(value) { return typeof FormData !== 'undefined' && value instanceof FormData; } function absoluteUrl(url) { return new URL(url, window.location.origin); } function shouldEncrypt(url) { const pathname = absoluteUrl(url).pathname; return !ENCRYPTION_EXCLUDE_PATHS.some(path => pathname === path || pathname.startsWith(path)); } function shouldAttachAuth(config) { return !config.skipAuth; } function textEncoder() { return new TextEncoder(); } function textDecoder() { return new TextDecoder(); } function bytesToBase64(bytes) { let binary = ''; bytes.forEach(byte => { binary += String.fromCharCode(byte); }); return btoa(binary); } function base64ToBytes(base64) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; } async function getCryptoKey() { if (!importedCryptoKeyPromise) { importedCryptoKeyPromise = window.crypto.subtle.importKey( 'raw', textEncoder().encode(AES_KEY), { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'] ); } return importedCryptoKeyPromise; } async function encryptPayload(plainText) { const iv = window.crypto.getRandomValues(new Uint8Array(16)); const key = await getCryptoKey(); const encrypted = await window.crypto.subtle.encrypt( { name: 'AES-CBC', iv }, key, textEncoder().encode(plainText) ); return `${bytesToBase64(iv)}:${bytesToBase64(new Uint8Array(encrypted))}`; } async function decryptPayload(cipherText) { const [ivBase64, encryptedBase64] = String(cipherText || '').split(':', 2); if (!ivBase64 || !encryptedBase64) { throw new Error('Encrypted payload format is invalid'); } const key = await getCryptoKey(); const decrypted = await window.crypto.subtle.decrypt( { name: 'AES-CBC', iv: base64ToBytes(ivBase64) }, key, base64ToBytes(encryptedBase64) ); return textDecoder().decode(decrypted); } function normalizeBusinessResponse(payload) { if (payload && typeof payload === 'object' && !Array.isArray(payload)) { return payload; } return { code: 0, message: 'success', data: payload }; } async function decodeResponsePayload(response) { const encrypted = String(response.headers?.['x-encrypted'] || '').toLowerCase() === 'true'; if (encrypted && typeof response.data === 'string') { const plainText = await decryptPayload(response.data); response.data = normalizeBusinessResponse(JSON.parse(plainText)); return response; } response.data = normalizeBusinessResponse(response.data); return response; } const client = axios.create({ withCredentials: false }); client.interceptors.request.use(async config => { const nextConfig = { ...config }; const url = absoluteUrl(nextConfig.url || '').toString(); if (shouldAttachAuth(nextConfig)) { const auth = getStoredAppAuth(); if (auth?.accessToken) { nextConfig.headers = { ...(nextConfig.headers || {}), Authorization: `${auth.tokenType || 'Bearer'} ${auth.accessToken}` }; } } if (!hasFormData(nextConfig.data) && nextConfig.method?.toLowerCase() !== 'get' && shouldEncrypt(url)) { const plainBody = JSON.stringify(nextConfig.data ?? {}); nextConfig.data = await encryptPayload(plainBody); nextConfig.headers = { ...(nextConfig.headers || {}), 'Content-Type': 'text/plain;charset=UTF-8', 'X-Timestamp': `${Math.floor(Date.now() / 1000)}` }; } return nextConfig; }); client.interceptors.response.use(async response => decodeResponsePayload(response)); async function refreshAccessToken() { if (refreshPromise) { return refreshPromise; } const auth = getStoredAppAuth(); if (!auth?.refreshToken) { clearStoredAppAuth(); throw new Error('登录状态已失效,请重新登录'); } refreshPromise = client.post(`${appConfig.appServer}/auth/refreshToken`, { refreshToken: auth.refreshToken }, { skipAuth: true }).then(response => { const payload = response.data || {}; if (Number(payload.code) !== 0 || !payload.data) { if (Number(payload.code) === REFRESH_ERROR_CODE) { clearStoredAppAuth(); } throw new Error(payload.message || '刷新登录状态失败'); } persistAppAuth(payload.data); return payload.data; }).finally(() => { refreshPromise = null; }); return refreshPromise; } async function request(config) { const response = await client.request(config); const payload = response.data || {}; if (Number(payload.code) === AUTH_ERROR_CODE && shouldAttachAuth(config) && !config._retriedAfterRefresh) { await refreshAccessToken(); return request({ ...config, _retriedAfterRefresh: true }); } return response; } export async function appServerRequest(config) { return request(config); } export async function appServerGet(url, config = {}) { return request({ ...config, method: 'get', url }); } export async function appServerPost(url, data = {}, config = {}) { return request({ ...config, method: 'post', url, data }); }