mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-07 21:48:03 +02:00
refactor: vpn refresh and remove openvpn support
This commit is contained in:
@@ -1068,7 +1068,7 @@ export function CreateProfileDialog({
|
||||
v.id === selectedProxyId.slice(4),
|
||||
);
|
||||
return vpn
|
||||
? `${vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"} — ${vpn.name}`
|
||||
? `WG — ${vpn.name}`
|
||||
: "No proxy / VPN";
|
||||
}
|
||||
const proxy = storedProxies.find(
|
||||
@@ -1154,9 +1154,7 @@ export function CreateProfileDialog({
|
||||
variant="outline"
|
||||
className="text-[10px] px-1 py-0 leading-tight mr-1"
|
||||
>
|
||||
{vpn.vpn_type === "WireGuard"
|
||||
? "WG"
|
||||
: "OVPN"}
|
||||
WG
|
||||
</Badge>
|
||||
{vpn.name}
|
||||
</CommandItem>
|
||||
@@ -1417,7 +1415,7 @@ export function CreateProfileDialog({
|
||||
v.id === selectedProxyId.slice(4),
|
||||
);
|
||||
return vpn
|
||||
? `${vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"} — ${vpn.name}`
|
||||
? `WG — ${vpn.name}`
|
||||
: "No proxy / VPN";
|
||||
}
|
||||
const proxy = storedProxies.find(
|
||||
@@ -1503,9 +1501,7 @@ export function CreateProfileDialog({
|
||||
variant="outline"
|
||||
className="text-[10px] px-1 py-0 leading-tight mr-1"
|
||||
>
|
||||
{vpn.vpn_type === "WireGuard"
|
||||
? "WG"
|
||||
: "OVPN"}
|
||||
WG
|
||||
</Badge>
|
||||
{vpn.name}
|
||||
</CommandItem>
|
||||
|
||||
@@ -2219,11 +2219,7 @@ export function ProfilesDataTable({
|
||||
: effectiveProxy
|
||||
? effectiveProxy.name
|
||||
: "Not Selected";
|
||||
const vpnBadge = effectiveVpn
|
||||
? effectiveVpn.vpn_type === "WireGuard"
|
||||
? "WG"
|
||||
: "OVPN"
|
||||
: null;
|
||||
const vpnBadge = effectiveVpn ? "WG" : null;
|
||||
const tooltipText = hasAssignment ? displayName : null;
|
||||
const isSelectorOpen = meta.openProxySelectorFor === profile.id;
|
||||
const selectedId = effectiveVpnId ?? effectiveProxyId ?? null;
|
||||
@@ -2385,7 +2381,7 @@ export function ProfilesDataTable({
|
||||
variant="outline"
|
||||
className="text-[10px] px-1 py-0 leading-tight mr-1"
|
||||
>
|
||||
{vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}
|
||||
WG
|
||||
</Badge>
|
||||
{vpn.name}
|
||||
</CommandItem>
|
||||
|
||||
@@ -179,9 +179,7 @@ export function ProxyAssignmentDialog({
|
||||
if (selectionType === "none") return "None";
|
||||
if (selectionType === "vpn") {
|
||||
const vpn = vpnConfigs.find((v) => v.id === selectedId);
|
||||
return vpn
|
||||
? `${vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"} — ${vpn.name}`
|
||||
: "None";
|
||||
return vpn ? `WG — ${vpn.name}` : "None";
|
||||
}
|
||||
const proxy = storedProxies.find(
|
||||
(p) => p.id === selectedId,
|
||||
@@ -264,7 +262,7 @@ export function ProxyAssignmentDialog({
|
||||
variant="outline"
|
||||
className="text-[10px] px-1 py-0 leading-tight mr-1"
|
||||
>
|
||||
{vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}
|
||||
WG
|
||||
</Badge>
|
||||
{vpn.name}
|
||||
</CommandItem>
|
||||
|
||||
@@ -670,11 +670,7 @@ export function ProxyManagementDialog({
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">
|
||||
{vpn.vpn_type === "WireGuard"
|
||||
? "WG"
|
||||
: "OVPN"}
|
||||
</Badge>
|
||||
<Badge variant="outline">WG</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
|
||||
@@ -111,7 +111,7 @@ function CommandGroup({
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-x-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium overflow-y-scroll",
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-x-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+162
-381
@@ -3,10 +3,8 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -19,15 +17,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RippleButton } from "@/components/ui/ripple";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { VpnConfig, VpnType } from "@/types";
|
||||
import type { VpnConfig } from "@/types";
|
||||
|
||||
interface VpnFormDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -48,19 +38,6 @@ interface WireGuardFormData {
|
||||
presharedKey: string;
|
||||
}
|
||||
|
||||
interface OpenVpnFormData {
|
||||
name: string;
|
||||
rawConfig: string;
|
||||
}
|
||||
|
||||
interface VpnDependencyStatus {
|
||||
isAvailable: boolean;
|
||||
requiresExternalInstall: boolean;
|
||||
missingBinary: boolean;
|
||||
missingWindowsAdapter: boolean;
|
||||
dependencyCheckFailed: boolean;
|
||||
}
|
||||
|
||||
const defaultWireGuardForm: WireGuardFormData = {
|
||||
name: "",
|
||||
privateKey: "",
|
||||
@@ -74,11 +51,6 @@ const defaultWireGuardForm: WireGuardFormData = {
|
||||
presharedKey: "",
|
||||
};
|
||||
|
||||
const defaultOpenVpnForm: OpenVpnFormData = {
|
||||
name: "",
|
||||
rawConfig: "",
|
||||
};
|
||||
|
||||
function buildWireGuardConfig(form: WireGuardFormData): string {
|
||||
const lines: string[] = ["[Interface]"];
|
||||
lines.push(`PrivateKey = ${form.privateKey.trim()}`);
|
||||
@@ -102,63 +74,24 @@ export function VpnFormDialog({
|
||||
onClose,
|
||||
editingVpn,
|
||||
}: VpnFormDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [vpnType, setVpnType] = useState<VpnType>("WireGuard");
|
||||
const [wireGuardForm, setWireGuardForm] =
|
||||
useState<WireGuardFormData>(defaultWireGuardForm);
|
||||
const [openVpnForm, setOpenVpnForm] =
|
||||
useState<OpenVpnFormData>(defaultOpenVpnForm);
|
||||
const [vpnDependencyStatus, setVpnDependencyStatus] =
|
||||
useState<VpnDependencyStatus | null>(null);
|
||||
|
||||
const resetForms = useCallback(() => {
|
||||
setVpnType("WireGuard");
|
||||
setWireGuardForm(defaultWireGuardForm);
|
||||
setOpenVpnForm(defaultOpenVpnForm);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingVpn) {
|
||||
setVpnType(editingVpn.vpn_type);
|
||||
if (editingVpn.vpn_type === "WireGuard") {
|
||||
setWireGuardForm({ ...defaultWireGuardForm, name: editingVpn.name });
|
||||
} else {
|
||||
setOpenVpnForm({ name: editingVpn.name, rawConfig: "" });
|
||||
}
|
||||
setWireGuardForm({ ...defaultWireGuardForm, name: editingVpn.name });
|
||||
} else {
|
||||
resetForms();
|
||||
}
|
||||
}
|
||||
}, [isOpen, editingVpn, resetForms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setVpnDependencyStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
void invoke<VpnDependencyStatus>("get_vpn_dependency_status", { vpnType })
|
||||
.then((status) => {
|
||||
if (!cancelled) {
|
||||
setVpnDependencyStatus(status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load VPN dependency status:", error);
|
||||
if (!cancelled) {
|
||||
setVpnDependencyStatus(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen, vpnType]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (!isSubmitting) {
|
||||
onClose();
|
||||
@@ -167,10 +100,7 @@ export function VpnFormDialog({
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (editingVpn) {
|
||||
const name =
|
||||
vpnType === "WireGuard"
|
||||
? wireGuardForm.name.trim()
|
||||
: openVpnForm.name.trim();
|
||||
const name = wireGuardForm.name.trim();
|
||||
|
||||
if (!name) {
|
||||
toast.error("VPN name is required");
|
||||
@@ -196,80 +126,49 @@ export function VpnFormDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (vpnType === "WireGuard") {
|
||||
const { name, privateKey, address, peerPublicKey, peerEndpoint } =
|
||||
wireGuardForm;
|
||||
const { name, privateKey, address, peerPublicKey, peerEndpoint } =
|
||||
wireGuardForm;
|
||||
|
||||
if (!name.trim()) {
|
||||
toast.error("VPN name is required");
|
||||
return;
|
||||
}
|
||||
if (!privateKey.trim()) {
|
||||
toast.error("Private key is required");
|
||||
return;
|
||||
}
|
||||
if (!address.trim()) {
|
||||
toast.error("Address is required");
|
||||
return;
|
||||
}
|
||||
if (!peerPublicKey.trim()) {
|
||||
toast.error("Peer public key is required");
|
||||
return;
|
||||
}
|
||||
if (!peerEndpoint.trim()) {
|
||||
toast.error("Peer endpoint is required");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const configData = buildWireGuardConfig(wireGuardForm);
|
||||
await invoke("create_vpn_config_manual", {
|
||||
name: name.trim(),
|
||||
vpnType: "WireGuard",
|
||||
configData,
|
||||
});
|
||||
await emit("vpn-configs-changed");
|
||||
toast.success("WireGuard VPN created successfully");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to create VPN: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
} else {
|
||||
const { name, rawConfig } = openVpnForm;
|
||||
|
||||
if (!name.trim()) {
|
||||
toast.error("VPN name is required");
|
||||
return;
|
||||
}
|
||||
if (!rawConfig.trim()) {
|
||||
toast.error("OpenVPN config content is required");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await invoke("create_vpn_config_manual", {
|
||||
name: name.trim(),
|
||||
vpnType: "OpenVPN",
|
||||
configData: rawConfig,
|
||||
});
|
||||
await emit("vpn-configs-changed");
|
||||
toast.success("OpenVPN configuration created successfully");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to create VPN: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
if (!name.trim()) {
|
||||
toast.error("VPN name is required");
|
||||
return;
|
||||
}
|
||||
}, [editingVpn, vpnType, wireGuardForm, openVpnForm, onClose]);
|
||||
if (!privateKey.trim()) {
|
||||
toast.error("Private key is required");
|
||||
return;
|
||||
}
|
||||
if (!address.trim()) {
|
||||
toast.error("Address is required");
|
||||
return;
|
||||
}
|
||||
if (!peerPublicKey.trim()) {
|
||||
toast.error("Peer public key is required");
|
||||
return;
|
||||
}
|
||||
if (!peerEndpoint.trim()) {
|
||||
toast.error("Peer endpoint is required");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const configData = buildWireGuardConfig(wireGuardForm);
|
||||
await invoke("create_vpn_config_manual", {
|
||||
name: name.trim(),
|
||||
vpnType: "WireGuard",
|
||||
configData,
|
||||
});
|
||||
await emit("vpn-configs-changed");
|
||||
toast.success("WireGuard VPN created successfully");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to create VPN: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [editingVpn, wireGuardForm, onClose]);
|
||||
|
||||
const updateWireGuard = useCallback(
|
||||
(field: keyof WireGuardFormData, value: string) => {
|
||||
@@ -278,54 +177,10 @@ export function VpnFormDialog({
|
||||
[],
|
||||
);
|
||||
|
||||
const updateOpenVpn = useCallback(
|
||||
(field: keyof OpenVpnFormData, value: string) => {
|
||||
setOpenVpnForm((prev) => ({ ...prev, [field]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const dialogTitle = editingVpn
|
||||
? "Edit VPN"
|
||||
: vpnType === "WireGuard"
|
||||
? "Create WireGuard VPN"
|
||||
: "Create OpenVPN Configuration";
|
||||
|
||||
const dialogTitle = editingVpn ? "Edit VPN" : "Create WireGuard VPN";
|
||||
const dialogDescription = editingVpn
|
||||
? "Update the name of your VPN configuration."
|
||||
: vpnType === "WireGuard"
|
||||
? "Enter your WireGuard interface and peer details."
|
||||
: "Paste your .ovpn configuration file content.";
|
||||
|
||||
let dependencyWarningTitle: string | null = null;
|
||||
let dependencyWarningDescription: string | null = null;
|
||||
|
||||
if (
|
||||
vpnType === "OpenVPN" &&
|
||||
vpnDependencyStatus?.requiresExternalInstall &&
|
||||
!vpnDependencyStatus.isAvailable
|
||||
) {
|
||||
if (vpnDependencyStatus.missingBinary) {
|
||||
dependencyWarningTitle = t("vpnForm.dependencies.openVpnMissingTitle");
|
||||
dependencyWarningDescription = t(
|
||||
"vpnForm.dependencies.openVpnMissingDescription",
|
||||
);
|
||||
} else if (vpnDependencyStatus.missingWindowsAdapter) {
|
||||
dependencyWarningTitle = t(
|
||||
"vpnForm.dependencies.openVpnAdapterMissingTitle",
|
||||
);
|
||||
dependencyWarningDescription = t(
|
||||
"vpnForm.dependencies.openVpnAdapterMissingDescription",
|
||||
);
|
||||
} else if (vpnDependencyStatus.dependencyCheckFailed) {
|
||||
dependencyWarningTitle = t(
|
||||
"vpnForm.dependencies.openVpnCheckFailedTitle",
|
||||
);
|
||||
dependencyWarningDescription = t(
|
||||
"vpnForm.dependencies.openVpnCheckFailedDescription",
|
||||
);
|
||||
}
|
||||
}
|
||||
: "Enter your WireGuard interface and peer details.";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
@@ -337,221 +192,147 @@ export function VpnFormDialog({
|
||||
|
||||
<ScrollArea className="max-h-[60vh] pr-4">
|
||||
<div className="grid gap-4 py-2">
|
||||
{dependencyWarningTitle && dependencyWarningDescription && (
|
||||
<Alert className="border-warning/50 bg-warning/10">
|
||||
<AlertTitle className="text-warning">
|
||||
{dependencyWarningTitle}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-warning">
|
||||
{dependencyWarningDescription}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-name">Name</Label>
|
||||
<Input
|
||||
id="wg-name"
|
||||
value={wireGuardForm.name}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("name", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. Home WireGuard"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!editingVpn && (
|
||||
<div className="grid gap-2">
|
||||
<Label>VPN Type</Label>
|
||||
<Select
|
||||
value={vpnType}
|
||||
onValueChange={(value) => {
|
||||
setVpnType(value as VpnType);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select VPN type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="WireGuard">WireGuard</SelectItem>
|
||||
<SelectItem value="OpenVPN">OpenVPN</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{vpnType === "WireGuard" && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-name">Name</Label>
|
||||
<Label htmlFor="wg-private-key">Private Key</Label>
|
||||
<Input
|
||||
id="wg-name"
|
||||
value={wireGuardForm.name}
|
||||
id="wg-private-key"
|
||||
value={wireGuardForm.privateKey}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("name", e.target.value);
|
||||
updateWireGuard("privateKey", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. Home WireGuard"
|
||||
placeholder="Base64-encoded private key"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!editingVpn && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-private-key">Private Key</Label>
|
||||
<Input
|
||||
id="wg-private-key"
|
||||
value={wireGuardForm.privateKey}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("privateKey", e.target.value);
|
||||
}}
|
||||
placeholder="Base64-encoded private key"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-address">Address</Label>
|
||||
<Input
|
||||
id="wg-address"
|
||||
value={wireGuardForm.address}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("address", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 10.0.0.2/24"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-dns">DNS (optional)</Label>
|
||||
<Input
|
||||
id="wg-dns"
|
||||
value={wireGuardForm.dns}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("dns", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 1.1.1.1"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-mtu">MTU (optional)</Label>
|
||||
<Input
|
||||
id="wg-mtu"
|
||||
type="number"
|
||||
value={wireGuardForm.mtu}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("mtu", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 1420"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-peer-public-key">
|
||||
Peer Public Key
|
||||
</Label>
|
||||
<Input
|
||||
id="wg-peer-public-key"
|
||||
value={wireGuardForm.peerPublicKey}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("peerPublicKey", e.target.value);
|
||||
}}
|
||||
placeholder="Base64-encoded peer public key"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-peer-endpoint">Peer Endpoint</Label>
|
||||
<Input
|
||||
id="wg-peer-endpoint"
|
||||
value={wireGuardForm.peerEndpoint}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("peerEndpoint", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. vpn.example.com:51820"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-allowed-ips">Allowed IPs</Label>
|
||||
<Input
|
||||
id="wg-allowed-ips"
|
||||
value={wireGuardForm.allowedIps}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("allowedIps", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 0.0.0.0/0, ::/0"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-keepalive">
|
||||
Persistent Keepalive (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="wg-keepalive"
|
||||
type="number"
|
||||
value={wireGuardForm.persistentKeepalive}
|
||||
onChange={(e) => {
|
||||
updateWireGuard(
|
||||
"persistentKeepalive",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
placeholder="e.g. 25"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-preshared-key">
|
||||
Preshared Key (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="wg-preshared-key"
|
||||
value={wireGuardForm.presharedKey}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("presharedKey", e.target.value);
|
||||
}}
|
||||
placeholder="Base64-encoded preshared key"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{vpnType === "OpenVPN" && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="ovpn-name">Name</Label>
|
||||
<Label htmlFor="wg-address">Address</Label>
|
||||
<Input
|
||||
id="ovpn-name"
|
||||
value={openVpnForm.name}
|
||||
id="wg-address"
|
||||
value={wireGuardForm.address}
|
||||
onChange={(e) => {
|
||||
updateOpenVpn("name", e.target.value);
|
||||
updateWireGuard("address", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. Work OpenVPN"
|
||||
placeholder="e.g. 10.0.0.2/24"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!editingVpn && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="ovpn-config">Raw Config</Label>
|
||||
<Textarea
|
||||
id="ovpn-config"
|
||||
value={openVpnForm.rawConfig}
|
||||
<Label htmlFor="wg-dns">DNS (optional)</Label>
|
||||
<Input
|
||||
id="wg-dns"
|
||||
value={wireGuardForm.dns}
|
||||
onChange={(e) => {
|
||||
updateOpenVpn("rawConfig", e.target.value);
|
||||
updateWireGuard("dns", e.target.value);
|
||||
}}
|
||||
placeholder="Paste your .ovpn file content here..."
|
||||
className="min-h-[200px] font-mono text-xs"
|
||||
placeholder="e.g. 1.1.1.1"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-mtu">MTU (optional)</Label>
|
||||
<Input
|
||||
id="wg-mtu"
|
||||
type="number"
|
||||
value={wireGuardForm.mtu}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("mtu", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 1420"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-peer-public-key">Peer Public Key</Label>
|
||||
<Input
|
||||
id="wg-peer-public-key"
|
||||
value={wireGuardForm.peerPublicKey}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("peerPublicKey", e.target.value);
|
||||
}}
|
||||
placeholder="Base64-encoded peer public key"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-peer-endpoint">Peer Endpoint</Label>
|
||||
<Input
|
||||
id="wg-peer-endpoint"
|
||||
value={wireGuardForm.peerEndpoint}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("peerEndpoint", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. vpn.example.com:51820"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-allowed-ips">Allowed IPs</Label>
|
||||
<Input
|
||||
id="wg-allowed-ips"
|
||||
value={wireGuardForm.allowedIps}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("allowedIps", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 0.0.0.0/0, ::/0"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-keepalive">
|
||||
Persistent Keepalive (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="wg-keepalive"
|
||||
type="number"
|
||||
value={wireGuardForm.persistentKeepalive}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("persistentKeepalive", e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 25"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="wg-preshared-key">
|
||||
Preshared Key (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="wg-preshared-key"
|
||||
value={wireGuardForm.presharedKey}
|
||||
onChange={(e) => {
|
||||
updateWireGuard("presharedKey", e.target.value);
|
||||
}}
|
||||
placeholder="Base64-encoded preshared key"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -52,17 +52,6 @@ const detectVpnType = (
|
||||
endpoint: endpointMatch ? endpointMatch[1] : null,
|
||||
};
|
||||
}
|
||||
if (
|
||||
lowerFilename.endsWith(".ovpn") ||
|
||||
(content.includes("remote ") &&
|
||||
(content.includes("client") || content.includes("dev tun")))
|
||||
) {
|
||||
const remoteMatch = content.match(/remote\s+(\S+)(?:\s+(\d+))?/i);
|
||||
const endpoint = remoteMatch
|
||||
? `${remoteMatch[1]}${remoteMatch[2] ? `:${remoteMatch[2]}` : ""}`
|
||||
: null;
|
||||
return { isVpn: true, type: "OpenVPN", endpoint };
|
||||
}
|
||||
return { isVpn: false, type: null, endpoint: null };
|
||||
};
|
||||
|
||||
@@ -105,7 +94,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
|
||||
endpoint: detection.endpoint,
|
||||
});
|
||||
const baseName = filename
|
||||
.replace(/\.(conf|ovpn)$/i, "")
|
||||
.replace(/\.conf$/i, "")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/-/g, " ");
|
||||
setVpnName(baseName || `${detection.type} VPN`);
|
||||
@@ -132,13 +121,11 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
const validFile = files.find(
|
||||
(f) => f.name.endsWith(".conf") || f.name.endsWith(".ovpn"),
|
||||
);
|
||||
const validFile = files.find((f) => f.name.endsWith(".conf"));
|
||||
if (validFile) {
|
||||
handleFileRead(validFile);
|
||||
} else {
|
||||
toast.error("Please drop a .conf or .ovpn file");
|
||||
toast.error("Please drop a WireGuard .conf file");
|
||||
}
|
||||
},
|
||||
[handleFileRead],
|
||||
@@ -200,7 +187,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
|
||||
<DialogTitle>Import VPN Config</DialogTitle>
|
||||
<DialogDescription>
|
||||
{step === "dropzone" &&
|
||||
"Import a WireGuard (.conf) or OpenVPN (.ovpn) configuration file"}
|
||||
"Import a WireGuard (.conf) configuration file"}
|
||||
{step === "vpn-preview" && "Review the VPN configuration to import"}
|
||||
{step === "vpn-result" && "VPN import completed"}
|
||||
</DialogDescription>
|
||||
@@ -230,16 +217,12 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
|
||||
>
|
||||
<LuUpload className="w-10 h-10 text-muted-foreground mb-4" />
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Drop a VPN config file here or click to browse
|
||||
<br />
|
||||
<span className="text-xs">
|
||||
(.conf for WireGuard, .ovpn for OpenVPN)
|
||||
</span>
|
||||
Drop a WireGuard .conf file here or click to browse
|
||||
</p>
|
||||
<input
|
||||
id="vpn-file-input"
|
||||
type="file"
|
||||
accept=".conf,.ovpn"
|
||||
accept=".conf"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "Clone"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "OpenVPN is not installed",
|
||||
"openVpnMissingDescription": "You can save this configuration, but Donut Browser cannot connect it until OpenVPN is installed on this device.",
|
||||
"openVpnAdapterMissingTitle": "OpenVPN adapter is missing",
|
||||
"openVpnAdapterMissingDescription": "OpenVPN is installed, but no TAP/Wintun/ovpn-dco adapter was found. Repair or reinstall OpenVPN before connecting on Windows.",
|
||||
"openVpnCheckFailedTitle": "OpenVPN install could not be verified",
|
||||
"openVpnCheckFailedDescription": "Donut Browser could not inspect the local OpenVPN installation. Repair or reinstall OpenVPN before connecting on Windows."
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "Extensions",
|
||||
"description": "Manage browser extensions and extension groups for your profiles.",
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "Clonar"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "OpenVPN no está instalado",
|
||||
"openVpnMissingDescription": "Puedes guardar esta configuración, pero Donut Browser no podrá conectarse hasta que OpenVPN esté instalado en este dispositivo.",
|
||||
"openVpnAdapterMissingTitle": "Falta el adaptador de OpenVPN",
|
||||
"openVpnAdapterMissingDescription": "OpenVPN está instalado, pero no se encontró ningún adaptador TAP/Wintun/ovpn-dco. Repara o reinstala OpenVPN antes de conectarte en Windows.",
|
||||
"openVpnCheckFailedTitle": "No se pudo verificar la instalación de OpenVPN",
|
||||
"openVpnCheckFailedDescription": "Donut Browser no pudo inspeccionar la instalación local de OpenVPN. Repara o reinstala OpenVPN antes de conectarte en Windows."
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "Extensiones",
|
||||
"description": "Administra extensiones de navegador y grupos de extensiones para tus perfiles.",
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "Cloner"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "OpenVPN n'est pas installé",
|
||||
"openVpnMissingDescription": "Vous pouvez enregistrer cette configuration, mais Donut Browser ne pourra pas s'y connecter tant qu'OpenVPN n'est pas installé sur cet appareil.",
|
||||
"openVpnAdapterMissingTitle": "L'adaptateur OpenVPN est manquant",
|
||||
"openVpnAdapterMissingDescription": "OpenVPN est installé, mais aucun adaptateur TAP/Wintun/ovpn-dco n'a été trouvé. Réparez ou réinstallez OpenVPN avant de vous connecter sous Windows.",
|
||||
"openVpnCheckFailedTitle": "L'installation d'OpenVPN n'a pas pu être vérifiée",
|
||||
"openVpnCheckFailedDescription": "Donut Browser n'a pas pu inspecter l'installation locale d'OpenVPN. Réparez ou réinstallez OpenVPN avant de vous connecter sous Windows."
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "Extensions",
|
||||
"description": "Gérez les extensions de navigateur et les groupes d'extensions pour vos profils.",
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "複製"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "OpenVPN がインストールされていません",
|
||||
"openVpnMissingDescription": "この設定は保存できますが、このデバイスに OpenVPN がインストールされるまで Donut Browser では接続できません。",
|
||||
"openVpnAdapterMissingTitle": "OpenVPN アダプターが見つかりません",
|
||||
"openVpnAdapterMissingDescription": "OpenVPN はインストールされていますが、TAP/Wintun/ovpn-dco アダプターが見つかりませんでした。Windows で接続する前に OpenVPN を修復または再インストールしてください。",
|
||||
"openVpnCheckFailedTitle": "OpenVPN のインストールを確認できませんでした",
|
||||
"openVpnCheckFailedDescription": "Donut Browser はローカルの OpenVPN インストールを確認できませんでした。Windows で接続する前に OpenVPN を修復または再インストールしてください。"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "拡張機能",
|
||||
"description": "プロファイル用のブラウザ拡張機能と拡張機能グループを管理します。",
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "Clonar"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "OpenVPN não está instalado",
|
||||
"openVpnMissingDescription": "Você pode salvar esta configuração, mas o Donut Browser não poderá se conectar até que o OpenVPN esteja instalado neste dispositivo.",
|
||||
"openVpnAdapterMissingTitle": "O adaptador do OpenVPN está ausente",
|
||||
"openVpnAdapterMissingDescription": "O OpenVPN está instalado, mas nenhum adaptador TAP/Wintun/ovpn-dco foi encontrado. Repare ou reinstale o OpenVPN antes de se conectar no Windows.",
|
||||
"openVpnCheckFailedTitle": "Não foi possível verificar a instalação do OpenVPN",
|
||||
"openVpnCheckFailedDescription": "O Donut Browser não conseguiu inspecionar a instalação local do OpenVPN. Repare ou reinstale o OpenVPN antes de se conectar no Windows."
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "Extensões",
|
||||
"description": "Gerencie extensões de navegador e grupos de extensões para seus perfis.",
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "Клонировать"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "OpenVPN не установлен",
|
||||
"openVpnMissingDescription": "Вы можете сохранить эту конфигурацию, но Donut Browser не сможет подключиться, пока OpenVPN не будет установлен на этом устройстве.",
|
||||
"openVpnAdapterMissingTitle": "Отсутствует адаптер OpenVPN",
|
||||
"openVpnAdapterMissingDescription": "OpenVPN установлен, но адаптер TAP/Wintun/ovpn-dco не найден. Восстановите или переустановите OpenVPN перед подключением в Windows.",
|
||||
"openVpnCheckFailedTitle": "Не удалось проверить установку OpenVPN",
|
||||
"openVpnCheckFailedDescription": "Donut Browser не смог проверить локальную установку OpenVPN. Восстановите или переустановите OpenVPN перед подключением в Windows."
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "Расширения",
|
||||
"description": "Управляйте расширениями браузера и группами расширений для ваших профилей.",
|
||||
|
||||
@@ -812,16 +812,6 @@
|
||||
"button": "克隆"
|
||||
}
|
||||
},
|
||||
"vpnForm": {
|
||||
"dependencies": {
|
||||
"openVpnMissingTitle": "未安装 OpenVPN",
|
||||
"openVpnMissingDescription": "你现在可以保存这个配置,但在此设备上安装 OpenVPN 之前,Donut Browser 无法连接它。",
|
||||
"openVpnAdapterMissingTitle": "缺少 OpenVPN 适配器",
|
||||
"openVpnAdapterMissingDescription": "已安装 OpenVPN,但未找到 TAP/Wintun/ovpn-dco 适配器。在 Windows 上连接前,请修复或重新安装 OpenVPN。",
|
||||
"openVpnCheckFailedTitle": "无法验证 OpenVPN 安装",
|
||||
"openVpnCheckFailedDescription": "Donut Browser 无法检查本机 OpenVPN 安装。在 Windows 上连接前,请修复或重新安装 OpenVPN。"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"title": "扩展程序",
|
||||
"description": "管理配置文件的浏览器扩展程序和扩展程序组。",
|
||||
|
||||
+1
-1
@@ -667,7 +667,7 @@ export type ProxyParseResult =
|
||||
| { status: "invalid"; line: string; reason: string };
|
||||
|
||||
// VPN types
|
||||
export type VpnType = "WireGuard" | "OpenVPN";
|
||||
export type VpnType = "WireGuard";
|
||||
|
||||
export interface VpnConfig {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user