fix(frontend): restore pending auth session flow
This commit is contained in:
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user