refactor(web): use generated OAuth query options (#39204)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-07-17 22:50:56 +08:00 committed by GitHub
parent a7ef41a8f5
commit 63ca2b94b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 137 additions and 58 deletions

View File

@ -261,11 +261,6 @@
"count": 1
}
},
"web/app/account/oauth/authorize/page.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"web/app/components/app-sidebar/app-info/app-info-modals.tsx": {
"jsx_a11y/label-has-associated-control": {
"count": 1
@ -6702,11 +6697,6 @@
"count": 1
}
},
"web/service/use-oauth.ts": {
"no-restricted-imports": {
"count": 1
}
},
"web/service/use-pipeline.ts": {
"@tanstack/query/exhaustive-deps": {
"count": 1

View File

@ -0,0 +1,113 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import OAuthAuthorize from '../page'
const mocks = vi.hoisted(() => ({
push: vi.fn(),
request: vi.fn(),
searchParams: new URLSearchParams(),
}))
vi.mock('@/next/navigation', () => ({
useRouter: () => ({ push: mocks.push }),
useSearchParams: () => mocks.searchParams,
}))
vi.mock('@/service/base', () => ({
get: vi.fn(
async () =>
new Response(
JSON.stringify({
avatar_url: null,
email: 'user@example.com',
name: 'Test User',
}),
{ status: 200 },
),
),
post: vi.fn(),
request: (...args: unknown[]) => mocks.request(...args),
sseGeneratorPost: vi.fn(),
}))
function renderPage() {
const queryClient = new QueryClient({
defaultOptions: {
mutations: { retry: false },
queries: { retry: false },
},
})
return render(
<QueryClientProvider client={queryClient}>
<OAuthAuthorize />
</QueryClientProvider>,
)
}
function jsonResponse(body: unknown) {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'content-type': 'application/json' },
})
}
function findRequest(path: string) {
return mocks.request.mock.calls.find(([url]) => String(url).endsWith(path))
}
describe('OAuthAuthorize', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.searchParams = new URLSearchParams({
client_id: 'client-1',
redirect_uri: 'https://client.example.com/callback?state=state-1',
})
mocks.request.mockImplementation(async (url: string) => {
if (url.endsWith('/oauth/provider/authorize')) return jsonResponse({ code: 'oauth-code' })
if (url.endsWith('/oauth/provider')) {
return jsonResponse({
app_icon: '',
app_label: { en_US: 'Test OAuth App' },
scope: '',
})
}
throw new Error(`Unexpected request: ${url}`)
})
vi.stubGlobal('location', {
href: 'https://dify.test/account/oauth/authorize',
origin: 'https://dify.test',
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('authorizes the displayed app and redirects with the returned code', async () => {
const user = userEvent.setup()
renderPage()
expect((await screen.findAllByText('Test OAuth App')).length).toBeGreaterThan(0)
const providerRequest = findRequest('/oauth/provider')
const providerTransportRequest = providerRequest?.[2]?.request as Request
await expect(providerTransportRequest.clone().json()).resolves.toEqual({
client_id: 'client-1',
redirect_uri: 'https://client.example.com/callback?state=state-1',
})
await user.click(screen.getByRole('button', { name: /continue/i }))
await waitFor(() => expect(findRequest('/oauth/provider/authorize')).toBeDefined())
const authorizeRequest = findRequest('/oauth/provider/authorize')
const transportRequest = authorizeRequest?.[2]?.request as Request
await expect(transportRequest.clone().json()).resolves.toEqual({ client_id: 'client-1' })
await waitFor(() =>
expect(globalThis.location.href).toBe(
'https://client.example.com/callback?state=state-1&code=oauth-code',
),
)
})
})

View File

@ -10,7 +10,7 @@ import {
RiMailLine,
RiTranslate2,
} from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import { skipToken, useMutation, useQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@ -18,8 +18,8 @@ import Loading from '@/app/components/base/loading'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client'
import { useRouter, useSearchParams } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { useLogout } from '@/service/use-common'
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
function buildReturnUrl(pathname: string, search: string) {
try {
@ -64,6 +64,7 @@ export default function OAuthAuthorize() {
const searchParams = useSearchParams()
const client_id = decodeURIComponent(searchParams.get('client_id') || '')
const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
const hasOAuthParams = Boolean(client_id && redirect_uri)
// Probe user profile. 401 stays as `error` (legitimate "not logged in" state),
// other errors throw to the nearest error.tsx; jumpTo same-pathname guard in
// service/base.ts prevents a redirect loop here.
@ -81,10 +82,23 @@ export default function OAuthAuthorize() {
data: authAppInfo,
isLoading: isOAuthLoading,
isError,
} = useOAuthAppInfo(client_id, redirect_uri)
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
} = useQuery(
consoleQuery.oauth.provider.post.queryOptions({
input: hasOAuthParams ? { body: { client_id, redirect_uri } } : skipToken,
context: { silent: true },
}),
)
const { mutateAsync: authorize, isPending: authorizing } = useMutation(
consoleQuery.oauth.provider.authorize.post.mutationOptions(),
)
const { mutateAsync: logout } = useLogout()
const hasNotifiedRef = useRef(false)
const localizedAppLabel = authAppInfo?.app_label[language]
const englishAppLabel = authAppInfo?.app_label.en_US
const appLabel =
(typeof localizedAppLabel === 'string' && localizedAppLabel) ||
(typeof englishAppLabel === 'string' && englishAppLabel) ||
t(($) => $.unknownApp, { ns: 'oauth' })
const isLoading = isOAuthLoading || isProfileLoading
const onLoginSwitchClick = async () => {
@ -100,12 +114,13 @@ export default function OAuthAuthorize() {
const onAuthorize = async () => {
if (!client_id || !redirect_uri) return
try {
const { code } = await authorize({ client_id })
const { code } = await authorize({ body: { client_id } })
const url = new URL(redirect_uri)
url.searchParams.set('code', code)
globalThis.location.href = url.toString()
} catch (err: any) {
toast.error(`${t(($) => $['error.authorizeFailed'], { ns: 'oauth' })}: ${err.message}`)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
toast.error(`${t(($) => $['error.authorizeFailed'], { ns: 'oauth' })}: ${message}`)
}
}
@ -143,11 +158,7 @@ export default function OAuthAuthorize() {
{isLoggedIn && (
<div className="text-text-primary">{t(($) => $.connect, { ns: 'oauth' })}</div>
)}
<div className="text-saas-dify-blue-inverted">
{authAppInfo?.app_label[language] ||
authAppInfo?.app_label?.en_US ||
t(($) => $.unknownApp, { ns: 'oauth' })}
</div>
<div className="text-saas-dify-blue-inverted">{appLabel}</div>
{!isLoggedIn && (
<div className="text-text-primary">
{t(($) => $['tips.notLoggedIn'], { ns: 'oauth' })}
@ -156,7 +167,7 @@ export default function OAuthAuthorize() {
</div>
<div className="body-md-regular text-text-secondary">
{isLoggedIn
? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t(($) => $.unknownApp, { ns: 'oauth' })} ${t(($) => $['tips.loggedIn'], { ns: 'oauth' })}`
? `${appLabel} ${t(($) => $['tips.loggedIn'], { ns: 'oauth' })}`
: t(($) => $['tips.needLogin'], { ns: 'oauth' })}
</div>
</div>

View File

@ -1,35 +0,0 @@
import { useMutation, useQuery } from '@tanstack/react-query'
import { post } from './base'
const NAME_SPACE = 'oauth-provider'
type OAuthAppInfo = {
app_icon: string
app_label: Record<string, string>
scope: string
}
type OAuthAuthorizeResponse = {
code: string
}
export const useOAuthAppInfo = (client_id: string, redirect_uri: string) => {
return useQuery<OAuthAppInfo>({
queryKey: [NAME_SPACE, 'authAppInfo', client_id, redirect_uri],
queryFn: () =>
post<OAuthAppInfo>(
'/oauth/provider',
{ body: { client_id, redirect_uri } },
{ silent: true },
),
enabled: Boolean(client_id && redirect_uri),
})
}
export const useAuthorizeOAuthApp = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'authorize'],
mutationFn: (payload: { client_id: string }) =>
post<OAuthAuthorizeResponse>('/oauth/provider/authorize', { body: payload }),
})
}