feat: 优化账户相关 API,移除显式传递 token,支持自动注入;添加访问令牌刷新机制

This commit is contained in:
SunWuyuan 2025-11-02 11:15:23 +08:00
parent 971f8c121e
commit 36d0da03fb
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
9 changed files with 250 additions and 80 deletions

View File

@ -56,6 +56,10 @@ watch(isOpen, (newVal) => {
if (newVal && activeTab.value === 'register' && !newUuid.value) { if (newVal && activeTab.value === 'register' && !newUuid.value) {
generateRandomUuid() generateRandomUuid()
} }
//
if (newVal && activeTab.value === 'register' && accountStore.isAuthenticated) {
bindToAccount.value = true
}
}) })
// //
@ -69,6 +73,10 @@ watch(activeTab, (newVal) => {
if (newVal === 'register' && !newUuid.value) { if (newVal === 'register' && !newUuid.value) {
generateRandomUuid() generateRandomUuid()
} }
//
if (newVal === 'register') {
bindToAccount.value = accountStore.isAuthenticated
}
}) })
// //
@ -113,7 +121,7 @@ const loadAccountDevices = async () => {
loadingDevices.value = true loadingDevices.value = true
try { try {
const response = await apiClient.getAccountDevices(accountStore.token) const response = await apiClient.getAccountDevices()
accountDevices.value = response.data || [] accountDevices.value = response.data || []
if (accountDevices.value.length === 0) { if (accountDevices.value.length === 0) {
@ -179,14 +187,13 @@ const registerDevice = async () => {
// 2. // 2.
await apiClient.registerDevice( await apiClient.registerDevice(
newUuid.value.trim(), newUuid.value.trim(),
deviceName.value.trim(), deviceName.value.trim()
accountStore.isAuthenticated ? accountStore.token : null
) )
// 3. // 3.
if (bindToAccount.value && accountStore.isAuthenticated) { if (bindToAccount.value && accountStore.isAuthenticated) {
try { try {
await apiClient.bindDeviceToAccount(accountStore.token, newUuid.value.trim()) await apiClient.bindDeviceToAccount(newUuid.value.trim())
} catch (error) { } catch (error) {
console.warn('设备绑定失败:', error.message) console.warn('设备绑定失败:', error.message)
toast.warning('设备注册成功,但绑定到账户失败') toast.warning('设备注册成功,但绑定到账户失败')

View File

@ -33,15 +33,6 @@ const features = [
iconBg: 'bg-purple-500/10', iconBg: 'bg-purple-500/10',
iconColor: 'text-purple-600 dark:text-purple-400', iconColor: 'text-purple-600 dark:text-purple-400',
}, },
{
title: 'KV 管理器',
description: '浏览和管理键值存储数据,支持批量操作',
icon: Database,
path: '/kv-manager',
color: 'from-green-500 to-emerald-500',
iconBg: 'bg-green-500/10',
iconColor: 'text-green-600 dark:text-green-400',
},
{ {
title: '设备管理', title: '设备管理',
description: '管理您账户下的所有设备,修改名称和密码', description: '管理您账户下的所有设备,修改名称和密码',

View File

@ -14,6 +14,11 @@ export function useOAuthCallback() {
const handleOAuthCallback = async () => { const handleOAuthCallback = async () => {
const { token, provider, color, success, error } = route.query const { token, provider, color, success, error } = route.query
// 新版参数access_token / refresh_token / expires_in / providerName / providerColor
const access_token = route.query.access_token || token
const refresh_token = route.query.refresh_token || null
const providerName = route.query.providerName || provider
const providerColor = route.query.providerColor || color
// 检查是否是OAuth回调 // 检查是否是OAuth回调
if (!success && !error) { if (!success && !error) {
@ -21,15 +26,15 @@ export function useOAuthCallback() {
} }
// 处理成功回调 // 处理成功回调
if (success === 'true' && token) { if (success === 'true' && access_token) {
try { try {
// 保存token到localStorage // 保存到 Store同时写入 localStorage
localStorage.setItem('auth_token', token) accountStore.setTokens(access_token, refresh_token)
localStorage.setItem('auth_provider', provider) if (providerName) localStorage.setItem('auth_provider', providerName)
if (color) localStorage.setItem('auth_provider_color', color) if (providerColor) localStorage.setItem('auth_provider_color', providerColor)
// 登录到store // 登录到store(加载资料、设备)
await accountStore.login(token) await accountStore.login(access_token)
// 显示成功提示 // 显示成功提示
toast.success('登录成功', { toast.success('登录成功', {
@ -37,22 +42,21 @@ export function useOAuthCallback() {
}) })
// 清除URL参数 // 清除URL参数
router.replace({ query: {} }) router.replace({ query: {} })
// 触发storage事件通知其他窗口 // 触发storage事件通知其他窗口
window.dispatchEvent(new StorageEvent('storage', { window.dispatchEvent(new StorageEvent('storage', { key: 'auth_token', newValue: access_token, url: window.location.href }))
key: 'auth_token', if (refresh_token) {
newValue: token, window.dispatchEvent(new StorageEvent('storage', { key: 'auth_refresh_token', newValue: refresh_token, url: window.location.href }))
url: window.location.href }
}))
// 如果是在新窗口中打开的OAuth回调自动关闭窗口 // 如果是在新窗口中打开的OAuth回调自动关闭窗口
if (window.opener) { if (window.opener) {
// 通知父窗口登录成功 // 通知父窗口登录成功
window.opener.postMessage({ window.opener.postMessage({
type: 'oauth_success', type: 'oauth_success',
token, token: access_token,
provider, provider: providerName,
}, window.location.origin) }, window.location.origin)
// 延迟关闭窗口,确保消息已发送 // 延迟关闭窗口,确保消息已发送
@ -111,6 +115,9 @@ export function useOAuthCallback() {
// 其他标签页已登录,刷新当前页面的状态 // 其他标签页已登录,刷新当前页面的状态
accountStore.login(e.newValue) accountStore.login(e.newValue)
} }
if (e.key === 'auth_refresh_token' && e.newValue) {
accountStore.setTokens(accountStore.token, e.newValue)
}
} }
onMounted(() => { onMounted(() => {

View File

@ -310,35 +310,29 @@ class ApiClient {
} }
} }
// 账户相关 API // 账户相关 APIAuthorization 由 axios 拦截器统一注入)
async getOAuthProviders() { async getOAuthProviders() {
return this.fetch('/accounts/oauth/providers') return this.fetch('/accounts/oauth/providers')
} }
async getAccountProfile(token) { async getAccountProfile() {
return this.fetch('/accounts/profile', { return this.fetch('/accounts/profile')
headers: { 'Authorization': `Bearer ${token}` }
})
} }
async getAccountDevices(token) { async getAccountDevices() {
return this.fetch('/accounts/devices', { return this.fetch('/accounts/devices')
headers: { 'Authorization': `Bearer ${token}` }
})
} }
async bindDevice(token, deviceUuid) { async bindDevice(deviceUuid) {
return this.fetch('/accounts/devices/bind', { return this.fetch('/accounts/devices/bind', {
method: 'POST', method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ uuid: deviceUuid }), body: JSON.stringify({ uuid: deviceUuid }),
}) })
} }
async unbindDevice(token, deviceUuid) { async unbindDevice(deviceUuid) {
return this.fetch('/accounts/devices/unbind', { return this.fetch('/accounts/devices/unbind', {
method: 'POST', method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ uuid: deviceUuid }), body: JSON.stringify({ uuid: deviceUuid }),
}) })
} }
@ -347,28 +341,44 @@ class ApiClient {
return this.fetch(`/accounts/device/${deviceUuid}/account`) return this.fetch(`/accounts/device/${deviceUuid}/account`)
} }
// ===== 账户 Token 刷新与信息 =====
async refreshAccessToken(refreshToken) {
return this.fetch('/accounts/refresh', {
method: 'POST',
// 刷新接口不应由请求拦截器附加旧的 Authorization
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
})
}
async getTokenInfo(accessToken) {
return this.fetch('/accounts/token-info', {
headers: { 'Authorization': `Bearer ${accessToken}` }
})
}
// 绑定设备到当前账户 // 绑定设备到当前账户
async bindDeviceToAccount(token, deviceUuid) { async bindDeviceToAccount(deviceUuid) {
return this.authenticatedFetch('/accounts/devices/bind', { return this.fetch('/accounts/devices/bind', {
method: 'POST', method: 'POST',
body: JSON.stringify({ uuid: deviceUuid }), body: JSON.stringify({ uuid: deviceUuid }),
}, token) })
} }
// 解绑设备 // 解绑设备
async unbindDeviceFromAccount(token, deviceUuid) { async unbindDeviceFromAccount(deviceUuid) {
return this.authenticatedFetch('/accounts/devices/unbind', { return this.fetch('/accounts/devices/unbind', {
method: 'POST', method: 'POST',
body: JSON.stringify({ uuid: deviceUuid }), body: JSON.stringify({ uuid: deviceUuid }),
}, token) })
} }
// 批量解绑设备 // 批量解绑设备
async batchUnbindDevices(token, deviceUuids) { async batchUnbindDevices(deviceUuids) {
return this.authenticatedFetch('/accounts/devices/unbind', { return this.fetch('/accounts/devices/unbind', {
method: 'POST', method: 'POST',
body: JSON.stringify({ uuids: deviceUuids }), body: JSON.stringify({ uuids: deviceUuids }),
}, token) })
} }
// 设备名称管理 API // 设备名称管理 API

View File

@ -14,9 +14,42 @@ const axiosInstance = axios.create({
}, },
}) })
// ===== 认证相关 Hooks由账户 Store 注入)=====
let authHandlers = {
// 可选:返回访问令牌
getAccessToken: () => null,
// 可选:返回刷新令牌
getRefreshToken: () => null,
// 可选:仅更新访问令牌
setAccessToken: (_t) => {},
// 可选:执行刷新动作,返回新的访问令牌
refreshAccessToken: null,
// 可选:当刷新失败时回调(例如触发登出)
onAuthFailure: () => {},
}
// 对外方法:由外部(如 Pinia store注入实际的处理函数
export function setAuthHandlers(handlers) {
authHandlers = { ...authHandlers, ...(handlers || {}) }
}
// 请求拦截器 // 请求拦截器
axiosInstance.interceptors.request.use( axiosInstance.interceptors.request.use(
(config) => { (config) => {
try {
// 为非刷新接口自动附加 Authorization 头
const isRefreshRequest = typeof config.url === 'string' && /\/accounts\/refresh(\b|\/|\?)/.test(config.url)
const skipAuth = config.__skipAuth === true || isRefreshRequest
if (!skipAuth && authHandlers?.getAccessToken) {
const token = authHandlers.getAccessToken()
if (token) {
config.headers = config.headers || {}
config.headers['Authorization'] = `Bearer ${token}`
}
}
} catch {
// ignore
}
return config return config
}, },
(error) => { (error) => {
@ -96,17 +129,62 @@ async function ensureDeviceRegistered(uuid, authHeader) {
} }
} }
// 响应拦截器(含自动注册并重试) // 刷新中的 Promise用于合并并发 401 刷新)
let refreshingPromise = null
// 响应拦截器含自动换发、自动注册并重试、401 刷新)
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
(response) => { (response) => {
// 如果服务端主动通过响应头下发新访问令牌,更新本地
try {
const headers = response?.headers || {}
const newToken = getHeaderIgnoreCase(headers, 'X-New-Access-Token')
if (newToken && authHandlers?.setAccessToken) {
authHandlers.setAccessToken(newToken)
}
} catch {}
return response.data return response.data
}, },
async (error) => { async (error) => {
const config = error.config || {} const config = error.config || {}
const resp = error.response
const status = resp?.status
const skip = config.skipDeviceRegistrationRetry || config.__isRegistrationRequest const skip = config.skipDeviceRegistrationRetry || config.__isRegistrationRequest
const backendMessage = error.response?.data?.message const backendMessage = resp?.data?.message
const message = backendMessage || error.message || 'Unknown error' const message = backendMessage || error.message || 'Unknown error'
// 优先处理 401尝试使用刷新令牌换发
if (status === 401 && !config.__retriedAfterRefresh) {
try {
// 若没有刷新能力或没有刷新令牌,则直接走失败逻辑
if (!authHandlers?.refreshAccessToken || !authHandlers?.getRefreshToken || !authHandlers.getRefreshToken()) {
throw new Error('NO_REFRESH_TOKEN')
}
// 合并并发刷新
if (!refreshingPromise) {
refreshingPromise = authHandlers.refreshAccessToken()
.catch((e) => {
// 刷新失败,触发失败处理
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(e) } catch {}
throw e
})
.finally(() => {
refreshingPromise = null
})
}
await refreshingPromise
// 刷新成功后重试一次原请求
config.__retriedAfterRefresh = true
// 由请求拦截器负责附加新 Authorization无需手动改 headers
return await axiosInstance.request(config)
} catch (refreshErr) {
// 刷新失败,返回原始错误信息
return Promise.reject(new Error(message))
}
}
// 仅在后端提示设备不存在时尝试注册并重试,且保证只重试一次 // 仅在后端提示设备不存在时尝试注册并重试,且保证只重试一次
if (!skip && !config.__retriedAfterRegistration && typeof backendMessage === 'string' && backendMessage.startsWith('设备不存在')) { if (!skip && !config.__retriedAfterRegistration && typeof backendMessage === 'string' && backendMessage.startsWith('设备不存在')) {
// 从 headers / url / body 提取 uuid // 从 headers / url / body 提取 uuid
@ -139,6 +217,7 @@ axiosInstance.interceptors.response.use(
} }
} }
// 其他错误:附带信息抛出
return Promise.reject(new Error(message)) return Promise.reject(new Error(message))
} }
) )

View File

@ -108,9 +108,9 @@ const checkDeviceAndLoad = async () => {
try { try {
deviceInfo.value = await apiClient.getDeviceInfo(uuid) deviceInfo.value = await apiClient.getDeviceInfo(uuid)
console.log(deviceInfo.value) console.log(deviceInfo.value)
console.log(accountStore) console.log(accountStore.profile)
// //
if (!deviceInfo.value.account.id || deviceInfo.value.account.id !== accountStore.profile.id) { if (!deviceInfo.value.account || !deviceInfo.value.account.id || deviceInfo.value.account.id !== accountStore.profile.id) {
toast.error('该设备未绑定到您的账户', { toast.error('该设备未绑定到您的账户', {
description: '请先在主页绑定设备到您的账户' description: '请先在主页绑定设备到您的账户'
}) })

View File

@ -48,7 +48,7 @@ const loadDevices = async () => {
isLoading.value = true isLoading.value = true
try { try {
const response = await apiClient.getAccountDevices(accountStore.token) const response = await apiClient.getAccountDevices()
devices.value = response.data || [] devices.value = response.data || []
} catch (error) { } catch (error) {
toast.error('加载设备列表失败:' + error.message) toast.error('加载设备列表失败:' + error.message)
@ -68,7 +68,7 @@ const unbindDevice = async () => {
if (!currentDevice.value) return if (!currentDevice.value) return
try { try {
await apiClient.unbindDeviceFromAccount(accountStore.token, currentDevice.value.uuid) await apiClient.unbindDeviceFromAccount(currentDevice.value.uuid)
toast.success('设备已解绑') toast.success('设备已解绑')
showDeleteDialog.value = false showDeleteDialog.value = false
currentDevice.value = null currentDevice.value = null

View File

@ -161,9 +161,7 @@ const authorizeApp = async () => {
options.password = authPassword.value options.password = authPassword.value
} }
if (accountStore.isAuthenticated) { // tokenaxios Authorization
options.token = accountStore.token
}
// //
await apiClient.authorizeApp( await apiClient.authorizeApp(
@ -327,7 +325,7 @@ const bindCurrentDevice = async () => {
} }
try { try {
await apiClient.bindDeviceToAccount(accountStore.token, deviceUuid.value) await apiClient.bindDeviceToAccount(deviceUuid.value)
await loadDeviceInfo() await loadDeviceInfo()
toast.success('设备已绑定到您的账户') toast.success('设备已绑定到您的账户')
} catch (error) { } catch (error) {
@ -336,10 +334,9 @@ const bindCurrentDevice = async () => {
try { try {
await apiClient.registerDevice( await apiClient.registerDevice(
deviceUuid.value, deviceUuid.value,
deviceInfo.value?.deviceName || null, deviceInfo.value?.deviceName || null
accountStore.token
) )
await apiClient.bindDeviceToAccount(accountStore.token, deviceUuid.value) await apiClient.bindDeviceToAccount(deviceUuid.value)
await loadDeviceInfo() await loadDeviceInfo()
toast.success('设备已注册并绑定到您的账户') toast.success('设备已注册并绑定到您的账户')
} catch (retryError) { } catch (retryError) {
@ -359,7 +356,7 @@ const unbindCurrentDevice = async () => {
} }
try { try {
await apiClient.unbindDeviceFromAccount(accountStore.token, deviceUuid.value) await apiClient.unbindDeviceFromAccount(deviceUuid.value)
await loadDeviceInfo() await loadDeviceInfo()
toast.success('设备已解绑') toast.success('设备已解绑')
} catch (error) { } catch (error) {
@ -421,7 +418,7 @@ onMounted(async () => {
<h1 class="text-2xl font-bold bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text"> <h1 class="text-2xl font-bold bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
Classworks KV Classworks KV
</h1> </h1>
<p class="text-sm text-muted-foreground">云原生键值数据库</p> <p class="text-sm text-muted-foreground">文档形键值数据库</p>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">

View File

@ -1,15 +1,20 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { apiClient } from '@/lib/api' import { apiClient } from '@/lib/api'
import { setAuthHandlers } from '@/lib/axios'
export const useAccountStore = defineStore('account', () => { export const useAccountStore = defineStore('account', () => {
// 状态 // 状态
const token = ref(localStorage.getItem('auth_token') || null) // 访问令牌(兼容旧 key: auth_token
const token = ref(localStorage.getItem('auth_token') || localStorage.getItem('auth_access_token') || null)
const refreshToken = ref(localStorage.getItem('auth_refresh_token') || null)
const accessExpiresAt = ref(Number(localStorage.getItem('auth_access_exp') || 0) || 0) // ms 时间戳
const profile = ref(null) const profile = ref(null)
const devices = ref([]) const devices = ref([])
const loading = ref(false) const loading = ref(false)
const providerName = ref(localStorage.getItem('auth_provider') || '') const providerName = ref(localStorage.getItem('auth_provider') || '')
const providerColor = ref(localStorage.getItem('auth_provider_color') || '') const providerColor = ref(localStorage.getItem('auth_provider_color') || '')
let proactiveTimer = null
// 计算属性 // 计算属性
const isAuthenticated = computed(() => !!token.value) const isAuthenticated = computed(() => !!token.value)
@ -19,17 +24,70 @@ export const useAccountStore = defineStore('account', () => {
const userProviderDisplay = computed(() => profile.value?.providerInfo?.displayName || profile.value?.providerInfo?.name || providerName.value || profile.value?.provider || '') const userProviderDisplay = computed(() => profile.value?.providerInfo?.displayName || profile.value?.providerInfo?.name || providerName.value || profile.value?.provider || '')
const userProviderColor = computed(() => profile.value?.providerInfo?.color || providerColor.value || '') const userProviderColor = computed(() => profile.value?.providerInfo?.color || providerColor.value || '')
// 工具:解析 JWT 过期时间(秒时间戳),返回 ms
function decodeJwtExpMs(jwt) {
try {
const [, payload] = jwt.split('.')
if (!payload) return 0
const json = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')))
if (!json?.exp) return 0
return Number(json.exp) * 1000
} catch {
return 0
}
}
function clearProactiveTimer() {
if (proactiveTimer) {
clearTimeout(proactiveTimer)
proactiveTimer = null
}
}
function scheduleProactiveRefresh() {
clearProactiveTimer()
if (!token.value) return
const expMs = accessExpiresAt.value || decodeJwtExpMs(token.value)
if (!expMs) return
const now = Date.now()
// 提前 2 分钟刷新,最小 5 秒
const lead = 2 * 60 * 1000
let delay = expMs - now - lead
if (delay < 5000) delay = 5000
proactiveTimer = setTimeout(() => {
refreshNow().catch(() => {})
}, delay)
}
// 方法 // 方法
const setToken = (newToken) => { const setToken = (newToken) => {
token.value = newToken token.value = newToken
if (newToken) { if (newToken) {
localStorage.setItem('auth_token', newToken) localStorage.setItem('auth_token', newToken)
localStorage.setItem('auth_access_token', newToken)
accessExpiresAt.value = decodeJwtExpMs(newToken) || 0
if (accessExpiresAt.value) localStorage.setItem('auth_access_exp', String(accessExpiresAt.value))
scheduleProactiveRefresh()
} else { } else {
localStorage.removeItem('auth_token') localStorage.removeItem('auth_token')
localStorage.removeItem('auth_access_token')
localStorage.removeItem('auth_access_exp')
localStorage.removeItem('auth_refresh_token')
localStorage.removeItem('auth_provider') localStorage.removeItem('auth_provider')
localStorage.removeItem('auth_provider_color') localStorage.removeItem('auth_provider_color')
providerName.value = '' providerName.value = ''
providerColor.value = '' providerColor.value = ''
refreshToken.value = null
accessExpiresAt.value = 0
clearProactiveTimer()
}
}
const setTokens = (access, refresh) => {
if (access) setToken(access)
if (refresh) {
refreshToken.value = refresh
localStorage.setItem('auth_refresh_token', refresh)
} }
} }
@ -38,7 +96,7 @@ export const useAccountStore = defineStore('account', () => {
loading.value = true loading.value = true
try { try {
const response = await apiClient.getAccountProfile(token.value) const response = await apiClient.getAccountProfile()
profile.value = response.data profile.value = response.data
// 若后端返回 providerInfo则回填前端展示字段 // 若后端返回 providerInfo则回填前端展示字段
const p = profile.value?.providerInfo const p = profile.value?.providerInfo
@ -50,10 +108,6 @@ export const useAccountStore = defineStore('account', () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to load profile:', error) console.error('Failed to load profile:', error)
// Token可能无效清除
if (error.message.includes('401')) {
logout()
}
} finally { } finally {
loading.value = false loading.value = false
} }
@ -63,7 +117,7 @@ export const useAccountStore = defineStore('account', () => {
if (!token.value) return if (!token.value) return
try { try {
const response = await apiClient.getAccountDevices(token.value) const response = await apiClient.getAccountDevices()
devices.value = response.data || [] devices.value = response.data || []
return devices.value return devices.value
} catch (error) { } catch (error) {
@ -75,7 +129,7 @@ export const useAccountStore = defineStore('account', () => {
const bindDevice = async (deviceUuid) => { const bindDevice = async (deviceUuid) => {
if (!token.value) throw new Error('未登录') if (!token.value) throw new Error('未登录')
const response = await apiClient.bindDevice(token.value, deviceUuid) const response = await apiClient.bindDevice(deviceUuid)
// 重新加载设备列表 // 重新加载设备列表
await loadDevices() await loadDevices()
return response return response
@ -84,7 +138,7 @@ export const useAccountStore = defineStore('account', () => {
const unbindDevice = async (deviceUuid) => { const unbindDevice = async (deviceUuid) => {
if (!token.value) throw new Error('未登录') if (!token.value) throw new Error('未登录')
const response = await apiClient.unbindDevice(token.value, deviceUuid) const response = await apiClient.unbindDevice(deviceUuid)
// 重新加载设备列表 // 重新加载设备列表
await loadDevices() await loadDevices()
return response return response
@ -100,15 +154,37 @@ export const useAccountStore = defineStore('account', () => {
token.value = null token.value = null
profile.value = null profile.value = null
devices.value = [] devices.value = []
localStorage.removeItem('auth_token') setToken(null)
localStorage.removeItem('auth_provider')
localStorage.removeItem('auth_provider_color')
providerName.value = ''
providerColor.value = ''
} }
// 初始化时加载用户信息 // 刷新访问令牌(按需刷新)
const refreshNow = async () => {
if (!refreshToken.value) throw new Error('No refresh token')
const res = await apiClient.refreshAccessToken(refreshToken.value)
// 期望结构:{ success, data: { access_token, expires_in, account? } }
const newAccess = res?.data?.access_token || res?.access_token || res?.token || null
if (!newAccess) throw new Error(res?.message || '刷新失败')
setToken(newAccess)
return newAccess
}
// 初始化注入 axios 认证处理
setAuthHandlers({
getAccessToken: () => token.value,
getRefreshToken: () => refreshToken.value,
setAccessToken: (t) => setToken(t),
refreshAccessToken: () => refreshNow(),
onAuthFailure: () => logout(),
})
// 初始化:迁移旧存储并设置定时刷新
if (token.value) { if (token.value) {
// 若未存储过期时间尝试从JWT解析
if (!accessExpiresAt.value) {
accessExpiresAt.value = decodeJwtExpMs(token.value) || 0
if (accessExpiresAt.value) localStorage.setItem('auth_access_exp', String(accessExpiresAt.value))
}
scheduleProactiveRefresh()
loadProfile() loadProfile()
loadDevices() loadDevices()
} }
@ -116,6 +192,7 @@ export const useAccountStore = defineStore('account', () => {
return { return {
// 状态 // 状态
token, token,
refreshToken,
profile, profile,
devices, devices,
loading, loading,
@ -130,11 +207,13 @@ export const useAccountStore = defineStore('account', () => {
userProviderColor, userProviderColor,
// 方法 // 方法
setToken, setToken,
setTokens,
loadProfile, loadProfile,
loadDevices, loadDevices,
bindDevice, bindDevice,
unbindDevice, unbindDevice,
login, login,
logout, logout,
refreshNow,
} }
}) })