refactor(web): remove app initializer and move auth boot logic to route boundaries (#36818)

This commit is contained in:
yyh
2026-05-29 20:26:34 +08:00
committed by GitHub
parent ae538ced47
commit 9490d63c50
62 changed files with 1284 additions and 668 deletions
@@ -0,0 +1,79 @@
import type { AccountProfileResponse } from '@/contract/console/account'
import { QueryClient } from '@tanstack/react-query'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { userProfileQueryOptions } from '../client'
import { resolveServerConsoleApiUrl } from '../server'
const headersMock = vi.fn()
const cookiesMock = vi.fn()
vi.mock('@/config/server', () => ({
SERVER_CONSOLE_API_PREFIX: undefined,
}))
vi.mock('@/next/headers', () => ({
headers: () => headersMock(),
cookies: () => cookiesMock(),
}))
const createProfile = (overrides: Partial<AccountProfileResponse> = {}): AccountProfileResponse => ({
id: 'account-id',
name: 'Dify User',
email: 'user@example.com',
avatar: '',
avatar_url: null,
is_password_set: true,
...overrides,
})
describe('serverUserProfileQueryOptions', () => {
beforeEach(() => {
vi.clearAllMocks()
headersMock.mockResolvedValue(new Headers({ cookie: 'session=abc' }))
cookiesMock.mockResolvedValue({
get: vi.fn(() => ({ value: 'csrf-token' })),
})
})
it('should reuse the client profile query key and return the same data shape', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify(createProfile()), {
status: 200,
headers: {
'content-type': 'application/json',
'x-version': '1.2.3',
'x-env': 'DEVELOPMENT',
},
}))
vi.stubGlobal('fetch', fetchMock)
const { serverUserProfileQueryOptions } = await import('../server')
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const data = await queryClient.fetchQuery(serverUserProfileQueryOptions())
expect(serverUserProfileQueryOptions().queryKey).toEqual(userProfileQueryOptions().queryKey)
expect(data).toEqual({
profile: createProfile(),
meta: {
currentVersion: '1.2.3',
currentEnv: 'DEVELOPMENT',
},
})
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:5001/console/api/account/profile',
expect.objectContaining({
method: 'GET',
cache: 'no-store',
headers: expect.any(Headers),
}),
)
})
it('should skip relative API prefixes unless a server API origin is configured', () => {
expect(resolveServerConsoleApiUrl('/account/profile', undefined, '/console/api')).toBeNull()
expect(resolveServerConsoleApiUrl('/account/profile', 'https://console.example.com/console/api', '/console/api')).toBe('https://console.example.com/console/api/account/profile')
})
it('should preserve absolute API prefixes', () => {
expect(resolveServerConsoleApiUrl('/account/profile', undefined, 'https://console.example.com/console/api')).toBe('https://console.example.com/console/api/account/profile')
})
})
+41
View File
@@ -0,0 +1,41 @@
import type { AccountProfileResponse } from '@/contract/console/account'
import { queryOptions } from '@tanstack/react-query'
import { IS_DEV } from '@/config'
// eslint-disable-next-line no-restricted-imports
import { get } from '@/service/base'
import { consoleQuery } from '@/service/client'
export type UserProfileWithMeta = {
profile: AccountProfileResponse
meta: {
currentVersion: string | null
currentEnv: string | null
}
}
export const isLegacyBase401 = (err: unknown): boolean =>
err instanceof Response && err.status === 401
export const userProfileQueryOptions = () =>
queryOptions<UserProfileWithMeta>({
queryKey: consoleQuery.account.profile.get.queryKey(),
queryFn: async () => {
const response = await get<Response>('/account/profile', {}, {
needAllResponseContent: true,
silent: true,
})
const profile: AccountProfileResponse = await response.clone().json()
return {
profile,
meta: {
currentVersion: response.headers.get('x-version'),
currentEnv: IS_DEV
? 'DEVELOPMENT'
: response.headers.get('x-env'),
},
}
},
staleTime: 0,
gcTime: 0,
retry: (failureCount, error) => !isLegacyBase401(error) && failureCount < 3,
})
+81
View File
@@ -0,0 +1,81 @@
import type { UserProfileWithMeta } from './client'
import type { AccountProfileResponse } from '@/contract/console/account'
import { queryOptions } from '@tanstack/react-query'
import { API_PREFIX, CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from '@/config'
import { SERVER_CONSOLE_API_PREFIX } from '@/config/server'
import { cookies, headers } from '@/next/headers'
import { consoleQuery } from '@/service/client'
const ACCOUNT_PROFILE_PATH = '/account/profile'
const withTrailingSlash = (value: string) => value.endsWith('/') ? value : `${value}/`
const withoutLeadingSlash = (value: string) => value.startsWith('/') ? value.slice(1) : value
const resolveAbsoluteUrlPrefix = (value: string) => {
try {
return new URL(value).toString()
}
catch {
return null
}
}
export const resolveServerConsoleApiUrl = (
pathname: string,
serverConsoleApiPrefix = SERVER_CONSOLE_API_PREFIX,
publicApiPrefix = API_PREFIX,
) => {
const requestPath = withoutLeadingSlash(pathname)
const apiPrefix = serverConsoleApiPrefix || resolveAbsoluteUrlPrefix(publicApiPrefix)
if (!apiPrefix)
return null
return new URL(requestPath, withTrailingSlash(apiPrefix)).toString()
}
const getServerRequestHeaders = async () => {
const requestHeaders = await headers()
const cookieStore = await cookies()
const outgoingHeaders = new Headers({
'Content-Type': 'application/json',
})
const cookie = requestHeaders.get('cookie')
if (cookie)
outgoingHeaders.set('cookie', cookie)
const csrfToken = cookieStore.get(CSRF_COOKIE_NAME())?.value
if (csrfToken)
outgoingHeaders.set(CSRF_HEADER_NAME, csrfToken)
return outgoingHeaders
}
export const serverUserProfileQueryOptions = () =>
queryOptions<UserProfileWithMeta>({
queryKey: consoleQuery.account.profile.get.queryKey(),
queryFn: async () => {
const profileUrl = resolveServerConsoleApiUrl(ACCOUNT_PROFILE_PATH)
if (!profileUrl)
throw new Error('Server account profile URL is not configured')
const response = await fetch(profileUrl, {
method: 'GET',
headers: await getServerRequestHeaders(),
cache: 'no-store',
})
if (!response.ok)
throw response
const profile: AccountProfileResponse = await response.clone().json()
return {
profile,
meta: {
currentVersion: response.headers.get('x-version'),
currentEnv: response.headers.get('x-env'),
},
}
},
staleTime: 0,
gcTime: 0,
retry: false,
})