feat: custom Stream port manager UI and WireGuard config Zip download API

This commit is contained in:
xtcnet 2026-03-08 15:50:25 +07:00
parent 7bf175da41
commit 34020bc562
4 changed files with 130 additions and 7 deletions

View file

@ -1,4 +1,5 @@
import express from "express"; import express from "express";
import archiver from "archiver";
import internalWireguard from "../internal/wireguard.js"; import internalWireguard from "../internal/wireguard.js";
import internalAuditLog from "../internal/audit-log.js"; import internalAuditLog from "../internal/audit-log.js";
import jwtdecode from "../lib/express/jwt-decode.js"; import jwtdecode from "../lib/express/jwt-decode.js";
@ -274,4 +275,36 @@ router.get("/client/:id/qrcode.svg", async (req, res, next) => {
} }
}); });
/**
* GET /api/wireguard/client/:id/configuration.zip
* Download WireGuard client configuration as a ZIP archive
*/
router.get("/client/:id/configuration.zip", async (req, res, next) => {
try {
const knex = db();
const client = await knex("wg_client").where("id", req.params.id).first();
if (!client) {
return res.status(404).json({ error: { message: "Client not found" } });
}
const configStr = await internalWireguard.getClientConfiguration(knex, req.params.id);
const svgStr = await internalWireguard.getClientQRCode(knex, req.params.id);
const safeName = client.name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32);
res.set("Content-Disposition", `attachment; filename="${safeName}.zip"`);
res.set("Content-Type", "application/zip");
const archive = archiver("zip", { zlib: { level: 9 } });
archive.on("error", (err) => next(err));
archive.pipe(res);
archive.append(configStr, { name: `${safeName}.conf` });
archive.append(svgStr, { name: `${safeName}-qrcode.svg` });
await archive.finalize();
} catch (err) {
next(err);
}
});
export default router; export default router;

View file

@ -79,3 +79,7 @@ export async function getWgClientConfig(id: number): Promise<string> {
export function downloadWgConfig(id: number, name: string) { export function downloadWgConfig(id: number, name: string) {
return api.download({ url: `/wireguard/client/${id}/configuration` }, `${name}.conf`); return api.download({ url: `/wireguard/client/${id}/configuration` }, `${name}.conf`);
} }
export function downloadWgConfigZip(id: number, name: string) {
return api.download({ url: `/wireguard/client/${id}/configuration.zip` }, `${name}.zip`);
}

View file

@ -9,10 +9,11 @@ import {
IconServer, IconServer,
IconEdit, IconEdit,
IconLink, IconLink,
IconZip,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import EasyModal from "ez-modal-react"; import EasyModal from "ez-modal-react";
import { useState } from "react"; import { useState } from "react";
import { downloadWgConfig } from "src/api/backend/wireguard"; import { downloadWgConfig, downloadWgConfigZip } from "src/api/backend/wireguard";
import { Loading } from "src/components"; import { Loading } from "src/components";
import { import {
useWgClients, useWgClients,
@ -154,6 +155,11 @@ function WireGuard() {
downloadWgConfig(id, cleanName); downloadWgConfig(id, cleanName);
}; };
const handleDownloadZip = (id: number, name: string) => {
const cleanName = name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32);
downloadWgConfigZip(id, cleanName);
};
return ( return (
<div className="container-xl"> <div className="container-xl">
{/* Page Header */} {/* Page Header */}
@ -411,6 +417,16 @@ function WireGuard() {
> >
<IconDownload size={16} /> <IconDownload size={16} />
</button> </button>
<button
type="button"
className="btn btn-outline-primary"
title="Download Config + QR (ZIP)"
onClick={() =>
handleDownloadZip(client.id, client.name)
}
>
<IconZip size={16} />
</button>
<button <button
type="button" type="button"
className={`btn ${client.enabled ? "btn-outline-warning" : "btn-outline-success"}`} className={`btn ${client.enabled ? "btn-outline-warning" : "btn-outline-success"}`}

View file

@ -139,6 +139,16 @@ generate_docker_compose() {
fi fi
log_step "Generating docker-compose.yml..." log_step "Generating docker-compose.yml..."
local custom_ports_block=""
if [ -f ".custom_ports" ]; then
while IFS= read -r port_mapping; do
# Ignore empty lines or comments
[[ -z "$port_mapping" || "$port_mapping" =~ ^# ]] && continue
custom_ports_block+=" - \"${port_mapping}\"\n"
done < ".custom_ports"
fi
cat > "$COMPOSE_FILE" <<YAML cat > "$COMPOSE_FILE" <<YAML
services: services:
d3v-npmwg: d3v-npmwg:
@ -156,7 +166,7 @@ services:
- "81:81" # Admin UI - "81:81" # Admin UI
- "443:443" # HTTPS - "443:443" # HTTPS
- "51820-51830:51820-51830/udp" # WireGuard Multi-Server Range - "51820-51830:51820-51830/udp" # WireGuard Multi-Server Range
volumes: $(echo -e "$custom_ports_block" | sed '/^$/d') volumes:
- ./data:/data - ./data:/data
- ./letsencrypt:/etc/letsencrypt - ./letsencrypt:/etc/letsencrypt
- ./wireguard:/etc/wireguard - ./wireguard:/etc/wireguard
@ -395,6 +405,62 @@ do_update() {
log_ok "Done." log_ok "Done."
} }
# -----------------------------------------------------------
# 7. Toggle Port 81 (Admin UI)
# -----------------------------------------------------------
# x. Custom Stream Ports Manager
# -----------------------------------------------------------
do_manage_ports() {
require_root
echo ""
log_step "TCP/UDP Stream Ports Manager"
echo "If you created a Stream in Nginx Proxy Manager (e.g., listening on port 10000),"
echo "you must expose that port down to the Docker container."
echo ""
local custom_ports_file=".custom_ports"
touch "$custom_ports_file"
echo "Current custom exposed ports:"
if [ -s "$custom_ports_file" ]; then
cat -n "$custom_ports_file"
else
echo " (None)"
fi
echo ""
read -rp "$(echo -e "${CYAN}[?]${NC} Enter new port mapping (e.g. 10000:10000) or 'clear' to remove all: ")" new_port
if [[ "$new_port" == "clear" ]]; then
> "$custom_ports_file"
log_ok "All custom ports cleared."
elif [[ -n "$new_port" ]]; then
echo "$new_port" >> "$custom_ports_file"
log_ok "Port $new_port added."
else
log_warn "No changes made."
return
fi
log_step "Regenerating docker-compose.yml and restarting container..."
local dc
dc=$(get_compose_cmd)
local current_wg_host=""
if [ -f "docker-compose.yml" ]; then
current_wg_host=$(grep -E 'WG_HOST:' docker-compose.yml | awk -F'"' '{print $2}')
fi
if [ -z "$current_wg_host" ]; then
current_wg_host=$(detect_public_ip)
fi
generate_docker_compose "$current_wg_host"
$dc up -d
log_ok "Container updated with new port configurations."
}
# ----------------------------------------------------------- # -----------------------------------------------------------
# 7. Toggle Port 81 (Admin UI) # 7. Toggle Port 81 (Admin UI)
# ----------------------------------------------------------- # -----------------------------------------------------------
@ -435,10 +501,11 @@ show_menu() {
echo " 3) Uninstall D3V-NPMWG + Docker (Purge)" echo " 3) Uninstall D3V-NPMWG + Docker (Purge)"
echo " 4) Reset Admin Password" echo " 4) Reset Admin Password"
echo " 5) Update D3V-NPMWG" echo " 5) Update D3V-NPMWG"
echo " 6) Toggle Admin Port 81 (Block/Unblock)" echo " 6) Manage Custom Stream Ports"
echo " 7) Exit" echo " 7) Toggle Admin Port 81 (Block/Unblock)"
echo " 8) Exit"
separator separator
read -rp " Select [1-7]: " choice read -rp " Select [1-8]: " choice
echo "" echo ""
case "$choice" in case "$choice" in
1) do_install ;; 1) do_install ;;
@ -446,8 +513,9 @@ show_menu() {
3) do_purge ;; 3) do_purge ;;
4) do_reset_password ;; 4) do_reset_password ;;
5) do_update ;; 5) do_update ;;
6) do_toggle_port_81 ;; 6) do_manage_ports ;;
7) echo "Bye!"; exit 0 ;; 7) do_toggle_port_81 ;;
8) echo "Bye!"; exit 0 ;;
*) log_err "Invalid option." ;; *) log_err "Invalid option." ;;
esac esac
done done
@ -462,6 +530,7 @@ show_help() {
echo " purge Remove D3V-NPMWG AND Docker" echo " purge Remove D3V-NPMWG AND Docker"
echo " reset Reset web admin password" echo " reset Reset web admin password"
echo " update Pull latest image and restart" echo " update Pull latest image and restart"
echo " manage-ports Add or remove custom exposed Stream TCP/UDP ports"
echo " toggle-port Block or unblock external access to Admin UI (Port 81) using iptables" echo " toggle-port Block or unblock external access to Admin UI (Port 81) using iptables"
echo " help Show this help" echo " help Show this help"
echo "" echo ""
@ -480,6 +549,7 @@ else
purge) do_purge ;; purge) do_purge ;;
reset) do_reset_password ;; reset) do_reset_password ;;
update) do_update ;; update) do_update ;;
manage-ports) do_manage_ports ;;
toggle-port) do_toggle_port_81 ;; toggle-port) do_toggle_port_81 ;;
help|-h|--help) show_help ;; help|-h|--help) show_help ;;
*) *)