Skip to main content
Glama

Authenticated Next.js MCP Server

page.tsx19.4 kB
'use client' import { useState, useEffect, useCallback } from 'react' import { useUser } from '@clerk/nextjs' import { useRouter } from 'next/navigation' import { format } from 'date-fns' import { Key, Activity, BarChart3, Plus, Settings, Trash2, Copy, CheckCircle, TrendingUp, Zap, RefreshCw, } from 'lucide-react' import { CreateApiKeyModal } from '@/components/create-api-key-modal' import { ApiKeySettingsModal } from '@/components/api-key-settings-modal' import { LazyRecentActivity } from '@/components/lazy-recent-activity' import { useDashboardAutoRefresh } from '@/hooks/useRealtimeUpdates' import type { UserApiKey } from '@/lib/api-keys' import { ErrorBoundary } from '@/components/error-boundary' import { DashboardErrorFallback } from '@/components/dashboard-error-fallback' function DashboardContent(): React.JSX.Element { const { user, isLoaded, isSignedIn } = useUser() const router = useRouter() const [copiedKey, setCopiedKey] = useState<string | null>(null) const [apiKeys, setApiKeys] = useState<UserApiKey[]>([]) const [isLoadingKeys, setIsLoadingKeys] = useState(true) const [showCreateModal, setShowCreateModal] = useState(false) const [showSettingsModal, setShowSettingsModal] = useState(false) const [selectedApiKey, setSelectedApiKey] = useState<UserApiKey | null>(null) const [error, setError] = useState('') const [requestStats, setRequestStats] = useState({ totalRequests: 0, requestsThisMonth: 0, requestsToday: 0, }) const [activityRefresh, setActivityRefresh] = useState<(() => void) | null>(null) // Handle activity refresh callback registration const handleActivityUpdate = (callback: () => void) => { setActivityRefresh(() => callback) } // Fetch API keys const fetchApiKeys = useCallback(async () => { try { setIsLoadingKeys(true) const response = await fetch('/api/keys') if (!response.ok) { console.error('API Keys error:', response.status, response.statusText) throw new Error(`Failed to fetch API keys (${response.status})`) } const keys = await response.json() setApiKeys(keys) setError('') } catch (err) { console.error('Failed to load API keys:', err) setError(err instanceof Error ? err.message : 'Failed to load API keys') } finally { setIsLoadingKeys(false) } }, []) // Fetch request analytics const fetchAnalytics = useCallback(async () => { try { const response = await fetch('/api/analytics') if (!response.ok) { console.error('Analytics API error:', response.status, response.statusText) // Set default values if analytics fail setRequestStats({ totalRequests: 0, requestsThisMonth: 0, requestsToday: 0, }) return } const stats = await response.json() setRequestStats(stats) } catch (err) { console.error('Failed to load analytics:', err) // Set default values if analytics fail setRequestStats({ totalRequests: 0, requestsThisMonth: 0, requestsToday: 0, }) } }, []) // Auto-refresh functionality const fetchAllData = () => { fetchApiKeys() fetchAnalytics() if (activityRefresh) { activityRefresh() } } const { manualRefresh } = useDashboardAutoRefresh(fetchAllData) useEffect(() => { if (isLoaded && !isSignedIn) { router.push('/') return } if (isLoaded && isSignedIn) { fetchAllData() } }, [isLoaded, isSignedIn, router]) const handleCreateSuccess = () => { fetchAllData() } const handleOpenSettings = (apiKey: UserApiKey) => { setSelectedApiKey(apiKey) setShowSettingsModal(true) } const handleCloseSettings = () => { setShowSettingsModal(false) setSelectedApiKey(null) } const handleSettingsUpdate = () => { fetchAllData() } const handleDeleteKey = async (keyId: string) => { if (!confirm('Are you sure you want to delete this API key? This action cannot be undone.')) { return } try { const response = await fetch(`/api/keys/${keyId}`, { method: 'DELETE', }) if (!response.ok) { throw new Error('Failed to delete API key') } // Refresh all data after deletion fetchAllData() } catch (err) { console.error('Failed to delete API key:', err) alert(err instanceof Error ? err.message : 'Failed to delete API key') } } const handleTestRequest = async (apiKey: string) => { try { const response = await fetch('/api/test-request', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, }) if (response.ok) { // Refresh all data after test fetchAllData() alert('Test request successful! Check your analytics.') } else { const error = await response.json() alert(`Test request failed: ${error.error}`) } } catch (err) { alert('Test request failed: Network error') } } const stats = [ { title: 'Active API Keys', value: apiKeys.length.toString(), change: `${apiKeys.length} total keys`, icon: Key, color: 'text-emerald-600 dark:text-emerald-400', bgColor: 'bg-emerald-100 dark:bg-emerald-900/30', trend: 'up', }, { title: 'Requests Today', value: requestStats.requestsToday.toString(), change: 'Last 24 hours', icon: TrendingUp, color: 'text-green-600 dark:text-green-400', bgColor: 'bg-green-100 dark:bg-green-900/30', trend: requestStats.requestsToday > 0 ? 'up' : 'stable', }, { title: 'This Month', value: requestStats.requestsThisMonth.toString(), change: 'Current month', icon: BarChart3, color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/30', trend: requestStats.requestsThisMonth > 0 ? 'up' : 'stable', }, { title: 'Total Requests', value: requestStats.totalRequests.toLocaleString(), change: `${requestStats.requestsThisMonth} this month`, icon: Activity, color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-900/30', trend: requestStats.requestsThisMonth > 0 ? 'up' : 'stable', }, ] const handleCopyKey = async (keyId: string, key: string) => { try { await navigator.clipboard.writeText(key) setCopiedKey(keyId) setTimeout(() => setCopiedKey(null), 2000) } catch (err) { console.error('Failed to copy key:', err) } } if (!isLoaded) { return ( <div className='min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-50 dark:from-slate-900 dark:via-teal-900 dark:to-cyan-900'> <div className='animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-600'></div> </div> ) } if (!isSignedIn) { return ( <div className='min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-50 dark:from-slate-900 dark:via-teal-900 dark:to-cyan-900'> <div className='text-emerald-600 text-lg'>Redirecting to home...</div> </div> ) } return ( <div className='min-h-screen bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-50 dark:from-slate-900 dark:via-teal-900 dark:to-cyan-900'> <div className='max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8'> {/* Header */} <div className='mb-8 flex items-center justify-between'> <div> <h1 className='text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 dark:from-emerald-400 dark:to-teal-400 bg-clip-text text-transparent'> Dashboard </h1> <p className='mt-2 text-gray-600 dark:text-gray-300'> Welcome back, {user?.firstName || 'User'}! Manage your Next.js MCP Server and API keys. </p> </div> <div className='flex items-center gap-3'> <button onClick={manualRefresh} className='flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-slate-700 hover:bg-gray-200 dark:hover:bg-slate-600 text-gray-700 dark:text-gray-300 rounded-lg text-sm font-medium transition-colors' title='Refresh dashboard data' > <RefreshCw className='h-4 w-4' /> Refresh </button> </div> </div> {/* Stats Grid */} <div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'> {stats.map((stat, index) => ( <div key={index} className='bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-xl p-6 border border-white/20 dark:border-slate-700/50 shadow-lg hover:shadow-xl transition-all duration-200' > <div className='flex items-center justify-between'> <div className={`p-3 rounded-lg ${stat.bgColor}`}> <stat.icon className={`h-6 w-6 ${stat.color}`} /> </div> {stat.trend === 'up' && ( <TrendingUp className='h-4 w-4 text-green-500' /> )} </div> <div className='mt-4'> <h3 className='text-2xl font-bold text-gray-900 dark:text-white'> {stat.value} </h3> <p className='text-sm font-medium text-gray-900 dark:text-white mt-1'> {stat.title} </p> <p className='text-xs text-gray-500 dark:text-gray-400 mt-1'> {stat.change} </p> </div> </div> ))} </div> {/* Main Content Grid */} <div className='grid grid-cols-1 xl:grid-cols-3 gap-8'> {/* API Keys Management */} <div className='xl:col-span-2'> <div className='bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-xl border border-white/20 dark:border-slate-700/50 shadow-lg'> <div className='p-6 border-b border-gray-200 dark:border-slate-700'> <div className='flex items-center justify-between'> <div> <div className='flex items-center gap-3'> <h2 className='text-xl font-semibold text-gray-900 dark:text-white'> API Keys </h2> <span className={`px-2 py-1 rounded-full text-xs font-medium ${ apiKeys.length >= 5 ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' : 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400' }`}> {apiKeys.length}/5 </span> </div> <div className='flex items-center gap-6 mt-2 text-xs text-gray-500 dark:text-gray-400'> <div className='flex items-center gap-1'> <Zap className='h-3 w-3 text-blue-400' /> <span>Test</span> </div> <div className='flex items-center gap-1'> <Settings className='h-3 w-3 text-gray-400' /> <span>Settings</span> </div> <div className='flex items-center gap-1'> <Trash2 className='h-3 w-3 text-red-400' /> <span>Delete</span> </div> </div> </div> <button onClick={() => setShowCreateModal(true)} disabled={apiKeys.length >= 5} className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ apiKeys.length >= 5 ? 'bg-gray-400 dark:bg-gray-600 text-gray-200 cursor-not-allowed' : 'bg-emerald-600 hover:bg-emerald-700 text-white' }`} title={apiKeys.length >= 5 ? 'Maximum 5 API keys allowed. Delete a key to create a new one.' : 'Create a new API key'} > <Plus className='h-4 w-4' /> Create New Key {apiKeys.length >= 5 && `(${apiKeys.length}/5)`} </button> </div> </div> <div className='divide-y divide-gray-200 dark:divide-slate-700'> {error && ( <div className='p-6'> <div className='p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg'> <p className='text-sm text-red-600 dark:text-red-400'>{error}</p> </div> </div> )} {isLoadingKeys ? ( <div className='p-6 text-center'> <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600 mx-auto'></div> <p className='mt-2 text-sm text-gray-500 dark:text-gray-400'>Loading API keys...</p> </div> ) : apiKeys.length === 0 ? ( <div className='p-6 text-center'> <Key className='h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4' /> <p className='text-gray-500 dark:text-gray-400 mb-2'>No API keys found</p> <p className='text-sm text-gray-400 dark:text-gray-500'>Create your first API key to get started. You can have up to 5 API keys.</p> </div> ) : ( apiKeys.map(key => ( <div key={key.id} className='p-6'> <div className='flex items-center justify-between mb-4'> <div className='flex items-center gap-3'> <div className='p-2 rounded-lg bg-green-100 dark:bg-green-900/30'> <Key className='h-4 w-4 text-green-600 dark:text-green-400' /> </div> <div> <h3 className='font-medium text-gray-900 dark:text-white'> {key.name} </h3> <p className='text-sm text-gray-500 dark:text-gray-400'> Created {format(new Date(key.createdAt), 'yyyy-MM-dd')} </p> </div> </div> <div className='flex items-center gap-2'> <span className='px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'> active </span> </div> </div> <div className='bg-gray-50 dark:bg-slate-900/50 rounded-lg p-3 mb-4'> <div className='flex items-center justify-between'> <code className='text-sm font-mono text-gray-600 dark:text-gray-300 truncate flex-1 mr-2'> {key.key} </code> <button onClick={() => handleCopyKey(key.id, key.key)} className='p-1 hover:bg-gray-200 dark:hover:bg-slate-700 rounded transition-colors' > {copiedKey === key.id ? ( <CheckCircle className='h-4 w-4 text-green-500' /> ) : ( <Copy className='h-4 w-4 text-gray-400' /> )} </button> </div> </div> <div className='flex items-center justify-between text-sm'> <div className='flex items-center gap-4 text-gray-500 dark:text-gray-400'> <span>Last used: {key.lastUsed ? format(new Date(key.lastUsed), 'yyyy-MM-dd') : 'Never'}</span> <span>Scopes: {key.scopes.join(', ')}</span> </div> <div className='flex items-center gap-2'> <button onClick={() => handleTestRequest(key.key)} className='p-1 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded transition-colors' title='Test this API key - Send a test request to verify it works' > <Zap className='h-4 w-4 text-blue-400' /> </button> <button onClick={() => handleOpenSettings(key)} className='p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors' title='API key settings - Edit name, view usage, and manage security' > <Settings className='h-4 w-4 text-gray-400' /> </button> <button onClick={() => handleDeleteKey(key.id)} className='p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors' title='Delete this API key permanently - This action cannot be undone' > <Trash2 className='h-4 w-4 text-red-400' /> </button> </div> </div> </div> )) )} </div> </div> </div> {/* Recent Activity */} <div className='space-y-6'> <LazyRecentActivity onActivityUpdate={handleActivityUpdate} /> </div> </div> </div> <CreateApiKeyModal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} onSuccess={handleCreateSuccess} /> <ApiKeySettingsModal isOpen={showSettingsModal} onClose={handleCloseSettings} apiKey={selectedApiKey} onUpdate={handleSettingsUpdate} /> </div> ) } export default function Dashboard(): React.JSX.Element { const handleError = (error: Error, errorInfo: React.ErrorInfo) => { // Log dashboard-specific errors console.error('Dashboard Error:', error) console.error('Error Info:', errorInfo) // In production, you might want to send this to an error reporting service // Example: errorReportingService.log(error, { context: 'dashboard', ...errorInfo }) } return ( <ErrorBoundary fallback={DashboardErrorFallback} onError={handleError} > <DashboardContent /> </ErrorBoundary> ) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vedaterenoglu/ve-nextjs-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server