Skip to content

Groq pkce demo #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions examples/chat-ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import ChatApp from './components/ChatApp'
import OAuthCallback from './components/OAuthCallback'
import PkceCallback from './components/PkceCallback.tsx'
import { OAuthCallback } from './components/OAuthCallback.tsx'

function App() {
return (
<Router>
<Routes>
<Route path="/oauth/openrouter/callback" element={<OAuthCallback provider="openrouter" />} />
<Route path="/oauth/callback" element={<OAuthCallback provider="openrouter" />} />
<Route path="/oauth/groq/callback" element={<PkceCallback provider="groq" />} />
<Route path="/oauth/openrouter/callback" element={<PkceCallback provider="openrouter" />} />
<Route path="/oauth/callback" element={<OAuthCallback />} />
<Route path="/" element={<ChatApp />} />
</Routes>
</Router>
Expand Down
92 changes: 92 additions & 0 deletions examples/chat-ui/src/components/ChatApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ const ChatApp: React.FC<ChatAppProps> = () => {
// Handle OAuth success messages from popups
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
console.log('DEBUG: Received message in parent window:', event.data)
if (event.data.type === 'oauth_success') {
console.log('DEBUG: OAuth success message received, triggering API key update')
handleApiKeyUpdate()
}
}
Expand All @@ -42,6 +44,96 @@ const ChatApp: React.FC<ChatAppProps> = () => {
return () => window.removeEventListener('message', handleMessage)
}, [])

// Poll for OAuth token changes (fallback for when popup messaging doesn't work)
useEffect(() => {
const oauthProviders = ['groq', 'openrouter'] as const
let initialTokens: Record<string, string | null> = {}

// Capture initial token state
const captureInitialTokens = () => {
for (const providerId of oauthProviders) {
const tokenKey = `aiChatTemplate_token_${providerId}`
initialTokens[providerId] = localStorage.getItem(tokenKey)
}
}

const checkForNewTokens = () => {
for (const providerId of oauthProviders) {
const tokenKey = `aiChatTemplate_token_${providerId}`
const currentToken = localStorage.getItem(tokenKey)

// Check if token was added or changed
if (currentToken !== initialTokens[providerId]) {
try {
const parsedToken = JSON.parse(currentToken || '{}')
if (parsedToken.access_token) {
console.log('DEBUG: New OAuth token detected for', providerId, 'via polling')
handleApiKeyUpdate()
return true // Stop polling once we find a new token
}
} catch (e) {
// Invalid token format, continue checking
}
}
}
return false
}

let pollInterval: NodeJS.Timeout | null = null

const startPolling = () => {
// Capture initial state
captureInitialTokens()

// Check immediately for existing valid tokens (in case we just redirected from OAuth)
console.log('DEBUG: Checking for existing OAuth tokens on startup')
for (const providerId of oauthProviders) {
const tokenKey = `aiChatTemplate_token_${providerId}`
const currentToken = localStorage.getItem(tokenKey)

console.log(`DEBUG: Checking ${providerId} token:`, currentToken ? 'exists' : 'not found')

if (currentToken) {
try {
const parsedToken = JSON.parse(currentToken)
console.log(`DEBUG: Parsed ${providerId} token:`, parsedToken)
if (parsedToken.access_token) {
console.log('DEBUG: Found existing OAuth token for', providerId, 'on startup')
handleApiKeyUpdate()
return // Don't start polling if we found a valid token
}
} catch (e) {
console.log(`DEBUG: Failed to parse ${providerId} token:`, e)
}
}
}

// Start polling every 500ms for new tokens
pollInterval = setInterval(() => {
if (checkForNewTokens()) {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
console.log('DEBUG: Stopped polling for OAuth tokens')
}
}
}, 500)

console.log('DEBUG: Started polling for OAuth token changes')
}

// Start polling after a short delay to allow for popup messages first
const delayedStart = setTimeout(startPolling, 100)

return () => {
if (delayedStart) clearTimeout(delayedStart)
if (pollInterval) {
clearInterval(pollInterval)
console.log('DEBUG: Cleaned up OAuth token polling')
}
}
}, [])

const handleModelChange = (model: Model) => {
setSelectedModel(model)
saveSelectedModel(model)
Expand Down
2 changes: 2 additions & 0 deletions examples/chat-ui/src/components/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ selectedModel, onModelCha
// Handle OAuth success - show provider models when OAuth completes
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
console.log('DEBUG: ModelSelector received message:', event.data)
if (event.data.type === 'oauth_success' && event.data.provider) {
console.log('DEBUG: ModelSelector opening provider models modal for:', event.data.provider)
const provider = providers[event.data.provider as keyof typeof providers]
if (provider) {
setProviderModelsModal({ isOpen: true, provider })
Expand Down
128 changes: 13 additions & 115 deletions examples/chat-ui/src/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,22 @@
import React, { useEffect, useState, useRef } from 'react'
import { useSearchParams } from 'react-router-dom'
import { completeOAuthFlow } from '../utils/auth'
import { SupportedProvider } from '../types/models'

interface OAuthCallbackProps {
provider: SupportedProvider
}

const OAuthCallback: React.FC<OAuthCallbackProps> = ({ provider }) => {
const [searchParams] = useSearchParams()
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
const [error, setError] = useState<string | null>(null)
const executedRef = useRef(false)
import { useEffect } from 'react'
import { onMcpAuthorization } from 'use-mcp'

export function OAuthCallback() {
useEffect(() => {
const handleCallback = async () => {
if (executedRef.current) {
console.log('DEBUG: Skipping duplicate OAuth callback execution')
return
}
executedRef.current = true

try {
const code = searchParams.get('code')
const state = searchParams.get('state')
const error = searchParams.get('error')

if (error) {
throw new Error(`OAuth error: ${error}`)
}

if (!code) {
throw new Error('Missing authorization code')
}

// OpenRouter doesn't use state parameter, but other providers might
const stateToUse = state || 'no-state'
await completeOAuthFlow(provider, code, stateToUse)
setStatus('success')

// Close popup after successful authentication
// Give extra time for debugging in development
setTimeout(() => {
if (window.opener) {
window.opener.postMessage({ type: 'oauth_success', provider }, '*')
window.close()
} else {
// Redirect to main page if not in popup
window.location.href = '/'
}
}, 3000)
} catch (err) {
console.error('OAuth callback error:', err)
setError(err instanceof Error ? err.message : 'Unknown error')
setStatus('error')
}
}

handleCallback()
}, [searchParams, provider])
onMcpAuthorization()
}, [])

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8">
<div className="text-center">
{status === 'loading' && (
<>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">Completing Authentication</h2>
<p className="text-gray-600">Connecting to {provider}...</p>
</>
)}

{status === 'success' && (
<>
<div className="text-green-500 mb-4">
<svg className="w-12 h-12 mx-auto" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">Authentication Successful!</h2>
<p className="text-gray-600 mb-4">Successfully connected to {provider}. You can now close this window.</p>
<button
onClick={() => window.close()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Close Window
</button>
</>
)}

{status === 'error' && (
<>
<div className="text-red-500 mb-4">
<svg className="w-12 h-12 mx-auto" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">Authentication Failed</h2>
<p className="text-gray-600 mb-4">{error || 'An error occurred during authentication'}</p>
<button
onClick={() => window.close()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Close Window
</button>
</>
)}
<div className="min-h-screen animated-bg-container flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl p-8 text-center max-w-md w-full">
<h1 className="text-2xl font-bold text-zinc-900 mb-4">Authenticating...</h1>
<p className="text-zinc-600 mb-2">Please wait while we complete your authentication.</p>
<p className="text-sm text-zinc-500">This window should close automatically.</p>

<div className="mt-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
</div>
</div>
</div>
)
}

export default OAuthCallback
Loading
Loading