fix: preserve desktop session across restarts
This commit is contained in:
@@ -1171,8 +1171,7 @@ if (!hasSingleInstanceLock) {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
suppressNativeWindowMenu()
|
suppressNativeWindowMenu()
|
||||||
registerDesktopProtocolClient()
|
registerDesktopProtocolClient()
|
||||||
currentWindowMode = 'login'
|
currentWindowMode = readPersistedWindowMode()
|
||||||
persistWindowMode('login')
|
|
||||||
initDesktopAppSettings()
|
initDesktopAppSettings()
|
||||||
registerBridgeHandlers()
|
registerBridgeHandlers()
|
||||||
await initSessionRegistry()
|
await initSessionRegistry()
|
||||||
@@ -1229,14 +1228,8 @@ if (!hasSingleInstanceLock) {
|
|||||||
quitReleaseInFlight = true
|
quitReleaseInFlight = true
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
currentWindowMode = 'login'
|
|
||||||
persistWindowMode('login')
|
|
||||||
|
|
||||||
void Promise.race([
|
void Promise.race([
|
||||||
(async () => {
|
releaseRuntimeSession(),
|
||||||
await releaseRuntimeSession()
|
|
||||||
await clearRendererDesktopSessions()
|
|
||||||
})(),
|
|
||||||
new Promise<void>((resolve) => setTimeout(resolve, 1500)),
|
new Promise<void>((resolve) => setTimeout(resolve, 1500)),
|
||||||
]).finally(() => {
|
]).finally(() => {
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ if (rendererWindowMode === 'main') {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rendererWindowMode === 'login') {
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
syncWindowMode(true)
|
||||||
|
}
|
||||||
|
watch(isAuthenticated, (next) => {
|
||||||
|
if (next) {
|
||||||
|
syncWindowMode(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (rendererWindowMode !== 'login') {
|
if (rendererWindowMode !== 'login') {
|
||||||
void syncRuntimeSession().catch((err) => {
|
void syncRuntimeSession().catch((err) => {
|
||||||
|
|||||||
@@ -68,11 +68,6 @@ function readStoredSession(): DesktopSession | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (shouldDiscardStoredSessionForWindow()) {
|
|
||||||
window.localStorage.removeItem(sessionStorageKey)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = window.localStorage.getItem(sessionStorageKey)
|
const raw = window.localStorage.getItem(sessionStorageKey)
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return null
|
return null
|
||||||
@@ -83,10 +78,6 @@ function readStoredSession(): DesktopSession | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldDiscardStoredSessionForWindow(): boolean {
|
|
||||||
return new URLSearchParams(window.location.search).get('window') === 'login'
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStoredSession(value: unknown): DesktopSession | null {
|
function normalizeStoredSession(value: unknown): DesktopSession | null {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
@@ -166,6 +157,7 @@ function createAuthenticatedClient() {
|
|||||||
return createApiClient({
|
return createApiClient({
|
||||||
baseURL: apiBaseURL.value,
|
baseURL: apiBaseURL.value,
|
||||||
timeoutMs: 15000,
|
timeoutMs: 15000,
|
||||||
|
clearTokensOnUnauthorized: false,
|
||||||
auth: {
|
auth: {
|
||||||
getAccessToken: () => session.value?.accessToken ?? null,
|
getAccessToken: () => session.value?.accessToken ?? null,
|
||||||
getRefreshToken: () => session.value?.refreshToken ?? null,
|
getRefreshToken: () => session.value?.refreshToken ?? null,
|
||||||
@@ -181,7 +173,18 @@ function createAuthenticatedClient() {
|
|||||||
refreshToken: tokens.refreshToken,
|
refreshToken: tokens.refreshToken,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearTokens: () => persistSession(null),
|
clearTokens: () => {
|
||||||
|
if (!session.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
persistSession({
|
||||||
|
...session.value,
|
||||||
|
accessToken: null,
|
||||||
|
accessTokenExpiresAt: null,
|
||||||
|
refreshToken: null,
|
||||||
|
})
|
||||||
|
},
|
||||||
async refresh(refreshToken: string): Promise<AuthTokens> {
|
async refresh(refreshToken: string): Promise<AuthTokens> {
|
||||||
const next = await createPublicClient().post<RefreshResponse, { refresh_token: string }>(
|
const next = await createPublicClient().post<RefreshResponse, { refresh_token: string }>(
|
||||||
'/api/auth/refresh',
|
'/api/auth/refresh',
|
||||||
@@ -239,6 +242,14 @@ function expiresAtMs(value: number | null | undefined): number | null {
|
|||||||
return value < 10_000_000_000 ? value * 1000 : value
|
return value < 10_000_000_000 ? value * 1000 : value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasUsableStoredSession(value: DesktopSession | null): boolean {
|
||||||
|
return Boolean(
|
||||||
|
value?.clientToken &&
|
||||||
|
value.desktopClient &&
|
||||||
|
(value.mode === 'authenticated' || value.mode === 'preview'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function isRenewDue(value: number | null | undefined, renewWindowMs: number): boolean {
|
function isRenewDue(value: number | null | undefined, renewWindowMs: number): boolean {
|
||||||
const expiresAt = expiresAtMs(value)
|
const expiresAt = expiresAtMs(value)
|
||||||
if (!expiresAt) {
|
if (!expiresAt) {
|
||||||
@@ -247,6 +258,17 @@ function isRenewDue(value: number | null | undefined, renewWindowMs: number): bo
|
|||||||
return expiresAt - Date.now() <= renewWindowMs
|
return expiresAt - Date.now() <= renewWindowMs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isApiAuthExpiredError(err: unknown): boolean {
|
||||||
|
return err instanceof ApiClientError && err.status === 401
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTransientRenewalError(err: unknown): boolean {
|
||||||
|
if (err instanceof ApiClientError) {
|
||||||
|
return !err.status || err.status >= 500 || err.message === 'request_timeout'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshLoginTokens(current: DesktopSession): Promise<DesktopSession> {
|
async function refreshLoginTokens(current: DesktopSession): Promise<DesktopSession> {
|
||||||
if (!current.refreshToken) {
|
if (!current.refreshToken) {
|
||||||
throw new Error('missing_refresh_token')
|
throw new Error('missing_refresh_token')
|
||||||
@@ -302,6 +324,16 @@ async function forceLogoutAfterRenewFailure(err: unknown): Promise<void> {
|
|||||||
await transitionWindowForLogout()
|
await transitionWindowForLogout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function keepSessionAfterRenewFailure(err: unknown): void {
|
||||||
|
console.warn('[desktop-session] token renewal deferred', err)
|
||||||
|
error.value = isTransientRenewalError(err)
|
||||||
|
? '登录状态暂时无法刷新,将保持当前客户端登录状态并自动重试'
|
||||||
|
: '客户端登录状态刷新失败,将保持当前会话并自动重试'
|
||||||
|
void syncRuntimeSessionToMain().catch((syncError) => {
|
||||||
|
logRuntimeSessionSyncFailure('renewal-deferred', syncError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function renewAuthenticatedSession(options: { force?: boolean } = {}): Promise<void> {
|
async function renewAuthenticatedSession(options: { force?: boolean } = {}): Promise<void> {
|
||||||
const current = session.value
|
const current = session.value
|
||||||
if (current?.mode !== 'authenticated') {
|
if (current?.mode !== 'authenticated') {
|
||||||
@@ -325,7 +357,15 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsAccessRefresh) {
|
if (needsAccessRefresh) {
|
||||||
next = await refreshLoginTokens(next)
|
try {
|
||||||
|
next = await refreshLoginTokens(next)
|
||||||
|
} catch (err) {
|
||||||
|
if (isApiAuthExpiredError(err)) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
keepSessionAfterRenewFailure(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (session.value !== expectedSession) {
|
if (session.value !== expectedSession) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -334,7 +374,15 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsClientRotate) {
|
if (needsClientRotate) {
|
||||||
next = await rotateRuntimeClientToken(next)
|
try {
|
||||||
|
next = await rotateRuntimeClientToken(next)
|
||||||
|
} catch (err) {
|
||||||
|
if (isApiAuthExpiredError(err)) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
keepSessionAfterRenewFailure(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (session.value !== expectedSession) {
|
if (session.value !== expectedSession) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -780,7 +828,7 @@ export function useDesktopSession() {
|
|||||||
apiBaseURL: readonly(apiBaseURL),
|
apiBaseURL: readonly(apiBaseURL),
|
||||||
pending: readonly(pending),
|
pending: readonly(pending),
|
||||||
error: readonly(error),
|
error: readonly(error),
|
||||||
isAuthenticated: computed(() => Boolean(session.value?.clientToken)),
|
isAuthenticated: computed(() => hasUsableStoredSession(session.value)),
|
||||||
isPreviewMode: computed(() => session.value?.mode === 'preview'),
|
isPreviewMode: computed(() => session.value?.mode === 'preview'),
|
||||||
setApiBaseURL: persistApiBaseURL,
|
setApiBaseURL: persistApiBaseURL,
|
||||||
syncRuntimeSession: syncRuntimeSessionToMain,
|
syncRuntimeSession: syncRuntimeSessionToMain,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface CreateApiClientOptions {
|
|||||||
baseURL?: string
|
baseURL?: string
|
||||||
timeoutMs?: number
|
timeoutMs?: number
|
||||||
auth?: AuthBindings
|
auth?: AuthBindings
|
||||||
|
clearTokensOnUnauthorized?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiClientError extends Error {
|
export class ApiClientError extends Error {
|
||||||
@@ -154,7 +155,9 @@ export function createApiClient(options: CreateApiClientOptions = {}): ApiClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.clearTokens()
|
if (options.clearTokensOnUnauthorized !== false) {
|
||||||
|
auth.clearTokens()
|
||||||
|
}
|
||||||
throw markHandled(normalizeError(error))
|
throw markHandled(normalizeError(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user