feat: Add Wild Central web app (Central-only UI)
Extract Central pages from the wild-cloud web app into a standalone React app for Wild Central. Includes: - Central overview, DNS, DHCP, Firewall, VPN, Ingress, CrowdSec pages - Simplified sidebar with Central-only navigation - Branding updated to "Wild Central" - All Cloud-specific pages, components, hooks, and API services removed - TypeScript type-check and production build pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
583
web/src/components/VpnComponent.tsx
Normal file
583
web/src/components/VpnComponent.tsx
Normal file
@@ -0,0 +1,583 @@
|
||||
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 (
|
||||
<Button variant="ghost" size="sm" onClick={handleCopy} className="h-7 w-7 p-0">
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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<VpnPeerConfig | null>(null);
|
||||
const [loadingPeerConfig, setLoadingPeerConfig] = useState<string | null>(null);
|
||||
const [deleteConfirmPeer, setDeleteConfirmPeer] = useState<VpnPeer | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<ConfigFormValues>();
|
||||
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-48">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Shield className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">VPN</h2>
|
||||
<p className="text-muted-foreground">WireGuard remote access to your Wild Cloud network</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alerts */}
|
||||
{successMessage && (
|
||||
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20">
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-700 dark:text-green-300">{successMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{(errorMessage || applyError || addPeerError) && (
|
||||
<Alert variant="error">
|
||||
<AlertDescription>{errorMessage ?? String(applyError ?? addPeerError)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Status card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Status</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={status?.running ? 'success' : config?.enabled ? 'warning' : 'secondary'} className="gap-1">
|
||||
{status?.running && <CheckCircle className="h-3 w-3" />}
|
||||
{status?.running ? 'Running' : 'Stopped'}
|
||||
</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApply}
|
||||
disabled={isApplying || !config?.enabled}
|
||||
className="gap-1 h-7 text-xs"
|
||||
>
|
||||
{isApplying ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Interface</p>
|
||||
<p className="font-mono">{status?.interface ?? 'wg0'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Connected peers</p>
|
||||
<p className="font-mono">{status?.peerCount ?? 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Listen port</p>
|
||||
<p className="font-mono">{status?.listenPort ?? config?.listenPort ?? 51820}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1 h-7 text-xs"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
Advanced
|
||||
</Button>
|
||||
</div>
|
||||
{showAdvanced && status?.rawOutput && (
|
||||
<div className="border-t pt-3">
|
||||
<span className="text-sm font-medium">WireGuard interface status</span>
|
||||
<pre className="bg-muted p-3 rounded-lg overflow-x-auto text-xs font-mono mt-2">
|
||||
{status.rawOutput}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Server configuration card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Server Configuration</CardTitle>
|
||||
{!editingConfig && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setConfirmKeygen(true)}
|
||||
disabled={isGeneratingKeypair}
|
||||
className="gap-1 h-7 text-xs"
|
||||
>
|
||||
{isGeneratingKeypair ? <Loader2 className="h-3 w-3 animate-spin" /> : <KeyRound className="h-3 w-3" />}
|
||||
Generate Keys
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleEditConfig}
|
||||
className="gap-1 h-7 text-xs"
|
||||
>
|
||||
<Edit2 className="h-3 w-3" />
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoadingConfig ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : editingConfig ? (
|
||||
<form onSubmit={handleSubmit(handleSaveConfig)} className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
{...register('enabled')}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor="enabled">Enabled</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="endpoint">Public Endpoint</Label>
|
||||
<Input
|
||||
id="endpoint"
|
||||
{...register('endpoint')}
|
||||
placeholder="vpn.example.com or 1.2.3.4"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Public hostname or IP clients use to connect. Port appended automatically if omitted.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="listenPort">Listen Port</Label>
|
||||
<Input
|
||||
id="listenPort"
|
||||
type="number"
|
||||
{...register('listenPort', { required: 'Required', min: { value: 1, message: 'Invalid port' }, max: { value: 65535, message: 'Invalid port' } })}
|
||||
className="mt-1"
|
||||
/>
|
||||
{errors.listenPort && <p className="text-sm text-red-600 mt-1">{errors.listenPort.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="address">Server VPN Address</Label>
|
||||
<Input
|
||||
id="address"
|
||||
{...register('address', { required: 'Required' })}
|
||||
placeholder="10.8.0.1/24"
|
||||
className="mt-1 font-mono"
|
||||
/>
|
||||
{errors.address && <p className="text-sm text-red-600 mt-1">{errors.address.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="dns">DNS for Clients</Label>
|
||||
<Input
|
||||
id="dns"
|
||||
{...register('dns')}
|
||||
placeholder="10.8.0.1"
|
||||
className="mt-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="lanCIDR">LAN CIDR (optional)</Label>
|
||||
<Input
|
||||
id="lanCIDR"
|
||||
{...register('lanCIDR')}
|
||||
placeholder="192.168.8.0/24"
|
||||
className="mt-1 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Routes LAN traffic through the VPN and enables masquerade on the server.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button type="submit" size="sm" disabled={isUpdatingConfig}>
|
||||
{isUpdatingConfig && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Save
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setEditingConfig(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Status</p>
|
||||
<Badge variant={config?.enabled ? 'success' : 'secondary'} className="mt-0.5 gap-1">
|
||||
{config?.enabled && <CheckCircle className="h-3 w-3" />}
|
||||
{config?.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Public Endpoint</p>
|
||||
<p className="font-mono bg-muted rounded px-2 py-1 mt-0.5">
|
||||
{config?.endpoint || <span className="text-muted-foreground italic">not set</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Listen Port</p>
|
||||
<p className="font-mono bg-muted rounded px-2 py-1 mt-0.5">{config?.listenPort ?? 51820}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">VPN Subnet</p>
|
||||
<p className="font-mono bg-muted rounded px-2 py-1 mt-0.5">{config?.address || '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">DNS for Clients</p>
|
||||
<p className="font-mono bg-muted rounded px-2 py-1 mt-0.5">{config?.dns || '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">LAN CIDR</p>
|
||||
<p className="font-mono bg-muted rounded px-2 py-1 mt-0.5">{config?.lanCIDR || '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Public Key</p>
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<p className="font-mono bg-muted rounded px-2 py-1 truncate text-xs flex-1">
|
||||
{config?.publicKey || <span className="italic">not generated</span>}
|
||||
</p>
|
||||
{config?.publicKey && <CopyButton text={config.publicKey} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!config?.publicKey && (
|
||||
<Alert>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
No server keypair yet. Click <strong>Generate Keys</strong> before adding peers.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Peers card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Peers</CardTitle>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setAddPeerOpen(true)}
|
||||
disabled={!config?.publicKey}
|
||||
className="gap-1 h-7 text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Peer
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoadingPeers ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : peers.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Shield className="h-10 w-10 mx-auto mb-2 opacity-30" />
|
||||
<p className="text-sm">No peers yet. Add a peer to get started.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{peers.map((peer) => (
|
||||
<div key={peer.id} className="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<p className="font-medium text-sm">{peer.name}</p>
|
||||
<p className="text-xs text-muted-foreground font-mono">{peer.allowedIPs}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1 h-7 text-xs"
|
||||
disabled={loadingPeerConfig === peer.id}
|
||||
onClick={() => handleShowPeerConfig(peer)}
|
||||
>
|
||||
{loadingPeerConfig === peer.id
|
||||
? <Loader2 className="h-3 w-3 animate-spin" />
|
||||
: <RefreshCw className="h-3 w-3" />}
|
||||
Config
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||
onClick={() => setDeleteConfirmPeer(peer)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Add peer drawer */}
|
||||
<Sheet open={addPeerOpen} onOpenChange={setAddPeerOpen}>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add Peer</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="space-y-4 py-6">
|
||||
<div>
|
||||
<Label htmlFor="peerName">Peer Name</Label>
|
||||
<Input
|
||||
id="peerName"
|
||||
value={newPeerName}
|
||||
onChange={(e) => setNewPeerName(e.target.value)}
|
||||
placeholder="e.g. my-phone, laptop"
|
||||
className="mt-1"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddPeer()}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
A VPN IP will be assigned automatically from the server subnet.
|
||||
</p>
|
||||
</div>
|
||||
{addPeerError && (
|
||||
<Alert variant="error">
|
||||
<AlertDescription>{String(addPeerError)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button onClick={handleAddPeer} disabled={isAddingPeer || !newPeerName.trim()}>
|
||||
{isAddingPeer && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
Add Peer
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setAddPeerOpen(false)}>Cancel</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* Peer config modal */}
|
||||
<Dialog open={!!peerConfigModal} onOpenChange={(open) => !open && setPeerConfigModal(null)}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{peerConfigModal?.name} — Client Config</DialogTitle>
|
||||
</DialogHeader>
|
||||
{peerConfigModal && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<QRCodeSVG value={peerConfigModal.config} size={220} />
|
||||
</div>
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
Scan with the WireGuard mobile app
|
||||
</p>
|
||||
<div className="relative">
|
||||
<pre className="text-xs font-mono bg-muted rounded-md p-3 overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{peerConfigModal.config}
|
||||
</pre>
|
||||
<div className="absolute top-2 right-2">
|
||||
<CopyButton text={peerConfigModal.config} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setPeerConfigModal(null)}>Close</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Generate keypair confirmation */}
|
||||
<AlertDialog open={confirmKeygen} onOpenChange={setConfirmKeygen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Regenerate Server Keypair?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
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.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleGenerateKeypair}>Generate</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Delete peer confirmation */}
|
||||
<AlertDialog open={!!deleteConfirmPeer} onOpenChange={(open) => !open && setDeleteConfirmPeer(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete peer "{deleteConfirmPeer?.name}"?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove the peer configuration. The peer will no longer be able to connect
|
||||
after you apply the VPN config.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeletePeer}>Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user