import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { Shield, Loader2, Edit2, Plus, Trash2, Copy, Check, CheckCircle, RefreshCw, KeyRound, Play, ChevronDown, ChevronUp } from 'lucide-react'; import { QRCodeSVG } from 'qrcode.react'; import { Card, CardHeader, CardTitle, CardContent } from './ui/card'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Badge } from './ui/badge'; import { Alert, AlertDescription } from './ui/alert'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from './ui/sheet'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from './ui/alert-dialog'; import { useVpn } from '../hooks/useVpn'; import { vpnApi, type VpnPeer, type VpnPeerConfig } from '../services/api/vpn'; interface ConfigFormValues { enabled: boolean; listenPort: number; address: string; endpoint: string; dns: string; lanCIDR: string; } function CopyButton({ text }: { text: string }) { const [copied, setCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( ); } export function VpnComponent() { const { status, isLoadingStatus, config, isLoadingConfig, peers, isLoadingPeers, updateConfig, isUpdatingConfig, generateKeypair, isGeneratingKeypair, apply, isApplying, applyError, addPeer, isAddingPeer, addPeerError, deletePeer, } = useVpn(); const [editingConfig, setEditingConfig] = useState(false); const [confirmKeygen, setConfirmKeygen] = useState(false); const [addPeerOpen, setAddPeerOpen] = useState(false); const [newPeerName, setNewPeerName] = useState(''); const [peerConfigModal, setPeerConfigModal] = useState(null); const [loadingPeerConfig, setLoadingPeerConfig] = useState(null); const [deleteConfirmPeer, setDeleteConfirmPeer] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); const [successMessage, setSuccessMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const { register, handleSubmit, reset, formState: { errors } } = useForm(); const showSuccess = (msg: string) => { setSuccessMessage(msg); setTimeout(() => setSuccessMessage(null), 5000); }; const showError = (msg: string) => { setErrorMessage(msg); setTimeout(() => setErrorMessage(null), 8000); }; const handleEditConfig = () => { reset({ enabled: config?.enabled ?? false, listenPort: config?.listenPort ?? 51820, address: config?.address ?? '10.8.0.1/24', endpoint: config?.endpoint ?? '', dns: config?.dns ?? '', lanCIDR: config?.lanCIDR ?? '', }); setEditingConfig(true); }; const handleSaveConfig = async (values: ConfigFormValues) => { try { await updateConfig({ ...values, listenPort: Number(values.listenPort) }); setEditingConfig(false); if (values.enabled) { await apply(); showSuccess('Configuration saved and applied'); } else { showSuccess('Configuration saved'); } } catch (e) { showError(`Save failed: ${e instanceof Error ? e.message : String(e)}`); } }; const handleGenerateKeypair = async () => { setConfirmKeygen(false); try { await generateKeypair(); showSuccess('Server keypair generated — re-add all peers to update their configs'); } catch (e) { showError(`Keygen failed: ${e instanceof Error ? e.message : String(e)}`); } }; const handleApply = async () => { try { await apply(); showSuccess('WireGuard interface applied'); } catch (e) { showError(`Apply failed: ${e instanceof Error ? e.message : String(e)}`); } }; const handleAddPeer = async () => { if (!newPeerName.trim()) return; try { await addPeer(newPeerName.trim()); setNewPeerName(''); setAddPeerOpen(false); showSuccess(`Peer "${newPeerName.trim()}" added`); } catch (e) { showError(`Add peer failed: ${e instanceof Error ? e.message : String(e)}`); } }; const handleShowPeerConfig = async (peer: VpnPeer) => { setLoadingPeerConfig(peer.id); try { const cfg = await vpnApi.getPeerConfig(peer.id); setPeerConfigModal(cfg); } catch (e) { showError(`Failed to get peer config: ${e instanceof Error ? e.message : String(e)}`); } finally { setLoadingPeerConfig(null); } }; const handleDeletePeer = async () => { if (!deleteConfirmPeer) return; try { await deletePeer(deleteConfirmPeer.id); setDeleteConfirmPeer(null); showSuccess(`Peer "${deleteConfirmPeer.name}" deleted`); } catch (e) { showError(`Delete failed: ${e instanceof Error ? e.message : String(e)}`); } }; if (isLoadingStatus && isLoadingConfig) { return (
); } return (
{/* Page header */}

VPN

WireGuard remote access to your Wild Cloud network

{/* Alerts */} {successMessage && ( {successMessage} )} {(errorMessage || applyError || addPeerError) && ( {errorMessage ?? String(applyError ?? addPeerError)} )} {/* Status card */}
Status
{status?.running && } {status?.running ? 'Running' : 'Stopped'}

Interface

{status?.interface ?? 'wg0'}

Connected peers

{status?.peerCount ?? 0}

Listen port

{status?.listenPort ?? config?.listenPort ?? 51820}

{showAdvanced && status?.rawOutput && (
WireGuard interface status
                {status.rawOutput}
              
)}
{/* Server configuration card */}
Server Configuration {!editingConfig && (
)}
{isLoadingConfig ? ( ) : editingConfig ? (

Public hostname or IP clients use to connect. Port appended automatically if omitted.

{errors.listenPort &&

{errors.listenPort.message}

}
{errors.address &&

{errors.address.message}

}

Routes LAN traffic through the VPN and enables masquerade on the server.

) : (

Status

{config?.enabled && } {config?.enabled ? 'Enabled' : 'Disabled'}

Public Endpoint

{config?.endpoint || not set}

Listen Port

{config?.listenPort ?? 51820}

VPN Subnet

{config?.address || '—'}

DNS for Clients

{config?.dns || '—'}

LAN CIDR

{config?.lanCIDR || '—'}

Public Key

{config?.publicKey || not generated}

{config?.publicKey && }
{!config?.publicKey && ( No server keypair yet. Click Generate Keys before adding peers. )}
)}
{/* Peers card */}
Peers
{isLoadingPeers ? ( ) : peers.length === 0 ? (

No peers yet. Add a peer to get started.

) : (
{peers.map((peer) => (

{peer.name}

{peer.allowedIPs}

))}
)}
{/* Add peer drawer */} Add Peer
setNewPeerName(e.target.value)} placeholder="e.g. my-phone, laptop" className="mt-1" onKeyDown={(e) => e.key === 'Enter' && handleAddPeer()} />

A VPN IP will be assigned automatically from the server subnet.

{addPeerError && ( {String(addPeerError)} )}
{/* Peer config modal */} !open && setPeerConfigModal(null)}> {peerConfigModal?.name} — Client Config {peerConfigModal && (

Scan with the WireGuard mobile app

                  {peerConfigModal.config}
                
)}
{/* Generate keypair confirmation */} Regenerate Server Keypair? This will generate a new server private and public key. All existing peer configs will become invalid — you'll need to regenerate and redistribute them. Cancel Generate {/* Delete peer confirmation */} !open && setDeleteConfirmPeer(null)}> Delete peer "{deleteConfirmPeer?.name}"? This will remove the peer configuration. The peer will no longer be able to connect after you apply the VPN config. Cancel Delete
); }