mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
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:
parent
a7ef41a8f5
commit
63ca2b94b5
@ -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
|
||||
|
||||
113
web/app/account/oauth/authorize/__tests__/page.spec.tsx
Normal file
113
web/app/account/oauth/authorize/__tests__/page.spec.tsx
Normal 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',
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -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 }),
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user