fix(frontend): restore pending auth session flow

This commit is contained in:
IanShaw027
2026-04-21 00:05:44 +08:00
parent 4f6966d7b3
commit 85fc54b205
6 changed files with 556 additions and 24 deletions

View File

@@ -211,6 +211,78 @@ describe('useAuthStore', () => {
expect(store.isAuthenticated).toBe(true)
})
it('恢复持久化 pending auth session', () => {
localStorage.setItem(
'pending_auth_session',
JSON.stringify({
token: 'pending-token',
token_field: 'pending_auth_token',
provider: 'wechat',
redirect: '/profile',
})
)
const store = useAuthStore()
store.checkAuth()
expect(store.hasPendingAuthSession).toBe(true)
expect(store.pendingAuthSession).toEqual({
token: 'pending-token',
token_field: 'pending_auth_token',
provider: 'wechat',
redirect: '/profile',
})
})
})
describe('pending auth session', () => {
it('persists and clears pending auth session state', () => {
const store = useAuthStore()
store.setPendingAuthSession({
token: 'pending-token',
token_field: 'pending_auth_token',
provider: 'wechat',
redirect: '/profile',
})
expect(store.hasPendingAuthSession).toBe(true)
expect(JSON.parse(localStorage.getItem('pending_auth_session') || 'null')).toEqual({
token: 'pending-token',
token_field: 'pending_auth_token',
provider: 'wechat',
redirect: '/profile',
})
store.clearPendingAuthSession()
expect(store.hasPendingAuthSession).toBe(false)
expect(localStorage.getItem('pending_auth_session')).toBeNull()
})
it('preserves pending auth session when registration fails', async () => {
const store = useAuthStore()
store.setPendingAuthSession({
token: 'pending-token',
token_field: 'pending_auth_token',
provider: 'oidc',
redirect: '/register',
})
mockRegister.mockRejectedValue(new Error('Register failed'))
await expect(
store.register({ email: 'user@example.com', password: 'secret-123' })
).rejects.toThrow('Register failed')
expect(store.hasPendingAuthSession).toBe(true)
expect(store.pendingAuthSession).toEqual({
token: 'pending-token',
token_field: 'pending_auth_token',
provider: 'oidc',
redirect: '/register',
})
})
})
// --- isAdmin ---

View File

@@ -12,9 +12,56 @@ const AUTH_TOKEN_KEY = 'auth_token'
const AUTH_USER_KEY = 'auth_user'
const REFRESH_TOKEN_KEY = 'refresh_token'
const TOKEN_EXPIRES_AT_KEY = 'token_expires_at' // 存储过期时间戳而非有效期
const PENDING_AUTH_SESSION_KEY = 'pending_auth_session'
const AUTO_REFRESH_INTERVAL = 60 * 1000 // 60 seconds for user data refresh
const TOKEN_REFRESH_BUFFER = 120 * 1000 // 120 seconds before expiry to refresh token
type PendingAuthTokenField = 'pending_auth_token' | 'pending_oauth_token'
interface PendingAuthSessionSummary {
token: string
token_field: PendingAuthTokenField
provider: string
redirect?: string
adoption_required?: boolean
suggested_display_name?: string
suggested_avatar_url?: string
}
function getPersistedPendingAuthSession(): PendingAuthSessionSummary | null {
const raw = localStorage.getItem(PENDING_AUTH_SESSION_KEY)
if (!raw) {
return null
}
try {
const parsed = JSON.parse(raw) as PendingAuthSessionSummary
if (!parsed?.token || !parsed?.provider) {
return null
}
return {
token: parsed.token,
token_field: parsed.token_field || 'pending_auth_token',
provider: parsed.provider,
redirect: parsed.redirect,
adoption_required: parsed.adoption_required,
suggested_display_name: parsed.suggested_display_name,
suggested_avatar_url: parsed.suggested_avatar_url
}
} catch {
localStorage.removeItem(PENDING_AUTH_SESSION_KEY)
return null
}
}
function persistPendingAuthSession(session: PendingAuthSessionSummary): void {
localStorage.setItem(PENDING_AUTH_SESSION_KEY, JSON.stringify(session))
}
function clearPendingAuthSessionStorage(): void {
localStorage.removeItem(PENDING_AUTH_SESSION_KEY)
}
export const useAuthStore = defineStore('auth', () => {
// ==================== State ====================
@@ -23,6 +70,7 @@ export const useAuthStore = defineStore('auth', () => {
const refreshTokenValue = ref<string | null>(null)
const tokenExpiresAt = ref<number | null>(null) // 过期时间戳(毫秒)
const runMode = ref<'standard' | 'simple'>('standard')
const pendingAuthSession = ref<PendingAuthSessionSummary | null>(null)
let refreshIntervalId: ReturnType<typeof setInterval> | null = null
let tokenRefreshTimeoutId: ReturnType<typeof setTimeout> | null = null
@@ -37,6 +85,7 @@ export const useAuthStore = defineStore('auth', () => {
})
const isSimpleMode = computed(() => runMode.value === 'simple')
const hasPendingAuthSession = computed(() => pendingAuthSession.value !== null)
// ==================== Actions ====================
@@ -50,6 +99,7 @@ export const useAuthStore = defineStore('auth', () => {
const savedUser = localStorage.getItem(AUTH_USER_KEY)
const savedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)
const savedExpiresAt = localStorage.getItem(TOKEN_EXPIRES_AT_KEY)
pendingAuthSession.value = getPersistedPendingAuthSession()
if (savedToken && savedUser) {
try {
@@ -73,7 +123,7 @@ export const useAuthStore = defineStore('auth', () => {
}
} catch (error) {
console.error('Failed to parse saved user data:', error)
clearAuth()
clearAuth({ preservePendingAuthSession: true })
}
}
}
@@ -196,7 +246,7 @@ export const useAuthStore = defineStore('auth', () => {
return response
} catch (error) {
// Clear any partial state on error
clearAuth()
clearAuth({ preservePendingAuthSession: pendingAuthSession.value !== null })
throw error
}
}
@@ -214,7 +264,7 @@ export const useAuthStore = defineStore('auth', () => {
setAuthFromResponse(response)
return user.value!
} catch (error) {
clearAuth()
clearAuth({ preservePendingAuthSession: pendingAuthSession.value !== null })
throw error
}
}
@@ -243,6 +293,7 @@ export const useAuthStore = defineStore('auth', () => {
// Persist to localStorage
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
clearPendingAuthSession()
// Start auto-refresh interval for user data
startAutoRefresh()
@@ -270,7 +321,7 @@ export const useAuthStore = defineStore('auth', () => {
return user.value!
} catch (error) {
// Clear any partial state on error
clearAuth()
clearAuth({ preservePendingAuthSession: pendingAuthSession.value !== null })
throw error
}
}
@@ -312,13 +363,29 @@ export const useAuthStore = defineStore('auth', () => {
scheduleTokenRefreshAt(tokenExpiresAt.value)
}
clearPendingAuthSession()
return userData
} catch (error) {
clearAuth()
clearAuth({ preservePendingAuthSession: pendingAuthSession.value !== null })
throw error
}
}
function setPendingAuthSession(session: PendingAuthSessionSummary | null): void {
pendingAuthSession.value = session
if (session) {
persistPendingAuthSession(session)
return
}
clearPendingAuthSessionStorage()
}
function clearPendingAuthSession(): void {
setPendingAuthSession(null)
}
/**
* User logout
* Clears all authentication state and persisted data
@@ -357,7 +424,7 @@ export const useAuthStore = defineStore('auth', () => {
} catch (error) {
// If refresh fails with 401, clear auth state
if ((error as { status?: number }).status === 401) {
clearAuth()
clearAuth({ preservePendingAuthSession: pendingAuthSession.value !== null })
}
throw error
}
@@ -367,7 +434,7 @@ export const useAuthStore = defineStore('auth', () => {
* Clear all authentication state
* Internal helper function
*/
function clearAuth(): void {
function clearAuth(options?: { preservePendingAuthSession?: boolean }): void {
// Stop auto-refresh
stopAutoRefresh()
// Stop token refresh
@@ -381,6 +448,14 @@ export const useAuthStore = defineStore('auth', () => {
localStorage.removeItem(AUTH_USER_KEY)
localStorage.removeItem(REFRESH_TOKEN_KEY)
localStorage.removeItem(TOKEN_EXPIRES_AT_KEY)
if (options?.preservePendingAuthSession) {
pendingAuthSession.value = getPersistedPendingAuthSession()
return
}
pendingAuthSession.value = null
clearPendingAuthSessionStorage()
}
// ==================== Return Store API ====================
@@ -390,11 +465,13 @@ export const useAuthStore = defineStore('auth', () => {
user,
token,
runMode: readonly(runMode),
pendingAuthSession: readonly(pendingAuthSession),
// Computed
isAuthenticated,
isAdmin,
isSimpleMode,
hasPendingAuthSession,
// Actions
login,
@@ -403,6 +480,8 @@ export const useAuthStore = defineStore('auth', () => {
setToken,
logout,
checkAuth,
refreshUser
refreshUser,
setPendingAuthSession,
clearPendingAuthSession
}
})