mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 10:01:12 +08:00
fix: update the get-session cache logic
Signed-off-by: saltbo <saltbo@foxmail.com>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user