Files
2fauth-worker/frontend/src/shared/utils/request.js
T

104 lines
4.3 KiB
JavaScript

import { ElMessage } from 'element-plus'
import router from '@/app/router'
import { useAuthUserStore } from '@/features/auth/store/authUserStore'
// 辅助函数:从 document.cookie 中安全地读取指定的 cookie 值
export function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
let isNavigatingToLogin = false; // 增加防重入锁,避免并发 401 触发无限弹窗和跳转
export async function request(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers,
}
const csrfToken = getCookie('csrf_token');
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
}
// --- 离线拦截逻辑 ---
// 拦截破坏性/写操作,在无网络时提供温和提示并短路执行
const method = (options.method || 'GET').toUpperCase();
if (!navigator.onLine && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
if (!options.silent) {
ElMessage.warning('当前处于离线模式,无法进行此操作');
}
throw new Error('Offline: Cannot perform mutative action');
}
try {
const response = await fetch(url, { ...options, headers, credentials: 'include' })
// 针对 204 No Content,没有 body,直接返回成功
if (response.status === 204) {
return { success: true };
}
const data = await response.json()
// 处理 401 Unauthorized (后端统一抛出的 AppError 会被 index.ts 捕获并返回 { code: 401, success: false, message: ...})
if (response.status === 401 || data.code === 401) {
if (options.silent) {
throw new Error(data.message || data.error || 'Unauthorized/Forbidden')
}
if (!isNavigatingToLogin) {
isNavigatingToLogin = true;
ElMessage.error(data.message || data.error || '会话已过期,请重新登录')
// 调用 Pinia Action 清空用户信息、缓存、以及重置内存状态,防止路由守卫死循环打回
try {
const authUserStore = useAuthUserStore()
authUserStore.clearUserInfo()
} catch (e) {
// Fallback to manual localstorage clear if store is unavailable
localStorage.removeItem('userInfo')
localStorage.removeItem('secure_vault')
localStorage.removeItem('backup_providers_cache')
sessionStorage.removeItem('vault_session_key')
}
if (router && router.currentRoute.value.path !== '/login') {
router.replace('/login').then(() => {
// 给个延迟再释放锁,防止快速回溯
setTimeout(() => { isNavigatingToLogin = false }, 1000)
}).catch(() => {
setTimeout(() => { isNavigatingToLogin = false }, 1000)
})
} else {
setTimeout(() => { isNavigatingToLogin = false }, 1000)
}
}
throw new Error(data.message || data.error || 'Unauthorized/Forbidden')
}
// 处理其他业务报错或 HTTP 错误
// 注意:兼容旧的 data.error 和新的 data.message
if (!response.ok || data.success === false) {
if (!options.silent) {
ElMessage.error(data.message || data.error || '请求失败')
}
throw new Error(data.message || data.error || '请求失败')
}
// 解构新的标准响应格式。如果后端返回 { success: true, data: [...] },这里直接将 data 铺平返回,保持对原有旧代码的兼容性。
// 或如果未被包装,直接 fallback 回 data 本身
if (data.data !== undefined && Object.keys(data).includes('code')) {
return { success: true, ...data.data }
}
return data
} catch (error) {
// 屏蔽被 silent 处理过的 Auth Error
if (error.message !== 'Unauthorized/Forbidden' && !options.silent) {
console.error('API Request Error:', error)
}
throw error
}
}