use popup window for oauth

This commit is contained in:
jZonG
2025-06-19 16:09:05 +08:00
parent 8c95cf359e
commit 81ea5f1b77
5 changed files with 89 additions and 52 deletions

View File

@ -1,6 +1,5 @@
'use client'
import React, { useCallback } from 'react'
import { useRouter } from 'next/navigation'
import React, { useCallback, useEffect } from 'react'
import type { FC } from 'react'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
@ -26,29 +25,35 @@ import {
useInvalidateMCPTools,
useMCPTools,
useUpdateMCP,
useUpdateMCPAuthorizationToken,
useUpdateMCPTools,
} from '@/service/use-tools'
import { openOAuthPopup } from '@/hooks/use-oauth'
import cn from '@/utils/classnames'
type Props = {
detail: ToolWithProvider
onUpdate: (isDelete?: boolean) => void
onHide: () => void
isCreation: boolean
onFirstCreate: () => void
}
const MCPDetailContent: FC<Props> = ({
detail,
onUpdate,
onHide,
isCreation,
onFirstCreate,
}) => {
const { t } = useTranslation()
const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '')
const invalidateMCPTools = useInvalidateMCPTools()
const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools()
const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP()
const { mutateAsync: updateMCPAuthorizationToken } = useUpdateMCPAuthorizationToken()
const toolList = data?.tools || []
const handleUpdateTools = useCallback(async () => {
@ -81,7 +86,22 @@ const MCPDetailContent: FC<Props> = ({
setFalse: hideDeleting,
}] = useBoolean(false)
const handleOAuthCallback = async (state: string, code: string) => {
if (!isCurrentWorkspaceManager)
return
if (detail.id !== state)
return
await updateMCPAuthorizationToken({
provider_id: state,
authorization_code: code,
})
handleUpdateTools()
}
const handleAuthorize = useCallback(async () => {
onFirstCreate()
if (!isCurrentWorkspaceManager)
return
if (!detail)
return
const res = await authorizeMcp({
@ -91,7 +111,7 @@ const MCPDetailContent: FC<Props> = ({
handleUpdateTools()
else if (res.authorization_url)
router.push(res.authorization_url)
openOAuthPopup(res.authorization_url, handleOAuthCallback)
}, [detail, updateMCP, hideUpdateModal, onUpdate])
const handleUpdate = useCallback(async (data: any) => {
@ -119,6 +139,11 @@ const MCPDetailContent: FC<Props> = ({
}
}, [detail, showDeleting, hideDeleting, hideDeleteConfirm, onUpdate])
useEffect(() => {
if (isCreation)
handleAuthorize()
}, [])
if (!detail)
return null

View File

@ -10,12 +10,16 @@ type Props = {
detail?: ToolWithProvider
onUpdate: () => void
onHide: () => void
isCreation: boolean
onFirstCreate: () => void
}
const MCPDetailPanel: FC<Props> = ({
detail,
onUpdate,
onHide,
isCreation,
onFirstCreate,
}) => {
const handleUpdate = (isDelete = false) => {
if (isDelete)
@ -41,6 +45,8 @@ const MCPDetailPanel: FC<Props> = ({
detail={detail}
onHide={onHide}
onUpdate={handleUpdate}
isCreation={isCreation}
onFirstCreate={onFirstCreate}
/>
)}
</Drawer>

View File

@ -1,15 +1,10 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useMemo, useState } from 'react'
import NewMCPCard from './create-card'
import MCPCard from './provider-card'
import MCPDetailPanel from './detail/provider-detail'
import {
useAllMCPTools,
useAuthorizeMCP,
useInvalidateMCPTools,
useUpdateMCPAuthorizationToken,
useUpdateMCPTools,
} from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
@ -39,17 +34,8 @@ function renderDefaultCard() {
const MCPList = ({
searchText,
}: Props) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const authCode = searchParams.get('code') || ''
const providerID = searchParams.get('state') || ''
const { data: list = [], refetch } = useAllMCPTools()
const { mutateAsync: authorizeMcp } = useAuthorizeMCP()
const { mutateAsync: updateTools } = useUpdateMCPTools()
const invalidateMCPTools = useInvalidateMCPTools()
const { mutateAsync: updateMCPAuthorizationToken } = useUpdateMCPAuthorizationToken()
const [isCreation, setIsCreation] = useState<boolean>(false)
const filteredList = useMemo(() => {
return list.filter((collection) => {
@ -68,40 +54,9 @@ const MCPList = ({
const handleCreate = async (provider: ToolWithProvider) => {
await refetch() // update list
setCurrentProviderID(provider.id)
const res = await authorizeMcp({
provider_id: provider.id,
})
if (res.result === 'success') {
await refetch() // update authorization in list
await updateTools(provider.id)
invalidateMCPTools(provider.id)
await refetch() // update tool list in provider list
}
else if (res.authorization_url) {
router.push(res.authorization_url)
}
setIsCreation(true)
}
const handleUpdateAuthorization = async (providerID: string, code: string) => {
const targetProvider = list.find(provider => provider.id === providerID)
router.replace(pathname)
if (!targetProvider) return
await updateMCPAuthorizationToken({
provider_id: providerID,
authorization_code: code,
})
await refetch()
setCurrentProviderID(providerID)
await updateTools(providerID)
invalidateMCPTools(providerID)
await refetch()
}
useEffect(() => {
if (authCode && providerID && list.length > 0)
handleUpdateAuthorization(providerID, authCode)
}, [authCode, providerID, list])
return (
<>
<div
@ -127,6 +82,8 @@ const MCPList = ({
detail={currentProvider}
onHide={() => setCurrentProviderID(undefined)}
onUpdate={refetch}
isCreation={isCreation}
onFirstCreate={() => setIsCreation(false)}
/>
)}
</>

View File

@ -20,11 +20,13 @@ import MCPList from './mcp'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAllToolProviders } from '@/service/use-tools'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useOAuthCallback } from '@/hooks/use-oauth'
const ProviderList = () => {
const { t } = useTranslation()
const containerRef = useRef<HTMLDivElement>(null)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
useOAuthCallback()
const searchParams = useSearchParams()
const authCode = searchParams.get('code') || ''

47
web/hooks/use-oauth.ts Normal file
View File

@ -0,0 +1,47 @@
'use client'
import { useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
export const useOAuthCallback = () => {
const searchParams = useSearchParams()
useEffect(() => {
const code = searchParams.get('code')
const state = searchParams.get('state')
if (code && state && window.opener) {
window.opener.postMessage({
type: 'oauth_callback',
payload: {
code,
state,
},
}, '*')
window.close()
}
}, [searchParams])
}
export const openOAuthPopup = (url: string, callback: (state: string, code: string) => void) => {
const width = 600
const height = 600
const left = window.screenX + (window.outerWidth - width) / 2
const top = window.screenY + (window.outerHeight - height) / 2
const popup = window.open(
url,
'OAuth',
`width=${width},height=${height},left=${left},top=${top},scrollbars=yes`,
)
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'oauth_callback') {
window.removeEventListener('message', handleMessage)
const { code, state } = event.data.payload
callback(state, code)
}
}
window.addEventListener('message', handleMessage)
return popup
}