mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-12-08 19:23:10 +00:00
Compare commits
3 Commits
971f8c121e
...
95dedb6e29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95dedb6e29 | ||
|
|
dfa1c0f2f8 | ||
|
|
36d0da03fb |
@ -56,6 +56,10 @@ watch(isOpen, (newVal) => {
|
||||
if (newVal && activeTab.value === 'register' && !newUuid.value) {
|
||||
generateRandomUuid()
|
||||
}
|
||||
// 对话框打开且当前为注册页时,根据登录状态默认勾选绑定
|
||||
if (newVal && activeTab.value === 'register' && accountStore.isAuthenticated) {
|
||||
bindToAccount.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选项卡切换
|
||||
@ -69,6 +73,10 @@ watch(activeTab, (newVal) => {
|
||||
if (newVal === 'register' && !newUuid.value) {
|
||||
generateRandomUuid()
|
||||
}
|
||||
// 进入注册页时,根据当前登录状态设置“绑定到账户”的默认勾选
|
||||
if (newVal === 'register') {
|
||||
bindToAccount.value = accountStore.isAuthenticated
|
||||
}
|
||||
})
|
||||
|
||||
// 监听是否登录,自动设置绑定选项
|
||||
@ -113,7 +121,7 @@ const loadAccountDevices = async () => {
|
||||
|
||||
loadingDevices.value = true
|
||||
try {
|
||||
const response = await apiClient.getAccountDevices(accountStore.token)
|
||||
const response = await apiClient.getAccountDevices()
|
||||
accountDevices.value = response.data || []
|
||||
|
||||
if (accountDevices.value.length === 0) {
|
||||
@ -179,14 +187,13 @@ const registerDevice = async () => {
|
||||
// 2. 调用设备注册接口(会自动在云端创建设备)
|
||||
await apiClient.registerDevice(
|
||||
newUuid.value.trim(),
|
||||
deviceName.value.trim(),
|
||||
accountStore.isAuthenticated ? accountStore.token : null
|
||||
deviceName.value.trim()
|
||||
)
|
||||
|
||||
// 3. 如果选择绑定到账户,现在可以安全地绑定
|
||||
if (bindToAccount.value && accountStore.isAuthenticated) {
|
||||
try {
|
||||
await apiClient.bindDeviceToAccount(accountStore.token, newUuid.value.trim())
|
||||
await apiClient.bindDeviceToAccount(newUuid.value.trim())
|
||||
} catch (error) {
|
||||
console.warn('设备绑定失败:', error.message)
|
||||
toast.warning('设备注册成功,但绑定到账户失败')
|
||||
|
||||
@ -32,8 +32,7 @@ const features = [
|
||||
color: 'from-purple-500 to-pink-500',
|
||||
iconBg: 'bg-purple-500/10',
|
||||
iconColor: 'text-purple-600 dark:text-purple-400',
|
||||
},
|
||||
{
|
||||
}, {
|
||||
title: 'KV 管理器',
|
||||
description: '浏览和管理键值存储数据,支持批量操作',
|
||||
icon: Database,
|
||||
|
||||
@ -14,6 +14,11 @@ export function useOAuthCallback() {
|
||||
|
||||
const handleOAuthCallback = async () => {
|
||||
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回调
|
||||
if (!success && !error) {
|
||||
@ -21,15 +26,15 @@ export function useOAuthCallback() {
|
||||
}
|
||||
|
||||
// 处理成功回调
|
||||
if (success === 'true' && token) {
|
||||
if (success === 'true' && access_token) {
|
||||
try {
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('auth_token', token)
|
||||
localStorage.setItem('auth_provider', provider)
|
||||
if (color) localStorage.setItem('auth_provider_color', color)
|
||||
// 保存到 Store(同时写入 localStorage)
|
||||
accountStore.setTokens(access_token, refresh_token)
|
||||
if (providerName) localStorage.setItem('auth_provider', providerName)
|
||||
if (providerColor) localStorage.setItem('auth_provider_color', providerColor)
|
||||
|
||||
// 登录到store
|
||||
await accountStore.login(token)
|
||||
// 登录到store(加载资料、设备)
|
||||
await accountStore.login(access_token)
|
||||
|
||||
// 显示成功提示
|
||||
toast.success('登录成功', {
|
||||
@ -40,19 +45,18 @@ export function useOAuthCallback() {
|
||||
router.replace({ query: {} })
|
||||
|
||||
// 触发storage事件,通知其他窗口
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'auth_token',
|
||||
newValue: token,
|
||||
url: window.location.href
|
||||
}))
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'auth_token', newValue: access_token, url: window.location.href }))
|
||||
if (refresh_token) {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'auth_refresh_token', newValue: refresh_token, url: window.location.href }))
|
||||
}
|
||||
|
||||
// 如果是在新窗口中打开的OAuth回调,自动关闭窗口
|
||||
if (window.opener) {
|
||||
// 通知父窗口登录成功
|
||||
window.opener.postMessage({
|
||||
type: 'oauth_success',
|
||||
token,
|
||||
provider,
|
||||
token: access_token,
|
||||
provider: providerName,
|
||||
}, window.location.origin)
|
||||
|
||||
// 延迟关闭窗口,确保消息已发送
|
||||
@ -111,6 +115,9 @@ export function useOAuthCallback() {
|
||||
// 其他标签页已登录,刷新当前页面的状态
|
||||
accountStore.login(e.newValue)
|
||||
}
|
||||
if (e.key === 'auth_refresh_token' && e.newValue) {
|
||||
accountStore.setTokens(accountStore.token, e.newValue)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -15,6 +15,7 @@ class ApiClient {
|
||||
const data = options.body
|
||||
const params = options.params
|
||||
|
||||
try {
|
||||
// 通过 axios 实例发起请求(已内置 baseURL 与 x-site-key)
|
||||
const result = await axiosInstance.request({
|
||||
url: endpoint,
|
||||
@ -27,6 +28,14 @@ class ApiClient {
|
||||
// axios 响应拦截器已返回 response.data,这里做空值统一
|
||||
if (result === '' || result === undefined || result === null) return {}
|
||||
return result
|
||||
} catch (err) {
|
||||
// 某些后端会在非 2xx 状态下直接返回有效数据,这里兜底返回 body
|
||||
const resp = err?.response
|
||||
if (resp && resp.data !== undefined) {
|
||||
return resp.data
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// 带认证的fetch
|
||||
@ -310,35 +319,29 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 账户相关 API
|
||||
// 账户相关 API(Authorization 由 axios 拦截器统一注入)
|
||||
async getOAuthProviders() {
|
||||
return this.fetch('/accounts/oauth/providers')
|
||||
}
|
||||
|
||||
async getAccountProfile(token) {
|
||||
return this.fetch('/accounts/profile', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
async getAccountProfile() {
|
||||
return this.fetch('/accounts/profile')
|
||||
}
|
||||
|
||||
async getAccountDevices(token) {
|
||||
return this.fetch('/accounts/devices', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
async getAccountDevices() {
|
||||
return this.fetch('/accounts/devices')
|
||||
}
|
||||
|
||||
async bindDevice(token, deviceUuid) {
|
||||
async bindDevice(deviceUuid) {
|
||||
return this.fetch('/accounts/devices/bind', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify({ uuid: deviceUuid }),
|
||||
})
|
||||
}
|
||||
|
||||
async unbindDevice(token, deviceUuid) {
|
||||
async unbindDevice(deviceUuid) {
|
||||
return this.fetch('/accounts/devices/unbind', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify({ uuid: deviceUuid }),
|
||||
})
|
||||
}
|
||||
@ -347,28 +350,44 @@ class ApiClient {
|
||||
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) {
|
||||
return this.authenticatedFetch('/accounts/devices/bind', {
|
||||
async bindDeviceToAccount(deviceUuid) {
|
||||
return this.fetch('/accounts/devices/bind', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ uuid: deviceUuid }),
|
||||
}, token)
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑设备
|
||||
async unbindDeviceFromAccount(token, deviceUuid) {
|
||||
return this.authenticatedFetch('/accounts/devices/unbind', {
|
||||
async unbindDeviceFromAccount(deviceUuid) {
|
||||
return this.fetch('/accounts/devices/unbind', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ uuid: deviceUuid }),
|
||||
}, token)
|
||||
})
|
||||
}
|
||||
|
||||
// 批量解绑设备
|
||||
async batchUnbindDevices(token, deviceUuids) {
|
||||
return this.authenticatedFetch('/accounts/devices/unbind', {
|
||||
async batchUnbindDevices(deviceUuids) {
|
||||
return this.fetch('/accounts/devices/unbind', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ uuids: deviceUuids }),
|
||||
}, token)
|
||||
})
|
||||
}
|
||||
|
||||
// 设备名称管理 API
|
||||
|
||||
@ -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(
|
||||
(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
|
||||
},
|
||||
(error) => {
|
||||
@ -96,17 +129,71 @@ async function ensureDeviceRegistered(uuid, authHeader) {
|
||||
}
|
||||
}
|
||||
|
||||
// 响应拦截器(含自动注册并重试)
|
||||
// 刷新中的 Promise(用于合并并发 401 刷新)
|
||||
let refreshingPromise = null
|
||||
|
||||
// 响应拦截器(含自动换发、自动注册并重试、401 刷新)
|
||||
axiosInstance.interceptors.response.use(
|
||||
(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
|
||||
},
|
||||
async (error) => {
|
||||
const config = error.config || {}
|
||||
const resp = error.response
|
||||
const status = resp?.status
|
||||
const skip = config.skipDeviceRegistrationRetry || config.__isRegistrationRequest
|
||||
const backendMessage = error.response?.data?.message
|
||||
const backendMessage = resp?.data?.message
|
||||
const message = backendMessage || error.message || 'Unknown error'
|
||||
|
||||
// 优先处理 401:尝试使用刷新令牌换发
|
||||
if (status === 401 && !config.__retriedAfterRefresh) {
|
||||
try {
|
||||
// 若没有刷新能力或没有刷新令牌,则直接走失败逻辑
|
||||
if (!authHandlers?.refreshAccessToken || !authHandlers?.getRefreshToken || !authHandlers.getRefreshToken()) {
|
||||
// 无法刷新,触发认证失败回调并退出
|
||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(new Error('NO_REFRESH_TOKEN')) } catch {}
|
||||
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) {
|
||||
// 刷新失败,触发认证失败并返回原始错误信息
|
||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(refreshErr) } catch {}
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
}
|
||||
|
||||
// 明确的权限问题同样触发登出(例如服务端使用 403 表示 Token 无效或权限已失效)
|
||||
if (status === 403) {
|
||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(new Error(message || 'FORBIDDEN')) } catch {}
|
||||
return Promise.reject(new Error(message || 'FORBIDDEN'))
|
||||
}
|
||||
|
||||
// 仅在后端提示设备不存在时尝试注册并重试,且保证只重试一次
|
||||
if (!skip && !config.__retriedAfterRegistration && typeof backendMessage === 'string' && backendMessage.startsWith('设备不存在')) {
|
||||
// 从 headers / url / body 提取 uuid
|
||||
@ -139,6 +226,7 @@ axiosInstance.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
|
||||
// 其他错误:附带信息抛出
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
)
|
||||
|
||||
@ -108,9 +108,9 @@ const checkDeviceAndLoad = async () => {
|
||||
try {
|
||||
deviceInfo.value = await apiClient.getDeviceInfo(uuid)
|
||||
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('该设备未绑定到您的账户', {
|
||||
description: '请先在主页绑定设备到您的账户'
|
||||
})
|
||||
|
||||
@ -48,7 +48,7 @@ const loadDevices = async () => {
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await apiClient.getAccountDevices(accountStore.token)
|
||||
const response = await apiClient.getAccountDevices()
|
||||
devices.value = response.data || []
|
||||
} catch (error) {
|
||||
toast.error('加载设备列表失败:' + error.message)
|
||||
@ -68,7 +68,7 @@ const unbindDevice = async () => {
|
||||
if (!currentDevice.value) return
|
||||
|
||||
try {
|
||||
await apiClient.unbindDeviceFromAccount(accountStore.token, currentDevice.value.uuid)
|
||||
await apiClient.unbindDeviceFromAccount(currentDevice.value.uuid)
|
||||
toast.success('设备已解绑')
|
||||
showDeleteDialog.value = false
|
||||
currentDevice.value = null
|
||||
|
||||
@ -161,9 +161,7 @@ const authorizeApp = async () => {
|
||||
options.password = authPassword.value
|
||||
}
|
||||
|
||||
if (accountStore.isAuthenticated) {
|
||||
options.token = accountStore.token
|
||||
}
|
||||
// 账户已登录时无需显式传 token,axios 会自动注入 Authorization
|
||||
|
||||
// 调用授权接口
|
||||
await apiClient.authorizeApp(
|
||||
@ -327,7 +325,7 @@ const bindCurrentDevice = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.bindDeviceToAccount(accountStore.token, deviceUuid.value)
|
||||
await apiClient.bindDeviceToAccount(deviceUuid.value)
|
||||
await loadDeviceInfo()
|
||||
toast.success('设备已绑定到您的账户')
|
||||
} catch (error) {
|
||||
@ -336,10 +334,9 @@ const bindCurrentDevice = async () => {
|
||||
try {
|
||||
await apiClient.registerDevice(
|
||||
deviceUuid.value,
|
||||
deviceInfo.value?.deviceName || null,
|
||||
accountStore.token
|
||||
deviceInfo.value?.deviceName || null
|
||||
)
|
||||
await apiClient.bindDeviceToAccount(accountStore.token, deviceUuid.value)
|
||||
await apiClient.bindDeviceToAccount(deviceUuid.value)
|
||||
await loadDeviceInfo()
|
||||
toast.success('设备已注册并绑定到您的账户')
|
||||
} catch (retryError) {
|
||||
@ -359,7 +356,7 @@ const unbindCurrentDevice = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.unbindDeviceFromAccount(accountStore.token, deviceUuid.value)
|
||||
await apiClient.unbindDeviceFromAccount(deviceUuid.value)
|
||||
await loadDeviceInfo()
|
||||
toast.success('设备已解绑')
|
||||
} 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">
|
||||
Classworks KV
|
||||
</h1>
|
||||
<p class="text-sm text-muted-foreground">云原生键值数据库</p>
|
||||
<p class="text-sm text-muted-foreground">文档形键值数据库</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,20 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiClient } from '@/lib/api'
|
||||
import { setAuthHandlers } from '@/lib/axios'
|
||||
|
||||
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 devices = ref([])
|
||||
const loading = ref(false)
|
||||
const providerName = ref(localStorage.getItem('auth_provider') || '')
|
||||
const providerColor = ref(localStorage.getItem('auth_provider_color') || '')
|
||||
let proactiveTimer = null
|
||||
|
||||
// 计算属性
|
||||
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 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) => {
|
||||
token.value = newToken
|
||||
if (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 {
|
||||
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_color')
|
||||
providerName.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
|
||||
try {
|
||||
const response = await apiClient.getAccountProfile(token.value)
|
||||
const response = await apiClient.getAccountProfile()
|
||||
profile.value = response.data
|
||||
// 若后端返回 providerInfo,则回填前端展示字段
|
||||
const p = profile.value?.providerInfo
|
||||
@ -50,10 +108,6 @@ export const useAccountStore = defineStore('account', () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile:', error)
|
||||
// Token可能无效,清除
|
||||
if (error.message.includes('401')) {
|
||||
logout()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -63,7 +117,7 @@ export const useAccountStore = defineStore('account', () => {
|
||||
if (!token.value) return
|
||||
|
||||
try {
|
||||
const response = await apiClient.getAccountDevices(token.value)
|
||||
const response = await apiClient.getAccountDevices()
|
||||
devices.value = response.data || []
|
||||
return devices.value
|
||||
} catch (error) {
|
||||
@ -75,7 +129,7 @@ export const useAccountStore = defineStore('account', () => {
|
||||
const bindDevice = async (deviceUuid) => {
|
||||
if (!token.value) throw new Error('未登录')
|
||||
|
||||
const response = await apiClient.bindDevice(token.value, deviceUuid)
|
||||
const response = await apiClient.bindDevice(deviceUuid)
|
||||
// 重新加载设备列表
|
||||
await loadDevices()
|
||||
return response
|
||||
@ -84,7 +138,7 @@ export const useAccountStore = defineStore('account', () => {
|
||||
const unbindDevice = async (deviceUuid) => {
|
||||
if (!token.value) throw new Error('未登录')
|
||||
|
||||
const response = await apiClient.unbindDevice(token.value, deviceUuid)
|
||||
const response = await apiClient.unbindDevice(deviceUuid)
|
||||
// 重新加载设备列表
|
||||
await loadDevices()
|
||||
return response
|
||||
@ -100,15 +154,37 @@ export const useAccountStore = defineStore('account', () => {
|
||||
token.value = null
|
||||
profile.value = null
|
||||
devices.value = []
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_provider')
|
||||
localStorage.removeItem('auth_provider_color')
|
||||
providerName.value = ''
|
||||
providerColor.value = ''
|
||||
setToken(null)
|
||||
}
|
||||
|
||||
// 初始化时加载用户信息
|
||||
// 刷新访问令牌(按需刷新)
|
||||
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) {
|
||||
// 若未存储过期时间,尝试从JWT解析
|
||||
if (!accessExpiresAt.value) {
|
||||
accessExpiresAt.value = decodeJwtExpMs(token.value) || 0
|
||||
if (accessExpiresAt.value) localStorage.setItem('auth_access_exp', String(accessExpiresAt.value))
|
||||
}
|
||||
scheduleProactiveRefresh()
|
||||
loadProfile()
|
||||
loadDevices()
|
||||
}
|
||||
@ -116,6 +192,7 @@ export const useAccountStore = defineStore('account', () => {
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
refreshToken,
|
||||
profile,
|
||||
devices,
|
||||
loading,
|
||||
@ -130,11 +207,13 @@ export const useAccountStore = defineStore('account', () => {
|
||||
userProviderColor,
|
||||
// 方法
|
||||
setToken,
|
||||
setTokens,
|
||||
loadProfile,
|
||||
loadDevices,
|
||||
bindDevice,
|
||||
unbindDevice,
|
||||
login,
|
||||
logout,
|
||||
refreshNow,
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user