feat(wireguard): implement WireGuard interface management and permissions
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m39s
Docker images / frontend-image (push) Successful in 2m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 53s
Docker images / publish-release (push) Successful in 10s
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m39s
Docker images / frontend-image (push) Successful in 2m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 53s
Docker images / publish-release (push) Successful in 10s
Added comprehensive support for managing WireGuard interfaces, including CRUD operations and peer management. Updated permissions to include access control for WireGuard routes. Enhanced the UI components to display and interact with WireGuard configurations, improving user experience and functionality. Introduced new tests for WireGuard-related functionalities to ensure reliability.
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import type { WgIfaceWithServer } from "@/components/data-grids/wireguard-data-grid"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Sheet, SheetContent, SheetHeader, SheetTitle,
|
||||
SheetDescription, SheetFooter, SheetClose,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
generateMikrotikRsc,
|
||||
generateNativeConf,
|
||||
generatePeerClientConf,
|
||||
} from "@/lib/wg-config"
|
||||
import { CheckIcon, CopyIcon, DownloadIcon } from "lucide-react"
|
||||
|
||||
function downloadText(filename: string, content: string) {
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function WgExportSheet({
|
||||
open,
|
||||
iface,
|
||||
onClose,
|
||||
liveContent,
|
||||
liveBusy,
|
||||
onRequestLiveExport,
|
||||
}: {
|
||||
open: boolean
|
||||
iface: WgIfaceWithServer | null
|
||||
onClose: () => void
|
||||
/** Optional server-fetched content (with private key) keyed by format */
|
||||
liveContent?: { rsc?: string; conf?: string; peerConf?: string } | null
|
||||
liveBusy?: boolean
|
||||
onRequestLiveExport?: (format: "rsc" | "conf" | "peer-conf") => void
|
||||
}) {
|
||||
const [tab, setTab] = useState<"rsc" | "conf" | "peer">("rsc")
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTab("rsc")
|
||||
setCopied(false)
|
||||
}
|
||||
}, [open, iface?.id])
|
||||
|
||||
const local = useMemo(() => {
|
||||
if (!iface) return { rsc: "", conf: "", peerConf: "" }
|
||||
const base = {
|
||||
name: iface.name,
|
||||
listenPort: iface.listenPort,
|
||||
mtu: iface.mtu,
|
||||
comment: iface.comment,
|
||||
enabled: iface.enabled,
|
||||
privateKey: iface.privateKey,
|
||||
publicKey: iface.publicKey,
|
||||
address: iface.address,
|
||||
serverName: iface.serverName,
|
||||
peers: iface.peers.map((p) => ({
|
||||
publicKey: p.publicKey,
|
||||
allowedIps: p.allowedIps,
|
||||
endpoint: p.endpoint,
|
||||
persistentKeepalive: p.persistentKeepalive,
|
||||
persistent: p.persistent,
|
||||
comment: p.comment,
|
||||
name: p.name,
|
||||
clientAddress: p.clientAddress,
|
||||
clientDns: p.clientDns,
|
||||
clientEndpoint: p.clientEndpoint,
|
||||
})),
|
||||
}
|
||||
const peer = iface.peers[0]
|
||||
return {
|
||||
rsc: generateMikrotikRsc(base),
|
||||
conf: generateNativeConf(base),
|
||||
peerConf:
|
||||
iface.publicKey && peer
|
||||
? generatePeerClientConf({
|
||||
peerAddress: peer.clientAddress,
|
||||
peerDns: peer.clientDns,
|
||||
serverPublicKey: iface.publicKey,
|
||||
allowedIps: peer.allowedIps,
|
||||
endpoint:
|
||||
peer.clientEndpoint ||
|
||||
peer.endpoint ||
|
||||
undefined,
|
||||
persistentKeepalive: peer.persistentKeepalive ?? 25,
|
||||
})
|
||||
: "# Нет public-key интерфейса или пиров для клиентского .conf\n",
|
||||
}
|
||||
}, [iface])
|
||||
|
||||
const code =
|
||||
tab === "rsc"
|
||||
? (liveContent?.rsc ?? local.rsc)
|
||||
: tab === "conf"
|
||||
? (liveContent?.conf ?? local.conf)
|
||||
: (liveContent?.peerConf ?? local.peerConf)
|
||||
|
||||
const filename =
|
||||
tab === "rsc"
|
||||
? `${iface?.name ?? "wg"}.rsc`
|
||||
: tab === "conf"
|
||||
? `${iface?.name ?? "wg"}.conf`
|
||||
: `${iface?.name ?? "wg"}-peer.conf`
|
||||
|
||||
function handleCopy() {
|
||||
void navigator.clipboard.writeText(code).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<SheetContent className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-2xl">
|
||||
<SheetHeader className="shrink-0 px-6 pt-5 pb-4 border-b">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<SheetTitle>Экспорт WireGuard</SheetTitle>
|
||||
<SheetDescription>
|
||||
{iface ? `${iface.name} · ${iface.serverName}` : "—"}
|
||||
</SheetDescription>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Button variant="outline" size="sm" onClick={handleCopy}>
|
||||
{copied
|
||||
? <><CheckIcon className="size-3.5 text-emerald-500" />Скопировано</>
|
||||
: <><CopyIcon className="size-3.5" />Копировать</>}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => downloadText(filename, code)}
|
||||
>
|
||||
<DownloadIcon className="size-3.5" />
|
||||
Файл
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="px-6 pt-3 shrink-0">
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as typeof tab)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="rsc">MikroTik .rsc</TabsTrigger>
|
||||
<TabsTrigger value="conf">Native .conf</TabsTrigger>
|
||||
<TabsTrigger value="peer">Peer .conf</TabsTrigger>
|
||||
</TabsList>
|
||||
{onRequestLiveExport && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={liveBusy}
|
||||
onClick={() =>
|
||||
onRequestLiveExport(
|
||||
tab === "peer" ? "peer-conf" : tab === "conf" ? "conf" : "rsc",
|
||||
)
|
||||
}
|
||||
>
|
||||
{liveBusy ? "Загрузка с роутера…" : "Подтянуть с роутера (с private-key)"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TabsContent value="rsc" className="mt-0" />
|
||||
<TabsContent value="conf" className="mt-0" />
|
||||
<TabsContent value="peer" className="mt-0" />
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<pre className="px-6 py-5 text-[12px] font-mono leading-relaxed text-foreground/85 whitespace-pre select-all">
|
||||
{code.split("\n").map((line, i) => {
|
||||
const isComment = line.startsWith("#")
|
||||
const isCmd = line.trimStart().startsWith("/interface") || line.trimStart().startsWith("/ip")
|
||||
const isSection = line.startsWith("[")
|
||||
const isParam = /^\s+[a-z]/.test(line) || /^[A-Za-z]+=/.test(line)
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={
|
||||
isComment
|
||||
? "text-muted-foreground"
|
||||
: isCmd || isSection
|
||||
? "text-sky-400"
|
||||
: isParam
|
||||
? "text-violet-300"
|
||||
: "text-foreground"
|
||||
}
|
||||
>
|
||||
{line}{"\n"}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="shrink-0 px-6 py-4 border-t flex-row gap-2">
|
||||
<SheetClose render={<Button variant="outline" className="flex-1" />}>Закрыть</SheetClose>
|
||||
<Button className="flex-1" onClick={handleCopy}>
|
||||
{copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
|
||||
{copied ? "Скопировано" : "Копировать"}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
export { WgExportSheet }
|
||||
Reference in New Issue
Block a user