mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(oauth): clean up oauth authorization flow
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Check if the user has authorized a specific OAuth provider
|
||||
*/
|
||||
async function hasAuthorizedProvider(
|
||||
userId: string,
|
||||
provider: OAuthProvider,
|
||||
requiredScopes?: string[]
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Determine the appropriate provider ID based on scopes
|
||||
let featureType = 'default'
|
||||
if (requiredScopes && requiredScopes.length > 0) {
|
||||
if (requiredScopes.some((scope) => scope.includes('repo'))) {
|
||||
featureType = 'repo'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('workflow'))) {
|
||||
featureType = 'workflow'
|
||||
} else if (
|
||||
requiredScopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))
|
||||
) {
|
||||
featureType = 'email'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('calendar'))) {
|
||||
featureType = 'calendar'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('drive'))) {
|
||||
featureType = 'drive'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('write'))) {
|
||||
featureType = 'write'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('read'))) {
|
||||
featureType = 'read'
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the provider ID based on the provider and feature type
|
||||
const providerId = `${provider}-${featureType}`
|
||||
|
||||
// Check if the user has this provider account
|
||||
const accounts = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
|
||||
.limit(1)
|
||||
|
||||
return accounts.length > 0
|
||||
} catch (error) {
|
||||
console.error('Error checking OAuth authorization:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API route to check if a tool requires OAuth and if the user is authorized
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ requiresAuth: true, isAuthorized: false, error: 'User not authenticated' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the tool from the request body
|
||||
const { tool } = await request.json()
|
||||
|
||||
// Check if the tool requires OAuth
|
||||
if (!tool.oauth || !tool.oauth.required) {
|
||||
return NextResponse.json({ requiresAuth: false, isAuthorized: true }, { status: 200 })
|
||||
}
|
||||
|
||||
// Get the provider and required scopes
|
||||
const provider = tool.oauth.provider
|
||||
const requiredScopes = tool.oauth.additionalScopes || []
|
||||
|
||||
// Check if the user has authorized this provider
|
||||
const isAuthorized = await hasAuthorizedProvider(session.user.id, provider, requiredScopes)
|
||||
|
||||
// Return the authorization status
|
||||
if (isAuthorized) {
|
||||
return NextResponse.json({ requiresAuth: true, isAuthorized: true }, { status: 200 })
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
requiresAuth: true,
|
||||
isAuthorized: false,
|
||||
error: JSON.stringify({
|
||||
type: 'oauth_required',
|
||||
provider,
|
||||
toolId: tool.id,
|
||||
toolName: tool.name,
|
||||
requiredScopes,
|
||||
}),
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking OAuth authorization:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasAuthorizedProviderServer } from '@/lib/oauth'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* API endpoint to check if a user has authorized a specific OAuth provider
|
||||
*
|
||||
* @param request - The request object with provider and optional scopes
|
||||
* @returns JSON response with authorization status
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ isAuthorized: false, error: 'Not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get the provider from the query string
|
||||
const url = new URL(request.url)
|
||||
const provider = url.searchParams.get('provider') as OAuthProvider | null
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json(
|
||||
{ isAuthorized: false, error: 'Provider is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get optional scopes from the query string
|
||||
const scopesParam = url.searchParams.get('scopes')
|
||||
const requiredScopes = scopesParam ? scopesParam.split(',') : undefined
|
||||
|
||||
// Check if the user has authorized this provider with the required scopes
|
||||
const isAuthorized = await hasAuthorizedProviderServer(provider, requiredScopes)
|
||||
|
||||
return NextResponse.json({ isAuthorized })
|
||||
} catch (error) {
|
||||
console.error('Error checking OAuth authorization:', error)
|
||||
return NextResponse.json(
|
||||
{ isAuthorized: false, error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useOAuthErrorHandler } from '@/lib/oauth'
|
||||
import { useConsoleStore } from '@/stores/console/store'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
@@ -21,6 +22,9 @@ export function useWorkflowExecution() {
|
||||
const { isExecuting, setIsExecuting } = useExecutionStore()
|
||||
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
|
||||
|
||||
// Add OAuth error handler
|
||||
const { handleOAuthError } = useOAuthErrorHandler()
|
||||
|
||||
const persistLogs = async (logs: any[], executionId: string) => {
|
||||
// Check if we're in local storage mode
|
||||
const useLocalStorage =
|
||||
@@ -176,29 +180,36 @@ export function useWorkflowExecution() {
|
||||
await persistLogs(blockLogs, executionId)
|
||||
} catch (error: any) {
|
||||
console.error('Workflow Execution Error:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
|
||||
// Set error result and show notification immediately
|
||||
setExecutionResult({
|
||||
success: false,
|
||||
output: { response: {} },
|
||||
error: errorMessage,
|
||||
logs: [],
|
||||
})
|
||||
addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId)
|
||||
// Check if this is an OAuth error first
|
||||
const isOAuthError = handleOAuthError(error)
|
||||
|
||||
// Persist error log after notification
|
||||
await persistLogs(
|
||||
[
|
||||
{
|
||||
level: 'error',
|
||||
message: `Manual workflow execution failed: ${errorMessage}`,
|
||||
duration: 'NA',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
executionId
|
||||
)
|
||||
// If not an OAuth error, handle normally
|
||||
if (!isOAuthError) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
|
||||
// Set error result and show notification immediately
|
||||
setExecutionResult({
|
||||
success: false,
|
||||
output: { response: {} },
|
||||
error: errorMessage,
|
||||
logs: [],
|
||||
})
|
||||
addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId)
|
||||
|
||||
// Persist error log after notification
|
||||
await persistLogs(
|
||||
[
|
||||
{
|
||||
level: 'error',
|
||||
message: `Manual workflow execution failed: ${errorMessage}`,
|
||||
duration: 'NA',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
executionId
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
}
|
||||
@@ -211,8 +222,8 @@ export function useWorkflowExecution() {
|
||||
isOpen,
|
||||
toggleConsole,
|
||||
getAllVariables,
|
||||
isExecuting,
|
||||
setIsExecuting,
|
||||
handleOAuthError,
|
||||
])
|
||||
|
||||
return { isExecuting, executionResult, handleRunWorkflow }
|
||||
|
||||
+60
-42
@@ -12,6 +12,8 @@ import ReactFlow, {
|
||||
useReactFlow,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { OAuthRequiredModal } from '@/components/ui/oauth-required-modal'
|
||||
import { useOAuthErrorHandler } from '@/lib/oauth'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import { getSyncManagers, initializeSyncManagers, isSyncInitialized } from '@/stores/sync-registry'
|
||||
@@ -50,6 +52,9 @@ function WorkflowContent() {
|
||||
const { blocks, edges, loops, addBlock, updateBlockPosition, addEdge, removeEdge } =
|
||||
useWorkflowStore()
|
||||
|
||||
// Add OAuth error handling
|
||||
const { modalState, handleOAuthError, closeModal } = useOAuthErrorHandler()
|
||||
|
||||
// Initialize workflow
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -310,48 +315,61 @@ function WorkflowContent() {
|
||||
if (!isInitialized) return null
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-[calc(100vh-4rem)]">
|
||||
<NotificationList />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edgesWithSelection}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onDrop={onDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
fitView
|
||||
minZoom={0.1}
|
||||
maxZoom={1}
|
||||
panOnScroll
|
||||
defaultEdgeOptions={{ type: 'custom' }}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
connectionLineStyle={{
|
||||
stroke: '#94a3b8',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '5,5',
|
||||
}}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
onNodeClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
onPaneClick={onPaneClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
elementsSelectable={true}
|
||||
selectNodesOnDrag={false}
|
||||
nodesConnectable={true}
|
||||
nodesDraggable={true}
|
||||
draggable={false}
|
||||
noWheelClassName="allow-scroll"
|
||||
edgesFocusable={true}
|
||||
edgesUpdatable={true}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<>
|
||||
{/* Add the OAuth modal */}
|
||||
{modalState.isOpen && modalState.provider && (
|
||||
<OAuthRequiredModal
|
||||
isOpen={modalState.isOpen}
|
||||
onClose={closeModal}
|
||||
provider={modalState.provider}
|
||||
toolName={modalState.toolName}
|
||||
requiredScopes={modalState.requiredScopes}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative w-full h-[calc(100vh-4rem)]">
|
||||
<NotificationList />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edgesWithSelection}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onDrop={onDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
fitView
|
||||
minZoom={0.1}
|
||||
maxZoom={1}
|
||||
panOnScroll
|
||||
defaultEdgeOptions={{ type: 'custom' }}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
connectionLineStyle={{
|
||||
stroke: '#94a3b8',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '5,5',
|
||||
}}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
onNodeClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
onPaneClick={onPaneClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
elementsSelectable={true}
|
||||
selectNodesOnDrag={false}
|
||||
nodesConnectable={true}
|
||||
nodesDraggable={true}
|
||||
draggable={false}
|
||||
noWheelClassName="allow-scroll"
|
||||
edgesFocusable={true}
|
||||
edgesUpdatable={true}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+45
-360
@@ -1,9 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
/**
|
||||
@@ -18,382 +15,70 @@ export interface OAuthRequiredError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has authorized the required OAuth provider with necessary scopes (server-side)
|
||||
*
|
||||
* @param provider - The OAuth provider to check
|
||||
* @param requiredScopes - Optional scopes to check
|
||||
* @returns Boolean indicating if the provider is authorized with required scopes
|
||||
*/
|
||||
export async function hasAuthorizedProviderServer(
|
||||
provider: OAuthProvider,
|
||||
requiredScopes?: string[]
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// If not authenticated, return false
|
||||
if (!session?.user?.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Determine the appropriate feature type based on the scopes
|
||||
let featureType = 'default'
|
||||
|
||||
if (requiredScopes && requiredScopes.length > 0) {
|
||||
if (requiredScopes.some((scope) => scope.includes('repo'))) {
|
||||
featureType = 'repo'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('workflow'))) {
|
||||
featureType = 'workflow'
|
||||
} else if (
|
||||
requiredScopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))
|
||||
) {
|
||||
featureType = 'email'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('calendar'))) {
|
||||
featureType = 'calendar'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('drive'))) {
|
||||
featureType = 'drive'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('write'))) {
|
||||
featureType = 'write'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('read'))) {
|
||||
featureType = 'read'
|
||||
}
|
||||
}
|
||||
|
||||
// We check the appropriate provider ID based on the feature type
|
||||
const providerId = `${provider}-${featureType}`
|
||||
|
||||
// Check if there's an account for this provider for the user
|
||||
const accounts = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, session.user.id), eq(account.providerId, providerId)))
|
||||
|
||||
return accounts.length > 0
|
||||
} catch (error) {
|
||||
console.error('Error checking provider authorization:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool requires OAuth and if the user has authorized it (server-side)
|
||||
*
|
||||
* @param tool - The tool configuration
|
||||
* @returns Object indicating if OAuth is required and if the user has authorized it
|
||||
*/
|
||||
export async function checkOAuthRequirementServer(tool: any): Promise<{
|
||||
requiresAuth: boolean
|
||||
isAuthorized: boolean
|
||||
provider?: OAuthProvider
|
||||
requiredScopes?: string[]
|
||||
}> {
|
||||
// Skip if no OAuth config or not required
|
||||
if (!tool.oauth || !tool.oauth.required) {
|
||||
return { requiresAuth: false, isAuthorized: false }
|
||||
}
|
||||
|
||||
const provider = tool.oauth.provider
|
||||
const additionalScopes = tool.oauth.additionalScopes || []
|
||||
|
||||
// Check if the user has authorized this provider with required scopes
|
||||
const isAuthorized = await hasAuthorizedProviderServer(provider, additionalScopes)
|
||||
|
||||
return {
|
||||
requiresAuth: true,
|
||||
isAuthorized,
|
||||
provider,
|
||||
requiredScopes: additionalScopes,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify OAuth requirements before executing a tool (server-side)
|
||||
* Throws an error if OAuth is required but not authorized
|
||||
*
|
||||
* @param tool - The tool configuration
|
||||
* @throws Error with JSON.stringify(OAuthRequiredError)
|
||||
*/
|
||||
export async function verifyOAuthBeforeExecutionServer(tool: any): Promise<void> {
|
||||
const { requiresAuth, isAuthorized, provider, requiredScopes } =
|
||||
await checkOAuthRequirementServer(tool)
|
||||
|
||||
if (requiresAuth && !isAuthorized && provider) {
|
||||
// Throw a structured error that can be caught and handled
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: 'oauth_required',
|
||||
provider,
|
||||
toolId: tool.id,
|
||||
toolName: tool.name,
|
||||
requiredScopes,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth tokens for a provider if the user has authorized it
|
||||
*
|
||||
* @param userId - The user's ID
|
||||
* @param provider - The OAuth provider to get tokens for
|
||||
* @returns The OAuth tokens or null if not authorized
|
||||
*/
|
||||
export async function getOAuthTokens(
|
||||
userId: string,
|
||||
provider: OAuthProvider
|
||||
): Promise<{
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
expiresAt?: Date
|
||||
} | null> {
|
||||
try {
|
||||
// Query the account table for this user and provider
|
||||
const accounts = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, userId), eq(account.providerId, provider)))
|
||||
.limit(1)
|
||||
|
||||
if (!accounts.length || !accounts[0].accessToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
const userAccount = accounts[0]
|
||||
|
||||
// Check if the token is expired
|
||||
if (
|
||||
userAccount.accessTokenExpiresAt &&
|
||||
new Date(userAccount.accessTokenExpiresAt) < new Date()
|
||||
) {
|
||||
// In a production app, we would use the refresh token to get a new access token here
|
||||
// But for simplicity, we'll just return null for expired tokens
|
||||
console.warn(`Token for ${provider} is expired and needs refresh`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Ensure accessToken is not null using the type guard we did earlier
|
||||
const accessToken = userAccount.accessToken as string
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: userAccount.refreshToken || undefined,
|
||||
expiresAt: userAccount.accessTokenExpiresAt
|
||||
? new Date(userAccount.accessTokenExpiresAt)
|
||||
: undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting OAuth tokens:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth tokens for a specific tool if the user has authorized it
|
||||
*
|
||||
* @param userId - The user's ID
|
||||
* @param tool - The tool configuration
|
||||
* @returns The OAuth tokens or null if not required or not authorized
|
||||
*/
|
||||
export async function getOAuthTokensForTool(
|
||||
userId: string,
|
||||
tool: any
|
||||
): Promise<{
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
expiresAt?: Date
|
||||
} | null> {
|
||||
// Skip if no OAuth config or not required
|
||||
if (!tool.oauth || !tool.oauth.required) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get tokens for the provider
|
||||
return getOAuthTokens(userId, tool.oauth.provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to check if a user has authorized an OAuth provider
|
||||
*
|
||||
* @param provider - The OAuth provider to check
|
||||
* @param requiredScopes - Optional array of scopes required for the operation
|
||||
* @returns An object with authorization status and loading state
|
||||
*/
|
||||
export function useProviderAuthorization(provider: OAuthProvider, requiredScopes?: string[]) {
|
||||
const { data: session, isPending } = useSession()
|
||||
const [isAuthorized, setIsAuthorized] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isPending || !session?.user) {
|
||||
setIsAuthorized(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has provider accounts in their session
|
||||
// This is a client-side check, so it may not be as accurate as the server-side check
|
||||
const checkAuthorization = async () => {
|
||||
try {
|
||||
// We'll use an API endpoint to check authorization status
|
||||
const response = await fetch(
|
||||
`/api/auth/oauth/check?provider=${provider}${
|
||||
requiredScopes ? `&scopes=${requiredScopes.join(',')}` : ''
|
||||
}`
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setIsAuthorized(data.isAuthorized || false)
|
||||
} else {
|
||||
setIsAuthorized(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking OAuth authorization:', error)
|
||||
setIsAuthorized(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkAuthorization()
|
||||
}, [session, isPending, provider, requiredScopes])
|
||||
|
||||
return {
|
||||
isAuthorized,
|
||||
isLoading: isPending,
|
||||
isLoggedIn: !!session?.user,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool requires OAuth and if the user has authorized it
|
||||
* This function must be used in a client component
|
||||
*
|
||||
* @param tool - The tool configuration
|
||||
* @returns Object indicating if OAuth is required and if the user has the necessary authorization
|
||||
*/
|
||||
export function useToolOAuthRequirement(tool: any) {
|
||||
// Skip if no OAuth config or not required
|
||||
if (!tool.oauth || !tool.oauth.required) {
|
||||
return {
|
||||
requiresAuth: false,
|
||||
isAuthorized: true,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
const provider = tool.oauth.provider
|
||||
const additionalScopes = tool.oauth.additionalScopes || []
|
||||
|
||||
// Use the provider authorization hook
|
||||
const { isAuthorized, isLoading, isLoggedIn } = useProviderAuthorization(
|
||||
provider,
|
||||
additionalScopes
|
||||
)
|
||||
|
||||
return {
|
||||
requiresAuth: true,
|
||||
isAuthorized: isAuthorized,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
provider,
|
||||
requiredScopes: additionalScopes,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify OAuth requirements before executing a tool
|
||||
* Throws an error if OAuth is required but not authorized
|
||||
* This function must be used in a client component
|
||||
*
|
||||
* @param toolOAuthStatus - The result from useToolOAuthRequirement
|
||||
* @param tool - The tool configuration
|
||||
* @throws Error with JSON stringified OAuthRequiredError
|
||||
*/
|
||||
export function verifyOAuthBeforeExecution(
|
||||
toolOAuthStatus: ReturnType<typeof useToolOAuthRequirement>,
|
||||
tool: any
|
||||
): void {
|
||||
const { requiresAuth, isAuthorized, isLoading, provider, requiredScopes } = toolOAuthStatus
|
||||
|
||||
// Don't verify while loading
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresAuth && !isAuthorized && provider) {
|
||||
// Throw a structured error that the frontend can catch and handle
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
type: 'oauth_required',
|
||||
provider,
|
||||
toolId: tool.id,
|
||||
toolName: tool.name,
|
||||
requiredScopes,
|
||||
} as OAuthRequiredError)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for handling OAuth errors during tool execution
|
||||
* Provides a modal state and error handler function
|
||||
* Custom hook to handle OAuth errors during workflow execution
|
||||
*/
|
||||
export function useOAuthErrorHandler() {
|
||||
const [oauthModalState, setOAuthModalState] = useState<{
|
||||
const [modalState, setModalState] = useState<{
|
||||
isOpen: boolean
|
||||
provider: OAuthProvider
|
||||
provider: OAuthProvider | null
|
||||
toolName: string
|
||||
requiredScopes?: string[]
|
||||
}>({
|
||||
isOpen: false,
|
||||
provider: 'github',
|
||||
provider: null,
|
||||
toolName: '',
|
||||
})
|
||||
|
||||
/**
|
||||
* Handle an error that might be an OAuth required error
|
||||
* Returns true if it was handled as an OAuth error, false otherwise
|
||||
*/
|
||||
const handleError = useCallback((error: any): boolean => {
|
||||
if (!error) return false
|
||||
|
||||
const handleOAuthError = useCallback((error: any) => {
|
||||
// Check if the error is an OAuth required error
|
||||
try {
|
||||
// Try to parse error message as JSON
|
||||
let errorObj
|
||||
if (typeof error === 'string' && error.includes('oauth_required')) {
|
||||
const errorData: OAuthRequiredError = JSON.parse(error)
|
||||
|
||||
if (typeof error === 'string') {
|
||||
errorObj = JSON.parse(error)
|
||||
} else if (error instanceof Error && error.message) {
|
||||
try {
|
||||
errorObj = JSON.parse(error.message)
|
||||
} catch {
|
||||
return false
|
||||
if (errorData.type === 'oauth_required' && errorData.provider) {
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
provider: errorData.provider,
|
||||
toolName: errorData.toolName || 'this tool',
|
||||
requiredScopes: errorData.requiredScopes,
|
||||
})
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if (
|
||||
error?.message &&
|
||||
typeof error.message === 'string' &&
|
||||
error.message.includes('oauth_required')
|
||||
) {
|
||||
try {
|
||||
const errorData: OAuthRequiredError = JSON.parse(error.message)
|
||||
|
||||
// Check if it's an OAuth required error
|
||||
if (errorObj?.type === 'oauth_required') {
|
||||
setOAuthModalState({
|
||||
isOpen: true,
|
||||
provider: errorObj.provider,
|
||||
toolName: errorObj.toolName,
|
||||
})
|
||||
return true
|
||||
if (errorData.type === 'oauth_required' && errorData.provider) {
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
provider: errorData.provider,
|
||||
toolName: errorData.toolName || 'this tool',
|
||||
requiredScopes: errorData.requiredScopes,
|
||||
})
|
||||
return true
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing OAuth error message:', parseError)
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Not a JSON error or not an OAuth error
|
||||
return false
|
||||
} catch (e) {
|
||||
console.error('Error handling OAuth error:', e)
|
||||
}
|
||||
|
||||
return false
|
||||
}, [])
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setOAuthModalState((prev) => ({ ...prev, isOpen: false }))
|
||||
setModalState((prev) => ({ ...prev, isOpen: false }))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
oauthModalState,
|
||||
handleOAuthError: handleError,
|
||||
closeOAuthModal: closeModal,
|
||||
modalState,
|
||||
handleOAuthError,
|
||||
closeModal,
|
||||
}
|
||||
}
|
||||
|
||||
+35
-3
@@ -1,4 +1,4 @@
|
||||
import { verifyOAuthBeforeExecutionServer } from '@/lib/oauth'
|
||||
import { OAuthRequiredError } from '@/lib/oauth'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
||||
import { visionTool as crewAIVision } from './crewai/vision'
|
||||
@@ -275,6 +275,39 @@ function getCustomTool(customToolId: string): ToolConfig | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check OAuth via API
|
||||
async function checkOAuth(tool: any): Promise<void> {
|
||||
// Skip if no OAuth config or not required or if running in browser
|
||||
if (!tool.oauth?.required || isBrowser()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the API route for OAuth checking
|
||||
const response = await fetch('/api/auth/oauth/check-tool', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ tool }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to check OAuth authorization')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// If requires auth but not authorized, throw the OAuth error
|
||||
if (data.requiresAuth && !data.isAuthorized && data.error) {
|
||||
throw new Error(data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
// Re-throw the error to be caught by execution error handlers
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Execute a tool by calling either the proxy for external APIs or directly for internal routes
|
||||
export async function executeTool(
|
||||
toolId: string,
|
||||
@@ -293,9 +326,8 @@ export async function executeTool(
|
||||
}
|
||||
|
||||
// Check OAuth requirements before executing the tool
|
||||
// This will throw an OAuthRequiredError if the tool requires OAuth but the user hasn't authorized it
|
||||
if (tool.oauth?.required && !isBrowser()) {
|
||||
await verifyOAuthBeforeExecutionServer(tool)
|
||||
await checkOAuth(tool)
|
||||
}
|
||||
|
||||
// For custom tools, try direct execution in browser first if available
|
||||
|
||||
Reference in New Issue
Block a user