fix: update the get-session cache logic

Signed-off-by: saltbo <saltbo@foxmail.com>
This commit is contained in:
saltbo
2026-06-10 21:51:37 -04:00
parent fa136a5112
commit 47b11bfcca
8 changed files with 94 additions and 22 deletions
+33
View File
@@ -0,0 +1,33 @@
import { AlertCircle, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
export function SessionGatePending() {
const { t } = useTranslation()
return (
<div className="flex min-h-screen items-center justify-center bg-canvas px-6">
<div className="space-y-3 text-center">
<Loader2 className="mx-auto size-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">{t('auth.session.loading')}</p>
</div>
</div>
)
}
export function SessionGateError({ reset }: { reset: () => void }) {
const { t } = useTranslation()
return (
<div className="flex min-h-screen items-center justify-center bg-canvas px-6">
<div className="w-full max-w-sm space-y-4 text-center">
<AlertCircle className="mx-auto size-10 text-destructive" />
<div className="space-y-2">
<h2 className="text-xl font-semibold">{t('auth.session.errorTitle')}</h2>
<p className="text-sm text-muted-foreground">{t('auth.session.errorDescription')}</p>
</div>
<Button onClick={reset}>{t('auth.session.retry')}</Button>
</div>
</div>
)
}
+4
View File
@@ -853,6 +853,10 @@
"common.globalSearchPlaceholder": "Search files, shares, teams and settings…",
"common.globalSearchComingSoon": "Global search is coming soon",
"common.moreOptions": "More options",
"auth.session.loading": "Checking your session...",
"auth.session.errorTitle": "Unable to load your session",
"auth.session.errorDescription": "The server did not respond. Check your connection and try again.",
"auth.session.retry": "Try again",
"teams.title": "Teams",
"teams.tabMembers": "Members",
"teams.tabActivity": "Activity",
+4
View File
@@ -853,6 +853,10 @@
"common.globalSearchPlaceholder": "搜索文件、分享、团队和设置…",
"common.globalSearchComingSoon": "全局搜索即将推出",
"common.moreOptions": "更多选项",
"auth.session.loading": "正在检查登录状态...",
"auth.session.errorTitle": "无法加载登录状态",
"auth.session.errorDescription": "服务器没有响应,请检查网络连接后重试。",
"auth.session.retry": "重试",
"teams.title": "团队",
"teams.tabMembers": "成员",
"teams.tabActivity": "动态",
+21 -3
View File
@@ -1616,14 +1616,32 @@ describe('api', () => {
const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit]
expect(url).toBe('/api/auth/get-session')
expect(init.credentials).toBe('include')
expect(init.signal).toBeInstanceOf(AbortSignal)
})
it('returns null when response is not ok', async () => {
it('throws ApiError when response is not ok', async () => {
vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'unauthorized' }, false, 401))
const result = await getSession()
const promise = getSession()
expect(result).toBeNull()
await expect(promise).rejects.toMatchObject({ name: 'ApiError', status: 401 })
})
it('aborts and throws when the session request times out', async () => {
vi.useFakeTimers()
vi.mocked(fetch).mockImplementationOnce(
(_url, init) =>
new Promise((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')))
}),
)
const promise = getSession()
const assertion = expect(promise).rejects.toThrow('Session request timed out')
await vi.advanceTimersByTimeAsync(10_000)
await assertion
vi.useRealTimers()
})
})
+18 -3
View File
@@ -128,6 +128,8 @@ export class ApiError extends Error {
}
}
const SESSION_REQUEST_TIMEOUT_MS = 10_000
export interface NameConflictBody extends ApiErrorBody {
code: 'NAME_CONFLICT'
conflictingName: string
@@ -1051,9 +1053,22 @@ export function disconnectCloud() {
// Auth API — Better Auth passthrough, not typed via Hono RPC
export async function getSession(): Promise<{ session: unknown; user: unknown } | null> {
const res = await fetch('/api/auth/get-session', { credentials: 'include' })
if (!res.ok) return null
return res.json()
const controller = new AbortController()
const timeout = window.setTimeout(() => controller.abort(), SESSION_REQUEST_TIMEOUT_MS)
try {
const res = await fetch('/api/auth/get-session', { credentials: 'include', signal: controller.signal })
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as ApiErrorBody
throw new ApiError(res.status, body)
}
return res.json()
} catch (error) {
if (controller.signal.aborted) throw new Error('Session request timed out')
throw error
} finally {
window.clearTimeout(timeout)
}
}
export interface UploadProgress {
+3
View File
@@ -1,5 +1,6 @@
import { createFileRoute, Outlet, redirect, useMatchRoute } from '@tanstack/react-router'
import { SiteAnnouncements } from '@/components/announcements/site-announcements'
import { SessionGateError, SessionGatePending } from '@/components/auth/session-gate'
import { AppSidebar } from '@/components/layout/app-sidebar'
import { GlobalSearchBar } from '@/components/layout/global-search-bar'
import { MusicPlayerButton } from '@/components/music/music-player-button'
@@ -17,6 +18,8 @@ export const Route = createFileRoute('/_authenticated')({
}
return { user: data.user }
},
pendingComponent: SessionGatePending,
errorComponent: SessionGateError,
component: AuthenticatedLayout,
})
+3
View File
@@ -2,6 +2,7 @@ import { createFileRoute, Link, redirect } from '@tanstack/react-router'
import { Loader2 } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SessionGateError, SessionGatePending } from '@/components/auth/session-gate'
import { Button } from '@/components/ui/button'
import {
ApiError,
@@ -30,6 +31,8 @@ export const Route = createFileRoute('/store/checkout')({
throw redirect({ to: '/sign-in', search: { redirect: redirectUrl } as never })
}
},
pendingComponent: SessionGatePending,
errorComponent: SessionGateError,
component: StorageCheckoutPage,
})
+8 -16
View File
@@ -16,11 +16,6 @@ interface Env {
[key: string]: unknown
}
// Cache auth instance at isolate scope to avoid per-request DB queries
// for OIDC config loading. Changes to OIDC provider configs or env vars
// (BETTER_AUTH_URL, TRUSTED_ORIGINS) take effect on isolate recycle.
let cachedAuth: Auth | null = null
const SHARE_TOKEN_RE = /^\/s\/([^/?#]+)/
export default {
@@ -30,24 +25,21 @@ export default {
throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.')
}
const platform = createCloudflarePlatform(env)
if (!cachedAuth) {
const origin = new URL(request.url).origin
const baseURL = env.BETTER_AUTH_URL || origin
const trustedOrigins = env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || [origin]
cachedAuth = await createAuth(platform, BETTER_AUTH_SECRET, baseURL, trustedOrigins)
}
const origin = new URL(request.url).origin
const baseURL = env.BETTER_AUTH_URL || origin
const trustedOrigins = env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || [origin]
const auth = await createAuth(platform, BETTER_AUTH_SECRET, baseURL, trustedOrigins)
const url = new URL(request.url)
const shareMatch = SHARE_TOKEN_RE.exec(url.pathname)
if (shareMatch && request.method === 'GET') {
return handleShareSsr(request, env, ctx, shareMatch[1], platform, cachedAuth)
return handleShareSsr(request, env, ctx, shareMatch[1], platform, auth)
}
return createApp(platform, cachedAuth).fetch(request, env, ctx)
return createApp(platform, auth).fetch(request, env, ctx)
},
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {