feat: implement wireguard multi-server UI and backend logic
This commit is contained in:
parent
5119f84558
commit
54d1623551
69 changed files with 7238 additions and 826 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
It contains:
|
||||
|
|
|
|||
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -27,8 +27,8 @@ Are you in the right place?
|
|||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
|
||||
**Nginx Proxy Manager Version**
|
||||
<!-- What version of Nginx Proxy Manager is reported on the login page? -->
|
||||
**xGat3 Version**
|
||||
<!-- What version of xGat3 is reported on the login page? -->
|
||||
|
||||
|
||||
**To Reproduce**
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# AI Context for NPM-WG Project
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
### Nginx Proxy Manager
|
||||
### xGat3
|
||||
- 🌐 Reverse proxy management with a beautiful UI
|
||||
- 🔒 Free SSL certificates via Let's Encrypt
|
||||
- 🔀 Proxy hosts, redirection hosts, streams, and 404 hosts
|
||||
|
|
@ -170,7 +170,7 @@ Alternatively, you can run the helper script:
|
|||
|
||||
## 📜 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
|
||||
|
||||
## 📄 License
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ let instance = null;
|
|||
const generateDbConfig = () => {
|
||||
if (!configHas("database")) {
|
||||
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/",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import errs from "../lib/error.js";
|
|||
import authModel from "../models/auth.js";
|
||||
import internalUser from "./user.js";
|
||||
|
||||
const APP_NAME = "Nginx Proxy Manager";
|
||||
const APP_NAME = "xGat3";
|
||||
const BACKUP_CODE_COUNT = 8;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,31 +22,71 @@ const internalWireguard = {
|
|||
async getOrCreateInterface(knex) {
|
||||
let iface = await knex("wg_interface").first();
|
||||
if (!iface) {
|
||||
// Generate keys
|
||||
const privateKey = await wgHelpers.generatePrivateKey();
|
||||
const publicKey = await wgHelpers.getPublicKey(privateKey);
|
||||
|
||||
const [id] = await knex("wg_interface").insert({
|
||||
name: WG_INTERFACE_NAME,
|
||||
private_key: privateKey,
|
||||
public_key: publicKey,
|
||||
ipv4_cidr: WG_DEFAULT_ADDRESS,
|
||||
listen_port: WG_DEFAULT_PORT,
|
||||
mtu: WG_DEFAULT_MTU,
|
||||
// Seed a default config if it doesn't exist
|
||||
const insertData = {
|
||||
name: "wg0",
|
||||
listen_port: 51820,
|
||||
ipv4_cidr: "10.0.0.1/24",
|
||||
ipv6_cidr: null,
|
||||
mtu: 1420,
|
||||
dns: WG_DEFAULT_DNS,
|
||||
host: WG_HOST,
|
||||
post_up: `iptables -A FORWARD -i %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_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 -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE",
|
||||
created_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();
|
||||
logger.info("WireGuard interface created with new keypair");
|
||||
logger.info("WireGuard interface created with default config");
|
||||
}
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
66
backend/migrations/20260308000000_wireguard_multi_server.js
Normal file
66
backend/migrations/20260308000000_wireguard_multi_server.js
Normal 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
5783
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,7 @@ const router = express.Router({
|
|||
* GET /api/wireguard
|
||||
* Get WireGuard interface info
|
||||
*/
|
||||
router.get("/", async (req, res, next) => {
|
||||
router.get("/", async (_req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const iface = await internalWireguard.getInterfaceInfo(knex);
|
||||
|
|
@ -26,7 +26,7 @@ router.get("/", async (req, res, next) => {
|
|||
* GET /api/wireguard/client
|
||||
* List all WireGuard clients with live status
|
||||
*/
|
||||
router.get("/client", async (req, res, next) => {
|
||||
router.get("/client", async (_req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const clients = await internalWireguard.getClients(knex);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Nginx Proxy Manager API",
|
||||
"title": "xGat3 API",
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
1112
backend/yarn.lock
1112
backend/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -77,6 +77,6 @@ ENTRYPOINT [ "/init" ]
|
|||
LABEL org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.license="MIT" \
|
||||
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.cmd="docker run --rm -ti --cap-add=NET_ADMIN --cap-add=SYS_MODULE d3v-npmwg:latest"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { defineConfig, type DefaultTheme } from 'vitepress';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
|
||||
export default defineConfig({
|
||||
title: "Nginx Proxy Manager",
|
||||
title: "xGat3",
|
||||
description: "Expose your services easily and securely",
|
||||
head: [
|
||||
["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", { 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:type", content: "website" }],
|
||||
["meta", { property: "og:url", content: "https://nginxproxymanager.com/" }],
|
||||
["meta", { property: "og:image", content: "https://nginxproxymanager.com/icon.png" }],
|
||||
["meta", { property: "og:url", content: "https://x.d3v.ac/" }],
|
||||
["meta", { property: "og:image", content: "https://x.d3v.ac/icon.png" }],
|
||||
["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:image", content: "https://nginxproxymanager.com/icon.png"}],
|
||||
["meta", { name: "twitter:alt", content: "Nginx Proxy Manager"}],
|
||||
["meta", { name: "twitter:image", content: "https://x.d3v.ac/icon.png"}],
|
||||
["meta", { name: "twitter:alt", content: "xGat3"}],
|
||||
// GA
|
||||
['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');"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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)
|
||||
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
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ services:
|
|||
- ./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,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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" />
|
||||
<link rel="preload" href="/images/logo-no-text.svg" as="image" type="image/svg+xml" fetchPriority="high">
|
||||
<link
|
||||
|
|
|
|||
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
|
|
@ -25,6 +25,7 @@
|
|||
"react": "^19.2.4",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-intl": "^8.1.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
@ -4904,6 +4905,22 @@
|
|||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
|
||||
"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": {
|
||||
"version": "8.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz",
|
||||
|
|
@ -6210,22 +6227,6 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"react": "^19.2.4",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-intl": "^8.1.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export interface WgClient {
|
|||
createdOn: string;
|
||||
updatedOn: string;
|
||||
expiresAt: string | null;
|
||||
interfaceId: number;
|
||||
interfaceName: string;
|
||||
latestHandshakeAt: string | null;
|
||||
endpoint: string | null;
|
||||
transferRx: number;
|
||||
|
|
@ -26,17 +28,35 @@ export interface WgInterface {
|
|||
mtu: number;
|
||||
dns: string;
|
||||
host: string;
|
||||
isolateClients: boolean;
|
||||
linkedServers: number[];
|
||||
}
|
||||
|
||||
export async function getWgClients(): Promise<WgClient[]> {
|
||||
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" });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function SiteHeader() {
|
|||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
Nginx Proxy Manager
|
||||
xGat3
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getWgClients,
|
||||
getWgInterface,
|
||||
getWgInterfaces,
|
||||
createWgInterface,
|
||||
updateWgInterface,
|
||||
deleteWgInterface,
|
||||
updateWgInterfaceLinks,
|
||||
createWgClient,
|
||||
deleteWgClient,
|
||||
enableWgClient,
|
||||
|
|
@ -20,19 +24,62 @@ export const useWgClients = (options = {}) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useWgInterface = (options = {}) => {
|
||||
return useQuery<WgInterface, Error>({
|
||||
queryKey: ["wg-interface"],
|
||||
queryFn: getWgInterface,
|
||||
staleTime: 60 * 1000,
|
||||
export const useWgInterfaces = (options = {}) => {
|
||||
return useQuery<WgInterface[], Error>({
|
||||
queryKey: ["wg-interfaces"],
|
||||
queryFn: getWgInterfaces,
|
||||
refetchInterval: 10000,
|
||||
staleTime: 5000,
|
||||
...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 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name: string }) => createWgClient(data),
|
||||
mutationFn: (data: { name: string; interface_id?: number }) => createWgClient(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["wg-clients"] });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL.
|
||||
|
||||
Прокси хостовете са най-често използваната функция в Nginx Proxy Manager.
|
||||
Прокси хостовете са най-често използваната функция в xGat3.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
Proxy-Hosts sind die häufigste Verwendung für den Nginx Proxy Manager.
|
||||
Proxy-Hosts sind die häufigste Verwendung für den xGat3.
|
||||
|
|
@ -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.
|
||||
|
||||
Proxy Hosts are the most common use for the Nginx Proxy Manager.
|
||||
Proxy Hosts are the most common use for the xGat3.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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 Proxy adalah penggunaan paling umum untuk Nginx Proxy Manager.
|
||||
Host Proxy adalah penggunaan paling umum untuk xGat3.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
Gli host proxy sono l'uso più comune per Nginx Proxy Manager.
|
||||
Gli host proxy sono l'uso più comune per xGat3.
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
サービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。
|
||||
|
||||
プロキシホストはNginx Proxy Managerのもっとも一般的な使用方法です。
|
||||
プロキシホストはxGat3のもっとも一般的な使用方法です。
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다.
|
||||
|
||||
이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.
|
||||
이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 xGat3가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.
|
||||
|
||||
다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.**
|
||||
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다.
|
||||
|
||||
프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다.
|
||||
프록시 호스트는 xGat3에서 가장 일반적으로 사용되는 기능입니다.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
Proxy Hosts worden het meest gebruikt in Nginx Proxy Manager.
|
||||
Proxy Hosts worden het meest gebruikt in xGat3.
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ En Proxy‑host er inngangspunktet (innkommende endepunkt) for en webtjeneste du
|
|||
|
||||
Den tilbyr valgfri SSL‑terminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL.
|
||||
|
||||
Proxy‑hosts er den vanligste bruken av Nginx Proxy Manager.
|
||||
Proxy‑hosts er den vanligste bruken av xGat3.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
Hosty proxy są najpopularniejszym zastosowaniem Nginx Proxy Manager
|
||||
Hosty proxy są najpopularniejszym zastosowaniem xGat3
|
||||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
Он может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL.
|
||||
|
||||
Прокси‑хосты — самый распространённый сценарий использования Nginx Proxy Manager.
|
||||
Прокси‑хосты — самый распространённый сценарий использования xGat3.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
Proxy hostitelia sú najbežnejším použitím pre Nginx Proxy Manager.
|
||||
Proxy hostitelia sú najbežnejším použitím pre xGat3.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
"defaultMessage": "Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията."
|
||||
},
|
||||
"certificates.http.reachability-404": {
|
||||
"defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е Nginx Proxy Manager. Уверете се, че домейнът сочи към IP адреса, където работи NPM."
|
||||
"defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е xGat3. Уверете се, че домейнът сочи към IP адреса, където работи NPM."
|
||||
},
|
||||
"certificates.http.reachability-failed-to-check": {
|
||||
"defaultMessage": "Неуспешна проверка поради грешка в комуникацията със site24x7.com."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Nepodařilo se ověřit dostupnost kvůli chybě komunikace se službou site24x7.com."
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden."
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com."
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
"defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Az elérhetőség ellenőrzése sikertelen a site24x7.com kommunikációs hiba miatt."
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
"defaultMessage": "Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com."
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultMessage": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com."
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultMessage": "このセクションはCertbotとそのDNSプラグインの知識が必要です。各プラグインのドキュメントを参照してください。"
|
||||
},
|
||||
"certificates.http.reachability-404": {
|
||||
"defaultMessage": "このドメインはNginx Proxy Managerではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。"
|
||||
"defaultMessage": "このドメインはxGat3ではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。"
|
||||
},
|
||||
"certificates.http.reachability-failed-to-check": {
|
||||
"defaultMessage": "site24x7.comへの接続でエラーが発生し、到達性チェックに失敗しました"
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
"defaultMessage": "이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요."
|
||||
},
|
||||
"certificates.http.reachability-404": {
|
||||
"defaultMessage": "해당 도메인에서 서버가 탐지되었지만 Nginx Proxy Manager가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요."
|
||||
"defaultMessage": "해당 도메인에서 서버가 탐지되었지만 xGat3가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요."
|
||||
},
|
||||
"certificates.http.reachability-failed-to-check": {
|
||||
"defaultMessage": "site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다."
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultMessage": "Deze sectie vereist wat informatie over Certbot en zijn DNS plugins. Gebruik de documentatie van de bijbehorende plugins."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Bereikbaarheid kan niet worden bepaald door een communicatiefout met site24x7.com."
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
"defaultMessage": "Denne seksjonen krever noe kunnskap om Certbot og dets DNS-plugins. Vennligst konsulter dokumentasjonen for de respektive pluginene."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Kunne ikke sjekke tilgjengeligheten på grunn av en kommunikasjonsfeil med site24x7.com."
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@
|
|||
"defaultMessage": "Ta sekcja wymaga pewnej wiedzy na temat Certbot i jego wtyczek DNS. Zapoznaj się z dokumentacją odpowiednich wtyczek."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Nie udało się sprawdzić dostępności z powodu błędu komunikacji z site24x7.com."
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
"defaultMessage": "Esta secção requer conhecimentos sobre o Certbot e os seus plugins DNS. Consulte a documentação dos plugins."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Falha ao verificar acessibilidade devido a um erro de comunicação com site24x7.com."
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultMessage": "Этот раздел требует знаний о Certbot и его DNS-плагинах. Пожалуйста, обратитесь к документации соответствующих плагинов."
|
||||
},
|
||||
"certificates.http.reachability-404": {
|
||||
"defaultMessage": "На этом домене найден сервер, но, похоже, это не Nginx Proxy Manager. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM."
|
||||
"defaultMessage": "На этом домене найден сервер, но, похоже, это не xGat3. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM."
|
||||
},
|
||||
"certificates.http.reachability-failed-to-check": {
|
||||
"defaultMessage": "Не удалось проверить доступность из‑за ошибки связи с site24x7.com."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Nepodarilo sa overiť dostupnosť kvôli chybe komunikácie so službou site24x7.com."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "site24x7.com ile iletişim hatası nedeniyle erişilebilirlik kontrolü başarısız oldu."
|
||||
|
|
|
|||
|
|
@ -1,16 +1,34 @@
|
|||
import EasyModal, { useModal } from "ez-modal-react";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
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 [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) => {
|
||||
e.preventDefault();
|
||||
if (name.trim()) {
|
||||
modal.resolve({ name: name.trim() });
|
||||
if (name.trim() && selectedInterfaceId) {
|
||||
modal.resolve({
|
||||
name: name.trim(),
|
||||
interface_id: selectedInterfaceId
|
||||
});
|
||||
modal.hide();
|
||||
}
|
||||
};
|
||||
|
|
@ -31,7 +49,7 @@ const WireGuardClientModal = EasyModal.create(() => {
|
|||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="wg-client-name" className="form-label">
|
||||
<label htmlFor="wg-client-name" className="form-label required">
|
||||
Client Name
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -48,12 +66,37 @@ const WireGuardClientModal = EasyModal.create(() => {
|
|||
A friendly name to identify this client.
|
||||
</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.Footer>
|
||||
<Button data-bs-dismiss="modal" onClick={handleClose}>
|
||||
Cancel
|
||||
</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
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
|
|
|||
109
frontend/src/modals/WireGuardLinkedServersModal.tsx
Normal file
109
frontend/src/modals/WireGuardLinkedServersModal.tsx
Normal 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;
|
||||
138
frontend/src/modals/WireGuardServerModal.tsx
Normal file
138
frontend/src/modals/WireGuardServerModal.tsx
Normal 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;
|
||||
|
|
@ -185,7 +185,7 @@ export default function Login() {
|
|||
<img
|
||||
className={styles.logo}
|
||||
src="/images/logo-text-horizontal-grey.png"
|
||||
alt="Nginx Proxy Manager"
|
||||
alt="xGat3"
|
||||
/>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<LocalePicker />
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default function Setup() {
|
|||
<img
|
||||
className={styles.logo}
|
||||
src="/images/logo-text-horizontal-grey.png"
|
||||
alt="Nginx Proxy Manager"
|
||||
alt="xGat3"
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-md">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import {
|
|||
IconPlayerPause,
|
||||
IconTrash,
|
||||
IconNetwork,
|
||||
IconServer,
|
||||
IconEdit,
|
||||
IconLink,
|
||||
} from "@tabler/icons-react";
|
||||
import EasyModal from "ez-modal-react";
|
||||
import { useState } from "react";
|
||||
|
|
@ -13,12 +16,18 @@ import { downloadWgConfig } from "src/api/backend/wireguard";
|
|||
import { Loading } from "src/components";
|
||||
import {
|
||||
useWgClients,
|
||||
useWgInterface,
|
||||
useWgInterfaces,
|
||||
useCreateWgInterface,
|
||||
useUpdateWgInterface,
|
||||
useDeleteWgInterface,
|
||||
useUpdateWgInterfaceLinks,
|
||||
useCreateWgClient,
|
||||
useDeleteWgClient,
|
||||
useToggleWgClient,
|
||||
} from "src/hooks/useWireGuard";
|
||||
import WireGuardClientModal from "src/modals/WireGuardClientModal";
|
||||
import WireGuardServerModal from "src/modals/WireGuardServerModal";
|
||||
import WireGuardLinkedServersModal from "src/modals/WireGuardLinkedServersModal";
|
||||
import WireGuardQRModal from "src/modals/WireGuardQRModal";
|
||||
|
||||
function formatBytes(bytes: number | null): string {
|
||||
|
|
@ -39,38 +48,78 @@ function timeAgo(date: string | null): string {
|
|||
}
|
||||
|
||||
function WireGuard() {
|
||||
const [activeTab, setActiveTab] = useState<"servers" | "clients">("clients");
|
||||
|
||||
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 deleteClient = useDeleteWgClient();
|
||||
const toggleClient = useToggleWgClient();
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const [clientFilter, setClientFilter] = useState("");
|
||||
|
||||
if (clientsLoading || ifaceLoading) {
|
||||
if (clientsLoading || ifacesLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const filteredClients = clients?.filter(
|
||||
(c) =>
|
||||
!filter ||
|
||||
c.name.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
c.ipv4Address.includes(filter),
|
||||
!clientFilter ||
|
||||
c.name.toLowerCase().includes(clientFilter.toLowerCase()) ||
|
||||
c.ipv4Address.includes(clientFilter) ||
|
||||
c.interfaceName?.toLowerCase().includes(clientFilter.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleNewClient = async () => {
|
||||
const result = (await EasyModal.show(WireGuardClientModal)) as any;
|
||||
if (result && result.name) {
|
||||
createClient.mutate({ name: result.name });
|
||||
// Server Handlers
|
||||
const handleNewServer = async () => {
|
||||
const result = (await EasyModal.show(WireGuardServerModal)) as any;
|
||||
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}"?`)) {
|
||||
deleteClient.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = (id: number, currentlyEnabled: boolean) => {
|
||||
const handleToggleClient = (id: number, currentlyEnabled: boolean) => {
|
||||
toggleClient.mutate({ id, enabled: !currentlyEnabled });
|
||||
};
|
||||
|
||||
|
|
@ -85,205 +134,274 @@ function WireGuard() {
|
|||
|
||||
return (
|
||||
<div className="container-xl">
|
||||
{/* Interface Info Card */}
|
||||
{/* Page Header */}
|
||||
<div className="page-header d-print-none">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<div className="col">
|
||||
<h2 className="page-title">
|
||||
<IconNetwork className="me-2" size={28} />
|
||||
WireGuard VPN
|
||||
</h2>
|
||||
</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>
|
||||
|
||||
{wgInterface && (
|
||||
<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 */}
|
||||
{/* Tabs */}
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="row align-items-center w-100">
|
||||
<div className="col">
|
||||
<h3 className="card-title">
|
||||
Clients ({clients?.length || 0})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs">
|
||||
<li className="nav-item cursor-pointer">
|
||||
<a className={`nav-link ${activeTab === "clients" ? "active" : ""}`} onClick={() => setActiveTab("clients")}>
|
||||
<IconNetwork className="me-1" size={16}/> Clients
|
||||
<span className="badge bg-green ms-2">{clients?.length || 0}</span>
|
||||
</a>
|
||||
</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
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Filter..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
placeholder="Filter clients..."
|
||||
value={clientFilter}
|
||||
onChange={(e) => setClientFilter(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={handleNewClient}
|
||||
id="wg-new-client-btn"
|
||||
>
|
||||
<IconPlus size={16} className="me-1" />
|
||||
New Client
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Name</th>
|
||||
<th>IP Address</th>
|
||||
<th>Last Handshake</th>
|
||||
<th>Transfer ↓</th>
|
||||
<th>Transfer ↑</th>
|
||||
<th className="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredClients?.map((client) => {
|
||||
const isConnected =
|
||||
client.latestHandshakeAt &&
|
||||
Date.now() - new Date(client.latestHandshakeAt).getTime() <
|
||||
3 * 60 * 1000;
|
||||
return (
|
||||
<tr key={client.id}>
|
||||
<td>
|
||||
<span
|
||||
className={`badge ${
|
||||
!client.enabled
|
||||
? "bg-secondary"
|
||||
<table className="table table-vcenter table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Name</th>
|
||||
<th>Server</th>
|
||||
<th>IP Address</th>
|
||||
<th>Last Handshake</th>
|
||||
<th>Transfer ↓ / ↑</th>
|
||||
<th className="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredClients?.map((client) => {
|
||||
const isConnected =
|
||||
client.latestHandshakeAt &&
|
||||
Date.now() - new Date(client.latestHandshakeAt).getTime() <
|
||||
3 * 60 * 1000;
|
||||
return (
|
||||
<tr key={client.id}>
|
||||
<td>
|
||||
<span
|
||||
className={`badge ${
|
||||
!client.enabled
|
||||
? "bg-secondary"
|
||||
: isConnected
|
||||
? "bg-success"
|
||||
: "bg-warning"
|
||||
}`}
|
||||
>
|
||||
{!client.enabled
|
||||
? "Disabled"
|
||||
: isConnected
|
||||
? "bg-success"
|
||||
: "bg-warning"
|
||||
}`}
|
||||
>
|
||||
{!client.enabled
|
||||
? "Disabled"
|
||||
: isConnected
|
||||
? "Connected"
|
||||
: "Idle"}
|
||||
</span>
|
||||
? "Connected"
|
||||
: "Idle"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="fw-bold">{client.name}</td>
|
||||
<td>
|
||||
<div className="text-muted">{client.interfaceName}</div>
|
||||
</td>
|
||||
<td>
|
||||
<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 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>
|
||||
<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>{timeAgo(client.latestHandshakeAt)}</td>
|
||||
<td>{formatBytes(client.transferRx)}</td>
|
||||
<td>{formatBytes(client.transferTx)}</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)
|
||||
}
|
||||
title="Linked Servers"
|
||||
onClick={() => handleManageLinks(iface)}
|
||||
>
|
||||
<IconQrcode size={16} />
|
||||
<IconLink size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary"
|
||||
title="Download Config"
|
||||
onClick={() =>
|
||||
handleDownload(client.id, client.name)
|
||||
}
|
||||
title="Edit Server"
|
||||
onClick={() => handleEditServer(iface)}
|
||||
>
|
||||
<IconDownload 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} />
|
||||
)}
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger"
|
||||
title="Delete"
|
||||
onClick={() =>
|
||||
handleDelete(client.id, client.name)
|
||||
}
|
||||
title="Delete Server"
|
||||
onClick={() => handleDeleteServer(iface.id, iface.name)}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{(!filteredClients || filteredClients.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center text-muted py-4">
|
||||
{filter
|
||||
? "No clients match your filter"
|
||||
: "No WireGuard clients yet. Click 'New Client' to create one."}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
{(!interfaces || interfaces.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center text-muted py-5">
|
||||
No WireGuard servers configured.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
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:
|
||||
version "8.1.3"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz"
|
||||
integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
|
||||
|
|
@ -2748,11 +2753,6 @@ yaml@^1.10.0:
|
|||
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
|
||||
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:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ set -e
|
|||
|
||||
# ============================================================
|
||||
# D3V-NPMWG Installer for Ubuntu/Debian
|
||||
# Nginx Proxy Manager + WireGuard VPN
|
||||
# xGat3 + WireGuard VPN
|
||||
# https://github.com/xtcnet/D3V-NPMWG
|
||||
# ============================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ describe('OAuth with Authentik', () => {
|
|||
// // we should be logged in
|
||||
// cy.get('#root p.chakra-text')
|
||||
// .first()
|
||||
// .should('have.text', 'Nginx Proxy Manager');
|
||||
// .should('have.text', 'xGat3');
|
||||
|
||||
// // logout:
|
||||
// cy.clearLocalStorage();
|
||||
|
|
|
|||
Loading…
Reference in a new issue