CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
secret-editor.tsx213 linesDownload Raw Back to database-manager
1'use client';2 3import React, { useState, useEffect } from 'react';4import { Secret } from '@/lib/vfs/types';5import {6  Dialog,7  DialogContent,8  DialogDescription,9  DialogHeader,10  DialogTitle,11} from '@/components/ui/dialog';12import { Button } from '@/components/ui/button';13import { Input } from '@/components/ui/input';14import { Label } from '@/components/ui/label';15import { Loader2, AlertCircle, Eye, EyeOff, Info } from 'lucide-react';16 17interface SecretEditorProps {18  secret: Secret | null;19  isOpen: boolean;20  onClose: () => void;21  onSave: (data: { name: string; value?: string; description?: string }) => Promise<void>;22}23 24export function SecretEditor({25  secret,26  isOpen,27  onClose,28  onSave,29}: SecretEditorProps) {30  const [name, setName] = useState(secret?.name || '');31  const [value, setValue] = useState('');32  const [description, setDescription] = useState(secret?.description || '');33  const [showValue, setShowValue] = useState(false);34  const [saving, setSaving] = useState(false);35  const [error, setError] = useState<string | null>(null);36 37  useEffect(() => {38    if (isOpen) {39      setName(secret?.name || '');40      setValue('');41      setDescription(secret?.description || '');42      setShowValue(false);43      setError(null);44    }45  }, [secret, isOpen]);46 47  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {48    // Convert to SCREAMING_SNAKE_CASE49    const newValue = e.target.value50      .toUpperCase()51      .replace(/[^A-Z0-9_]/g, '')52      .replace(/^[0-9]+/, ''); // Remove leading numbers53    setName(newValue);54  };55 56  const handleSave = async () => {57    setError(null);58 59    // Basic validation60    if (!name.trim()) {61      setError('Secret name is required');62      return;63    }64    // Validate SCREAMING_SNAKE_CASE65    if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {66      setError('Name must be SCREAMING_SNAKE_CASE (uppercase letters, numbers, underscores; must start with letter)');67      return;68    }69    // Value required for new secrets70    if (!secret && !value.trim()) {71      setError('Secret value is required');72      return;73    }74 75    setSaving(true);76    try {77      await onSave({78        name: name.trim(),79        value: value.trim() || undefined,80        description: description.trim() || undefined,81      });82    } catch (err) {83      setError(err instanceof Error ? err.message : 'Failed to save secret');84    } finally {85      setSaving(false);86    }87  };88 89  return (90    <Dialog open={isOpen} onOpenChange={onClose}>91      <DialogContent className="sm:max-w-lg">92        <DialogHeader>93          <DialogTitle>94            {secret ? 'Edit Secret' : 'Create Secret'}95          </DialogTitle>96          <DialogDescription>97            Store sensitive values like API keys securely. Edge functions can access them via secrets.get('{name || 'NAME'}').98          </DialogDescription>99        </DialogHeader>100 101        <div className="space-y-4">102          {/* Name */}103          <div className="space-y-2">104            <Label htmlFor="name">Secret Name</Label>105            <Input106              id="name"107              value={name}108              onChange={handleNameChange}109              placeholder="STRIPE_API_KEY"110              disabled={!!secret}111              className="font-mono"112            />113            <p className="text-xs text-muted-foreground">114              Use SCREAMING_SNAKE_CASE (e.g., API_KEY, SENDGRID_TOKEN)115            </p>116          </div>117 118          {/* Value */}119          <div className="space-y-2">120            <Label htmlFor="value">121              {secret ? 'New Value (leave empty to keep current)' : 'Secret Value'}122            </Label>123            <div className="relative">124              <Input125                id="value"126                type={showValue ? 'text' : 'password'}127                value={value}128                onChange={e => setValue(e.target.value)}129                placeholder={secret ? 'Enter new value to change...' : 'sk_live_...'}130                className="pr-10 font-mono"131              />132              <Button133                type="button"134                variant="ghost"135                size="sm"136                className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"137                onClick={() => setShowValue(!showValue)}138              >139                {showValue ? (140                  <EyeOff className="h-4 w-4" />141                ) : (142                  <Eye className="h-4 w-4" />143                )}144              </Button>145            </div>146            <p className="text-xs text-muted-foreground">147              {secret148                ? 'Leave empty to keep the existing value'149                : 'This value will be encrypted and never displayed again'}150            </p>151          </div>152 153          {/* Description */}154          <div className="space-y-2">155            <Label htmlFor="description">Description (optional)</Label>156            <Input157              id="description"158              value={description}159              onChange={e => setDescription(e.target.value)}160              placeholder="Production Stripe API key"161            />162          </div>163 164          {/* Usage Reference */}165          <div className="bg-muted/30 border rounded-lg p-4 space-y-2">166            <div className="flex items-center gap-2 text-sm font-medium">167              <Info className="h-4 w-4" />168              Usage in Edge Functions169            </div>170            <pre className="text-xs font-mono bg-background p-2 rounded overflow-x-auto">171{`// Get secret value172const apiKey = secrets.get('${name || 'STRIPE_API_KEY'}');173 174// Check if secret exists175if (secrets.has('${name || 'STRIPE_API_KEY'}')) {176  // Use the secret177}178 179// List all available secrets180const allSecrets = secrets.list(); // ['${name || 'STRIPE_API_KEY'}', ...]`}181            </pre>182          </div>183 184          {/* Error */}185          {error && (186            <div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 p-3 rounded-lg">187              <AlertCircle className="h-4 w-4" />188              {error}189            </div>190          )}191        </div>192 193        {/* Footer */}194        <div className="flex items-center justify-end gap-2 pt-4 border-t">195          <Button variant="outline" onClick={onClose} disabled={saving}>196            Cancel197          </Button>198          <Button onClick={handleSave} disabled={saving}>199            {saving ? (200              <>201                <Loader2 className="h-4 w-4 mr-2 animate-spin" />202                Saving...203              </>204            ) : (205              secret ? 'Save Changes' : 'Create Secret'206            )}207          </Button>208        </div>209      </DialogContent>210    </Dialog>211  );212}213