import { isVoiceHubApi } from '~/utils/url' export default defineNuxtPlugin({ name: 'auth-client', enforce: 'pre', setup(nuxtApp) { // 立即设置请求拦截器,确保在任何API调用之前生效 const originalFetch = window.fetch const errorHandler = useErrorHandler() const auth = useAuth() const router = useRouter() let passwordStateRefresh: Promise | null = null const refreshPasswordState = async () => { if (!passwordStateRefresh) { passwordStateRefresh = auth.initAuth(true).finally(() => { passwordStateRefresh = null }) } return passwordStateRefresh } const handlePasswordChangeRequired = async (errorData: any) => { const requirePasswordChange = errorData?.data?.requirePasswordChange === true || errorData?.requirePasswordChange === true if (!requirePasswordChange) return false // 以服务端刷新后的状态为准,仅刷新失败时回退信任 403 信号,避免过期信号把已改密用户锁在改密页 let refreshed = true try { await refreshPasswordState() } catch { refreshed = false } if (!refreshed && auth.user.value && auth.user.value.requirePasswordChange !== true) { auth.user.value.requirePasswordChange = true } if ( auth.user.value?.requirePasswordChange === true && window.location.pathname !== '/change-password' ) { await router.replace('/change-password') } return true } // 拦截window.fetch window.fetch = async function ( input: RequestInfo | URL, init?: RequestInit ): Promise { // 处理所有API请求 if (isVoiceHubApi(input)) { init = init || {} init.headers = init.headers || {} // 确保cookie会被发送 init.credentials = 'include' } const response = await originalFetch(input, init) if (response.status === 403 && isVoiceHubApi(input)) { try { await handlePasswordChangeRequired(await response.clone().json()) } catch { // 非强制改密的 403 保持原有响应处理。 } } // 检查是否为401错误,并且该请求属于VoiceHub API if (response.status === 401 && isVoiceHubApi(input)) { const currentPath = window.location.pathname // 如果在登录页面,解析错误响应体并抛出具体错误信息 if (currentPath === '/login') { try { const errorData = await response.clone().json() const errorMessage = errorData.message || '登录失败,请检查账号密码' return Promise.reject(new Error(errorMessage)) } catch (parseError) { return Promise.reject(new Error('登录失败,请检查账号密码')) } } // 只有在非登录页面才触发认证失效处理 await errorHandler.handle401Error('您的登录信息已失效,请重新登录') return Promise.reject(new Error('Authentication expired')) } return response } // 初始化认证状态 nuxtApp.hook('app:created', async () => { await auth.initAuth() // 拦截$fetch请求,确保cookie会被发送 const originalUseFetch = nuxtApp.$fetch if (originalUseFetch) { nuxtApp.$fetch = async function (request: any, options: any = {}) { // 为所有API请求确保cookie会被发送 if (isVoiceHubApi(request)) { options.headers = options.headers || {} // 确保cookie会被发送 options.credentials = 'include' } try { return await originalUseFetch(request, options) } catch (error: any) { if ((error?.status === 403 || error?.statusCode === 403) && isVoiceHubApi(request)) { await handlePasswordChangeRequired(error?.data) } // 检查是否为401错误,并且该请求属于VoiceHub API if ((error?.status === 401 || error?.statusCode === 401) && isVoiceHubApi(request)) { const currentPath = window.location.pathname // 如果在登录页面,解析错误信息并抛出具体错误 if (currentPath === '/login') { // 从error对象中提取具体错误信息,过滤掉网络请求信息 let errorMessage = error?.data?.message || error?.message || '登录失败,请检查账号密码' // 如果错误信息包含网络请求格式(如 [POST] "/api/auth/login": ),则提取纯净的错误信息 if (typeof errorMessage === 'string') { // 匹配并移除网络请求前缀格式 const cleanMessage = errorMessage .replace(/^\[\w+\]\s+"[^"]+":\s+(\s+)?/, '') .trim() errorMessage = cleanMessage || '登录失败,请检查账号密码' } const newError = new Error(errorMessage) throw newError } // 只有在非登录页面才触发认证失效处理 await errorHandler.handle401Error('您的登录信息已失效,请重新登录') throw error } throw error } } } }) // 全局错误处理 nuxtApp.hook('vue:error', async (error: any) => { if (error?.status === 401 || error?.statusCode === 401) { const currentPath = window.location.pathname if (currentPath !== '/login') { await errorHandler.handle401Error('您的登录信息已失效,请重新登录') } } }) } })