'use client'; import React, { useState, useEffect } from 'react'; import { Secret } from '@/lib/vfs/types'; import { Plus, Loader2, AlertCircle, Key, MoreVertical, Pencil, Trash2, AlertTriangle } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { SecretEditor } from './secret-editor'; import { cn } from '@/lib/utils'; import type { SecretsDataProvider } from './data-providers'; interface SecretsManagerProps { deploymentId?: string; dataProvider?: SecretsDataProvider; workspaceId?: string; } export function SecretsManager({ deploymentId, dataProvider, workspaceId }: SecretsManagerProps) { const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api'; const [secrets, setSecrets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingSecret, setEditingSecret] = useState(null); const [isCreating, setIsCreating] = useState(false); const [encryptionConfigured, setEncryptionConfigured] = useState(true); useEffect(() => { loadSecrets(); }, [deploymentId, dataProvider]); const loadSecrets = async () => { try { setLoading(true); setError(null); if (dataProvider) { const result = await dataProvider.list(); setSecrets(result.secrets); setEncryptionConfigured(result.encryptionConfigured); } else if (deploymentId) { const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets`); if (!res.ok) { const data = await res.json(); throw new Error(data.error || 'Failed to load secrets'); } const data = await res.json(); setSecrets(data.secrets); setEncryptionConfigured(data.encryptionConfigured); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load secrets'); } finally { setLoading(false); } }; const deleteSecret = async (secret: Secret) => { if (!confirm(`Delete secret "${secret.name}"? This cannot be undone.`)) return; try { if (dataProvider) { await dataProvider.remove(secret.id); } else if (deploymentId) { const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets/${secret.id}`, { method: 'DELETE', }); if (!res.ok) throw new Error('Failed to delete secret'); } else { return; } await loadSecrets(); } catch (err) { console.error('Failed to delete secret:', err); } }; const handleSave = async (data: { name: string; value?: string; description?: string }) => { try { if (dataProvider) { await dataProvider.save(editingSecret?.id || null, data); } else if (!deploymentId) { throw new Error('No deployment ID available'); } else if (editingSecret) { const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets/${editingSecret.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to update secret'); } } else { const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to create secret'); } } setEditingSecret(null); setIsCreating(false); await loadSecrets(); } catch (err) { throw err; } }; const formatDate = (date: Date) => { return new Date(date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', }); }; if (loading) { return (
); } if (error) { return (

{error}

); } return (

Secrets

{/* Warning if encryption not configured */} {!encryptionConfigured && (

Encryption not configured

Set the SECRETS_ENCRYPTION_KEY environment variable to enable secrets.

)}
{secrets.length === 0 ? (

No secrets yet

Store API keys and tokens securely for your edge functions

) : (
{secrets.map(secret => (
{secret.name} {!secret.hasValue && ( Value not set )}
{secret.description && (

{secret.description}

)}
Updated {formatDate(secret.updatedAt)} secrets.get('{secret.name}')
setEditingSecret(secret)}> Edit deleteSecret(secret)} className="text-destructive" > Delete
))}
)}
{/* Secret Editor Dialog */} {(isCreating || editingSecret) && ( { setIsCreating(false); setEditingSecret(null); }} onSave={handleSave} /> )}
); }