mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 01:37:51 +02:00
371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
"use client";
|
|
|
|
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 {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
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 type { VpnConfig } from "@/types";
|
|
|
|
interface VpnFormDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
editingVpn?: VpnConfig | null;
|
|
}
|
|
|
|
interface WireGuardFormData {
|
|
name: string;
|
|
privateKey: string;
|
|
address: string;
|
|
dns: string;
|
|
mtu: string;
|
|
peerPublicKey: string;
|
|
peerEndpoint: string;
|
|
allowedIps: string;
|
|
persistentKeepalive: string;
|
|
presharedKey: string;
|
|
}
|
|
|
|
const defaultWireGuardForm: WireGuardFormData = {
|
|
name: "",
|
|
privateKey: "",
|
|
address: "",
|
|
dns: "",
|
|
mtu: "",
|
|
peerPublicKey: "",
|
|
peerEndpoint: "",
|
|
allowedIps: "0.0.0.0/0, ::/0",
|
|
persistentKeepalive: "",
|
|
presharedKey: "",
|
|
};
|
|
|
|
function buildWireGuardConfig(form: WireGuardFormData): string {
|
|
const lines: string[] = ["[Interface]"];
|
|
lines.push(`PrivateKey = ${form.privateKey.trim()}`);
|
|
lines.push(`Address = ${form.address.trim()}`);
|
|
if (form.dns.trim()) lines.push(`DNS = ${form.dns.trim()}`);
|
|
if (form.mtu.trim()) lines.push(`MTU = ${form.mtu.trim()}`);
|
|
lines.push("");
|
|
lines.push("[Peer]");
|
|
lines.push(`PublicKey = ${form.peerPublicKey.trim()}`);
|
|
lines.push(`Endpoint = ${form.peerEndpoint.trim()}`);
|
|
lines.push(`AllowedIPs = ${form.allowedIps.trim()}`);
|
|
if (form.persistentKeepalive.trim())
|
|
lines.push(`PersistentKeepalive = ${form.persistentKeepalive.trim()}`);
|
|
if (form.presharedKey.trim())
|
|
lines.push(`PresharedKey = ${form.presharedKey.trim()}`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function VpnFormDialog({
|
|
isOpen,
|
|
onClose,
|
|
editingVpn,
|
|
}: VpnFormDialogProps) {
|
|
const { t } = useTranslation();
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [wireGuardForm, setWireGuardForm] =
|
|
useState<WireGuardFormData>(defaultWireGuardForm);
|
|
|
|
const resetForms = useCallback(() => {
|
|
setWireGuardForm(defaultWireGuardForm);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
if (editingVpn) {
|
|
setWireGuardForm({ ...defaultWireGuardForm, name: editingVpn.name });
|
|
} else {
|
|
resetForms();
|
|
}
|
|
}
|
|
}, [isOpen, editingVpn, resetForms]);
|
|
|
|
const handleClose = useCallback(() => {
|
|
if (!isSubmitting) {
|
|
onClose();
|
|
}
|
|
}, [isSubmitting, onClose]);
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (editingVpn) {
|
|
const name = wireGuardForm.name.trim();
|
|
|
|
if (!name) {
|
|
toast.error(t("vpns.form.nameRequired"));
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
await invoke("update_vpn_config", {
|
|
vpnId: editingVpn.id,
|
|
name,
|
|
});
|
|
await emit("vpn-configs-changed");
|
|
toast.success(t("vpns.form.updated"));
|
|
onClose();
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
toast.error(t("vpns.form.updateFailed", { error: errorMessage }));
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const { name, privateKey, address, peerPublicKey, peerEndpoint } =
|
|
wireGuardForm;
|
|
|
|
if (!name.trim()) {
|
|
toast.error(t("vpns.form.nameRequired"));
|
|
return;
|
|
}
|
|
if (!privateKey.trim()) {
|
|
toast.error(t("vpns.form.privateKeyRequired"));
|
|
return;
|
|
}
|
|
if (!address.trim()) {
|
|
toast.error(t("vpns.form.addressRequired"));
|
|
return;
|
|
}
|
|
if (!peerPublicKey.trim()) {
|
|
toast.error(t("vpns.form.peerPublicKeyRequired"));
|
|
return;
|
|
}
|
|
if (!peerEndpoint.trim()) {
|
|
toast.error(t("vpns.form.peerEndpointRequired"));
|
|
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(t("vpns.form.created"));
|
|
onClose();
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
toast.error(t("vpns.form.createFailed", { error: errorMessage }));
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [editingVpn, wireGuardForm, onClose, t]);
|
|
|
|
const updateWireGuard = useCallback(
|
|
(field: keyof WireGuardFormData, value: string) => {
|
|
setWireGuardForm((prev) => ({ ...prev, [field]: value }));
|
|
},
|
|
[],
|
|
);
|
|
|
|
const dialogTitle = editingVpn
|
|
? t("vpns.form.titleEdit")
|
|
: t("vpns.form.titleCreate");
|
|
const dialogDescription = editingVpn
|
|
? t("vpns.form.descEdit")
|
|
: t("vpns.form.descCreate");
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
|
<DialogDescription>{dialogDescription}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea className="max-h-[60vh] pr-4">
|
|
<div className="grid gap-4 py-2">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-name">{t("vpns.form.name")}</Label>
|
|
<Input
|
|
id="wg-name"
|
|
value={wireGuardForm.name}
|
|
onChange={(e) => {
|
|
updateWireGuard("name", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.namePlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
{!editingVpn && (
|
|
<>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-private-key">
|
|
{t("vpns.form.privateKey")}
|
|
</Label>
|
|
<Input
|
|
id="wg-private-key"
|
|
value={wireGuardForm.privateKey}
|
|
onChange={(e) => {
|
|
updateWireGuard("privateKey", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.privateKeyPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-address">{t("vpns.form.address")}</Label>
|
|
<Input
|
|
id="wg-address"
|
|
value={wireGuardForm.address}
|
|
onChange={(e) => {
|
|
updateWireGuard("address", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.addressPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-dns">{t("vpns.form.dnsOptional")}</Label>
|
|
<Input
|
|
id="wg-dns"
|
|
value={wireGuardForm.dns}
|
|
onChange={(e) => {
|
|
updateWireGuard("dns", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.dnsPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-mtu">{t("vpns.form.mtuOptional")}</Label>
|
|
<Input
|
|
id="wg-mtu"
|
|
type="number"
|
|
value={wireGuardForm.mtu}
|
|
onChange={(e) => {
|
|
updateWireGuard("mtu", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.mtuPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-peer-public-key">
|
|
{t("vpns.form.peerPublicKey")}
|
|
</Label>
|
|
<Input
|
|
id="wg-peer-public-key"
|
|
value={wireGuardForm.peerPublicKey}
|
|
onChange={(e) => {
|
|
updateWireGuard("peerPublicKey", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.peerPublicKeyPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-peer-endpoint">
|
|
{t("vpns.form.peerEndpoint")}
|
|
</Label>
|
|
<Input
|
|
id="wg-peer-endpoint"
|
|
value={wireGuardForm.peerEndpoint}
|
|
onChange={(e) => {
|
|
updateWireGuard("peerEndpoint", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.peerEndpointPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-allowed-ips">
|
|
{t("vpns.form.allowedIps")}
|
|
</Label>
|
|
<Input
|
|
id="wg-allowed-ips"
|
|
value={wireGuardForm.allowedIps}
|
|
onChange={(e) => {
|
|
updateWireGuard("allowedIps", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.allowedIpsPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-keepalive">
|
|
{t("vpns.form.keepaliveOptional")}
|
|
</Label>
|
|
<Input
|
|
id="wg-keepalive"
|
|
type="number"
|
|
value={wireGuardForm.persistentKeepalive}
|
|
onChange={(e) => {
|
|
updateWireGuard("persistentKeepalive", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.keepalivePlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="wg-preshared-key">
|
|
{t("vpns.form.presharedKeyOptional")}
|
|
</Label>
|
|
<Input
|
|
id="wg-preshared-key"
|
|
value={wireGuardForm.presharedKey}
|
|
onChange={(e) => {
|
|
updateWireGuard("presharedKey", e.target.value);
|
|
}}
|
|
placeholder={t("vpns.form.presharedKeyPlaceholder")}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<DialogFooter>
|
|
<RippleButton
|
|
variant="outline"
|
|
onClick={handleClose}
|
|
disabled={isSubmitting}
|
|
>
|
|
{t("common.buttons.cancel")}
|
|
</RippleButton>
|
|
<LoadingButton isLoading={isSubmitting} onClick={handleSubmit}>
|
|
{editingVpn
|
|
? t("vpns.form.updateButton")
|
|
: t("vpns.form.createButton")}
|
|
</LoadingButton>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|