mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement: standardized oauth implementation & ui
This commit is contained in:
@@ -2,9 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { OAuthService } from '@/lib/oauth'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
interface GoogleIdToken {
|
||||
email?: string
|
||||
@@ -66,7 +66,7 @@ export async function GET(request: NextRequest) {
|
||||
} else {
|
||||
// Create new connection
|
||||
connections.push({
|
||||
provider: provider as OAuthProvider,
|
||||
provider: provider as OAuthService,
|
||||
featureType,
|
||||
isConnected: true,
|
||||
scopes: acc.scope ? acc.scope.split(' ') : [],
|
||||
|
||||
@@ -3,9 +3,9 @@ import { and, eq, like } from 'drizzle-orm'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { parseProvider } from '@/lib/oauth'
|
||||
import { OAuthService } from '@/lib/oauth'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
interface GoogleIdToken {
|
||||
email?: string
|
||||
@@ -27,7 +27,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Get the provider from the query params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const provider = searchParams.get('provider') as OAuthProvider | null
|
||||
const provider = searchParams.get('provider') as OAuthService | null
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json({ error: 'Provider is required' }, { status: 400 })
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, Key, RefreshCw } from 'lucide-react'
|
||||
import { GithubIcon, GoogleIcon, xIcon as TwitterIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
@@ -14,8 +13,16 @@ import {
|
||||
} from '@/components/ui/command'
|
||||
import { OAuthRequiredModal } from '@/components/ui/oauth-required-modal'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import {
|
||||
Credential,
|
||||
OAUTH_PROVIDERS,
|
||||
OAuthProvider,
|
||||
getProviderIdFromServiceId,
|
||||
getServiceByProviderAndId,
|
||||
getServiceIdFromScopes,
|
||||
} from '@/lib/oauth'
|
||||
import { saveToStorage } from '@/stores/workflows/persistence'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
interface CredentialSelectorProps {
|
||||
value: string
|
||||
@@ -27,14 +34,6 @@ interface CredentialSelectorProps {
|
||||
serviceId?: string
|
||||
}
|
||||
|
||||
interface Credential {
|
||||
id: string
|
||||
name: string
|
||||
provider: OAuthProvider
|
||||
lastUsed?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
export function CredentialSelector({
|
||||
value,
|
||||
onChange,
|
||||
@@ -54,58 +53,13 @@ export function CredentialSelector({
|
||||
// Determine the appropriate service ID based on provider and scopes
|
||||
const getServiceId = (): string => {
|
||||
if (serviceId) return serviceId
|
||||
|
||||
if (provider === 'google') {
|
||||
if (requiredScopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))) {
|
||||
return 'gmail'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('drive'))) {
|
||||
return 'google-drive'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('docs'))) {
|
||||
return 'google-docs'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('sheets'))) {
|
||||
return 'google-sheets'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('calendar'))) {
|
||||
return 'google-calendar'
|
||||
} else {
|
||||
return 'gmail' // Default Google service
|
||||
}
|
||||
} else if (provider === 'github') {
|
||||
return 'github'
|
||||
} else if (provider === 'twitter') {
|
||||
return 'twitter'
|
||||
}
|
||||
|
||||
return provider
|
||||
return getServiceIdFromScopes(provider, requiredScopes)
|
||||
}
|
||||
|
||||
// Determine the appropriate provider ID based on service and scopes
|
||||
const getProviderId = (): string => {
|
||||
const effectiveServiceId = getServiceId()
|
||||
|
||||
switch (effectiveServiceId) {
|
||||
case 'gmail':
|
||||
return 'google-email'
|
||||
case 'google-drive':
|
||||
return 'google-drive'
|
||||
case 'google-sheets':
|
||||
return 'google-sheets'
|
||||
case 'google-docs':
|
||||
return 'google-docs'
|
||||
case 'google-calendar':
|
||||
return 'google-calendar'
|
||||
case 'github':
|
||||
if (requiredScopes.some((scope) => scope.includes('workflow'))) {
|
||||
return 'github-workflow'
|
||||
}
|
||||
return 'github-repo'
|
||||
case 'twitter':
|
||||
if (requiredScopes.some((scope) => scope.includes('write'))) {
|
||||
return 'twitter-write'
|
||||
}
|
||||
return 'twitter-read'
|
||||
default:
|
||||
return `${provider}-default`
|
||||
}
|
||||
return getProviderIdFromServiceId(effectiveServiceId)
|
||||
}
|
||||
|
||||
// Fetch available credentials for this provider
|
||||
@@ -187,50 +141,53 @@ export function CredentialSelector({
|
||||
const providerId = getProviderId()
|
||||
|
||||
// Store information about the required connection
|
||||
saveToStorage('pending_service_id', effectiveServiceId)
|
||||
saveToStorage('pending_oauth_scopes', requiredScopes)
|
||||
saveToStorage('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage('pending_oauth_provider_id', providerId)
|
||||
saveToStorage<string>('pending_service_id', effectiveServiceId)
|
||||
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
|
||||
saveToStorage<string>('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage<string>('pending_oauth_provider_id', providerId)
|
||||
|
||||
// Show the OAuth modal
|
||||
setShowOAuthModal(true)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Get provider icon
|
||||
const getProviderIcon = (provider: OAuthProvider) => {
|
||||
const baseProvider = provider.split('-')[0] as OAuthProvider
|
||||
switch (baseProvider) {
|
||||
case 'google':
|
||||
return <GoogleIcon className="h-4 w-4" />
|
||||
case 'github':
|
||||
return <GithubIcon className="h-4 w-4" />
|
||||
case 'twitter':
|
||||
return <TwitterIcon className="h-4 w-4" />
|
||||
default:
|
||||
return <ExternalLink className="h-4 w-4" />
|
||||
// Handle direct OAuth flow
|
||||
const handleDirectOAuth = async () => {
|
||||
try {
|
||||
const providerId = getProviderId()
|
||||
|
||||
// Begin OAuth flow with the appropriate provider
|
||||
await client.signIn.oauth2({
|
||||
providerId,
|
||||
callbackURL: window.location.href,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('OAuth login error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get provider icon
|
||||
const getProviderIcon = (providerName: OAuthProvider) => {
|
||||
const providerConfig = OAUTH_PROVIDERS[providerName]
|
||||
if (providerConfig) {
|
||||
return providerConfig.icon({ className: 'h-4 w-4' })
|
||||
}
|
||||
return <ExternalLink className="h-4 w-4" />
|
||||
}
|
||||
|
||||
// Get provider name
|
||||
const getProviderName = (provider: OAuthProvider) => {
|
||||
switch (provider) {
|
||||
case 'google':
|
||||
return 'Google'
|
||||
case 'google-email':
|
||||
return 'Gmail'
|
||||
case 'google-drive':
|
||||
return 'Google Drive'
|
||||
case 'google-sheets':
|
||||
return 'Google Sheets'
|
||||
case 'google-docs':
|
||||
return 'Google Docs'
|
||||
case 'github':
|
||||
return 'GitHub'
|
||||
case 'twitter':
|
||||
return 'X (Twitter)'
|
||||
default:
|
||||
return provider
|
||||
const getProviderName = (providerName: OAuthProvider) => {
|
||||
const effectiveServiceId = getServiceId()
|
||||
try {
|
||||
const service = getServiceByProviderAndId(providerName, effectiveServiceId)
|
||||
return service.name
|
||||
} catch (error) {
|
||||
// Fallback to provider name if service not found
|
||||
const providerConfig = OAUTH_PROVIDERS[providerName]
|
||||
if (providerConfig) {
|
||||
return providerConfig.name
|
||||
}
|
||||
return providerName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +208,7 @@ export function CredentialSelector({
|
||||
<span>{selectedCredential.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
@@ -259,61 +216,65 @@ export function CredentialSelector({
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[250px] p-0">
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={`Search credentials...`} />
|
||||
<CommandInput placeholder="Search credentials..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span className="ml-2">Loading credentials...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">No credentials found</p>
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-sm">No credentials found.</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect a new account to continue.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
{credentials.length > 0 && (
|
||||
<CommandGroup>
|
||||
{credentials.map((credential) => (
|
||||
{credentials.map((cred) => (
|
||||
<CommandItem
|
||||
key={credential.id}
|
||||
value={credential.id}
|
||||
onSelect={() => handleSelect(credential.id)}
|
||||
key={cred.id}
|
||||
value={cred.id}
|
||||
onSelect={() => handleSelect(cred.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{getProviderIcon(credential.provider)}
|
||||
<span>{credential.name}</span>
|
||||
{getProviderIcon(cred.provider)}
|
||||
<span>{cred.name}</span>
|
||||
</div>
|
||||
{credential.id === selectedId && <Check className="ml-auto h-4 w-4" />}
|
||||
{cred.id === selectedId && <Check className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
<div className="p-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={handleAddCredential}
|
||||
>
|
||||
<span>Add New Credential</span>
|
||||
</Button>
|
||||
</div>
|
||||
<CommandGroup>
|
||||
<CommandItem onSelect={handleAddCredential}>
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<span>Connect {getProviderName(provider)} account</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
provider={provider}
|
||||
toolName={`${getProviderName(provider)} Integration`}
|
||||
requiredScopes={requiredScopes}
|
||||
serviceId={getServiceId()}
|
||||
/>
|
||||
{showOAuthModal && (
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
provider={provider}
|
||||
toolName={getProviderName(provider)}
|
||||
requiredScopes={requiredScopes}
|
||||
serviceId={getServiceId()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,36 +3,20 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Check, ExternalLink, Plus, RefreshCw } from 'lucide-react'
|
||||
import {
|
||||
GithubIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
GoogleSheetsIcon,
|
||||
xIcon as XIcon,
|
||||
} from '@/components/icons'
|
||||
import { GmailIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { client, useSession } from '@/lib/auth-client'
|
||||
import { OAUTH_PROVIDERS, OAuthServiceConfig } from '@/lib/oauth'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { loadFromStorage, removeFromStorage, saveToStorage } from '@/stores/workflows/persistence'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
interface CredentialsProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
interface ServiceInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
provider: OAuthProvider
|
||||
providerId: string
|
||||
icon: React.ReactNode
|
||||
interface ServiceInfo extends OAuthServiceConfig {
|
||||
isConnected: boolean
|
||||
scopes: string[]
|
||||
lastConnected?: string
|
||||
accounts?: { id: string; name: string }[]
|
||||
}
|
||||
@@ -50,133 +34,53 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
const [pendingScopes, setPendingScopes] = useState<string[]>([])
|
||||
const [authSuccess, setAuthSuccess] = useState(false)
|
||||
|
||||
// Define available services
|
||||
const defineServices = (): ServiceInfo[] => [
|
||||
{
|
||||
id: 'gmail',
|
||||
name: 'Gmail',
|
||||
description: 'Automate email workflows and enhance communication efficiency.',
|
||||
provider: 'google',
|
||||
providerId: 'google-email',
|
||||
icon: <GmailIcon className="h-5 w-5" />,
|
||||
isConnected: false,
|
||||
scopes: [],
|
||||
},
|
||||
{
|
||||
id: 'google-drive',
|
||||
name: 'Google Drive',
|
||||
description: 'Streamline file organization and document workflows.',
|
||||
provider: 'google',
|
||||
providerId: 'google-drive',
|
||||
icon: <GoogleDriveIcon className="h-5 w-5" />,
|
||||
isConnected: false,
|
||||
scopes: [],
|
||||
},
|
||||
{
|
||||
id: 'google-docs',
|
||||
name: 'Google Docs',
|
||||
description: 'Create, read, and edit Google Documents programmatically.',
|
||||
provider: 'google',
|
||||
providerId: 'google-docs',
|
||||
icon: <GoogleDocsIcon className="h-5 w-5" />,
|
||||
isConnected: false,
|
||||
scopes: [],
|
||||
},
|
||||
{
|
||||
id: 'google-sheets',
|
||||
name: 'Google Sheets',
|
||||
description: 'Create, read, and edit Google Sheets programmatically.',
|
||||
provider: 'google',
|
||||
providerId: 'google-sheets',
|
||||
icon: <GoogleSheetsIcon className="h-5 w-5" />,
|
||||
isConnected: false,
|
||||
scopes: [],
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
description: 'Access repositories, issues, and other GitHub features.',
|
||||
provider: 'github',
|
||||
providerId: 'github-repo',
|
||||
icon: <GithubIcon className="h-5 w-5" />,
|
||||
isConnected: false,
|
||||
scopes: [],
|
||||
},
|
||||
{
|
||||
id: 'twitter',
|
||||
name: 'X (Twitter)',
|
||||
description: 'Read and post tweets, access user data, and more.',
|
||||
provider: 'twitter',
|
||||
providerId: 'twitter-read',
|
||||
icon: <XIcon className="h-5 w-5" />,
|
||||
isConnected: false,
|
||||
scopes: [],
|
||||
},
|
||||
]
|
||||
// Define available services from our standardized OAuth providers
|
||||
const defineServices = (): ServiceInfo[] => {
|
||||
const servicesList: ServiceInfo[] = []
|
||||
|
||||
// Fetch connection status
|
||||
// Convert our standardized providers to ServiceInfo objects
|
||||
Object.values(OAUTH_PROVIDERS).forEach((provider) => {
|
||||
Object.values(provider.services).forEach((service) => {
|
||||
servicesList.push({
|
||||
...service,
|
||||
isConnected: false,
|
||||
scopes: service.scopes || [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return servicesList
|
||||
}
|
||||
|
||||
// Fetch services and their connection status
|
||||
const fetchServices = async () => {
|
||||
if (!userId) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Get the base services
|
||||
const baseServices = defineServices()
|
||||
// Start with the base service definitions
|
||||
const serviceDefinitions = defineServices()
|
||||
|
||||
// Call your API to check connections
|
||||
// Fetch all OAuth connections for the user
|
||||
const response = await fetch('/api/auth/oauth/connections')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const connections = data.connections || []
|
||||
|
||||
// Update services with connection status
|
||||
const updatedServices = baseServices.map((service) => {
|
||||
// Update services with connection status and account info
|
||||
const updatedServices = serviceDefinitions.map((service) => {
|
||||
// Find matching connection
|
||||
const connection = connections.find((conn: any) => {
|
||||
if (
|
||||
service.id === 'gmail' &&
|
||||
conn.provider === 'google' &&
|
||||
conn.featureType === 'email'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
service.id === 'google-drive' &&
|
||||
conn.provider === 'google' &&
|
||||
conn.featureType === 'drive'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
service.id === 'google-docs' &&
|
||||
conn.provider === 'google' &&
|
||||
conn.featureType === 'docs'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
service.id === 'google-sheets' &&
|
||||
conn.provider === 'google' &&
|
||||
conn.featureType === 'sheets'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (service.id === 'github' && conn.provider === 'github') {
|
||||
return true
|
||||
}
|
||||
if (service.id === 'twitter' && conn.provider === 'twitter') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
const [provider, featureType] = conn.provider.split('-')
|
||||
return service.providerId.startsWith(provider)
|
||||
})
|
||||
|
||||
if (connection) {
|
||||
return {
|
||||
...service,
|
||||
isConnected: connection.accounts?.length > 0,
|
||||
scopes: connection.scopes || [],
|
||||
lastConnected: connection.lastConnected,
|
||||
accounts: connection.accounts || [],
|
||||
lastConnected: connection.lastConnected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,24 +89,25 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
|
||||
setServices(updatedServices)
|
||||
} else {
|
||||
// If API fails, set default state
|
||||
setServices(baseServices)
|
||||
// If there's an error, just use the base definitions
|
||||
setServices(serviceDefinitions)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching connections:', error)
|
||||
// Set default state on error
|
||||
console.error('Error fetching services:', error)
|
||||
// Use base definitions on error
|
||||
setServices(defineServices())
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle OAuth callback
|
||||
// Check for OAuth callback
|
||||
useEffect(() => {
|
||||
// Check if this is an OAuth callback
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
|
||||
// Handle OAuth callback
|
||||
if (code && state) {
|
||||
// This is an OAuth callback - set success flag
|
||||
setAuthSuccess(true)
|
||||
@@ -211,8 +116,14 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
if (userId) {
|
||||
fetchServices()
|
||||
}
|
||||
|
||||
// Clear the URL parameters
|
||||
router.replace('/w')
|
||||
} else if (error) {
|
||||
console.error('OAuth error:', error)
|
||||
router.replace('/w')
|
||||
}
|
||||
}, [searchParams, userId])
|
||||
}, [searchParams, router, userId])
|
||||
|
||||
// Check for pending OAuth connections and return URL
|
||||
useEffect(() => {
|
||||
@@ -253,18 +164,23 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
}
|
||||
}, [authSuccess, onOpenChange, router])
|
||||
|
||||
// Fetch connection status on component mount
|
||||
// Fetch services on mount
|
||||
useEffect(() => {
|
||||
fetchServices()
|
||||
if (userId) {
|
||||
fetchServices()
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// Handle connect button click
|
||||
const handleConnect = async (service: ServiceInfo) => {
|
||||
setIsConnecting(service.id)
|
||||
try {
|
||||
// Store information about the required connection
|
||||
saveToStorage('auth_return_url', window.location.href)
|
||||
saveToStorage('pending_service_id', service.id)
|
||||
saveToStorage('pending_oauth_provider_id', service.providerId)
|
||||
setIsConnecting(service.id)
|
||||
|
||||
// Store information about the connection
|
||||
saveToStorage<string>('pending_service_id', service.id)
|
||||
saveToStorage<string[]>('pending_oauth_scopes', service.scopes)
|
||||
saveToStorage<string>('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage<string>('pending_oauth_provider_id', service.providerId)
|
||||
|
||||
// Begin OAuth flow with the appropriate provider
|
||||
await client.signIn.oauth2({
|
||||
@@ -277,18 +193,18 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle disconnect button click
|
||||
const handleDisconnect = async (service: ServiceInfo, accountId: string) => {
|
||||
setIsConnecting(`${service.id}-${accountId}`)
|
||||
try {
|
||||
// Call your API to disconnect the provider
|
||||
// Call the API to disconnect the account
|
||||
const response = await fetch('/api/auth/oauth/disconnect', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: service.provider,
|
||||
providerId: service.providerId,
|
||||
provider: service.providerId.split('-')[0],
|
||||
accountId,
|
||||
}),
|
||||
})
|
||||
@@ -307,14 +223,35 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
return svc
|
||||
})
|
||||
)
|
||||
} else {
|
||||
console.error('Error disconnecting service')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting provider:', error)
|
||||
console.error('Error disconnecting service:', error)
|
||||
} finally {
|
||||
setIsConnecting(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Group services by provider
|
||||
const groupedServices = services.reduce(
|
||||
(acc, service) => {
|
||||
// Find the provider for this service
|
||||
const providerKey =
|
||||
Object.keys(OAUTH_PROVIDERS).find((key) =>
|
||||
Object.keys(OAUTH_PROVIDERS[key].services).includes(service.id)
|
||||
) || 'other'
|
||||
|
||||
if (!acc[providerKey]) {
|
||||
acc[providerKey] = []
|
||||
}
|
||||
|
||||
acc[providerKey].push(service)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ServiceInfo[]>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
@@ -324,6 +261,21 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Success message */}
|
||||
{authSuccess && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-4 mb-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Check className="h-5 w-5 text-green-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-green-800">Account connected successfully!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pending service message */}
|
||||
{pendingService && (
|
||||
<div className="mb-6 p-4 bg-primary/10 border border-primary rounded-md text-sm flex items-start gap-2">
|
||||
<div className="min-w-4 mt-0.5">
|
||||
@@ -337,112 +289,128 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<ConnectionSkeleton />
|
||||
<ConnectionSkeleton />
|
||||
<ConnectionSkeleton />
|
||||
<ConnectionSkeleton />
|
||||
</>
|
||||
) : (
|
||||
services.map((service) => (
|
||||
<Card
|
||||
key={service.id}
|
||||
className={cn(
|
||||
'p-6 transition-all hover:shadow-md',
|
||||
pendingService === service.id && 'border-primary shadow-md'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-muted shrink-0">
|
||||
{service.icon}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<h4 className="font-medium leading-none">{service.name}</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">{service.description}</p>
|
||||
</div>
|
||||
{service.accounts && service.accounts.length > 0 && (
|
||||
<div className="pt-3 space-y-2">
|
||||
{service.accounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className="flex items-center justify-between gap-2 rounded-md border bg-card/50 p-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">{account.name}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDisconnect(service, account.id)}
|
||||
disabled={isConnecting === `${service.id}-${account.id}`}
|
||||
className="h-7 px-2"
|
||||
>
|
||||
{isConnecting === `${service.id}-${account.id}` ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
'Disconnect'
|
||||
)}
|
||||
</Button>
|
||||
{/* Loading state */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<ConnectionSkeleton />
|
||||
<ConnectionSkeleton />
|
||||
<ConnectionSkeleton />
|
||||
<ConnectionSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Group services by provider */}
|
||||
{Object.entries(groupedServices).map(([providerKey, providerServices]) => (
|
||||
<div key={providerKey} className="space-y-4">
|
||||
<h4 className="text-sm font-medium text-muted-foreground">
|
||||
{OAUTH_PROVIDERS[providerKey]?.name || 'Other Services'}
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{providerServices.map((service) => (
|
||||
<Card
|
||||
key={service.id}
|
||||
className={cn(
|
||||
'p-6 transition-all hover:shadow-md',
|
||||
pendingService === service.id && 'border-primary shadow-md'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-muted shrink-0">
|
||||
{typeof service.icon === 'function'
|
||||
? service.icon({ className: 'h-5 w-5' })
|
||||
: service.icon}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<h4 className="font-medium leading-none">{service.name}</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{service.accounts && service.accounts.length > 0 && (
|
||||
<div className="pt-3 space-y-2">
|
||||
{service.accounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className="flex items-center justify-between gap-2 rounded-md border bg-card/50 p-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">{account.name}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDisconnect(service, account.id)}
|
||||
disabled={isConnecting === `${service.id}-${account.id}`}
|
||||
className="h-7 px-2"
|
||||
>
|
||||
{isConnecting === `${service.id}-${account.id}` ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
'Disconnect'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mt-2"
|
||||
onClick={() => handleConnect(service)}
|
||||
disabled={isConnecting === service.id}
|
||||
>
|
||||
{isConnecting === service.id ? (
|
||||
<>
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-3 w-3 mr-2" />
|
||||
Connect Another Account
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!service.accounts?.length && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="w-full mt-2"
|
||||
onClick={() => handleConnect(service)}
|
||||
disabled={isConnecting === service.id}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isConnecting === service.id ? (
|
||||
<>
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-3 w-3 mr-2" />
|
||||
Connect Another Account
|
||||
</>
|
||||
'Connect'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!service.accounts?.length && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handleConnect(service)}
|
||||
disabled={isConnecting === service.id}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isConnecting === service.id ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
'Connect'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Loading skeleton for connections
|
||||
function ConnectionSkeleton() {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
|
||||
@@ -1589,8 +1589,8 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
<path
|
||||
d="M204 229a25 22 1 1 1 25 27h-9h9a25 22 1 1 1-25 27M270 231l27-19h4v-7V308"
|
||||
stroke="#4285f4"
|
||||
stroke-width="15"
|
||||
stroke-linejoin="bevel"
|
||||
strokeWidth="15"
|
||||
strokeLinejoin="bevel"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Check } from 'lucide-react'
|
||||
import {
|
||||
GithubIcon,
|
||||
GmailIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
GoogleIcon,
|
||||
GoogleSheetsIcon,
|
||||
xIcon as XIcon,
|
||||
} from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,8 +10,14 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import {
|
||||
OAUTH_PROVIDERS,
|
||||
OAuthProvider,
|
||||
getProviderIdFromServiceId,
|
||||
getServiceIdFromScopes,
|
||||
} from '@/lib/oauth'
|
||||
import { saveToStorage } from '@/stores/workflows/persistence'
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
|
||||
export interface OAuthRequiredModalProps {
|
||||
isOpen: boolean
|
||||
@@ -31,28 +28,6 @@ export interface OAuthRequiredModalProps {
|
||||
serviceId?: string
|
||||
}
|
||||
|
||||
// Map of provider names to friendly display names
|
||||
const PROVIDER_NAMES: Record<OAuthProvider, string> = {
|
||||
github: 'GitHub',
|
||||
google: 'Google',
|
||||
'google-email': 'Gmail',
|
||||
'google-drive': 'Google Drive',
|
||||
'google-docs': 'Google Docs',
|
||||
'google-sheets': 'Google Sheets',
|
||||
twitter: 'X (Twitter)',
|
||||
}
|
||||
|
||||
// Map of provider to icons
|
||||
const PROVIDER_ICONS: Record<OAuthProvider, React.FC<React.SVGProps<SVGSVGElement>>> = {
|
||||
github: GithubIcon,
|
||||
google: GoogleIcon,
|
||||
'google-email': GmailIcon,
|
||||
'google-drive': GoogleDriveIcon,
|
||||
'google-docs': GoogleDocsIcon,
|
||||
'google-sheets': GoogleSheetsIcon,
|
||||
twitter: XIcon,
|
||||
}
|
||||
|
||||
// Map of OAuth scopes to user-friendly descriptions
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'https://www.googleapis.com/auth/gmail.send': 'Send emails on your behalf',
|
||||
@@ -85,8 +60,10 @@ export function OAuthRequiredModal({
|
||||
requiredScopes = [],
|
||||
serviceId,
|
||||
}: OAuthRequiredModalProps) {
|
||||
const providerName = PROVIDER_NAMES[provider] || provider
|
||||
const ProviderIcon = PROVIDER_ICONS[provider]
|
||||
// Get provider configuration
|
||||
const providerConfig = OAUTH_PROVIDERS[provider]
|
||||
const providerName = providerConfig?.name || provider
|
||||
const ProviderIcon = providerConfig?.icon || (() => null)
|
||||
|
||||
// Filter out userinfo scopes as they're not relevant to show to users
|
||||
const displayScopes = requiredScopes.filter(
|
||||
@@ -95,81 +72,15 @@ export function OAuthRequiredModal({
|
||||
|
||||
const handleRedirectToSettings = () => {
|
||||
try {
|
||||
// Determine the appropriate providerId and serviceId based on the provider and required scopes
|
||||
let providerId: string
|
||||
let effectiveServiceId = serviceId
|
||||
|
||||
// If no serviceId is provided, determine it based on scopes
|
||||
if (!effectiveServiceId) {
|
||||
if (provider === 'google') {
|
||||
if (requiredScopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))) {
|
||||
effectiveServiceId = 'gmail'
|
||||
providerId = 'google-email'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('drive'))) {
|
||||
effectiveServiceId = 'google-drive'
|
||||
providerId = 'google-drive'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('docs'))) {
|
||||
effectiveServiceId = 'google-docs'
|
||||
providerId = 'google-docs'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('sheets'))) {
|
||||
effectiveServiceId = 'google-sheets'
|
||||
providerId = 'google-sheets'
|
||||
} else if (requiredScopes.some((scope) => scope.includes('calendar'))) {
|
||||
effectiveServiceId = 'google-calendar'
|
||||
providerId = 'google-calendar'
|
||||
} else {
|
||||
effectiveServiceId = 'gmail' // Default Google service
|
||||
providerId = 'google-email'
|
||||
}
|
||||
} else if (provider === 'github') {
|
||||
effectiveServiceId = 'github'
|
||||
if (requiredScopes.some((scope) => scope.includes('workflow'))) {
|
||||
providerId = 'github-workflow'
|
||||
} else {
|
||||
providerId = 'github-repo'
|
||||
}
|
||||
} else if (provider === 'twitter') {
|
||||
effectiveServiceId = 'twitter'
|
||||
if (requiredScopes.some((scope) => scope.includes('write'))) {
|
||||
providerId = 'twitter-write'
|
||||
} else {
|
||||
providerId = 'twitter-read'
|
||||
}
|
||||
} else {
|
||||
effectiveServiceId = provider
|
||||
providerId = `${provider}-default`
|
||||
}
|
||||
} else {
|
||||
// Use the provided serviceId to determine the providerId
|
||||
switch (effectiveServiceId) {
|
||||
case 'gmail':
|
||||
providerId = 'google-email'
|
||||
break
|
||||
case 'google-drive':
|
||||
providerId = 'google-drive'
|
||||
break
|
||||
case 'google-sheets':
|
||||
providerId = 'google-sheets'
|
||||
break
|
||||
case 'google-docs':
|
||||
providerId = 'google-docs'
|
||||
break
|
||||
case 'github':
|
||||
providerId = 'github-repo'
|
||||
break
|
||||
case 'twitter':
|
||||
providerId = 'twitter-read'
|
||||
break
|
||||
default:
|
||||
providerId = `${provider}-default`
|
||||
}
|
||||
}
|
||||
// Determine the appropriate serviceId and providerId
|
||||
const effectiveServiceId = serviceId || getServiceIdFromScopes(provider, requiredScopes)
|
||||
const providerId = getProviderIdFromServiceId(effectiveServiceId)
|
||||
|
||||
// Store information about the required connection
|
||||
saveToStorage('pending_service_id', effectiveServiceId)
|
||||
saveToStorage('pending_oauth_scopes', requiredScopes)
|
||||
saveToStorage('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage('pending_oauth_provider_id', providerId)
|
||||
saveToStorage<string>('pending_service_id', effectiveServiceId)
|
||||
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
|
||||
saveToStorage<string>('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage<string>('pending_oauth_provider_id', providerId)
|
||||
|
||||
// Close the modal
|
||||
onClose()
|
||||
@@ -184,6 +95,31 @@ export function OAuthRequiredModal({
|
||||
}
|
||||
}
|
||||
|
||||
const handleConnectDirectly = async () => {
|
||||
try {
|
||||
// Determine the appropriate serviceId and providerId
|
||||
const effectiveServiceId = serviceId || getServiceIdFromScopes(provider, requiredScopes)
|
||||
const providerId = getProviderIdFromServiceId(effectiveServiceId)
|
||||
|
||||
// Store information about the required connection
|
||||
saveToStorage<string>('pending_service_id', effectiveServiceId)
|
||||
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
|
||||
saveToStorage<string>('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage<string>('pending_oauth_provider_id', providerId)
|
||||
|
||||
// Close the modal
|
||||
onClose()
|
||||
|
||||
// Begin OAuth flow with the appropriate provider
|
||||
await client.signIn.oauth2({
|
||||
providerId,
|
||||
callbackURL: window.location.href,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error initiating OAuth flow:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
@@ -202,7 +138,7 @@ export function OAuthRequiredModal({
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Connect {providerName}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You need to connect your {providerName} account in settings
|
||||
You need to connect your {providerName} account to continue
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,11 +161,19 @@ export function OAuthRequiredModal({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter className="flex space-x-2 sm:justify-end">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-2">
|
||||
<Button variant="outline" onClick={onClose} className="sm:order-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={handleRedirectToSettings}>
|
||||
<Button type="button" onClick={handleConnectDirectly} className="sm:order-3">
|
||||
Connect Now
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleRedirectToSettings}
|
||||
className="sm:order-2"
|
||||
>
|
||||
Go to Settings
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
211
lib/oauth.ts
211
lib/oauth.ts
@@ -1,6 +1,213 @@
|
||||
import { OAuthProvider } from '@/tools/types'
|
||||
import { ReactNode } from 'react'
|
||||
import {
|
||||
GithubIcon,
|
||||
GmailIcon,
|
||||
GoogleCalendarIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
GoogleIcon,
|
||||
GoogleSheetsIcon,
|
||||
xIcon as TwitterIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
interface ProviderConfig {
|
||||
// Define the base OAuth provider type
|
||||
export type OAuthProvider = 'google' | 'github' | 'twitter' | string
|
||||
export type OAuthService =
|
||||
| 'google'
|
||||
| 'google-email'
|
||||
| 'google-drive'
|
||||
| 'google-docs'
|
||||
| 'google-sheets'
|
||||
| 'github'
|
||||
| 'twitter'
|
||||
|
||||
// Define the interface for OAuth provider configuration
|
||||
export interface OAuthProviderConfig {
|
||||
id: OAuthProvider
|
||||
name: string
|
||||
icon: (props: { className?: string }) => ReactNode
|
||||
services: Record<string, OAuthServiceConfig>
|
||||
defaultService: string
|
||||
}
|
||||
|
||||
// Define the interface for OAuth service configuration
|
||||
export interface OAuthServiceConfig {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
providerId: string
|
||||
icon: (props: { className?: string }) => ReactNode
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
// Define the available OAuth providers
|
||||
export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
google: {
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
icon: (props) => GoogleIcon(props),
|
||||
services: {
|
||||
gmail: {
|
||||
id: 'gmail',
|
||||
name: 'Gmail',
|
||||
description: 'Automate email workflows and enhance communication efficiency.',
|
||||
providerId: 'google-email',
|
||||
icon: (props) => GmailIcon(props),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
],
|
||||
},
|
||||
'google-drive': {
|
||||
id: 'google-drive',
|
||||
name: 'Google Drive',
|
||||
description: 'Streamline file organization and document workflows.',
|
||||
providerId: 'google-drive',
|
||||
icon: (props) => GoogleDriveIcon(props),
|
||||
scopes: ['https://www.googleapis.com/auth/drive'],
|
||||
},
|
||||
'google-docs': {
|
||||
id: 'google-docs',
|
||||
name: 'Google Docs',
|
||||
description: 'Create, read, and edit Google Documents programmatically.',
|
||||
providerId: 'google-docs',
|
||||
icon: (props) => GoogleDocsIcon(props),
|
||||
scopes: ['https://www.googleapis.com/auth/documents'],
|
||||
},
|
||||
'google-sheets': {
|
||||
id: 'google-sheets',
|
||||
name: 'Google Sheets',
|
||||
description: 'Manage and analyze data with Google Sheets integration.',
|
||||
providerId: 'google-sheets',
|
||||
icon: (props) => GoogleSheetsIcon(props),
|
||||
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
||||
},
|
||||
'google-calendar': {
|
||||
id: 'google-calendar',
|
||||
name: 'Google Calendar',
|
||||
description: 'Schedule and manage events with Google Calendar.',
|
||||
providerId: 'google-calendar',
|
||||
icon: (props) => GoogleCalendarIcon(props),
|
||||
scopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
},
|
||||
},
|
||||
defaultService: 'gmail',
|
||||
},
|
||||
github: {
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
icon: (props) => GithubIcon(props),
|
||||
services: {
|
||||
github: {
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
description: 'Manage repositories, issues, and pull requests.',
|
||||
providerId: 'github-repo',
|
||||
icon: (props) => GithubIcon(props),
|
||||
scopes: ['repo', 'user'],
|
||||
},
|
||||
'github-workflow': {
|
||||
id: 'github-workflow',
|
||||
name: 'GitHub Actions',
|
||||
description: 'Trigger and manage GitHub Actions workflows.',
|
||||
providerId: 'github-workflow',
|
||||
icon: (props) => GithubIcon(props),
|
||||
scopes: ['repo', 'workflow'],
|
||||
},
|
||||
},
|
||||
defaultService: 'github',
|
||||
},
|
||||
twitter: {
|
||||
id: 'twitter',
|
||||
name: 'Twitter',
|
||||
icon: (props) => TwitterIcon(props),
|
||||
services: {
|
||||
twitter: {
|
||||
id: 'twitter',
|
||||
name: 'Twitter',
|
||||
description: 'Post tweets and interact with the Twitter API.',
|
||||
providerId: 'twitter',
|
||||
icon: (props) => TwitterIcon(props),
|
||||
scopes: ['tweet.read', 'tweet.write', 'users.read'],
|
||||
},
|
||||
},
|
||||
defaultService: 'twitter',
|
||||
},
|
||||
}
|
||||
|
||||
// Helper function to get a service by provider and service ID
|
||||
export function getServiceByProviderAndId(
|
||||
provider: OAuthProvider,
|
||||
serviceId?: string
|
||||
): OAuthServiceConfig {
|
||||
const providerConfig = OAUTH_PROVIDERS[provider]
|
||||
if (!providerConfig) {
|
||||
throw new Error(`Provider ${provider} not found`)
|
||||
}
|
||||
|
||||
if (!serviceId) {
|
||||
return providerConfig.services[providerConfig.defaultService]
|
||||
}
|
||||
|
||||
return (
|
||||
providerConfig.services[serviceId] || providerConfig.services[providerConfig.defaultService]
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to determine service ID from scopes
|
||||
export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]): string {
|
||||
const providerConfig = OAUTH_PROVIDERS[provider]
|
||||
if (!providerConfig) {
|
||||
return provider
|
||||
}
|
||||
|
||||
if (provider === 'google') {
|
||||
if (scopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))) {
|
||||
return 'gmail'
|
||||
} else if (scopes.some((scope) => scope.includes('drive'))) {
|
||||
return 'google-drive'
|
||||
} else if (scopes.some((scope) => scope.includes('docs'))) {
|
||||
return 'google-docs'
|
||||
} else if (scopes.some((scope) => scope.includes('sheets'))) {
|
||||
return 'google-sheets'
|
||||
} else if (scopes.some((scope) => scope.includes('calendar'))) {
|
||||
return 'google-calendar'
|
||||
}
|
||||
} else if (provider === 'github') {
|
||||
if (scopes.some((scope) => scope.includes('workflow'))) {
|
||||
return 'github-workflow'
|
||||
}
|
||||
}
|
||||
|
||||
return providerConfig.defaultService
|
||||
}
|
||||
|
||||
// Helper function to get provider ID from service ID
|
||||
export function getProviderIdFromServiceId(serviceId: string): string {
|
||||
for (const provider of Object.values(OAUTH_PROVIDERS)) {
|
||||
for (const [id, service] of Object.entries(provider.services)) {
|
||||
if (id === serviceId) {
|
||||
return service.providerId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return serviceId
|
||||
}
|
||||
|
||||
// Interface for credential objects
|
||||
export interface Credential {
|
||||
id: string
|
||||
name: string
|
||||
provider: OAuthProvider
|
||||
serviceId?: string
|
||||
lastUsed?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
// Interface for provider configuration
|
||||
export interface ProviderConfig {
|
||||
baseProvider: string
|
||||
featureType: string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { OAuthService } from '@/lib/oauth'
|
||||
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
export type OAuthProvider =
|
||||
| 'google'
|
||||
| 'google-email'
|
||||
| 'google-drive'
|
||||
| 'google-docs'
|
||||
| 'google-sheets'
|
||||
| 'github'
|
||||
| 'twitter'
|
||||
|
||||
export interface ToolResponse {
|
||||
success: boolean // Whether the tool execution was successful
|
||||
@@ -16,7 +10,7 @@ export interface ToolResponse {
|
||||
|
||||
export interface OAuthConfig {
|
||||
required: boolean // Whether this tool requires OAuth authentication
|
||||
provider: OAuthProvider // The provider that needs to be authorized
|
||||
provider: OAuthService // The service that needs to be authorized
|
||||
additionalScopes?: string[] // Additional scopes required for the tool
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user