mirror of
https://github.com/langgenius/dify.git
synced 2026-06-05 23:50:06 +08:00
refactor(web): remove app initializer and move auth boot logic to route boundaries (#36818)
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user