feat: implement wireguard multi-server UI and backend logic

This commit is contained in:
xtcnet 2026-03-08 09:33:24 +07:00
parent 5119f84558
commit 54d1623551
69 changed files with 7238 additions and 826 deletions

View file

@ -1,6 +1,6 @@
# AI Assistant Instructions # AI Assistant Instructions
You are working on **NPM-WG** (Nginx Proxy Manager + WireGuard). You are working on **NPM-WG** (xGat3 + WireGuard).
Whenever you start a task in this workspace or are asked to fix a bug, please **FIRST READ the file `AI_CONTEXT.md`** at the root of the project. Whenever you start a task in this workspace or are asked to fix a bug, please **FIRST READ the file `AI_CONTEXT.md`** at the root of the project.
It contains: It contains:

View file

@ -27,8 +27,8 @@ Are you in the right place?
<!-- A clear and concise description of what the bug is. --> <!-- A clear and concise description of what the bug is. -->
**Nginx Proxy Manager Version** **xGat3 Version**
<!-- What version of Nginx Proxy Manager is reported on the login page? --> <!-- What version of xGat3 is reported on the login page? -->
**To Reproduce** **To Reproduce**

View file

@ -1,9 +1,9 @@
# AI Context for NPM-WG Project # AI Context for NPM-WG Project
## 1. Project Overview ## 1. Project Overview
**NPM-WG** is a custom fork of [Nginx Proxy Manager](https://github.com/NginxProxyManager/nginx-proxy-manager) integrated with **WireGuard VPN** management capabilities, inspired by `wg-easy`. **NPM-WG** is a custom fork of [xGat3](https://github.com/NginxProxyManager/nginx-proxy-manager) integrated with **WireGuard VPN** management capabilities, inspired by `wg-easy`.
The project structure remains mostly identical to Nginx Proxy Manager, but specific backend and frontend modules have been added to manage WireGuard securely inside the Docker container without needing external dependencies. The project structure remains mostly identical to xGat3, but specific backend and frontend modules have been added to manage WireGuard securely inside the Docker container without needing external dependencies.
--- ---

View file

@ -1,10 +1,10 @@
# D3V-NPMWG — Nginx Proxy Manager + WireGuard VPN # D3V-NPMWG — xGat3 + WireGuard VPN
A powerful, all-in-one Docker container that combines **Nginx Proxy Manager** (reverse proxy with SSL) and **WireGuard VPN** management in a single, beautiful web interface. A powerful, all-in-one Docker container that combines **xGat3** (reverse proxy with SSL) and **WireGuard VPN** management in a single, beautiful web interface.
## ✨ Features ## ✨ Features
### Nginx Proxy Manager ### xGat3
- 🌐 Reverse proxy management with a beautiful UI - 🌐 Reverse proxy management with a beautiful UI
- 🔒 Free SSL certificates via Let's Encrypt - 🔒 Free SSL certificates via Let's Encrypt
- 🔀 Proxy hosts, redirection hosts, streams, and 404 hosts - 🔀 Proxy hosts, redirection hosts, streams, and 404 hosts
@ -170,7 +170,7 @@ Alternatively, you can run the helper script:
## 📜 Credits ## 📜 Credits
- [Nginx Proxy Manager](https://github.com/NginxProxyManager/nginx-proxy-manager) — Original proxy manager - [xGat3](https://github.com/NginxProxyManager/nginx-proxy-manager) — Original proxy manager
- [wg-easy](https://github.com/wg-easy/wg-easy) — WireGuard management inspiration - [wg-easy](https://github.com/wg-easy/wg-easy) — WireGuard management inspiration
## 📄 License ## 📄 License

View file

@ -6,7 +6,7 @@ let instance = null;
const generateDbConfig = () => { const generateDbConfig = () => {
if (!configHas("database")) { if (!configHas("database")) {
throw new Error( throw new Error(
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/", "Database config does not exist! Please read the instructions: https://x.d3v.ac/setup/",
); );
} }

View file

@ -5,7 +5,7 @@ import errs from "../lib/error.js";
import authModel from "../models/auth.js"; import authModel from "../models/auth.js";
import internalUser from "./user.js"; import internalUser from "./user.js";
const APP_NAME = "Nginx Proxy Manager"; const APP_NAME = "xGat3";
const BACKUP_CODE_COUNT = 8; const BACKUP_CODE_COUNT = 8;
/** /**

View file

@ -22,31 +22,71 @@ const internalWireguard = {
async getOrCreateInterface(knex) { async getOrCreateInterface(knex) {
let iface = await knex("wg_interface").first(); let iface = await knex("wg_interface").first();
if (!iface) { if (!iface) {
// Generate keys // Seed a default config if it doesn't exist
const privateKey = await wgHelpers.generatePrivateKey(); const insertData = {
const publicKey = await wgHelpers.getPublicKey(privateKey); name: "wg0",
listen_port: 51820,
const [id] = await knex("wg_interface").insert({ ipv4_cidr: "10.0.0.1/24",
name: WG_INTERFACE_NAME, ipv6_cidr: null,
private_key: privateKey, mtu: 1420,
public_key: publicKey,
ipv4_cidr: WG_DEFAULT_ADDRESS,
listen_port: WG_DEFAULT_PORT,
mtu: WG_DEFAULT_MTU,
dns: WG_DEFAULT_DNS, dns: WG_DEFAULT_DNS,
host: WG_HOST, host: WG_HOST,
post_up: `iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE`, post_up: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
post_down: `iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE`, post_down: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE",
created_on: knex.fn.now(), created_on: knex.fn.now(),
modified_on: knex.fn.now(), modified_on: knex.fn.now(),
}); };
const [id] = await knex("wg_interface").insert(insertData);
iface = await knex("wg_interface").where("id", id).first(); iface = await knex("wg_interface").where("id", id).first();
logger.info("WireGuard interface created with new keypair"); logger.info("WireGuard interface created with default config");
} }
return iface; return iface;
}, },
/**
* Render PostUp and PostDown iptables rules based on interface, isolation, and links
*/
async renderIptablesRules(knex, iface) {
const basePostUp = [];
const basePostDown = [];
// Default forward and NAT
basePostUp.push("iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE");
basePostDown.push("iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE");
// Client Isolation: Prevent clients on this interface from communicating with each other
if (iface.isolate_clients) {
basePostUp.push("iptables -I FORWARD -i %i -o %i -j REJECT");
basePostDown.push("iptables -D FORWARD -i %i -o %i -j REJECT");
}
// Server Isolation (Default DROP) & Server Peering
// 1. By default, prevent this interface from talking to ANY OTHER wg+ interfaces
basePostUp.push("iptables -I FORWARD -i %i -o wg+ -j DROP");
basePostDown.push("iptables -D FORWARD -i %i -o wg+ -j DROP");
// 2. Fetch linked servers to punch holes in the DROP rule
// wg_server_link has interface_id_1 and interface_id_2
const links = await knex("wg_server_link")
.where("interface_id_1", iface.id)
.orWhere("interface_id_2", iface.id);
for (const link of links) {
const peerIfaceId = link.interface_id_1 === iface.id ? link.interface_id_2 : link.interface_id_1;
const peerIface = await knex("wg_interface").where("id", peerIfaceId).first();
if (peerIface) {
basePostUp.push(`iptables -I FORWARD -i %i -o ${peerIface.name} -j ACCEPT`);
basePostDown.push(`iptables -D FORWARD -i %i -o ${peerIface.name} -j ACCEPT`);
}
}
return {
postUp: basePostUp.join("; "),
postDown: basePostDown.join("; "),
};
},
/** /**
* Save WireGuard config to /etc/wireguard/wg0.conf and sync * Save WireGuard config to /etc/wireguard/wg0.conf and sync
*/ */

View file

@ -0,0 +1,66 @@
const migrate_name = "wireguard_multi_server";
/**
* Migration to add multi-server support to WireGuard tables
*/
export async function up(knex) {
// First, check if the tables exist
const hasInterfaceTable = await knex.schema.hasTable("wg_interface");
const hasClientTable = await knex.schema.hasTable("wg_client");
if (!hasInterfaceTable || !hasClientTable) {
throw new Error("Missing wg_interface or wg_client tables. Ensure previous migrations ran.");
}
// 1. Add isolate_clients to wg_interface
await knex.schema.alterTable("wg_interface", (table) => {
table.boolean("isolate_clients").notNullable().defaultTo(false);
});
// 2. Add interface_id to wg_client
await knex.schema.alterTable("wg_client", (table) => {
table.integer("interface_id").unsigned().nullable(); // Initially nullable to allow adding
});
// 3. Assign existing clients to the first interface (wg0)
const firstInterface = await knex("wg_interface").orderBy("id").first();
if (firstInterface) {
await knex("wg_client").whereNull("interface_id").update({
interface_id: firstInterface.id,
});
}
// 4. Make interface_id not nullable and add foreign key
await knex.schema.alterTable("wg_client", (table) => {
table.integer("interface_id")
.unsigned()
.notNullable()
.references("id")
.inTable("wg_interface")
.onDelete("CASCADE")
.alter();
});
// 5. Create wg_server_link for server peering
await knex.schema.createTable("wg_server_link", (table) => {
table.integer("interface_id_1").unsigned().notNullable()
.references("id").inTable("wg_interface").onDelete("CASCADE");
table.integer("interface_id_2").unsigned().notNullable()
.references("id").inTable("wg_interface").onDelete("CASCADE");
table.primary(["interface_id_1", "interface_id_2"]);
});
}
export async function down(knex) {
await knex.schema.dropTableIfExists("wg_server_link");
await knex.schema.alterTable("wg_client", (table) => {
table.dropForeign("interface_id");
table.dropColumn("interface_id");
});
await knex.schema.alterTable("wg_interface", (table) => {
table.dropColumn("isolate_clients");
});
}

5783
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@ const router = express.Router({
* GET /api/wireguard * GET /api/wireguard
* Get WireGuard interface info * Get WireGuard interface info
*/ */
router.get("/", async (req, res, next) => { router.get("/", async (_req, res, next) => {
try { try {
const knex = db(); const knex = db();
const iface = await internalWireguard.getInterfaceInfo(knex); const iface = await internalWireguard.getInterfaceInfo(knex);
@ -26,7 +26,7 @@ router.get("/", async (req, res, next) => {
* GET /api/wireguard/client * GET /api/wireguard/client
* List all WireGuard clients with live status * List all WireGuard clients with live status
*/ */
router.get("/client", async (req, res, next) => { router.get("/client", async (_req, res, next) => {
try { try {
const knex = db(); const knex = db();
const clients = await internalWireguard.getClients(knex); const clients = await internalWireguard.getClients(knex);

View file

@ -1,9 +1,9 @@
{ {
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "Nginx Proxy Manager API", "title": "xGat3 API",
"version": "2.x.x", "version": "2.x.x",
"description": "This is the official API documentation for Nginx Proxy Manager.\n\nMost endpoints require authentication via Bearer Token (JWT). You can generate a token by logging in via the `POST /tokens` endpoint.\n\nFor more information, visit the [Nginx Proxy Manager Documentation](https://nginxproxymanager.com)." "description": "This is the official API documentation for xGat3.\n\nMost endpoints require authentication via Bearer Token (JWT). You can generate a token by logging in via the `POST /tokens` endpoint."
}, },
"servers": [ "servers": [
{ {

File diff suppressed because it is too large Load diff

View file

@ -77,6 +77,6 @@ ENTRYPOINT [ "/init" ]
LABEL org.label-schema.schema-version="1.0" \ LABEL org.label-schema.schema-version="1.0" \
org.label-schema.license="MIT" \ org.label-schema.license="MIT" \
org.label-schema.name="d3v-npmwg" \ org.label-schema.name="d3v-npmwg" \
org.label-schema.description="D3V-NPMWG: Nginx Proxy Manager + WireGuard VPN Manager" \ org.label-schema.description="xGat3 : xGat3 + WireGuard VPN Manager" \
org.label-schema.url="https://github.com/xtcnet/D3V-NPMWG" \ org.label-schema.url="https://github.com/xtcnet/D3V-NPMWG" \
org.label-schema.cmd="docker run --rm -ti --cap-add=NET_ADMIN --cap-add=SYS_MODULE d3v-npmwg:latest" org.label-schema.cmd="docker run --rm -ti --cap-add=NET_ADMIN --cap-add=SYS_MODULE d3v-npmwg:latest"

View file

@ -1,22 +1,22 @@
import { defineConfig, type DefaultTheme } from 'vitepress'; import { defineConfig, type DefaultTheme } from 'vitepress';
// https://vitepress.dev/reference/site-config
export default defineConfig({ export default defineConfig({
title: "Nginx Proxy Manager", title: "xGat3",
description: "Expose your services easily and securely", description: "Expose your services easily and securely",
head: [ head: [
["link", { rel: "icon", href: "/icon.png" }], ["link", { rel: "icon", href: "/icon.png" }],
["meta", { name: "description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt" }], ["meta", { name: "description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt" }],
["meta", { property: "og:title", content: "Nginx Proxy Manager" }], ["meta", { property: "og:title", content: "xGat3" }],
["meta", { property: "og:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}], ["meta", { property: "og:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}],
["meta", { property: "og:type", content: "website" }], ["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:url", content: "https://nginxproxymanager.com/" }], ["meta", { property: "og:url", content: "https://x.d3v.ac/" }],
["meta", { property: "og:image", content: "https://nginxproxymanager.com/icon.png" }], ["meta", { property: "og:image", content: "https://x.d3v.ac/icon.png" }],
["meta", { name: "twitter:card", content: "summary"}], ["meta", { name: "twitter:card", content: "summary"}],
["meta", { name: "twitter:title", content: "Nginx Proxy Manager"}], ["meta", { name: "twitter:title", content: "xGat3"}],
["meta", { name: "twitter:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}], ["meta", { name: "twitter:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}],
["meta", { name: "twitter:image", content: "https://nginxproxymanager.com/icon.png"}], ["meta", { name: "twitter:image", content: "https://x.d3v.ac/icon.png"}],
["meta", { name: "twitter:alt", content: "Nginx Proxy Manager"}], ["meta", { name: "twitter:alt", content: "xGat3"}],
// GA // GA
['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}], ['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}],
['script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');"], ['script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');"],

View file

@ -50,7 +50,7 @@ I won't go in to too much detail here but here are the basics for someone new to
1. Your home router will have a Port Forwarding section somewhere. Log in and find it 1. Your home router will have a Port Forwarding section somewhere. Log in and find it
2. Add port forwarding for port 80 and 443 to the server hosting this project 2. Add port forwarding for port 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) 3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services 4. Use the xGat3 as your gateway to forward to your other web based services
## Quick Setup ## Quick Setup
@ -77,7 +77,7 @@ services:
- ./letsencrypt:/etc/letsencrypt - ./letsencrypt:/etc/letsencrypt
``` ```
This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more. This is the bare minimum configuration required. See the [documentation](https://x.d3v.ac/setup/) for more.
3. Bring up your stack by running 3. Bring up your stack by running

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nginx Proxy Manager</title> <title>xGat3</title>
<meta name="description" content="Manage Nginx hosts with a simple, powerful interface" /> <meta name="description" content="Manage Nginx hosts with a simple, powerful interface" />
<link rel="preload" href="/images/logo-no-text.svg" as="image" type="image/svg+xml" fetchPriority="high"> <link rel="preload" href="/images/logo-no-text.svg" as="image" type="image/svg+xml" fetchPriority="high">
<link <link

View file

@ -25,6 +25,7 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-hook-form": "^7.71.2",
"react-intl": "^8.1.3", "react-intl": "^8.1.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
@ -4904,6 +4905,22 @@
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-hook-form": {
"version": "7.71.2",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-intl": { "node_modules/react-intl": {
"version": "8.1.3", "version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz",
@ -6210,22 +6227,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"extraneous": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zwitch": { "node_modules/zwitch": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View file

@ -32,6 +32,7 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-hook-form": "^7.71.2",
"react-intl": "^8.1.3", "react-intl": "^8.1.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",

View file

@ -11,6 +11,8 @@ export interface WgClient {
createdOn: string; createdOn: string;
updatedOn: string; updatedOn: string;
expiresAt: string | null; expiresAt: string | null;
interfaceId: number;
interfaceName: string;
latestHandshakeAt: string | null; latestHandshakeAt: string | null;
endpoint: string | null; endpoint: string | null;
transferRx: number; transferRx: number;
@ -26,17 +28,35 @@ export interface WgInterface {
mtu: number; mtu: number;
dns: string; dns: string;
host: string; host: string;
isolateClients: boolean;
linkedServers: number[];
} }
export async function getWgClients(): Promise<WgClient[]> { export async function getWgClients(): Promise<WgClient[]> {
return await api.get({ url: "/wireguard/client" }); return await api.get({ url: "/wireguard/client" });
} }
export async function getWgInterface(): Promise<WgInterface> { export async function getWgInterfaces(): Promise<WgInterface[]> {
return await api.get({ url: "/wireguard" }); return await api.get({ url: "/wireguard" });
} }
export async function createWgClient(data: { name: string }): Promise<WgClient> { export async function createWgInterface(data: { mtu?: number; dns?: string; host?: string; isolate_clients?: boolean; linked_servers?: number[] }): Promise<WgInterface> {
return await api.post({ url: "/wireguard", data });
}
export async function updateWgInterface(id: number, data: { mtu?: number; dns?: string; host?: string; isolate_clients?: boolean; linked_servers?: number[] }): Promise<WgInterface> {
return await api.put({ url: `/wireguard/${id}`, data });
}
export async function deleteWgInterface(id: number): Promise<boolean> {
return await api.del({ url: `/wireguard/${id}` });
}
export async function updateWgInterfaceLinks(id: number, data: { linked_servers: number[] }): Promise<WgInterface> {
return await api.post({ url: `/wireguard/${id}/links`, data });
}
export async function createWgClient(data: { name: string; interface_id?: number }): Promise<WgClient> {
return await api.post({ url: "/wireguard/client", data }); return await api.post({ url: "/wireguard/client", data });
} }

View file

@ -36,7 +36,7 @@ export function SiteHeader() {
alt="Logo" alt="Logo"
/> />
</div> </div>
Nginx Proxy Manager xGat3
</NavLink> </NavLink>
</div> </div>
<div className="navbar-nav flex-row order-md-last"> <div className="navbar-nav flex-row order-md-last">

View file

@ -1,7 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
getWgClients, getWgClients,
getWgInterface, getWgInterfaces,
createWgInterface,
updateWgInterface,
deleteWgInterface,
updateWgInterfaceLinks,
createWgClient, createWgClient,
deleteWgClient, deleteWgClient,
enableWgClient, enableWgClient,
@ -20,19 +24,62 @@ export const useWgClients = (options = {}) => {
}); });
}; };
export const useWgInterface = (options = {}) => { export const useWgInterfaces = (options = {}) => {
return useQuery<WgInterface, Error>({ return useQuery<WgInterface[], Error>({
queryKey: ["wg-interface"], queryKey: ["wg-interfaces"],
queryFn: getWgInterface, queryFn: getWgInterfaces,
staleTime: 60 * 1000, refetchInterval: 10000,
staleTime: 5000,
...options, ...options,
}); });
}; };
export const useCreateWgInterface = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { mtu?: number; dns?: string; host?: string; isolate_clients?: boolean; linked_servers?: number[] }) => createWgInterface(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["wg-interfaces"] });
},
});
};
export const useUpdateWgInterface = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: { mtu?: number; dns?: string; host?: string; isolate_clients?: boolean; linked_servers?: number[] } }) => updateWgInterface(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["wg-interfaces"] });
},
});
};
export const useDeleteWgInterface = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteWgInterface(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["wg-interfaces"] });
queryClient.invalidateQueries({ queryKey: ["wg-clients"] });
},
});
};
export const useUpdateWgInterfaceLinks = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: { linked_servers: number[] } }) => updateWgInterfaceLinks(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["wg-interfaces"] });
},
});
};
export const useCreateWgClient = () => { export const useCreateWgClient = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (data: { name: string }) => createWgClient(data), mutationFn: (data: { name: string; interface_id?: number }) => createWgClient(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["wg-clients"] }); queryClient.invalidateQueries({ queryKey: ["wg-clients"] });
}, },

View file

@ -4,4 +4,4 @@
Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL. Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL.
Прокси хостовете са най-често използваната функция в Nginx Proxy Manager. Прокси хостовете са най-често използваната функция в xGat3.

View file

@ -4,4 +4,4 @@ Proxy hostitel je příchozí koncový bod pro webovou službu, kterou chcete p
Poskytuje volitelné ukončení SSL pro vaši službu, která nemusí mít zabudovanou podporu SSL. Poskytuje volitelné ukončení SSL pro vaši službu, která nemusí mít zabudovanou podporu SSL.
Proxy hostitelé jsou nejběžnějším použitím pro Nginx Proxy Manager. Proxy hostitelé jsou nejběžnějším použitím pro xGat3.

View file

@ -4,4 +4,4 @@ Ein Proxy-Host ist der eingehende Endpunkt für einen Webdienst, den Sie weiterl
Er bietet optionale SSL-Terminierung für Ihren Dienst, der möglicherweise keine integrierte SSL-Unterstützung hat. Er bietet optionale SSL-Terminierung für Ihren Dienst, der möglicherweise keine integrierte SSL-Unterstützung hat.
Proxy-Hosts sind die häufigste Verwendung für den Nginx Proxy Manager. Proxy-Hosts sind die häufigste Verwendung für den xGat3.

View file

@ -4,4 +4,4 @@ A Proxy Host is the incoming endpoint for a web service that you want to forward
It provides optional SSL termination for your service that might not have SSL support built in. It provides optional SSL termination for your service that might not have SSL support built in.
Proxy Hosts are the most common use for the Nginx Proxy Manager. Proxy Hosts are the most common use for the xGat3.

View file

@ -4,4 +4,4 @@ Un Host Proxy es el punto de entrada para un servicio web que deseas reenviar.
Proporciona terminación SSL opcional para tu servicio que podría no tener soporte SSL integrado. Proporciona terminación SSL opcional para tu servicio que podría no tener soporte SSL integrado.
Los Hosts Proxy son el uso más común del Nginx Proxy Manager. Los Hosts Proxy son el uso más común del xGat3.

View file

@ -4,4 +4,4 @@ Un Hôte Proxy est le point de terminaison entrant d'un service web que vous sou
Il assure la terminaison SSL optionnelle pour votre service qui ne prend pas en charge SSL nativement. Il assure la terminaison SSL optionnelle pour votre service qui ne prend pas en charge SSL nativement.
Les Hôtes Proxy constituent l'utilisation la plus courante du Nginx Proxy Manager. Les Hôtes Proxy constituent l'utilisation la plus courante du xGat3.

View file

@ -4,4 +4,4 @@ A Proxy Kiszolgáló egy bejövő végpont egy olyan webszolgáltatáshoz, amely
Opcionális SSL lezárást biztosít a szolgáltatásodhoz, amelyben esetleg nincs beépített SSL támogatás. Opcionális SSL lezárást biztosít a szolgáltatásodhoz, amelyben esetleg nincs beépített SSL támogatás.
A Proxy Kiszolgálók az Nginx Proxy Manager leggyakoribb felhasználási módjai. A Proxy Kiszolgálók az xGat3 leggyakoribb felhasználási módjai.

View file

@ -4,4 +4,4 @@ Host Proxy adalah endpoint masuk untuk layanan web yang ingin Anda teruskan.
Host ini menyediakan terminasi SSL opsional untuk layanan Anda yang mungkin tidak memiliki dukungan SSL bawaan. Host ini menyediakan terminasi SSL opsional untuk layanan Anda yang mungkin tidak memiliki dukungan SSL bawaan.
Host Proxy adalah penggunaan paling umum untuk Nginx Proxy Manager. Host Proxy adalah penggunaan paling umum untuk xGat3.

View file

@ -4,4 +4,4 @@ Un host proxy è l'endpoint in entrata per un servizio web che si desidera inolt
Fornisce la terminazione SSL opzionale per il servizio che potrebbe non avere il supporto SSL integrato. Fornisce la terminazione SSL opzionale per il servizio che potrebbe non avere il supporto SSL integrato.
Gli host proxy sono l'uso più comune per Nginx Proxy Manager. Gli host proxy sono l'uso più comune per xGat3.

View file

@ -4,4 +4,4 @@
サービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。 サービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。
プロキシホストはNginx Proxy Managerのもっとも一般的な使用方法です。 プロキシホストはxGat3のもっとも一般的な使用方法です。

View file

@ -4,7 +4,7 @@
HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다. HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다.
이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다. 이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 xGat3가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.
다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.** 다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.**

View file

@ -4,5 +4,5 @@
원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다. 원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다.
프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다. 프록시 호스트는 xGat3에서 가장 일반적으로 사용되는 기능입니다.

View file

@ -4,4 +4,4 @@ Een Proxy Host is de inkomende endpoint voor een webdienst dat je wilt doorsture
Het biedt optionele SSL voor je dienst die mogelijk geen SSL ondersteuning heeft. Het biedt optionele SSL voor je dienst die mogelijk geen SSL ondersteuning heeft.
Proxy Hosts worden het meest gebruikt in Nginx Proxy Manager. Proxy Hosts worden het meest gebruikt in xGat3.

View file

@ -4,4 +4,4 @@ En Proxyhost er inngangspunktet (innkommende endepunkt) for en webtjeneste du
Den tilbyr valgfri SSLterminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL. Den tilbyr valgfri SSLterminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL.
Proxyhosts er den vanligste bruken av Nginx Proxy Manager. Proxyhosts er den vanligste bruken av xGat3.

View file

@ -4,4 +4,4 @@ Host proxy to punkt wejściowy dla usługi internetowej, którą chcesz przekier
Zapewnia opcjonalne zakończenie SSL dla twojej usługi, która może nie mieć wbudowanej obsługi SSL. Zapewnia opcjonalne zakończenie SSL dla twojej usługi, która może nie mieć wbudowanej obsługi SSL.
Hosty proxy są najpopularniejszym zastosowaniem Nginx Proxy Manager Hosty proxy są najpopularniejszym zastosowaniem xGat3

View file

@ -4,4 +4,4 @@ Um *Proxy Host* é o ponto de entrada para um serviço web que pretendes encamin
Permite, opcionalmente, fazer terminação SSL para um serviço que possa não ter suporte SSL nativo. Permite, opcionalmente, fazer terminação SSL para um serviço que possa não ter suporte SSL nativo.
Os *Proxy Hosts* são a utilização mais comum do Nginx Proxy Manager. Os *Proxy Hosts* são a utilização mais comum do xGat3.

View file

@ -4,4 +4,4 @@
Он может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL. Он может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL.
Прокси‑хосты — самый распространённый сценарий использования Nginx Proxy Manager. Прокси‑хосты — самый распространённый сценарий использования xGat3.

View file

@ -4,4 +4,4 @@ Proxy hostiteľ je prichádzajúci koncový bod pre webovú službu, ktorú chce
Poskytuje voliteľné ukončenie SSL pre vašu službu, ktorá nemusí mať zabudovanú podporu SSL. Poskytuje voliteľné ukončenie SSL pre vašu službu, ktorá nemusí mať zabudovanú podporu SSL.
Proxy hostitelia sú najbežnejším použitím pre Nginx Proxy Manager. Proxy hostitelia sú najbežnejším použitím pre xGat3.

View file

@ -4,5 +4,5 @@ Proxy Host, iletilmek istediğiniz bir web hizmeti için gelen uç noktadır.
SSL desteği yerleşik olmayan hizmetiniz için isteğe bağlı SSL sonlandırma sağlar. SSL desteği yerleşik olmayan hizmetiniz için isteğe bağlı SSL sonlandırma sağlar.
Proxy Host'lar, Nginx Proxy Manager'ın en yaygın kullanımıdır. Proxy Host'lar, xGat3'ın en yaygın kullanımıdır.

View file

@ -4,4 +4,4 @@ Proxy Host là điểm truy cập đầu vào cho một dịch vụ web mà bạ
Nó cung cấp khả năng kết thúc SSL (SSL termination) tùy chọn cho các dịch vụ vốn không hỗ trợ SSL tích hợp. Nó cung cấp khả năng kết thúc SSL (SSL termination) tùy chọn cho các dịch vụ vốn không hỗ trợ SSL tích hợp.
Proxy Host là loại cấu hình phổ biến nhất trong Nginx Proxy Manager. Proxy Host là loại cấu hình phổ biến nhất trong xGat3.

View file

@ -147,7 +147,7 @@
"defaultMessage": "Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията." "defaultMessage": "Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е Nginx Proxy Manager. Уверете се, че домейнът сочи към IP адреса, където работи NPM." "defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е xGat3. Уверете се, че домейнът сочи към IP адреса, където работи NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Неуспешна проверка поради грешка в комуникацията със site24x7.com." "defaultMessage": "Неуспешна проверка поради грешка в комуникацията със site24x7.com."

View file

@ -204,7 +204,7 @@
"defaultMessage": "Tato sekce vyžaduje znalost Certbotu a jeho DNS doplňků. Prosím, podívejte se do dokumentace příslušného doplňku." "defaultMessage": "Tato sekce vyžaduje znalost Certbotu a jeho DNS doplňků. Prosím, podívejte se do dokumentace příslušného doplňku."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Na této doméně byl nalezen server, ale nezdá se, že jde o Nginx Proxy Manager. Ujistěte se, že vaše doména směřuje na IP, kde běží vaše instance NPM." "defaultMessage": "Na této doméně byl nalezen server, ale nezdá se, že jde o xGat3. Ujistěte se, že vaše doména směřuje na IP, kde běží vaše instance NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Nepodařilo se ověřit dostupnost kvůli chybě komunikace se službou site24x7.com." "defaultMessage": "Nepodařilo se ověřit dostupnost kvůli chybě komunikace se službou site24x7.com."

View file

@ -132,7 +132,7 @@
"defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation." "defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Unter dieser Domain wurde ein Server gefunden, aber es scheint sich nicht um Nginx Proxy Manager zu handeln. Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird." "defaultMessage": "Unter dieser Domain wurde ein Server gefunden, aber es scheint sich nicht um xGat3 zu handeln. Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden." "defaultMessage": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden."

View file

@ -204,7 +204,7 @@
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation." "defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running." "defaultMessage": "There is a server found at this domain but it does not seem to be xGat3. Please make sure your domain points to the IP where your NPM instance is running."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com." "defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."

View file

@ -147,7 +147,7 @@
"defaultMessage": "Esta sección requiere algunos conocimientos sobre Certbot y sus plugins DNS. Consulta la documentación de los plugins respectivos." "defaultMessage": "Esta sección requiere algunos conocimientos sobre Certbot y sus plugins DNS. Consulta la documentación de los plugins respectivos."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Se encontró un servidor en este dominio pero no parece ser Nginx Proxy Manager. Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM." "defaultMessage": "Se encontró un servidor en este dominio pero no parece ser xGat3. Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com." "defaultMessage": "No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com."

View file

@ -204,7 +204,7 @@
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation." "defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running." "defaultMessage": "There is a server found at this domain but it does not seem to be xGat3. Please make sure your domain points to the IP where your NPM instance is running."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com." "defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."

View file

@ -132,7 +132,7 @@
"defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants." "defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de xGat3. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com." "defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com."

View file

@ -204,7 +204,7 @@
"defaultMessage": "Ez a szakasz némi ismeretet igényel a Certbot-ról és a DNS plugin-jeiről. Kérjük, olvassa el a megfelelő plugin dokumentációját." "defaultMessage": "Ez a szakasz némi ismeretet igényel a Certbot-ról és a DNS plugin-jeiről. Kérjük, olvassa el a megfelelő plugin dokumentációját."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Található szerver ezen a domain-en, de nem úgy tűnik, hogy Nginx Proxy Manager lenne. Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut." "defaultMessage": "Található szerver ezen a domain-en, de nem úgy tűnik, hogy xGat3 lenne. Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Az elérhetőség ellenőrzése sikertelen a site24x7.com kommunikációs hiba miatt." "defaultMessage": "Az elérhetőség ellenőrzése sikertelen a site24x7.com kommunikációs hiba miatt."

View file

@ -147,7 +147,7 @@
"defaultMessage": "Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait." "defaultMessage": "Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Ada server yang ditemukan pada domain ini tetapi tampaknya bukan Nginx Proxy Manager. Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan." "defaultMessage": "Ada server yang ditemukan pada domain ini tetapi tampaknya bukan xGat3. Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com." "defaultMessage": "Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com."

View file

@ -132,7 +132,7 @@
"defaultMessage": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin." "defaultMessage": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "È stato trovato un server su questo dominio, ma non sembra essere Nginx Proxy Manager. Assicurati che il dominio punti all'IP dove è in esecuzione NPM." "defaultMessage": "È stato trovato un server su questo dominio, ma non sembra essere xGat3. Assicurati che il dominio punti all'IP dove è in esecuzione NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com." "defaultMessage": "Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com."

View file

@ -132,7 +132,7 @@
"defaultMessage": "このセクションはCertbotとそのDNSプラグインの知識が必要です。各プラグインのドキュメントを参照してください。" "defaultMessage": "このセクションはCertbotとそのDNSプラグインの知識が必要です。各プラグインのドキュメントを参照してください。"
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "このドメインはNginx Proxy Managerではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。" "defaultMessage": "このドメインはxGat3ではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。"
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "site24x7.comへの接続でエラーが発生し、到達性チェックに失敗しました" "defaultMessage": "site24x7.comへの接続でエラーが発生し、到達性チェックに失敗しました"

View file

@ -147,7 +147,7 @@
"defaultMessage": "이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요." "defaultMessage": "이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "해당 도메인에서 서버가 탐지되었지만 Nginx Proxy Manager가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요." "defaultMessage": "해당 도메인에서 서버가 탐지되었지만 xGat3가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다." "defaultMessage": "site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다."

View file

@ -132,7 +132,7 @@
"defaultMessage": "Deze sectie vereist wat informatie over Certbot en zijn DNS plugins. Gebruik de documentatie van de bijbehorende plugins." "defaultMessage": "Deze sectie vereist wat informatie over Certbot en zijn DNS plugins. Gebruik de documentatie van de bijbehorende plugins."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Er is een server gevonden op deze domeinnaam, maar dat lijkt niet Nginx Proxy Manager te zijn. Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst." "defaultMessage": "Er is een server gevonden op deze domeinnaam, maar dat lijkt niet xGat3 te zijn. Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Bereikbaarheid kan niet worden bepaald door een communicatiefout met site24x7.com." "defaultMessage": "Bereikbaarheid kan niet worden bepaald door een communicatiefout met site24x7.com."

View file

@ -204,7 +204,7 @@
"defaultMessage": "Denne seksjonen krever noe kunnskap om Certbot og dets DNS-plugins. Vennligst konsulter dokumentasjonen for de respektive pluginene." "defaultMessage": "Denne seksjonen krever noe kunnskap om Certbot og dets DNS-plugins. Vennligst konsulter dokumentasjonen for de respektive pluginene."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Det finnes en server på dette domenet, men det ser ikke ut til å være Nginx Proxy Manager. Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører." "defaultMessage": "Det finnes en server på dette domenet, men det ser ikke ut til å være xGat3. Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Kunne ikke sjekke tilgjengeligheten på grunn av en kommunikasjonsfeil med site24x7.com." "defaultMessage": "Kunne ikke sjekke tilgjengeligheten på grunn av en kommunikasjonsfeil med site24x7.com."

View file

@ -138,7 +138,7 @@
"defaultMessage": "Ta sekcja wymaga pewnej wiedzy na temat Certbot i jego wtyczek DNS. Zapoznaj się z dokumentacją odpowiednich wtyczek." "defaultMessage": "Ta sekcja wymaga pewnej wiedzy na temat Certbot i jego wtyczek DNS. Zapoznaj się z dokumentacją odpowiednich wtyczek."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Znaleziono serwer pod tą domeną, ale nie wygląda na to, że jest to Nginx Proxy Manager. Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM." "defaultMessage": "Znaleziono serwer pod tą domeną, ale nie wygląda na to, że jest to xGat3. Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Nie udało się sprawdzić dostępności z powodu błędu komunikacji z site24x7.com." "defaultMessage": "Nie udało się sprawdzić dostępności z powodu błędu komunikacji z site24x7.com."

View file

@ -147,7 +147,7 @@
"defaultMessage": "Esta secção requer conhecimentos sobre o Certbot e os seus plugins DNS. Consulte a documentação dos plugins." "defaultMessage": "Esta secção requer conhecimentos sobre o Certbot e os seus plugins DNS. Consulte a documentação dos plugins."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Foi encontrado um servidor neste domínio, mas não parece ser o Nginx Proxy Manager. Certifique-se de que o domínio aponta para o IP onde a sua instância está a correr." "defaultMessage": "Foi encontrado um servidor neste domínio, mas não parece ser o xGat3. Certifique-se de que o domínio aponta para o IP onde a sua instância está a correr."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Falha ao verificar acessibilidade devido a um erro de comunicação com site24x7.com." "defaultMessage": "Falha ao verificar acessibilidade devido a um erro de comunicação com site24x7.com."

View file

@ -132,7 +132,7 @@
"defaultMessage": "Этот раздел требует знаний о Certbot и его DNS-плагинах. Пожалуйста, обратитесь к документации соответствующих плагинов." "defaultMessage": "Этот раздел требует знаний о Certbot и его DNS-плагинах. Пожалуйста, обратитесь к документации соответствующих плагинов."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "На этом домене найден сервер, но, похоже, это не Nginx Proxy Manager. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM." "defaultMessage": "На этом домене найден сервер, но, похоже, это не xGat3. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Не удалось проверить доступность из‑за ошибки связи с site24x7.com." "defaultMessage": "Не удалось проверить доступность из‑за ошибки связи с site24x7.com."

View file

@ -204,7 +204,7 @@
"defaultMessage": "Táto sekcia vyžaduje znalosť Certbotu a jeho DNS doplnkov. Prosím, pozrite si dokumentáciu príslušného doplnku." "defaultMessage": "Táto sekcia vyžaduje znalosť Certbotu a jeho DNS doplnkov. Prosím, pozrite si dokumentáciu príslušného doplnku."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Na tejto doméne bol nájdený server, ale nezdá sa, že ide o Nginx Proxy Manager. Uistite sa, že vaša doména smeruje na IP, kde beží vaša inštancia NPM." "defaultMessage": "Na tejto doméne bol nájdený server, ale nezdá sa, že ide o xGat3. Uistite sa, že vaša doména smeruje na IP, kde beží vaša inštancia NPM."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "Nepodarilo sa overiť dostupnosť kvôli chybe komunikácie so službou site24x7.com." "defaultMessage": "Nepodarilo sa overiť dostupnosť kvôli chybe komunikácie so službou site24x7.com."

View file

@ -147,7 +147,7 @@
"defaultMessage": "Bu bölüm Certbot ve DNS eklentileri hakkında bazı bilgiler gerektirir. Lütfen ilgili eklenti dokümantasyonuna bakın." "defaultMessage": "Bu bölüm Certbot ve DNS eklentileri hakkında bazı bilgiler gerektirir. Lütfen ilgili eklenti dokümantasyonuna bakın."
}, },
"certificates.http.reachability-404": { "certificates.http.reachability-404": {
"defaultMessage": "Bu alan adında bir sunucu bulundu ancak Nginx Proxy Manager gibi görünmüyor. Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun." "defaultMessage": "Bu alan adında bir sunucu bulundu ancak xGat3 gibi görünmüyor. Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun."
}, },
"certificates.http.reachability-failed-to-check": { "certificates.http.reachability-failed-to-check": {
"defaultMessage": "site24x7.com ile iletişim hatası nedeniyle erişilebilirlik kontrolü başarısız oldu." "defaultMessage": "site24x7.com ile iletişim hatası nedeniyle erişilebilirlik kontrolü başarısız oldu."

View file

@ -1,16 +1,34 @@
import EasyModal, { useModal } from "ez-modal-react"; import EasyModal, { useModal } from "ez-modal-react";
import { useState } from "react"; import { useState, useEffect } from "react";
import Modal from "react-bootstrap/Modal"; import Modal from "react-bootstrap/Modal";
import { Button } from "src/components"; import { Button } from "src/components";
import type { WgInterface } from "src/api/backend/wireguard";
const WireGuardClientModal = EasyModal.create(() => { interface WireGuardClientModalProps {
interfaces: WgInterface[];
defaultInterfaceId?: number;
}
const WireGuardClientModal = EasyModal.create(({ interfaces, defaultInterfaceId }: WireGuardClientModalProps) => {
const modal = useModal<any>(); const modal = useModal<any>();
const [name, setName] = useState(""); const [name, setName] = useState("");
const [selectedInterfaceId, setSelectedInterfaceId] = useState<number>(0);
useEffect(() => {
if (defaultInterfaceId) {
setSelectedInterfaceId(defaultInterfaceId);
} else if (interfaces && interfaces.length > 0) {
setSelectedInterfaceId(interfaces[0].id);
}
}, [interfaces, defaultInterfaceId]);
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (name.trim()) { if (name.trim() && selectedInterfaceId) {
modal.resolve({ name: name.trim() }); modal.resolve({
name: name.trim(),
interface_id: selectedInterfaceId
});
modal.hide(); modal.hide();
} }
}; };
@ -31,7 +49,7 @@ const WireGuardClientModal = EasyModal.create(() => {
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="wg-client-name" className="form-label"> <label htmlFor="wg-client-name" className="form-label required">
Client Name Client Name
</label> </label>
<input <input
@ -48,12 +66,37 @@ const WireGuardClientModal = EasyModal.create(() => {
A friendly name to identify this client. A friendly name to identify this client.
</div> </div>
</div> </div>
{interfaces && interfaces.length > 0 && (
<div className="mb-3">
<label htmlFor="wg-server-select" className="form-label required">
WireGuard Server
</label>
<select
className="form-select"
id="wg-server-select"
value={selectedInterfaceId}
onChange={(e) => setSelectedInterfaceId(Number(e.target.value))}
required
>
{interfaces.map(iface => (
<option key={iface.id} value={iface.id}>
{iface.name} ({iface.ipv4Cidr})
</option>
))}
</select>
<div className="form-text">
Select which server this client will connect to.
</div>
</div>
)}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button data-bs-dismiss="modal" onClick={handleClose}> <Button data-bs-dismiss="modal" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button type="submit" className="ms-auto btn-primary" disabled={!name.trim()}> <Button type="submit" className="ms-auto btn-primary" disabled={!name.trim() || !selectedInterfaceId}>
Create Client Create Client
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View file

@ -0,0 +1,109 @@
import {
IconDeviceFloppy,
IconX,
} from "@tabler/icons-react";
import { useState, useEffect } from "react";
import type { WgInterface } from "src/api/backend/wireguard";
interface WireGuardLinkedServersModalProps {
wgInterface: WgInterface;
allInterfaces: WgInterface[];
onHide?: () => void;
resolve?: (data: { linked_servers: number[] }) => void;
}
function WireGuardLinkedServersModal({ wgInterface, allInterfaces, onHide, resolve }: WireGuardLinkedServersModalProps) {
// A map or set to manage checked status easily
const [selected, setSelected] = useState<Set<number>>(new Set());
useEffect(() => {
if (wgInterface && wgInterface.linkedServers) {
setSelected(new Set(wgInterface.linkedServers));
}
}, [wgInterface]);
const toggleServer = (id: number) => {
const newSelected = new Set(selected);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
setSelected(newSelected);
};
const onSave = () => {
if (resolve) {
resolve({ linked_servers: Array.from(selected) });
}
if (onHide) {
onHide();
}
};
const availableServers = allInterfaces.filter(i => i.id !== wgInterface.id);
return (
<div className="modal modal-blur fade show d-block" tabIndex={-1}>
<div className="modal-dialog modal-dialog-centered" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">
Linked Servers for {wgInterface.name}
</h5>
<button
type="button"
className="btn-close"
onClick={onHide}
aria-label="Close"
></button>
</div>
<div className="modal-body">
<p className="text-muted mb-3">
Select the WireGuard servers that clients from <strong>{wgInterface.name}</strong> will be allowed to communicate with.
</p>
{availableServers.length === 0 ? (
<div className="alert alert-info border-0 mb-0">
There are no other servers available to link to.
</div>
) : (
<div className="list-group list-group-flush mb-3">
{availableServers.map(server => (
<label key={server.id} className="list-group-item d-flex align-items-center cursor-pointer px-0">
<input
className="form-check-input mt-0 me-3"
type="checkbox"
checked={selected.has(server.id)}
onChange={() => toggleServer(server.id)}
/>
<div className="flex-grow-1">
<div><strong>{server.name}</strong></div>
<div className="text-muted small border border-light border-1 bg-light rounded mt-1 p-1 d-inline-block">
Subnet: {server.ipv4Cidr}
</div>
</div>
</label>
))}
</div>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-link link-secondary me-auto"
onClick={onHide}
>
<IconX size={16} className="me-1" /> Cancel
</button>
<button type="button" className="btn btn-primary" onClick={onSave} disabled={availableServers.length === 0 && selected.size === 0}>
<IconDeviceFloppy size={16} className="me-1" /> Save Links
</button>
</div>
</div>
</div>
</div>
);
}
export default WireGuardLinkedServersModal;

View file

@ -0,0 +1,138 @@
import {
IconDeviceFloppy,
IconX,
} from "@tabler/icons-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
interface WireGuardServerModalProps {
wgInterface?: any;
onHide?: () => void;
resolve?: (data: any) => void;
}
const WG_DEFAULT_MTU = 1420;
const WG_DEFAULT_DNS = "1.1.1.1, 8.8.8.8";
function WireGuardServerModal({ wgInterface, onHide, resolve }: WireGuardServerModalProps) {
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm({
defaultValues: {
host: "",
dns: WG_DEFAULT_DNS,
mtu: WG_DEFAULT_MTU,
isolate_clients: false,
},
});
useEffect(() => {
if (wgInterface) {
setValue("host", wgInterface.host || "");
setValue("dns", wgInterface.dns || WG_DEFAULT_DNS);
setValue("mtu", wgInterface.mtu || WG_DEFAULT_MTU);
setValue("isolate_clients", wgInterface.isolateClients || false);
}
}, [wgInterface, setValue]);
const onSubmit = (data: any) => {
// Convert number types appropriately
const submitData = {
...data,
mtu: Number(data.mtu) || WG_DEFAULT_MTU,
};
if (resolve) {
resolve(submitData);
}
if (onHide) {
onHide();
}
};
return (
<div className="modal modal-blur fade show d-block" tabIndex={-1}>
<div className="modal-dialog modal-dialog-centered" role="document">
<form className="modal-content" onSubmit={handleSubmit(onSubmit)}>
<div className="modal-header">
<h5 className="modal-title">
{wgInterface ? `Edit Server: ${wgInterface.name}` : "New WireGuard Server"}
</h5>
<button
type="button"
className="btn-close"
onClick={onHide}
aria-label="Close"
></button>
</div>
<div className="modal-body">
<div className="mb-3">
<label className="form-label required">Host / Endpoint IP</label>
<input
type="text"
className={`form-control ${errors.host ? "is-invalid" : ""}`}
placeholder="e.g., 203.0.113.1"
{...register("host", { required: "Host IP is required" })}
/>
{errors.host && (
<div className="invalid-feedback">{errors.host.message as string}</div>
)}
<small className="form-hint">
The public IP or hostname domain that clients will use to connect.
</small>
</div>
<div className="mb-3">
<label className="form-label">Client DNS Servers</label>
<input
type="text"
className="form-control"
placeholder={WG_DEFAULT_DNS}
{...register("dns")}
/>
<small className="form-hint">Comma separated list. Assigned to clients.</small>
</div>
<div className="mb-3">
<label className="form-label">MTU</label>
<input
type="number"
className="form-control"
placeholder={WG_DEFAULT_MTU.toString()}
{...register("mtu")}
/>
</div>
<div className="mb-3">
<label className="form-label">Client Isolation</label>
<label className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
{...register("isolate_clients")}
/>
<span className="form-check-label">Prevent clients on this server from communicating with each other</span>
</label>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-link link-secondary me-auto"
onClick={onHide}
>
<IconX size={16} className="me-1" /> Cancel
</button>
<button type="submit" className="btn btn-primary">
<IconDeviceFloppy size={16} className="me-1" /> Save
</button>
</div>
</form>
</div>
</div>
);
}
export default WireGuardServerModal;

View file

@ -185,7 +185,7 @@ export default function Login() {
<img <img
className={styles.logo} className={styles.logo}
src="/images/logo-text-horizontal-grey.png" src="/images/logo-text-horizontal-grey.png"
alt="Nginx Proxy Manager" alt="xGat3"
/> />
<div className="d-flex align-items-center gap-1"> <div className="d-flex align-items-center gap-1">
<LocalePicker /> <LocalePicker />

View file

@ -69,7 +69,7 @@ export default function Setup() {
<img <img
className={styles.logo} className={styles.logo}
src="/images/logo-text-horizontal-grey.png" src="/images/logo-text-horizontal-grey.png"
alt="Nginx Proxy Manager" alt="xGat3"
/> />
</div> </div>
<div className="card card-md"> <div className="card card-md">

View file

@ -6,6 +6,9 @@ import {
IconPlayerPause, IconPlayerPause,
IconTrash, IconTrash,
IconNetwork, IconNetwork,
IconServer,
IconEdit,
IconLink,
} 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";
@ -13,12 +16,18 @@ import { downloadWgConfig } from "src/api/backend/wireguard";
import { Loading } from "src/components"; import { Loading } from "src/components";
import { import {
useWgClients, useWgClients,
useWgInterface, useWgInterfaces,
useCreateWgInterface,
useUpdateWgInterface,
useDeleteWgInterface,
useUpdateWgInterfaceLinks,
useCreateWgClient, useCreateWgClient,
useDeleteWgClient, useDeleteWgClient,
useToggleWgClient, useToggleWgClient,
} from "src/hooks/useWireGuard"; } from "src/hooks/useWireGuard";
import WireGuardClientModal from "src/modals/WireGuardClientModal"; import WireGuardClientModal from "src/modals/WireGuardClientModal";
import WireGuardServerModal from "src/modals/WireGuardServerModal";
import WireGuardLinkedServersModal from "src/modals/WireGuardLinkedServersModal";
import WireGuardQRModal from "src/modals/WireGuardQRModal"; import WireGuardQRModal from "src/modals/WireGuardQRModal";
function formatBytes(bytes: number | null): string { function formatBytes(bytes: number | null): string {
@ -39,38 +48,78 @@ function timeAgo(date: string | null): string {
} }
function WireGuard() { function WireGuard() {
const [activeTab, setActiveTab] = useState<"servers" | "clients">("clients");
const { data: clients, isLoading: clientsLoading } = useWgClients(); const { data: clients, isLoading: clientsLoading } = useWgClients();
const { data: wgInterface, isLoading: ifaceLoading } = useWgInterface(); const { data: interfaces, isLoading: ifacesLoading } = useWgInterfaces();
const createServer = useCreateWgInterface();
const updateServer = useUpdateWgInterface();
const deleteServer = useDeleteWgInterface();
const updateLinks = useUpdateWgInterfaceLinks();
const createClient = useCreateWgClient(); const createClient = useCreateWgClient();
const deleteClient = useDeleteWgClient(); const deleteClient = useDeleteWgClient();
const toggleClient = useToggleWgClient(); const toggleClient = useToggleWgClient();
const [filter, setFilter] = useState("");
if (clientsLoading || ifaceLoading) { const [clientFilter, setClientFilter] = useState("");
if (clientsLoading || ifacesLoading) {
return <Loading />; return <Loading />;
} }
const filteredClients = clients?.filter( const filteredClients = clients?.filter(
(c) => (c) =>
!filter || !clientFilter ||
c.name.toLowerCase().includes(filter.toLowerCase()) || c.name.toLowerCase().includes(clientFilter.toLowerCase()) ||
c.ipv4Address.includes(filter), c.ipv4Address.includes(clientFilter) ||
c.interfaceName?.toLowerCase().includes(clientFilter.toLowerCase()),
); );
const handleNewClient = async () => { // Server Handlers
const result = (await EasyModal.show(WireGuardClientModal)) as any; const handleNewServer = async () => {
if (result && result.name) { const result = (await EasyModal.show(WireGuardServerModal)) as any;
createClient.mutate({ name: result.name }); if (result) {
createServer.mutate(result);
} }
}; };
const handleDelete = async (id: number, name: string) => { const handleEditServer = async (wgInterface: any) => {
const result = (await EasyModal.show(WireGuardServerModal, { wgInterface })) as any;
if (result) {
updateServer.mutate({ id: wgInterface.id, data: result });
}
};
const handleManageLinks = async (wgInterface: any) => {
if (!interfaces) return;
const result = (await EasyModal.show(WireGuardLinkedServersModal, { wgInterface, allInterfaces: interfaces })) as any;
if (result) {
updateLinks.mutate({ id: wgInterface.id, data: result });
}
};
const handleDeleteServer = async (id: number, name: string) => {
if (window.confirm(`Are you absolutely sure you want to delete server "${name}"? This will also delete all associated clients and peering links.`)) {
deleteServer.mutate(id);
}
};
// Client Handlers
const handleNewClient = async () => {
const result = (await EasyModal.show(WireGuardClientModal, { interfaces: interfaces || [] })) as any;
if (result && result.name && result.interface_id) {
createClient.mutate({ name: result.name, interface_id: result.interface_id });
}
};
const handleDeleteClient = async (id: number, name: string) => {
if (window.confirm(`Are you sure you want to delete client "${name}"?`)) { if (window.confirm(`Are you sure you want to delete client "${name}"?`)) {
deleteClient.mutate(id); deleteClient.mutate(id);
} }
}; };
const handleToggle = (id: number, currentlyEnabled: boolean) => { const handleToggleClient = (id: number, currentlyEnabled: boolean) => {
toggleClient.mutate({ id, enabled: !currentlyEnabled }); toggleClient.mutate({ id, enabled: !currentlyEnabled });
}; };
@ -85,205 +134,274 @@ function WireGuard() {
return ( return (
<div className="container-xl"> <div className="container-xl">
{/* Interface Info Card */} {/* Page Header */}
<div className="page-header d-print-none"> <div className="page-header d-print-none">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col-auto"> <div className="col">
<h2 className="page-title"> <h2 className="page-title">
<IconNetwork className="me-2" size={28} /> <IconNetwork className="me-2" size={28} />
WireGuard VPN WireGuard VPN
</h2> </h2>
</div> </div>
<div className="col-auto ms-auto d-print-none">
<div className="btn-list">
{activeTab === "servers" ? (
<button
type="button"
className="btn btn-primary d-none d-sm-inline-block"
onClick={handleNewServer}
>
<IconPlus size={16} className="me-1" />
New Server
</button>
) : (
<button
type="button"
className="btn btn-primary d-none d-sm-inline-block"
onClick={handleNewClient}
id="wg-new-client-btn"
>
<IconPlus size={16} className="me-1" />
New Client
</button>
)}
</div>
</div>
</div> </div>
</div> </div>
{wgInterface && ( {/* Tabs */}
<div className="card mb-3">
<div className="card-body">
<div className="row">
<div className="col-md-3">
<div className="mb-2">
<span className="text-muted small">Interface</span>
<div className="fw-bold">{wgInterface.name}</div>
</div>
</div>
<div className="col-md-3">
<div className="mb-2">
<span className="text-muted small">Public Key</span>
<div
className="fw-bold text-truncate"
title={wgInterface.publicKey}
style={{ maxWidth: 200 }}
>
{wgInterface.publicKey}
</div>
</div>
</div>
<div className="col-md-2">
<div className="mb-2">
<span className="text-muted small">Address</span>
<div className="fw-bold">{wgInterface.ipv4Cidr}</div>
</div>
</div>
<div className="col-md-2">
<div className="mb-2">
<span className="text-muted small">Port</span>
<div className="fw-bold">{wgInterface.listenPort}</div>
</div>
</div>
<div className="col-md-2">
<div className="mb-2">
<span className="text-muted small">DNS</span>
<div className="fw-bold">{wgInterface.dns}</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Clients Card */}
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
<div className="row align-items-center w-100"> <ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs">
<div className="col"> <li className="nav-item cursor-pointer">
<h3 className="card-title"> <a className={`nav-link ${activeTab === "clients" ? "active" : ""}`} onClick={() => setActiveTab("clients")}>
Clients ({clients?.length || 0}) <IconNetwork className="me-1" size={16}/> Clients
</h3> <span className="badge bg-green ms-2">{clients?.length || 0}</span>
</div> </a>
<div className="col-auto"> </li>
<li className="nav-item cursor-pointer">
<a className={`nav-link ${activeTab === "servers" ? "active" : ""}`} onClick={() => setActiveTab("servers")}>
<IconServer className="me-1" size={16}/> Servers
<span className="badge bg-blue ms-2">{interfaces?.length || 0}</span>
</a>
</li>
</ul>
</div>
{/* Tab Content */}
{activeTab === "clients" && (
<div className="table-responsive">
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
<h3 className="card-title mb-0">Clients</h3>
<input <input
type="text" type="text"
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Filter..." placeholder="Filter clients..."
value={filter} value={clientFilter}
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setClientFilter(e.target.value)}
style={{ width: 200 }} style={{ width: 250 }}
/> />
</div> </div>
<div className="col-auto"> <table className="table table-vcenter table-nowrap card-table">
<button <thead>
type="button" <tr>
className="btn btn-primary btn-sm" <th>Status</th>
onClick={handleNewClient} <th>Name</th>
id="wg-new-client-btn" <th>Server</th>
> <th>IP Address</th>
<IconPlus size={16} className="me-1" /> <th>Last Handshake</th>
New Client <th>Transfer / </th>
</button> <th className="text-end">Actions</th>
</div> </tr>
</div> </thead>
</div> <tbody>
<div className="table-responsive"> {filteredClients?.map((client) => {
<table className="table table-vcenter card-table"> const isConnected =
<thead> client.latestHandshakeAt &&
<tr> Date.now() - new Date(client.latestHandshakeAt).getTime() <
<th>Status</th> 3 * 60 * 1000;
<th>Name</th> return (
<th>IP Address</th> <tr key={client.id}>
<th>Last Handshake</th> <td>
<th>Transfer </th> <span
<th>Transfer </th> className={`badge ${
<th className="text-end">Actions</th> !client.enabled
</tr> ? "bg-secondary"
</thead> : isConnected
<tbody> ? "bg-success"
{filteredClients?.map((client) => { : "bg-warning"
const isConnected = }`}
client.latestHandshakeAt && >
Date.now() - new Date(client.latestHandshakeAt).getTime() < {!client.enabled
3 * 60 * 1000; ? "Disabled"
return (
<tr key={client.id}>
<td>
<span
className={`badge ${
!client.enabled
? "bg-secondary"
: isConnected : isConnected
? "bg-success" ? "Connected"
: "bg-warning" : "Idle"}
}`} </span>
> </td>
{!client.enabled <td className="fw-bold">{client.name}</td>
? "Disabled" <td>
: isConnected <div className="text-muted">{client.interfaceName}</div>
? "Connected" </td>
: "Idle"} <td>
</span> <code>{client.ipv4Address}</code>
</td>
<td>{timeAgo(client.latestHandshakeAt)}</td>
<td>
<div className="d-flex flex-column text-muted small">
<span> {formatBytes(client.transferRx)}</span>
<span> {formatBytes(client.transferTx)}</span>
</div>
</td>
<td className="text-end">
<div className="btn-group btn-group-sm">
<button
type="button"
className="btn btn-outline-primary"
title="QR Code"
onClick={() =>
handleQR(client.id, client.name)
}
>
<IconQrcode size={16} />
</button>
<button
type="button"
className="btn btn-outline-primary"
title="Download Config"
onClick={() =>
handleDownload(client.id, client.name)
}
>
<IconDownload size={16} />
</button>
<button
type="button"
className={`btn ${client.enabled ? "btn-outline-warning" : "btn-outline-success"}`}
title={
client.enabled ? "Disable" : "Enable"
}
onClick={() =>
handleToggleClient(client.id, client.enabled)
}
>
{client.enabled ? (
<IconPlayerPause size={16} />
) : (
<IconPlayerPlay size={16} />
)}
</button>
<button
type="button"
className="btn btn-outline-danger"
title="Delete"
onClick={() =>
handleDeleteClient(client.id, client.name)
}
>
<IconTrash size={16} />
</button>
</div>
</td>
</tr>
);
})}
{(!filteredClients || filteredClients.length === 0) && (
<tr>
<td colSpan={7} className="text-center text-muted py-5">
{clientFilter
? "No clients match your filter"
: "No WireGuard clients yet. Click 'New Client' to create one."}
</td> </td>
<td className="fw-bold">{client.name}</td> </tr>
)}
</tbody>
</table>
</div>
)}
{activeTab === "servers" && (
<div className="table-responsive">
<table className="table table-vcenter table-nowrap card-table">
<thead>
<tr>
<th>Interface</th>
<th>Subnet</th>
<th>Port</th>
<th>Endpoint Host</th>
<th>Isolation</th>
<th>Links</th>
<th className="text-end">Actions</th>
</tr>
</thead>
<tbody>
{interfaces?.map((iface) => (
<tr key={iface.id}>
<td className="fw-bold">{iface.name}</td>
<td> <td>
<code>{client.ipv4Address}</code> <code>{iface.ipv4Cidr}</code>
</td>
<td>{iface.listenPort}</td>
<td className="text-muted">{iface.host || "None"}</td>
<td>
{iface.isolateClients ? (
<span className="badge bg-green text-green-fg">Enabled</span>
) : (
<span className="badge bg-secondary text-secondary-fg">Disabled</span>
)}
</td>
<td>
<div className="d-flex align-items-center">
<span className="badge bg-azure me-2">{iface.linkedServers?.length || 0}</span>
{iface.linkedServers?.length > 0 && (
<span className="text-muted small">
({interfaces.filter(i => iface.linkedServers.includes(i.id)).map(i => i.name).join(", ")})
</span>
)}
</div>
</td> </td>
<td>{timeAgo(client.latestHandshakeAt)}</td>
<td>{formatBytes(client.transferRx)}</td>
<td>{formatBytes(client.transferTx)}</td>
<td className="text-end"> <td className="text-end">
<div className="btn-group btn-group-sm"> <div className="btn-group btn-group-sm">
<button <button
type="button" type="button"
className="btn btn-outline-primary" className="btn btn-outline-primary"
title="QR Code" title="Linked Servers"
onClick={() => onClick={() => handleManageLinks(iface)}
handleQR(client.id, client.name)
}
> >
<IconQrcode size={16} /> <IconLink size={16} />
</button> </button>
<button <button
type="button" type="button"
className="btn btn-outline-primary" className="btn btn-outline-primary"
title="Download Config" title="Edit Server"
onClick={() => onClick={() => handleEditServer(iface)}
handleDownload(client.id, client.name)
}
> >
<IconDownload size={16} /> <IconEdit size={16} />
</button>
<button
type="button"
className={`btn ${client.enabled ? "btn-outline-warning" : "btn-outline-success"}`}
title={
client.enabled ? "Disable" : "Enable"
}
onClick={() =>
handleToggle(client.id, client.enabled)
}
>
{client.enabled ? (
<IconPlayerPause size={16} />
) : (
<IconPlayerPlay size={16} />
)}
</button> </button>
<button <button
type="button" type="button"
className="btn btn-outline-danger" className="btn btn-outline-danger"
title="Delete" title="Delete Server"
onClick={() => onClick={() => handleDeleteServer(iface.id, iface.name)}
handleDelete(client.id, client.name)
}
> >
<IconTrash size={16} /> <IconTrash size={16} />
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
); ))}
})} {(!interfaces || interfaces.length === 0) && (
{(!filteredClients || filteredClients.length === 0) && ( <tr>
<tr> <td colSpan={7} className="text-center text-muted py-5">
<td colSpan={7} className="text-center text-muted py-4"> No WireGuard servers configured.
{filter </td>
? "No clients match your filter" </tr>
: "No WireGuard clients yet. Click 'New Client' to create one."} )}
</td> </tbody>
</tr> </table>
)} </div>
</tbody> )}
</table>
</div>
</div> </div>
</div> </div>
); );

View file

@ -2101,6 +2101,11 @@ react-fast-compare@^2.0.1:
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-hook-form@^7.71.2:
version "7.71.2"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz"
integrity sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==
react-intl@^8.1.3: react-intl@^8.1.3:
version "8.1.3" version "8.1.3"
resolved "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz" resolved "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz"
@ -2198,7 +2203,7 @@ react-transition-group@^4.3.0, react-transition-group@^4.4.5:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react@^18 || ^19", "react@^18.0.0 || ^19.0.0", react@^19.2.4, "react@>= 16", react@>=0.14.0, react@>=15.0.0, react@>=16.14.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.9.0, react@>=18, react@>16.8.0, react@19: "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react@^18 || ^19", "react@^18.0.0 || ^19.0.0", react@^19.2.4, "react@>= 16", react@>=0.14.0, react@>=15.0.0, react@>=16.14.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.9.0, react@>=18, react@>16.8.0, react@19:
version "19.2.4" version "19.2.4"
resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz" resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz"
integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
@ -2748,11 +2753,6 @@ yaml@^1.10.0:
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.4.2:
version "2.8.2"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz"
integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==
zwitch@^2.0.0, zwitch@^2.0.4: zwitch@^2.0.0, zwitch@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"

View file

@ -3,7 +3,7 @@ set -e
# ============================================================ # ============================================================
# D3V-NPMWG Installer for Ubuntu/Debian # D3V-NPMWG Installer for Ubuntu/Debian
# Nginx Proxy Manager + WireGuard VPN # xGat3 + WireGuard VPN
# https://github.com/xtcnet/D3V-NPMWG # https://github.com/xtcnet/D3V-NPMWG
# ============================================================ # ============================================================

View file

@ -87,7 +87,7 @@ describe('OAuth with Authentik', () => {
// // we should be logged in // // we should be logged in
// cy.get('#root p.chakra-text') // cy.get('#root p.chakra-text')
// .first() // .first()
// .should('have.text', 'Nginx Proxy Manager'); // .should('have.text', 'xGat3');
// // logout: // // logout:
// cy.clearLocalStorage(); // cy.clearLocalStorage();