Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect } from 'react';4import { Secret } from '@/lib/vfs/types';5import {6 Plus, Loader2, AlertCircle, Key, MoreVertical, Pencil, Trash2, AlertTriangle7} from 'lucide-react';8import { Badge } from '@/components/ui/badge';9import { Button } from '@/components/ui/button';10import {11 DropdownMenu,12 DropdownMenuContent,13 DropdownMenuItem,14 DropdownMenuTrigger,15} from '@/components/ui/dropdown-menu';16import { SecretEditor } from './secret-editor';17import { cn } from '@/lib/utils';18import type { SecretsDataProvider } from './data-providers';19 20interface SecretsManagerProps {21 deploymentId?: string;22 dataProvider?: SecretsDataProvider;23 workspaceId?: string;24}25 26export function SecretsManager({ deploymentId, dataProvider, workspaceId }: SecretsManagerProps) {27 const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api';28 const [secrets, setSecrets] = useState<Secret[]>([]);29 const [loading, setLoading] = useState(true);30 const [error, setError] = useState<string | null>(null);31 const [editingSecret, setEditingSecret] = useState<Secret | null>(null);32 const [isCreating, setIsCreating] = useState(false);33 const [encryptionConfigured, setEncryptionConfigured] = useState(true);34 35 useEffect(() => {36 loadSecrets();37 }, [deploymentId, dataProvider]);38 39 const loadSecrets = async () => {40 try {41 setLoading(true);42 setError(null);43 if (dataProvider) {44 const result = await dataProvider.list();45 setSecrets(result.secrets);46 setEncryptionConfigured(result.encryptionConfigured);47 } else if (deploymentId) {48 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets`);49 if (!res.ok) {50 const data = await res.json();51 throw new Error(data.error || 'Failed to load secrets');52 }53 const data = await res.json();54 setSecrets(data.secrets);55 setEncryptionConfigured(data.encryptionConfigured);56 }57 } catch (err) {58 setError(err instanceof Error ? err.message : 'Failed to load secrets');59 } finally {60 setLoading(false);61 }62 };63 64 const deleteSecret = async (secret: Secret) => {65 if (!confirm(`Delete secret "${secret.name}"? This cannot be undone.`)) return;66 67 try {68 if (dataProvider) {69 await dataProvider.remove(secret.id);70 } else if (deploymentId) {71 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets/${secret.id}`, {72 method: 'DELETE',73 });74 if (!res.ok) throw new Error('Failed to delete secret');75 } else {76 return;77 }78 await loadSecrets();79 } catch (err) {80 console.error('Failed to delete secret:', err);81 }82 };83 84 const handleSave = async (data: { name: string; value?: string; description?: string }) => {85 try {86 if (dataProvider) {87 await dataProvider.save(editingSecret?.id || null, data);88 } else if (!deploymentId) {89 throw new Error('No deployment ID available');90 } else if (editingSecret) {91 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets/${editingSecret.id}`, {92 method: 'PUT',93 headers: { 'Content-Type': 'application/json' },94 body: JSON.stringify(data),95 });96 if (!res.ok) {97 const err = await res.json();98 throw new Error(err.error || 'Failed to update secret');99 }100 } else {101 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/secrets`, {102 method: 'POST',103 headers: { 'Content-Type': 'application/json' },104 body: JSON.stringify(data),105 });106 if (!res.ok) {107 const err = await res.json();108 throw new Error(err.error || 'Failed to create secret');109 }110 }111 112 setEditingSecret(null);113 setIsCreating(false);114 await loadSecrets();115 } catch (err) {116 throw err;117 }118 };119 120 const formatDate = (date: Date) => {121 return new Date(date).toLocaleDateString(undefined, {122 month: 'short',123 day: 'numeric',124 year: 'numeric',125 });126 };127 128 if (loading) {129 return (130 <div className="flex items-center justify-center h-full">131 <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />132 </div>133 );134 }135 136 if (error) {137 return (138 <div className="flex flex-col items-center justify-center h-full gap-4">139 <AlertCircle className="h-8 w-8 text-destructive" />140 <p className="text-sm text-muted-foreground">{error}</p>141 <Button variant="outline" onClick={loadSecrets}>142 Retry143 </Button>144 </div>145 );146 }147 148 return (149 <div className="h-full flex flex-col">150 <div className="flex items-center justify-between mb-4">151 <h3 className="text-sm font-medium">Secrets</h3>152 <Button153 size="sm"154 onClick={() => setIsCreating(true)}155 disabled={!encryptionConfigured}156 >157 <Plus className="h-4 w-4 mr-1" />158 New Secret159 </Button>160 </div>161 162 {/* Warning if encryption not configured */}163 {!encryptionConfigured && (164 <div className="flex items-center gap-2 text-sm bg-yellow-500/10 border border-yellow-500/30 text-yellow-600 dark:text-yellow-400 p-3 rounded-lg mb-4">165 <AlertTriangle className="h-4 w-4 shrink-0" />166 <div>167 <p className="font-medium">Encryption not configured</p>168 <p className="text-xs opacity-80">169 Set the SECRETS_ENCRYPTION_KEY environment variable to enable secrets.170 </p>171 </div>172 </div>173 )}174 175 <div className="flex-1 overflow-auto">176 {secrets.length === 0 ? (177 <div className="flex flex-col items-center justify-center h-full p-8 text-center border rounded-lg">178 <Key className="h-8 w-8 text-muted-foreground mb-2" />179 <p className="text-sm text-muted-foreground">No secrets yet</p>180 <p className="text-xs text-muted-foreground mt-1 mb-4">181 Store API keys and tokens securely for your edge functions182 </p>183 <Button184 size="sm"185 onClick={() => setIsCreating(true)}186 disabled={!encryptionConfigured}187 >188 <Plus className="h-4 w-4 mr-1" />189 Create Secret190 </Button>191 </div>192 ) : (193 <div className="grid gap-3">194 {secrets.map(secret => (195 <div196 key={secret.id}197 className="border rounded-lg p-4 transition-colors"198 >199 <div className="flex items-start justify-between gap-2">200 <div className="flex-1 min-w-0 overflow-hidden">201 <div className="flex items-center gap-2 flex-wrap">202 <Key className="h-4 w-4 text-yellow-500 shrink-0" />203 <span className="font-mono font-medium truncate">{secret.name}</span>204 {!secret.hasValue && (205 <Badge variant="outline" className="text-amber-600 border-amber-500/50 bg-amber-500/10 text-xs shrink-0">206 Value not set207 </Badge>208 )}209 </div>210 {secret.description && (211 <p className="text-sm text-muted-foreground mt-1 truncate">212 {secret.description}213 </p>214 )}215 <div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">216 <span className="shrink-0">Updated {formatDate(secret.updatedAt)}</span>217 <span className="font-mono truncate">secrets.get('{secret.name}')</span>218 </div>219 </div>220 221 <DropdownMenu>222 <DropdownMenuTrigger asChild>223 <Button variant="ghost" size="sm">224 <MoreVertical className="h-4 w-4" />225 </Button>226 </DropdownMenuTrigger>227 <DropdownMenuContent align="end">228 <DropdownMenuItem onClick={() => setEditingSecret(secret)}>229 <Pencil className="h-4 w-4 mr-2" />230 Edit231 </DropdownMenuItem>232 <DropdownMenuItem233 onClick={() => deleteSecret(secret)}234 className="text-destructive"235 >236 <Trash2 className="h-4 w-4 mr-2" />237 Delete238 </DropdownMenuItem>239 </DropdownMenuContent>240 </DropdownMenu>241 </div>242 </div>243 ))}244 </div>245 )}246 </div>247 248 {/* Secret Editor Dialog */}249 {(isCreating || editingSecret) && (250 <SecretEditor251 secret={editingSecret}252 isOpen={true}253 onClose={() => {254 setIsCreating(false);255 setEditingSecret(null);256 }}257 onSave={handleSave}258 />259 )}260 </div>261 );262}263 