Leon4gr45/builder
0
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 