From 63ca2b94b5871d7fc338334e70930500006b99b3 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Fri, 17 Jul 2026 22:50:56 +0800
Subject: [PATCH] refactor(web): use generated OAuth query options (#39204)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
---
oxlint-suppressions.json | 10 --
.../oauth/authorize/__tests__/page.spec.tsx | 113 ++++++++++++++++++
web/app/account/oauth/authorize/page.tsx | 37 ++++--
web/service/use-oauth.ts | 35 ------
4 files changed, 137 insertions(+), 58 deletions(-)
create mode 100644 web/app/account/oauth/authorize/__tests__/page.spec.tsx
delete mode 100644 web/service/use-oauth.ts
diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json
index e7d17b20f9e..a236d235f8a 100644
--- a/oxlint-suppressions.json
+++ b/oxlint-suppressions.json
@@ -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
diff --git a/web/app/account/oauth/authorize/__tests__/page.spec.tsx b/web/app/account/oauth/authorize/__tests__/page.spec.tsx
new file mode 100644
index 00000000000..003673b0225
--- /dev/null
+++ b/web/app/account/oauth/authorize/__tests__/page.spec.tsx
@@ -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(
+
+
+ ,
+ )
+}
+
+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',
+ ),
+ )
+ })
+})
diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx
index 5d2c5ab45b4..a927e318bc8 100644
--- a/web/app/account/oauth/authorize/page.tsx
+++ b/web/app/account/oauth/authorize/page.tsx
@@ -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 && (
{t(($) => $.connect, { ns: 'oauth' })}
)}
-
- {authAppInfo?.app_label[language] ||
- authAppInfo?.app_label?.en_US ||
- t(($) => $.unknownApp, { ns: 'oauth' })}
-
+ {appLabel}
{!isLoggedIn && (
{t(($) => $['tips.notLoggedIn'], { ns: 'oauth' })}
@@ -156,7 +167,7 @@ export default function OAuthAuthorize() {
{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' })}
diff --git a/web/service/use-oauth.ts b/web/service/use-oauth.ts
deleted file mode 100644
index 4ef1b955e17..00000000000
--- a/web/service/use-oauth.ts
+++ /dev/null
@@ -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
- scope: string
-}
-
-type OAuthAuthorizeResponse = {
- code: string
-}
-
-export const useOAuthAppInfo = (client_id: string, redirect_uri: string) => {
- return useQuery({
- queryKey: [NAME_SPACE, 'authAppInfo', client_id, redirect_uri],
- queryFn: () =>
- post(
- '/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('/oauth/provider/authorize', { body: payload }),
- })
-}