refactor(workspace): enforce layered crate boundaries

This commit is contained in:
elky
2026-07-15 23:47:19 +08:00
parent a728c090a9
commit 8616fe6ee2
969 changed files with 40187 additions and 27240 deletions
+113
View File
@@ -106,6 +106,119 @@ describe('auth store logout', () => {
expect(store.token).toBeNull()
})
it('deduplicates concurrent current-user requests', async () => {
let resolveCurrentUser: ((user: {
id: string
username: string
role: string
is_active: boolean
created_at: string
}) => void) | null = null
getTokenMock.mockReturnValue('access-token')
getCurrentUserMock.mockImplementation(
() => new Promise((resolve) => {
resolveCurrentUser = resolve
})
)
const store = useAuthStore()
const firstRequest = store.fetchCurrentUser()
const secondRequest = store.fetchCurrentUser()
expect(getCurrentUserMock).toHaveBeenCalledTimes(1)
const currentUser = {
id: 'user-1',
username: 'tester',
role: 'user',
is_active: true,
created_at: '2026-03-16T00:00:00Z',
}
resolveCurrentUser?.(currentUser)
await expect(firstRequest).resolves.toEqual(currentUser)
await expect(secondRequest).resolves.toEqual(currentUser)
})
it('does not repeat a failed current-user request on every navigation', async () => {
getTokenMock.mockReturnValue('access-token')
getCurrentUserMock.mockRejectedValue(new Error('network unavailable'))
const store = useAuthStore()
await store.fetchCurrentUser()
await store.fetchCurrentUser()
expect(store.token).toBe('access-token')
expect(store.user).toBeNull()
expect(getCurrentUserMock).toHaveBeenCalledTimes(1)
})
it('does not restore a stale user after logout while the request is in flight', async () => {
let resolveCurrentUser: ((user: {
id: string
username: string
role: string
is_active: boolean
created_at: string
}) => void) | null = null
getTokenMock.mockReturnValue('access-token')
getCurrentUserMock.mockImplementation(
() => new Promise((resolve) => {
resolveCurrentUser = resolve
})
)
const store = useAuthStore()
const currentUserRequest = store.fetchCurrentUser()
await store.logout()
resolveCurrentUser?.({
id: 'stale-user',
username: 'stale',
role: 'user',
is_active: true,
created_at: '2026-03-16T00:00:00Z',
})
await expect(currentUserRequest).resolves.toBeNull()
expect(store.user).toBeNull()
expect(store.token).toBeNull()
})
it('does not restore a stale token when an in-flight request fails after logout', async () => {
let rejectCurrentUser: ((error: Error) => void) | null = null
getTokenMock.mockReturnValue('access-token')
getCurrentUserMock.mockImplementation(
() => new Promise((_resolve, reject) => {
rejectCurrentUser = reject
})
)
const store = useAuthStore()
const currentUserRequest = store.fetchCurrentUser()
await store.logout()
rejectCurrentUser?.(new Error('stale request failed'))
await expect(currentUserRequest).resolves.toBeNull()
expect(store.user).toBeNull()
expect(store.token).toBeNull()
})
it('skips the delayed auth check when the router already loaded the user', async () => {
getTokenMock.mockReturnValue('access-token')
const store = useAuthStore()
store.user = {
id: 'user-1',
username: 'tester',
role: 'user',
is_active: true,
created_at: '2026-03-16T00:00:00Z',
}
await store.checkAuth()
expect(getCurrentUserMock).not.toHaveBeenCalled()
})
it('separates admin access from admin operations for audit administrators', () => {
const store = useAuthStore()
+84 -20
View File
@@ -7,6 +7,8 @@ import { parseApiError } from '@/utils/errorParser'
import { getErrorStatus } from '@/types/api-error'
export const useAuthStore = defineStore('auth', () => {
const CURRENT_USER_FAILURE_BACKOFF_MS = 15_000
// 初始化时从 localStorage 恢复 token
const storedToken = apiClient.getToken()
@@ -14,6 +16,21 @@ export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(storedToken)
const loading = ref(false)
const error = ref<string | null>(null)
let fetchCurrentUserPromise: Promise<User | null> | null = null
let fetchCurrentUserToken: string | null = null
let lastCurrentUserFailureAt = 0
let lastCurrentUserFailureToken: string | null = null
let authStateVersion = 0
function resetCurrentUserFailure() {
lastCurrentUserFailureAt = 0
lastCurrentUserFailureToken = null
}
function markAuthStateChanged() {
authStateVersion += 1
resetCurrentUserFailure()
}
const isAuthenticated = computed(() => {
// 使用 store 中的 token 状态判断认证状态
@@ -29,6 +46,7 @@ export const useAuthStore = defineStore('auth', () => {
const currentToken = apiClient.getToken()
if (token.value !== currentToken) {
token.value = currentToken
markAuthStateChanged()
}
}
const isAdmin = computed(() => user.value?.role === 'admin')
@@ -43,10 +61,12 @@ export const useAuthStore = defineStore('auth', () => {
try {
const response = await authApi.login({ email, password, auth_type: authType })
token.value = response.access_token
markAuthStateChanged()
// 获取用户信息
const userInfo = await authApi.getCurrentUser()
user.value = userInfo
resetCurrentUserFailure()
return true
} catch (err: unknown) {
@@ -73,6 +93,7 @@ export const useAuthStore = defineStore('auth', () => {
async function logout() {
user.value = null
token.value = null
markAuthStateChanged()
await authApi.logout()
}
@@ -80,32 +101,75 @@ export const useAuthStore = defineStore('auth', () => {
user.value = null
token.value = null
error.value = null
markAuthStateChanged()
}
async function fetchCurrentUser() {
try {
const userInfo = await authApi.getCurrentUser()
user.value = userInfo
return userInfo
} catch (err: unknown) {
log.error('Failed to fetch user info', err)
syncToken()
if (!token.value) {
user.value = null
}
// 根据用户要求,不管什么错误都不清除状态
// 保持登录状态,除非用户手动退出
log.info('Keeping session despite error, as per user requirement')
return null
function fetchCurrentUser(): Promise<User | null> {
const requestToken = token.value || apiClient.getToken()
if (!requestToken) {
user.value = null
return Promise.resolve(null)
}
// 路由守卫、App 初始化和认证同步可能同时触发,复用同一个请求。
if (fetchCurrentUserPromise && fetchCurrentUserToken === requestToken) {
return fetchCurrentUserPromise
}
// 后端暂时不可用时,不要让每一次导航都重新等待全局请求超时。
if (
lastCurrentUserFailureToken === requestToken &&
Date.now() - lastCurrentUserFailureAt < CURRENT_USER_FAILURE_BACKOFF_MS
) {
return Promise.resolve(null)
}
fetchCurrentUserToken = requestToken
const requestAuthStateVersion = authStateVersion
const request = (async () => {
try {
const userInfo = await authApi.getCurrentUser()
if (requestAuthStateVersion !== authStateVersion || !token.value) {
return null
}
user.value = userInfo
resetCurrentUserFailure()
return userInfo
} catch (err: unknown) {
log.error('Failed to fetch user info', err)
if (requestAuthStateVersion !== authStateVersion) {
return null
}
syncToken()
if (requestAuthStateVersion !== authStateVersion) {
if (!token.value) user.value = null
return null
}
if (!token.value) {
user.value = null
} else {
lastCurrentUserFailureAt = Date.now()
lastCurrentUserFailureToken = token.value
}
// 保留登录状态;短暂退避后允许再次校验。
log.info('Keeping session despite error, as per user requirement')
return null
} finally {
if (fetchCurrentUserPromise === request) {
fetchCurrentUserPromise = null
fetchCurrentUserToken = null
}
}
})()
fetchCurrentUserPromise = request
return request
}
async function checkAuth() {
const storedToken = apiClient.getToken()
if (storedToken) {
token.value = storedToken
// 即使获取用户信息失败,也保留 token
// 只有 401 错误才表示 token 真正失效
syncToken()
if (token.value && !user.value) {
// 即使获取用户信息失败,也保留 token。
await fetchCurrentUser()
}
}