Skip to main content
Glama

Authenticated Next.js MCP Server

api-key-settings-modal.tsx19.2 kB
'use client' import { useState, useEffect } from 'react' import { X, Edit2, Save, RefreshCw, Eye, BarChart3, AlertTriangle } from 'lucide-react' import { useTheme } from 'next-themes' import { format } from 'date-fns' import type { UserApiKey } from '@/lib/api-keys' interface ApiKeySettingsModalProps { isOpen: boolean onClose: () => void apiKey: UserApiKey | null onUpdate: () => void } interface KeyUsageStats { totalRequests: number requestsThisWeek: number requestsThisMonth: number lastUsed: string | null mostActiveDay: string } export function ApiKeySettingsModal({ isOpen, onClose, apiKey, onUpdate }: ApiKeySettingsModalProps) { const { theme } = useTheme() const [mounted, setMounted] = useState(false) const [isEditingName, setIsEditingName] = useState(false) const [newName, setNewName] = useState('') const [currentDisplayName, setCurrentDisplayName] = useState('') const [isLoading, setIsLoading] = useState(false) const [usageStats, setUsageStats] = useState<KeyUsageStats | null>(null) const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false) const [error, setError] = useState('') useEffect(() => { setMounted(true) }, []) useEffect(() => { if (apiKey) { setNewName(apiKey.name) setCurrentDisplayName(apiKey.name) fetchUsageStats() } }, [apiKey]) const fetchUsageStats = async () => { if (!apiKey) return try { const response = await fetch(`/api/keys/${apiKey.id}/stats`) if (response.ok) { const stats = await response.json() setUsageStats(stats) } else { // Mock data if endpoint doesn't exist yet setUsageStats({ totalRequests: Math.floor(Math.random() * 1000) + 50, requestsThisWeek: Math.floor(Math.random() * 100) + 10, requestsThisMonth: Math.floor(Math.random() * 300) + 50, lastUsed: apiKey.lastUsed || null, mostActiveDay: 'Monday' }) } } catch (err) { console.error('Failed to fetch usage stats:', err) setUsageStats({ totalRequests: 0, requestsThisWeek: 0, requestsThisMonth: 0, lastUsed: apiKey.lastUsed || null, mostActiveDay: 'N/A' }) } } const handleUpdateName = async () => { if (!apiKey || newName.trim() === apiKey.name) { setIsEditingName(false) return } setIsLoading(true) setError('') try { const response = await fetch(`/api/keys/${apiKey.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: newName.trim() }), }) if (!response.ok) { throw new Error('Failed to update API key name') } const result = await response.json() setIsEditingName(false) onUpdate() // Update the displayed name immediately in the modal setCurrentDisplayName(result.apiKey.name) setNewName(result.apiKey.name) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update name') } finally { setIsLoading(false) } } const handleRegenerateKey = async () => { if (!apiKey) return setIsLoading(true) setError('') try { const response = await fetch(`/api/keys/${apiKey.id}/regenerate`, { method: 'POST', }) if (!response.ok) { throw new Error('Failed to regenerate API key') } const result = await response.json() setShowRegenerateConfirm(false) onUpdate() // Show the new key to the user alert(`New API key generated: ${result.newKey}\n\nPlease copy this key now - you won't be able to see it again!`) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to regenerate key') } finally { setIsLoading(false) } } if (!isOpen || !apiKey || !mounted) return null // Theme-aware color variables const colors = { background: theme === 'dark' ? '#1f2937' : 'white', text: theme === 'dark' ? '#f9fafb' : '#111827', textSecondary: theme === 'dark' ? '#d1d5db' : '#6b7280', border: theme === 'dark' ? '#374151' : '#e5e7eb', inputBackground: theme === 'dark' ? '#374151' : 'white', inputBorder: theme === 'dark' ? '#4b5563' : '#d1d5db', cardBackground: theme === 'dark' ? '#374151' : '#f9fafb', errorBackground: theme === 'dark' ? '#7f1d1d' : '#fef2f2', errorBorder: theme === 'dark' ? '#991b1b' : '#fecaca', errorText: theme === 'dark' ? '#fca5a5' : '#dc2626', } return ( <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '20px' }}> <div style={{ background: colors.background, borderRadius: '12px', width: '100%', maxWidth: '600px', maxHeight: '90vh', overflow: 'auto', boxShadow: theme === 'dark' ? '0 25px 50px -12px rgba(0, 0, 0, 0.5)' : '0 25px 50px -12px rgba(0, 0, 0, 0.25)' }}> {/* Header */} <div style={{ padding: '24px', borderBottom: theme === 'dark' ? '1px solid #374151' : '1px solid #e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: theme === 'dark' ? '#f9fafb' : '#111827' }}> API Key Settings </h2> <button onClick={onClose} style={{ padding: '8px', background: 'transparent', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }} > <X style={{ width: '20px', height: '20px', color: colors.textSecondary }} /> </button> </div> {/* Content */} <div style={{ padding: '24px' }}> {error && ( <div style={{ background: theme === 'dark' ? '#7f1d1d' : '#fef2f2', border: `1px solid ${colors.errorBorder}`, borderRadius: '6px', padding: '12px', marginBottom: '20px', color: colors.errorText }}> {error} </div> )} {/* Edit Name Section */} <div style={{ marginBottom: '32px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}> <Edit2 style={{ width: '16px', height: '16px', color: '#059669' }} /> <h3 style={{ fontSize: '1.125rem', fontWeight: '600', color: colors.text }}> Name </h3> </div> {isEditingName ? ( <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> <input type="text" value={newName} onChange={(e) => setNewName(e.target.value)} style={{ flex: 1, padding: '8px 12px', border: `1px solid ${colors.inputBorder}`, borderRadius: '6px', fontSize: '14px', background: colors.inputBackground, color: colors.text }} placeholder="Enter API key name" /> <button onClick={handleUpdateName} disabled={isLoading} style={{ padding: '8px 12px', background: '#059669', color: 'white', border: 'none', borderRadius: '6px', cursor: isLoading ? 'not-allowed' : 'pointer', opacity: isLoading ? 0.6 : 1, display: 'flex', alignItems: 'center', gap: '4px' }} > <Save style={{ width: '14px', height: '14px' }} /> Save </button> <button onClick={() => { setIsEditingName(false) setNewName(currentDisplayName) }} style={{ padding: '8px 12px', background: '#6b7280', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }} > Cancel </button> </div> ) : ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px', background: colors.cardBackground, borderRadius: '6px' }}> <span style={{ fontWeight: '500', color: colors.text }}>{currentDisplayName}</span> <button onClick={() => setIsEditingName(true)} style={{ padding: '6px 12px', background: '#059669', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '4px' }} > <Edit2 style={{ width: '12px', height: '12px' }} /> Edit </button> </div> )} </div> {/* View Scopes Section */} <div style={{ marginBottom: '32px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}> <Eye style={{ width: '16px', height: '16px', color: '#3b82f6' }} /> <h3 style={{ fontSize: '1.125rem', fontWeight: '600', color: colors.text }}> Permissions & Scopes </h3> </div> <div style={{ background: colors.cardBackground, borderRadius: '6px', padding: '16px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}> {apiKey.scopes.map((scope) => ( <span key={scope} style={{ background: '#dbeafe', color: '#1e40af', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', fontWeight: '500' }} > {scope} </span> ))} </div> {apiKey.scopes.length === 0 && ( <p style={{ color: colors.textSecondary, fontSize: '14px', margin: 0 }}> No specific scopes assigned - full access </p> )} </div> </div> {/* Usage Stats Section */} <div style={{ marginBottom: '32px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}> <BarChart3 style={{ width: '16px', height: '16px', color: '#8b5cf6' }} /> <h3 style={{ fontSize: '1.125rem', fontWeight: '600', color: colors.text }}> Usage Statistics </h3> </div> {usageStats ? ( <div style={{ background: colors.cardBackground, borderRadius: '6px', padding: '16px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '16px' }}> <div> <p style={{ fontSize: '12px', color: colors.textSecondary, margin: '0 0 4px 0' }}>Total Requests</p> <p style={{ fontSize: '20px', fontWeight: 'bold', color: colors.text, margin: 0 }}> {usageStats.totalRequests.toLocaleString()} </p> </div> <div> <p style={{ fontSize: '12px', color: colors.textSecondary, margin: '0 0 4px 0' }}>This Week</p> <p style={{ fontSize: '20px', fontWeight: 'bold', color: colors.text, margin: 0 }}> {usageStats.requestsThisWeek} </p> </div> <div> <p style={{ fontSize: '12px', color: colors.textSecondary, margin: '0 0 4px 0' }}>This Month</p> <p style={{ fontSize: '20px', fontWeight: 'bold', color: colors.text, margin: 0 }}> {usageStats.requestsThisMonth} </p> </div> <div> <p style={{ fontSize: '12px', color: colors.textSecondary, margin: '0 0 4px 0' }}>Last Used</p> <p style={{ fontSize: '14px', fontWeight: '500', color: colors.text, margin: 0 }}> {usageStats.lastUsed ? format(new Date(usageStats.lastUsed), 'yyyy-MM-dd') : 'Never'} </p> </div> </div> </div> ) : ( <div style={{ background: colors.cardBackground, borderRadius: '6px', padding: '16px', textAlign: 'center' }}> <p style={{ color: colors.textSecondary, margin: 0 }}>Loading usage statistics...</p> </div> )} </div> {/* Regenerate Key Section */} <div style={{ marginBottom: '20px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}> <RefreshCw style={{ width: '16px', height: '16px', color: '#dc2626' }} /> <h3 style={{ fontSize: '1.125rem', fontWeight: '600', color: colors.text }}> Security Actions </h3> </div> {!showRegenerateConfirm ? ( <div style={{ background: colors.errorBackground, border: `1px solid ${colors.errorBorder}`, borderRadius: '6px', padding: '16px' }}> <p style={{ fontSize: '14px', color: colors.text, margin: '0 0 12px 0' }}> Regenerating the API key will create a new key value. The old key will stop working immediately. </p> <button onClick={() => setShowRegenerateConfirm(true)} style={{ padding: '8px 16px', background: '#dc2626', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: '500', display: 'flex', alignItems: 'center', gap: '6px' }} > <RefreshCw style={{ width: '14px', height: '14px' }} /> Regenerate API Key </button> </div> ) : ( <div style={{ background: colors.errorBackground, border: `1px solid ${colors.errorBorder}`, borderRadius: '6px', padding: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}> <AlertTriangle style={{ width: '20px', height: '20px', color: '#dc2626' }} /> <p style={{ fontSize: '14px', fontWeight: '600', color: '#dc2626', margin: 0 }}> Are you sure? </p> </div> <p style={{ fontSize: '14px', color: colors.text, margin: '0 0 16px 0' }}> This action cannot be undone. All applications using the current key will need to be updated. </p> <div style={{ display: 'flex', gap: '8px' }}> <button onClick={handleRegenerateKey} disabled={isLoading} style={{ padding: '8px 16px', background: '#dc2626', color: 'white', border: 'none', borderRadius: '6px', cursor: isLoading ? 'not-allowed' : 'pointer', opacity: isLoading ? 0.6 : 1, fontSize: '14px', fontWeight: '500' }} > {isLoading ? 'Regenerating...' : 'Yes, Regenerate'} </button> <button onClick={() => setShowRegenerateConfirm(false)} style={{ padding: '8px 16px', background: '#6b7280', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: '500' }} > Cancel </button> </div> </div> )} </div> </div> </div> </div> ) }

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