CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
secrets-manager.tsx263 linesDownload Raw Back to database-manager
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