Compare commits
74 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9068151e9e | |||
| a6724b1575 | |||
| 8383eb5101 | |||
| 87c4a3c357 | |||
| 752d80e11c | |||
| 62593d8316 | |||
| fd7398be9f | |||
| 14c4e1ee5c | |||
| 680c9261f6 | |||
| 554130afbb | |||
| 547360d0e3 | |||
| 1ac4c5e1db | |||
| adf738bc33 | |||
| 9b5152d81f | |||
| 44f0a080ff | |||
| 9929d77326 | |||
| 91fe419821 | |||
| 144eb9e5d5 | |||
| d73582e75d | |||
| 9a5325a38d | |||
| e9367b535d | |||
|
|
23f197aeb1 | ||
|
|
89c9ed842f | ||
|
|
2345f10b21 | ||
|
|
376d27367c | ||
|
|
9536c8a75a | ||
|
|
4ec63f0fe8 | ||
|
|
c8801b97c6 | ||
|
|
71f5477db3 | ||
|
|
b8d64b150c | ||
|
|
4c0d3952cb | ||
|
|
91e493d81f | ||
|
|
649d252a0f | ||
|
|
4369b1a3e4 | ||
|
|
50dff1712e | ||
|
|
6ef729eb45 | ||
|
|
2e9ed07708 | ||
|
|
04a22dcc7d | ||
|
|
ce0d4f7611 | ||
|
|
6c3122d03d | ||
|
|
dd525adaef | ||
|
|
1b97b8b0ad | ||
|
|
b49bcf90cb | ||
|
|
5d65eafc65 | ||
|
|
fd8baf878c | ||
|
|
08ce4b8390 | ||
|
|
335490ac06 | ||
|
|
090894021a | ||
|
|
b77da8e6de | ||
|
|
66dc95bc6b | ||
|
|
787e3bb243 | ||
|
|
bd04298843 | ||
|
|
e057aee8ba | ||
|
|
b99b623355 | ||
|
|
3f0d529d14 | ||
|
|
d67081492d | ||
|
|
497482aef3 | ||
|
|
e48fef3154 | ||
|
|
34020bc562 | ||
|
|
7bf175da41 | ||
|
|
2cbaab23c5 | ||
|
|
9eeb3f7c7d | ||
|
|
a0edaccfc4 | ||
|
|
af5cfbea84 | ||
|
|
f5323ce8fa | ||
|
|
8c91886de6 | ||
|
|
ec55362d15 | ||
|
|
f8ad3fe807 | ||
|
|
f9d687c131 | ||
|
|
dd8dd605f1 | ||
|
|
3960d6025f | ||
|
|
5f4acb755e | ||
|
|
36acc3ea65 | ||
|
|
54d1623551 |
121 changed files with 13086 additions and 1620 deletions
1
.agents/skills/spawn-agent
Submodule
1
.agents/skills/spawn-agent
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 288a767d8b251004f7bd7999dcdf408cbbaa86c7
|
||||
64
.agents/workflows/init.md
Normal file
64
.agents/workflows/init.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
description: Initialize agent context for the D3V-Server project
|
||||
---
|
||||
|
||||
## /init Workflow
|
||||
|
||||
Use this workflow at the start of a new session.
|
||||
Goal: understand the repo quickly without scanning the full codebase.
|
||||
|
||||
### Required first step
|
||||
|
||||
1. Read `D:\AntiGravity\D3V-Server\AI_CONTEXT.md`
|
||||
|
||||
Do not start with a broad recursive scan unless the user explicitly asks for a repo-wide audit.
|
||||
|
||||
### Minimal orientation sequence
|
||||
|
||||
After reading `AI_CONTEXT.md`, open only these files:
|
||||
|
||||
1. `D:\AntiGravity\D3V-Server\README.md`
|
||||
2. `D:\AntiGravity\D3V-Server\backend\index.js`
|
||||
3. `D:\AntiGravity\D3V-Server\backend\app.js`
|
||||
4. `D:\AntiGravity\D3V-Server\backend\routes\main.js`
|
||||
5. `D:\AntiGravity\D3V-Server\frontend\src\main.tsx`
|
||||
6. `D:\AntiGravity\D3V-Server\frontend\src\App.tsx`
|
||||
7. `D:\AntiGravity\D3V-Server\frontend\src\Router.tsx`
|
||||
8. `D:\AntiGravity\D3V-Server\docker\Dockerfile`
|
||||
|
||||
That set is normally enough to understand:
|
||||
|
||||
- top-level architecture
|
||||
- backend startup flow
|
||||
- frontend startup flow
|
||||
- route structure
|
||||
- production packaging model
|
||||
|
||||
### Area-specific follow-up
|
||||
|
||||
Only then open files for the requested area:
|
||||
|
||||
- Backend/API task: inspect `backend/routes/`, `backend/internal/`, `backend/models/`
|
||||
- Frontend/UI task: inspect `frontend/src/pages/`, `frontend/src/hooks/`, `frontend/src/api/`
|
||||
- WireGuard task: inspect `backend/internal/wireguard.js`, `backend/routes/wireguard.js`, `backend/routes/wg_public.js`, `frontend/src/pages/WireGuard/`
|
||||
- Build/deploy task: inspect `docker/`, `scripts/`, `install.sh`
|
||||
- Docs task: inspect `docs/`
|
||||
- Test task: inspect `test/`
|
||||
|
||||
### Important reminders
|
||||
|
||||
- This is a multi-package repo. Commands run in subdirectories, not from one obvious root workspace.
|
||||
- Docker packaging is part of the product behavior, not just deployment.
|
||||
- The frontend must be built before the production Docker image can be built.
|
||||
- Some runtime behavior depends on env vars, mounted volumes, and container capabilities.
|
||||
- Avoid rewriting upstream-shaped structure unless the task requires it.
|
||||
|
||||
### Ready message
|
||||
|
||||
When initialization is complete, summarize the project in 3-5 lines:
|
||||
|
||||
- what the app is
|
||||
- backend entry
|
||||
- frontend entry
|
||||
- production assembly path
|
||||
- the specific area relevant to the user's task
|
||||
|
|
@ -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
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
frontend/node_modules/
|
||||
backend/node_modules/
|
||||
dist/
|
||||
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**
|
||||
|
|
|
|||
38
.github/workflows/docker-publish.yml
vendored
38
.github/workflows/docker-publish.yml
vendored
|
|
@ -4,11 +4,21 @@ on:
|
|||
push:
|
||||
branches: [ "master" ]
|
||||
tags: [ 'v*.*.*' ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'docker/**'
|
||||
- '.github/workflows/docker-publish.yml'
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'docker/**'
|
||||
- '.github/workflows/docker-publish.yml'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
REGISTRY: src.d3v.ac
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
|
|
@ -23,9 +33,6 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -33,12 +40,21 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: frontend/yarn.lock
|
||||
|
||||
- name: Install Frontend Dependencies & Build
|
||||
run: |
|
||||
total_ram_mb=$(awk '/MemTotal/ { printf "%d", $2/1024 }' /proc/meminfo)
|
||||
if [ "$total_ram_mb" -lt 2048 ]; then
|
||||
node_mem=768
|
||||
elif [ "$total_ram_mb" -lt 4096 ]; then
|
||||
node_mem=1536
|
||||
else
|
||||
node_mem=3072
|
||||
fi
|
||||
echo "RAM: ${total_ram_mb}MB — setting Node.js max to ${node_mem}MB"
|
||||
export NODE_OPTIONS="--max-old-space-size=${node_mem}"
|
||||
cd frontend
|
||||
npm install -g yarn
|
||||
yarn install --frozen-lockfile
|
||||
yarn locale-compile
|
||||
yarn build
|
||||
|
|
@ -49,7 +65,7 @@ jobs:
|
|||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
|
|
@ -61,6 +77,10 @@ jobs:
|
|||
type=semver,pattern={{version}}
|
||||
type=sha
|
||||
|
||||
- name: Lowercase IMAGE_NAME
|
||||
run: |
|
||||
echo "IMAGE_NAME_LOWER=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
|
|
@ -70,5 +90,5 @@ jobs:
|
|||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:buildcache
|
||||
cache-to: ${{ github.event_name != 'pull_request' && format('type=registry,ref={0}/{1}:buildcache,mode=max', env.REGISTRY, env.IMAGE_NAME_LOWER) || '' }}
|
||||
|
|
|
|||
360
AI_CONTEXT.md
360
AI_CONTEXT.md
|
|
@ -1,64 +1,328 @@
|
|||
# AI Context for NPM-WG Project
|
||||
# AI Context for D3V-Server
|
||||
|
||||
## 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`.
|
||||
This file is the fast-start map for AI agents working in this repository.
|
||||
Read this first. Do not scan the entire repo unless the task actually requires it.
|
||||
|
||||
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.
|
||||
## 1. What this project is
|
||||
|
||||
---
|
||||
`D3V-Server` is a custom fork of Nginx Proxy Manager / xGat3 with an added
|
||||
WireGuard management module and some project-specific admin features.
|
||||
|
||||
## 2. Technology Stack
|
||||
- **Backend**: Node.js, Express.js, Knex (Query Builder), SQLite/MySQL/PostgreSQL. Uses ES Modules (`"type": "module"`).
|
||||
- **Frontend**: React 18, TypeScript, Vite, React Router, React Bootstrap (`ez-modal-react`), Formik, React Query (`@tanstack/react-query`).
|
||||
- **Container**: Alpine Linux with `s6-overlay` for service process management.
|
||||
Canonical repository:
|
||||
|
||||
---
|
||||
- `https://src.d3v.ac/d3v/D3V-Server`
|
||||
|
||||
## 3. WireGuard Integration Architecture
|
||||
Treat that repository as the source of truth for this project.
|
||||
If another upstream is mentioned, assume it is a historical ancestor or reference,
|
||||
not the primary project remote, unless the task says otherwise.
|
||||
|
||||
### Core Idea
|
||||
WireGuard functionality is disabled by default and enabled via the `WG_ENABLED` environment variable. The system uses a Node.js cron wrapper to manipulate the WireGuard `wg` and `wg-quick` CLI tools directly. It leverages Docker volume mapping (`/etc/wireguard`) to maintain state.
|
||||
At a high level:
|
||||
|
||||
### Backend Map (Node.js)
|
||||
If you need to edit WireGuard logic, check these files:
|
||||
- **`backend/lib/wg-helpers.js`**: Shell wrappers for `wg` CLI (create keys, parse CIDR, parse `wg show` dumps, gen configurations).
|
||||
- **`backend/internal/wireguard.js`**: Core business logic. Manages interface start/stop, adding/removing clients, IP allocation, and token expiration checking via cron.
|
||||
- **`backend/routes/wireguard.js`**: REST APIs exposing CRUD operations to the frontend. Note: Handlers use ES module export functions syntax.
|
||||
- **`backend/routes/main.js`**: Mounts the `/api/wireguard` routes.
|
||||
- **`backend/index.js`**: Contains the startup hook `internalWireguard.startup(knex)` and graceful SIGTERM shutdown hooks.
|
||||
- **`backend/migrations/20260307000000_wireguard.js`**: Knex schema initialization for tables `wg_interface` and `wg_client`. *Note: Must use ES Module `export function up()` instead of `exports.up`!*
|
||||
- `backend/` is the API server and system orchestration layer.
|
||||
- `frontend/` is the admin web UI.
|
||||
- `docker/` is the production runtime assembly.
|
||||
- `test/` contains Cypress and API-spec checks.
|
||||
- `docs/` is a separate VitePress docs site.
|
||||
|
||||
### Frontend Map (React)
|
||||
If you need to edit the UI/UX, check these files:
|
||||
- **`frontend/src/api/backend/wireguard.ts`**: API fetch helper definitions.
|
||||
- **`frontend/src/hooks/useWireGuard.ts`**: `@tanstack/react-query` data fetchers and mutators.
|
||||
- **`frontend/src/pages/WireGuard/index.tsx`**: Main UI Page rendering the interface stats and clients table.
|
||||
- **`frontend/src/modals/WireGuardClientModal.tsx`**: Form to create a new client. *Note: Modal built explicitly over `react-bootstrap/Modal` to prevent backdrop freezing issues.*
|
||||
- **`frontend/src/modals/WireGuardQRModal.tsx`**: Generates and parses QR codes.
|
||||
- **`frontend/src/Router.tsx` & `SiteMenu.tsx`**: Routing and UI Navigation injection points for WireGuard UI.
|
||||
The production app is not "just backend" or "just frontend":
|
||||
Docker assembles the backend and the prebuilt frontend into one image.
|
||||
|
||||
---
|
||||
## 2. Repo map
|
||||
|
||||
## 4. Build & Deployment Gotchas
|
||||
```text
|
||||
D3V-Server/
|
||||
|- backend/ Node.js + Express API, DB, WireGuard logic
|
||||
| |- routes/ REST endpoints
|
||||
| |- internal/ background/system services
|
||||
| |- models/ Objection models
|
||||
| |- migrations/ Knex migrations
|
||||
| |- schema/ API schema compilation
|
||||
| |- templates/ config/template generation
|
||||
| |- app.js Express app wiring
|
||||
| `- index.js backend startup entry
|
||||
|- frontend/ React + TypeScript + Vite SPA
|
||||
| |- src/
|
||||
| | |- pages/ top-level screens
|
||||
| | |- components/ shared UI
|
||||
| | |- hooks/ data and UI hooks
|
||||
| | |- api/ backend API wrappers
|
||||
| | |- modals/ modal flows
|
||||
| | |- context/ auth/theme/locale providers
|
||||
| | |- Router.tsx route table
|
||||
| | `- main.tsx frontend entry
|
||||
| `- vite.config.ts dev/build config
|
||||
|- docker/ production image and runtime scripts
|
||||
|- docs/ VitePress docs site
|
||||
|- test/ Cypress E2E and swagger lint
|
||||
|- scripts/ helper scripts
|
||||
|- install.sh install/update/reset automation for deployment
|
||||
`- README.md user-facing overview
|
||||
```
|
||||
|
||||
### Line Endings (CRLF vs LF)
|
||||
- **CRITICAL**: All files in `docker/rootfs` and `docker/scripts` are used by `s6-overlay` inside Alpine Linux. **They MUST be formatted using UNIX Line Endings (LF)**. If you download this repository on Windows, ensure the git config does not automatically convert text files to `CRLF`, otherwise container booting will crash with `s6-rc-compile: fatal: invalid type: must be oneshot, longrun, or bundle`.
|
||||
## 3. Runtime architecture
|
||||
|
||||
### Compilation Steps
|
||||
- The React Frontend **MUST** be pre-built before Docker can build.
|
||||
- You must run `yarn install`, `yarn locale-compile`, and `yarn build` inside the `frontend/` directory before `docker build`.
|
||||
- Use the script `./scripts/build-project.sh` to execute the full pipeline if you have a bash environment.
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Browser"] --> B["Frontend SPA<br/>frontend/src/main.tsx"]
|
||||
B --> C["React Router + pages<br/>frontend/src/Router.tsx"]
|
||||
C --> D["Backend API<br/>backend/app.js"]
|
||||
D --> E["Route handlers<br/>backend/routes/*"]
|
||||
E --> F["Models / DB<br/>Knex + Objection"]
|
||||
D --> G["Internal services<br/>backend/internal/*"]
|
||||
G --> H["WireGuard CLI / system state"]
|
||||
I["docker/Dockerfile"] --> D
|
||||
I --> B
|
||||
```
|
||||
|
||||
### Docker Config Requirements
|
||||
- **Required capabilities**: `--cap-add=NET_ADMIN` and `--cap-add=SYS_MODULE` are required for WireGuard to manipulate interfaces.
|
||||
- **Sysctls**: `--sysctl net.ipv4.ip_forward=1` must be applied to the container.
|
||||
- **Volumes**: Volume `/etc/letsencrypt` is severely required by original NPM core.
|
||||
## 4. First files to open for orientation
|
||||
|
||||
---
|
||||
If a new session needs project understanding, open these files in order:
|
||||
|
||||
## 5. Agent Instructions
|
||||
If you are an AI reading this file:
|
||||
1. Treat existing NPM-specific code as sacred. Do not modify global `.ts` hooks or Knex config unless instructed.
|
||||
2. If fixing a bug in the Frontend, use `useWgClients()` / `useInterfaceStatus()` standard hooks. Use React-Bootstrap `Modal` instead of raw div class names.
|
||||
3. If changing the DB, create a new `backend/migrations/*.js` file in ES Module format.
|
||||
4. When testing out scripts, remember that the docker container requires port mapping to 51820/udp.
|
||||
1. `AI_CONTEXT.md`
|
||||
2. `README.md`
|
||||
3. `backend/index.js`
|
||||
4. `backend/app.js`
|
||||
5. `backend/routes/main.js`
|
||||
6. `frontend/src/main.tsx`
|
||||
7. `frontend/src/App.tsx`
|
||||
8. `frontend/src/Router.tsx`
|
||||
9. `docker/Dockerfile`
|
||||
10. the relevant package manifest for the area being changed
|
||||
|
||||
That is usually enough to understand the system without scanning everything.
|
||||
|
||||
## 5. How the backend starts
|
||||
|
||||
Main entry: `backend/index.js`
|
||||
|
||||
Startup flow:
|
||||
|
||||
1. Run DB migrations via `migrateUp()`
|
||||
2. Run setup/bootstrap via `setup()`
|
||||
3. Compile/load schema via `getCompiledSchema()`
|
||||
4. Optionally fetch IP ranges
|
||||
5. Start timers for internal certificate/IP-range jobs
|
||||
6. If `WG_ENABLED != false`, start WireGuard service hooks
|
||||
7. Start Express server on port `3000`
|
||||
|
||||
Important implication:
|
||||
|
||||
- Many issues that look like "API bugs" can actually come from startup/setup/migration state.
|
||||
- WireGuard startup failure is logged, but the backend may continue without WireGuard functionality.
|
||||
|
||||
## 6. How the backend is organized
|
||||
|
||||
Core files:
|
||||
|
||||
- `backend/index.js`: process startup and shutdown
|
||||
- `backend/app.js`: Express middleware, helmet, rate limit, auth, route mounting
|
||||
- `backend/routes/main.js`: root API router
|
||||
- `backend/db.js`: DB handle creation
|
||||
- `backend/lib/config.js`: database/config/env resolution
|
||||
- `backend/setup.js`: instance bootstrap/setup flow
|
||||
|
||||
Important route groups from `backend/routes/main.js`:
|
||||
|
||||
- `/api/schema`
|
||||
- `/api/tokens`
|
||||
- `/api/users`
|
||||
- `/api/audit-log`
|
||||
- `/api/reports`
|
||||
- `/api/settings`
|
||||
- `/api/version`
|
||||
- `/api/nginx/proxy-hosts`
|
||||
- `/api/nginx/redirection-hosts`
|
||||
- `/api/nginx/dead-hosts`
|
||||
- `/api/nginx/streams`
|
||||
- `/api/nginx/access-lists`
|
||||
- `/api/nginx/certificates`
|
||||
- `/api/wireguard`
|
||||
- `/api/database`
|
||||
|
||||
Public WireGuard portal:
|
||||
|
||||
- `backend/app.js` mounts `wg_public` before JWT middleware at `/wg-public`
|
||||
- frontend has a dedicated route branch for `/wg-public`
|
||||
|
||||
## 7. How the frontend is organized
|
||||
|
||||
Entry path:
|
||||
|
||||
- `frontend/src/main.tsx` -> `frontend/src/App.tsx` -> `frontend/src/Router.tsx`
|
||||
|
||||
Frontend routing model:
|
||||
|
||||
- `Router.tsx` checks health/setup/auth before rendering the main app
|
||||
- `/wg-public/*` bypasses the standard authenticated admin shell
|
||||
- most admin pages are lazy-loaded from `frontend/src/pages/*`
|
||||
|
||||
Useful places by task:
|
||||
|
||||
- New screen or route: `frontend/src/Router.tsx` and `frontend/src/pages/`
|
||||
- Backend API bindings: `frontend/src/api/`
|
||||
- Server state/query logic: `frontend/src/hooks/`
|
||||
- Shared app state: `frontend/src/context/`
|
||||
- Reusable UI: `frontend/src/components/`
|
||||
- Modal workflows: `frontend/src/modals/`
|
||||
|
||||
## 8. Build, test, lint commands
|
||||
|
||||
This repo is multi-package. There is no obvious root workspace command.
|
||||
Run commands in the relevant subdirectory.
|
||||
|
||||
### Backend
|
||||
|
||||
Working directory: `backend/`
|
||||
|
||||
- `yarn lint`
|
||||
- `yarn prettier`
|
||||
- `yarn validate-schema`
|
||||
- `yarn regenerate-config`
|
||||
- likely local start: `node index.js`
|
||||
|
||||
### Frontend
|
||||
|
||||
Working directory: `frontend/`
|
||||
|
||||
- `yarn dev`
|
||||
- `yarn build`
|
||||
- `yarn lint`
|
||||
- `yarn preview`
|
||||
- `yarn test`
|
||||
- `yarn locale-extract`
|
||||
- `yarn locale-compile`
|
||||
- `yarn locale-sort`
|
||||
|
||||
### Docs
|
||||
|
||||
Working directory: `docs/`
|
||||
|
||||
- `yarn dev`
|
||||
- `yarn build`
|
||||
- `yarn preview`
|
||||
|
||||
### Test
|
||||
|
||||
Working directory: `test/`
|
||||
|
||||
- `yarn cypress`
|
||||
- `yarn cypress:headless`
|
||||
- `yarn cypress:dev`
|
||||
- `yarn swagger-lint`
|
||||
|
||||
### Full build
|
||||
|
||||
The production Docker build expects the frontend to be built first.
|
||||
|
||||
Common flow:
|
||||
|
||||
1. build frontend in `frontend/`
|
||||
2. build image with `docker/Dockerfile`
|
||||
|
||||
Helper script:
|
||||
|
||||
- `scripts/build-project.sh`
|
||||
|
||||
## 9. Key technologies
|
||||
|
||||
- Backend: Node.js, Express 5, Knex, Objection, AJV
|
||||
- Databases: SQLite by default, MySQL/Postgres supported
|
||||
- Frontend: React, TypeScript, Vite, React Router, TanStack Query, Tabler UI
|
||||
- Testing: Vitest for frontend, Cypress for E2E
|
||||
- Packaging/runtime: Docker, nginx, s6 overlay
|
||||
- System integration: WireGuard CLI tools inside the container/runtime
|
||||
|
||||
## 10. Configuration model
|
||||
|
||||
The backend resolves configuration in this order:
|
||||
|
||||
1. JSON config file from `NODE_CONFIG_DIR` / `config/*.json`
|
||||
2. environment variables
|
||||
3. SQLite fallback using `/data/database.sqlite`
|
||||
|
||||
Important file:
|
||||
|
||||
- `backend/lib/config.js`
|
||||
|
||||
Notes:
|
||||
|
||||
- JWT keys are persisted to `/data/keys.json`
|
||||
- default DB fallback is SQLite
|
||||
- production behavior often depends on mounted volumes and env vars, not just source code
|
||||
|
||||
## 11. WireGuard-specific map
|
||||
|
||||
When the task is specifically about WireGuard, start here:
|
||||
|
||||
- `backend/internal/wireguard.js`
|
||||
- `backend/routes/wireguard.js`
|
||||
- `backend/routes/wg_public.js`
|
||||
- `frontend/src/pages/WireGuard/`
|
||||
- `frontend/src/api/backend/wireguard.ts`
|
||||
- `frontend/src/hooks/useWireGuard.ts`
|
||||
- relevant migration files in `backend/migrations/`
|
||||
|
||||
Operational assumptions:
|
||||
|
||||
- `WG_ENABLED` controls whether WireGuard startup hooks run
|
||||
- runtime requires container/network capabilities such as `NET_ADMIN`
|
||||
- some behavior only makes sense inside the Dockerized runtime
|
||||
|
||||
## 12. Production packaging model
|
||||
|
||||
Production image definition:
|
||||
|
||||
- `docker/Dockerfile`
|
||||
|
||||
Important facts:
|
||||
|
||||
- it copies `backend/` into `/app`
|
||||
- it copies `frontend/dist` into `/app/frontend`
|
||||
- it installs runtime dependencies inside the image
|
||||
- it relies on s6-overlay and rootfs scripts
|
||||
|
||||
This means:
|
||||
|
||||
- frontend must be built before image build
|
||||
- Docker/runtime files are part of app behavior, not just deployment plumbing
|
||||
|
||||
## 13. Known gotchas
|
||||
|
||||
- `docker/Dockerfile` assumes `frontend/dist` already exists
|
||||
- several scripts are shell-oriented and may be awkward on native Windows
|
||||
- docs and names are inherited from upstream in places, so naming is mixed:
|
||||
`nginx-proxy-manager`, `xGat3`, `D3V-Server`, `D3V-NPMWG`
|
||||
- `README.md` currently has encoding artifacts; do not assume every displayed character is trustworthy
|
||||
- backend package has no clear `start` script, so use `backend/index.js` as the canonical runtime entry
|
||||
- do not treat this as a simple SPA-only repo or API-only repo; the Docker assembly matters
|
||||
|
||||
## 14. Change strategy for AI agents
|
||||
|
||||
When handling a task:
|
||||
|
||||
1. Read this file
|
||||
2. Open only the subsystem entry files for the relevant area
|
||||
3. Avoid broad repo scans unless the task is cross-cutting
|
||||
4. Verify whether the change affects Docker/runtime assumptions
|
||||
5. If the task touches WireGuard, include container/runtime implications in reasoning
|
||||
|
||||
Open only what you need:
|
||||
|
||||
- backend bug: `backend/index.js`, `backend/app.js`, matching route/service/model files
|
||||
- frontend bug: `frontend/src/main.tsx`, `App.tsx`, `Router.tsx`, matching page/hook/api files
|
||||
- build/deploy bug: `docker/Dockerfile`, `scripts/build-project.sh`, `install.sh`
|
||||
- auth/setup issue: `backend/setup.js`, `backend/routes/tokens.js`, `frontend/src/context/`
|
||||
|
||||
## 15. Fast summary
|
||||
|
||||
If you only remember one thing:
|
||||
|
||||
`D3V-Server` is a Docker-assembled full-stack app where:
|
||||
|
||||
- backend runs the API and system logic
|
||||
- frontend is a Vite React admin UI
|
||||
- WireGuard is an integrated subsystem, not a separate service
|
||||
- Docker/runtime files are part of the product behavior
|
||||
- `backend/index.js`, `backend/app.js`, `backend/routes/main.js`,
|
||||
`frontend/src/main.tsx`, `frontend/src/App.tsx`,
|
||||
`frontend/src/Router.tsx`, and `docker/Dockerfile` are the core orientation files
|
||||
|
|
|
|||
195
README.md
195
README.md
|
|
@ -1,71 +1,75 @@
|
|||
# D3V-NPMWG — Nginx Proxy Manager + WireGuard VPN
|
||||
# D3V Gateway — Reverse Proxy + 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 lightweight all-in-one Docker deployment that combines reverse proxy management with SSL and **WireGuard VPN** in a single web interface.
|
||||
|
||||
## ✨ Features
|
||||
## Features
|
||||
|
||||
### Nginx Proxy Manager
|
||||
- 🌐 Reverse proxy management with a beautiful UI
|
||||
- 🔒 Free SSL certificates via Let's Encrypt
|
||||
- 🔀 Proxy hosts, redirection hosts, streams, and 404 hosts
|
||||
- 🛡️ Access control lists
|
||||
- 📊 Audit logging
|
||||
### Gateway
|
||||
- Reverse proxy management with SSL (Let's Encrypt)
|
||||
- Proxy hosts, redirection hosts, streams, and 404 hosts
|
||||
- Access control lists and audit logging
|
||||
|
||||
### WireGuard VPN Manager
|
||||
- 🔑 Create, enable, disable, and delete VPN clients
|
||||
- 📱 QR code generation for mobile clients
|
||||
- 📥 Download `.conf` configuration files
|
||||
- 📡 Real-time client status (connected, idle, data transfer)
|
||||
- ⏰ Client expiration support
|
||||
- 🔄 Auto-sync WireGuard configs
|
||||
- Create, enable, disable, and delete VPN clients
|
||||
- QR code generation for mobile clients
|
||||
- Download `.conf` configuration files
|
||||
- Real-time client status (connected, idle, data transfer)
|
||||
- Client expiration support
|
||||
- Auto-sync WireGuard configs
|
||||
- Client isolation (block inter-client traffic)
|
||||
- Encrypted per-client file storage
|
||||
|
||||
## 🚀 Quick Start (Auto Install)
|
||||
### Forgejo Integration (optional)
|
||||
- Self-hosted Git server on the same VPS
|
||||
- Accessible only via domain through NPM proxy
|
||||
- CI/CD via Forgejo Runner for automated Docker builds
|
||||
|
||||
The easiest way to install, update, and manage your D3V-NPMWG instance on Linux is by using our interactive manager script.
|
||||
### Blog Starter
|
||||
- `blog-starter/` contains a ready-to-use Hugo + LoveIt starter
|
||||
- includes a Forgejo Actions workflow that deploys generated files to `/opt/blog/public`
|
||||
- intended to be used as the base of a separate blog repository
|
||||
- `install.sh blog-deploy-info` prints the deploy user and the secret values needed by Forgejo Actions
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Download and run the install script
|
||||
curl -sSL https://raw.githubusercontent.com/xtcnet/D3V-NPMWG/master/install.sh -o install.sh
|
||||
curl -sSL https://src.d3v.ac/d3v/D3V-Server/raw/branch/master/install.sh -o install.sh
|
||||
chmod +x install.sh
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
**Features included in the script:**
|
||||
- `Install D3V-NPMWG`: Automatically setup docker-compose and directories in `/opt/d3v-npmwg`.
|
||||
- `Uninstall D3V-NPMWG`: Remove containers and wipe data.
|
||||
- `Reset Password`: Resets the admin login to `admin@example.com` / `changeme`.
|
||||
- `Update`: Pulls the latest image and updates the docker-compose stack.
|
||||
**Main menu:**
|
||||
- `1` Gateway
|
||||
- `2` Blog
|
||||
- `3` Forgejo
|
||||
- `4` Status / Logs / Health Check
|
||||
- `5` Exit
|
||||
|
||||
You can also run specific commands directly: `sudo ./install.sh {install|uninstall|reset|update}`
|
||||
**Gateway submenu:**
|
||||
- `1` Install Gateway
|
||||
- `2` Uninstall Gateway
|
||||
- `3` Uninstall Gateway + Docker (Purge)
|
||||
- `4` Reset Admin Password
|
||||
- `5` Update Gateway
|
||||
- `6` Manage Custom Stream Ports
|
||||
- `7` Toggle Admin Port 81 (Block/Unblock)
|
||||
|
||||
You can also run commands directly:
|
||||
```bash
|
||||
sudo ./install.sh {gateway|gateway-install|gateway-uninstall|gateway-purge|gateway-reset|gateway-update|manage-ports|toggle-port|blog|blog-install|blog-update|blog-uninstall|forgejo|runner-update|ops}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐋 Manual Docker Run```bash
|
||||
docker run -d \
|
||||
--name npm-wg \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
--sysctl net.ipv4.ip_forward=1 \
|
||||
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
|
||||
-p 80:80 \
|
||||
-p 81:81 \
|
||||
-p 443:443 \
|
||||
-p 51820:51820/udp \
|
||||
-v npm-wg-data:/data \
|
||||
-v npm-wg-letsencrypt:/etc/letsencrypt \
|
||||
-v npm-wg-wireguard:/etc/wireguard \
|
||||
-e WG_HOST=your.server.ip \
|
||||
npm-wg:latest
|
||||
```
|
||||
|
||||
## 📋 Docker Compose
|
||||
## Manual Docker Compose
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
npm-wg:
|
||||
image: npm-wg:latest
|
||||
container_name: npm-wg
|
||||
d3v-npmwg:
|
||||
image: src.d3v.ac/d3v/d3v-server:latest
|
||||
container_name: d3v-npmwg
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
|
@ -74,38 +78,26 @@ services:
|
|||
- net.ipv4.ip_forward=1
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
ports:
|
||||
- "80:80" # HTTP
|
||||
- "81:81" # Admin UI
|
||||
- "443:443" # HTTPS
|
||||
- "51820:51820/udp" # WireGuard
|
||||
- "80:80"
|
||||
- "81:81"
|
||||
- "443:443"
|
||||
- "51820-51830:51820-51830/udp"
|
||||
volumes:
|
||||
- data:/data
|
||||
- letsencrypt:/etc/letsencrypt
|
||||
- wireguard:/etc/wireguard
|
||||
- ./data:/data
|
||||
- ./letsencrypt:/etc/letsencrypt
|
||||
- ./wireguard:/etc/wireguard
|
||||
environment:
|
||||
WG_HOST: "your.server.ip" # REQUIRED: Your server's public IP or domain
|
||||
# WG_PORT: 51820 # WireGuard listen port
|
||||
# WG_DEFAULT_ADDRESS: 10.8.0.0/24 # VPN subnet
|
||||
# WG_DNS: 1.1.1.1,8.8.8.8 # DNS for VPN clients
|
||||
# WG_MTU: 1420 # MTU for VPN
|
||||
# WG_ALLOWED_IPS: 0.0.0.0/0,::/0 # Allowed IPs for clients
|
||||
# WG_PERSISTENT_KEEPALIVE: 25
|
||||
# WG_ENABLED: true # Set to false to disable WireGuard
|
||||
|
||||
volumes:
|
||||
data:
|
||||
letsencrypt:
|
||||
wireguard:
|
||||
WG_HOST: "your.server.ip" # REQUIRED
|
||||
```
|
||||
|
||||
## 🔧 Environment Variables
|
||||
---
|
||||
|
||||
### WireGuard Settings
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `WG_ENABLED` | `true` | Enable/disable WireGuard VPN |
|
||||
| `WG_HOST` | *(required)* | Public IP or domain of your server |
|
||||
| `WG_ENABLED` | `true` | Enable/disable WireGuard |
|
||||
| `WG_PORT` | `51820` | WireGuard UDP listen port |
|
||||
| `WG_DEFAULT_ADDRESS` | `10.8.0.0/24` | VPN subnet CIDR |
|
||||
| `WG_DNS` | `1.1.1.1, 8.8.8.8` | DNS servers for VPN clients |
|
||||
|
|
@ -113,66 +105,45 @@ volumes:
|
|||
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | Default allowed IPs for clients |
|
||||
| `WG_PERSISTENT_KEEPALIVE` | `25` | Keepalive interval in seconds |
|
||||
|
||||
## 🌍 Ports
|
||||
## Ports
|
||||
|
||||
| Port | Protocol | Description |
|
||||
|------|----------|-------------|
|
||||
| `80` | TCP | HTTP |
|
||||
| `81` | TCP | Admin Web UI |
|
||||
| `443` | TCP | HTTPS |
|
||||
| `51820` | UDP | WireGuard VPN |
|
||||
| `51820–51830` | UDP | WireGuard Multi-Server |
|
||||
|
||||
## 📖 Usage
|
||||
---
|
||||
|
||||
1. **Access the Admin UI** at `http://your-server:81`
|
||||
2. **Set up NPM** with your admin email and password
|
||||
3. **Navigate to WireGuard** from the sidebar menu
|
||||
4. **Create VPN clients** by clicking "New Client"
|
||||
5. **Scan QR code** or **download .conf** file to configure WireGuard on your devices
|
||||
|
||||
## 🏗️ Building and CI/CD
|
||||
|
||||
### ☁️ Automated Build (Docker Cloud Build)
|
||||
This project is configured with **GitHub Actions** (`.github/workflows/docker-publish.yml`) to automatically build and push multi-arch Docker images (`amd64`, `arm64`) to **GitHub Container Registry (GHCR)** whenever a push is made to the `master` branch or a version tag is created.
|
||||
|
||||
Images are available at: `ghcr.io/xtcnet/d3v-npmwg:latest`
|
||||
|
||||
### 🏗️ Building from Source Local
|
||||
To build D3V-NPMWG from source manually, you must build the React frontend before building the Docker image:
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/xtcnet/D3V-NPMWG.git
|
||||
cd D3V-NPMWG
|
||||
git clone https://src.d3v.ac/d3v/D3V-Server.git
|
||||
cd D3V-Server
|
||||
|
||||
# 1. Build the Frontend
|
||||
cd frontend
|
||||
yarn install
|
||||
yarn build
|
||||
cd ..
|
||||
|
||||
# 2. Build the Docker Image
|
||||
# IMPORTANT: Do not forget the trailing dot '.' at the end of the command!
|
||||
docker build -t npm-wg -f docker/Dockerfile .
|
||||
cd frontend && yarn install && yarn locale-compile && yarn build && cd ..
|
||||
docker build -t d3v-gateway -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
Alternatively, you can run the helper script:
|
||||
```bash
|
||||
./scripts/build-project.sh
|
||||
```
|
||||
### CI/CD
|
||||
|
||||
## ⚠️ Requirements
|
||||
Pushes to `master` that touch `backend/`, `frontend/`, or `docker/` automatically build and push the Docker image to `src.d3v.ac/d3v/d3v-server:latest` via Forgejo Actions.
|
||||
|
||||
- **Docker** with Linux containers
|
||||
- **Host kernel** must support WireGuard (Linux 5.6+ or WireGuard kernel module)
|
||||
- Container requires `NET_ADMIN` and `SYS_MODULE` capabilities
|
||||
- IP forwarding must be enabled (`net.ipv4.ip_forward=1`)
|
||||
---
|
||||
|
||||
## 📜 Credits
|
||||
## Requirements
|
||||
|
||||
- Docker with Linux containers
|
||||
- Host kernel with WireGuard support (Linux 5.6+)
|
||||
- `NET_ADMIN` and `SYS_MODULE` capabilities
|
||||
- `net.ipv4.ip_forward=1`
|
||||
|
||||
## Credits
|
||||
|
||||
- [Nginx Proxy Manager](https://github.com/NginxProxyManager/nginx-proxy-manager) — Original proxy manager
|
||||
- [wg-easy](https://github.com/wg-easy/wg-easy) — WireGuard management inspiration
|
||||
|
||||
## 📄 License
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import bodyParser from "body-parser";
|
|||
import compression from "compression";
|
||||
import express from "express";
|
||||
import fileUpload from "express-fileupload";
|
||||
import helmet from "helmet";
|
||||
import { rateLimit } from "express-rate-limit";
|
||||
import { isDebugMode } from "./lib/config.js";
|
||||
import cors from "./lib/express/cors.js";
|
||||
import jwt from "./lib/express/jwt.js";
|
||||
import { debug, express as logger } from "./logger.js";
|
||||
import mainRoutes from "./routes/main.js";
|
||||
import wgPublicRoutes from "./routes/wg_public.js";
|
||||
|
||||
/**
|
||||
* App
|
||||
|
|
@ -35,25 +38,54 @@ if (isDebugMode()) {
|
|||
// CORS for everything
|
||||
app.use(cors);
|
||||
|
||||
// General security/cache related headers + server header
|
||||
app.use((_, res, next) => {
|
||||
let x_frame_options = "DENY";
|
||||
|
||||
if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) {
|
||||
x_frame_options = process.env.X_FRAME_OPTIONS;
|
||||
}
|
||||
|
||||
res.set({
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": x_frame_options,
|
||||
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: 0,
|
||||
});
|
||||
next();
|
||||
/**
|
||||
* Global Rate Limiter: 100 requests per minute per IP
|
||||
*/
|
||||
const globalLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 100,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: { message: "Too many requests, please try again later." } },
|
||||
});
|
||||
|
||||
/**
|
||||
* Login Rate Limiter: 10 requests per 15 minutes per IP
|
||||
*/
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: { message: "Too many login attempts, please try again in 15 minutes." } },
|
||||
});
|
||||
|
||||
/**
|
||||
* Helmet Security Headers
|
||||
*/
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // unsafe-inline/eval required by some React libs
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:", "https://www.gravatar.com"],
|
||||
connectSrc: ["'self'"],
|
||||
frameAncestors: [process.env.X_FRAME_OPTIONS === "ALLOWALL" ? "*" : "'none'"],
|
||||
},
|
||||
},
|
||||
crossOriginResourcePolicy: { policy: "cross-origin" },
|
||||
}),
|
||||
);
|
||||
|
||||
// Apply rate limiting
|
||||
app.use("/api/tokens", loginLimiter);
|
||||
app.use("/api/", globalLimiter);
|
||||
|
||||
// Bypass JWT for public authenticated requests mapped by WireGuard IP
|
||||
app.use("/wg-public", wgPublicRoutes);
|
||||
|
||||
app.use(jwt());
|
||||
app.use("/", mainRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const internalAuditLog = {
|
|||
|
||||
const query = auditLogModel
|
||||
.query()
|
||||
.andWhere("user_id", access.token.getUserId(1))
|
||||
.orderBy("created_on", "DESC")
|
||||
.orderBy("id", "DESC")
|
||||
.limit(100)
|
||||
|
|
@ -49,6 +50,7 @@ const internalAuditLog = {
|
|||
const query = auditLogModel
|
||||
.query()
|
||||
.andWhere("id", data.id)
|
||||
.andWhere("user_id", access.token.getUserId(1))
|
||||
.allowGraph("[user]")
|
||||
.first();
|
||||
|
||||
|
|
|
|||
66
backend/internal/database.js
Normal file
66
backend/internal/database.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import db from "../db.js";
|
||||
import { debug, express as logger } from "../logger.js";
|
||||
|
||||
const internalDatabase = {
|
||||
/**
|
||||
* Get all tables in the database (SQLite specific, but Knex supports raw queries for others too)
|
||||
*/
|
||||
async getTables() {
|
||||
const knex = db();
|
||||
|
||||
// Attempt SQLite first, fallback to generic if using mysql/mariadb
|
||||
try {
|
||||
// For SQLite
|
||||
const tables = await knex.raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
|
||||
return tables.map(t => t.name).sort();
|
||||
} catch (e) {
|
||||
// For MySQL/MariaDB
|
||||
const tables = await knex.raw("SHOW TABLES");
|
||||
return tables[0].map(t => Object.values(t)[0]).sort();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get table schema and paginated rows
|
||||
*/
|
||||
async getTableData(tableName, limit = 50, offset = 0) {
|
||||
const knex = db();
|
||||
|
||||
// 1. Get Schema/PRAGMA
|
||||
let schema = [];
|
||||
try {
|
||||
const info = await knex.raw(`PRAGMA table_info("${tableName}")`);
|
||||
schema = info; // SQLite structure
|
||||
} catch (e) {
|
||||
// MySQL fallback
|
||||
const info = await knex.raw(`DESCRIBE \`${tableName}\``);
|
||||
schema = info[0];
|
||||
}
|
||||
|
||||
// 2. Count total rows
|
||||
const countResult = await knex(tableName).count("id as count").first();
|
||||
const total = parseInt(countResult.count || 0, 10);
|
||||
|
||||
// 3. Get rows
|
||||
const rows = await knex(tableName)
|
||||
.select("*")
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
// Try ordering by ID or created_on if possible, fallback to whatever db returns
|
||||
.orderBy(
|
||||
schema.find(col => col.name === 'created_on' || col.Field === 'created_on') ? 'created_on' :
|
||||
(schema.find(col => col.name === 'id' || col.Field === 'id') ? 'id' : undefined) || '1',
|
||||
'desc'
|
||||
)
|
||||
.catch(() => knex(tableName).select("*").limit(limit).offset(offset)); // If order fails
|
||||
|
||||
return {
|
||||
name: tableName,
|
||||
schema,
|
||||
total,
|
||||
rows
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default internalDatabase;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import db from "../db.js";
|
||||
import internalDeadHost from "./dead-host.js";
|
||||
import internalProxyHost from "./proxy-host.js";
|
||||
import internalRedirectionHost from "./redirection-host.js";
|
||||
|
|
@ -23,15 +24,29 @@ const internalReport = {
|
|||
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then((counts) => {
|
||||
.then(async (counts) => {
|
||||
const knex = db();
|
||||
let wgServers = 0;
|
||||
let wgClients = 0;
|
||||
try {
|
||||
const srvResult = await knex("wg_interface").count("id as count").first();
|
||||
wgServers = srvResult?.count || 0;
|
||||
const cliResult = await knex("wg_client").count("id as count").first();
|
||||
wgClients = cliResult?.count || 0;
|
||||
} catch (_) {
|
||||
// WireGuard tables may not exist yet
|
||||
}
|
||||
return {
|
||||
proxy: counts.shift(),
|
||||
redirection: counts.shift(),
|
||||
stream: counts.shift(),
|
||||
dead: counts.shift(),
|
||||
wgServers: Number(wgServers),
|
||||
wgClients: Number(wgClients),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default internalReport;
|
||||
|
||||
|
|
|
|||
192
backend/internal/wireguard-fs.js
Normal file
192
backend/internal/wireguard-fs.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
import { debug, express as logger } from "../logger.js";
|
||||
|
||||
const WG_FILES_DIR = process.env.WG_FILES_DIR || "/data/wg_clients";
|
||||
|
||||
// Ensure root dir exists
|
||||
if (!fs.existsSync(WG_FILES_DIR)) {
|
||||
fs.mkdirSync(WG_FILES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Derive a 32-byte AES-256 key from the client's private key
|
||||
*/
|
||||
getKey(privateKey) {
|
||||
return crypto.createHash("sha256").update(privateKey).digest();
|
||||
},
|
||||
|
||||
getClientDir(ipv4Address) {
|
||||
// Clean the IP address to prevent traversal
|
||||
const safeIp = ipv4Address.replace(/[^0-9.]/g, "");
|
||||
const dirPath = path.join(WG_FILES_DIR, safeIp);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
return dirPath;
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroys a client's entire isolated file directory and all encrypted contents
|
||||
*/
|
||||
async deleteClientDir(ipv4Address) {
|
||||
const safeIp = ipv4Address.replace(/[^0-9.]/g, "");
|
||||
const dirPath = path.join(WG_FILES_DIR, safeIp);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
await fs.promises.rm(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Scans a client partition and returns the total byte size utilized
|
||||
*/
|
||||
async getClientStorageUsage(ipv4Address) {
|
||||
const dir = this.getClientDir(ipv4Address);
|
||||
try {
|
||||
const files = await fs.promises.readdir(dir);
|
||||
let totalBytes = 0;
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = await fs.promises.stat(filePath);
|
||||
if (stats.isFile()) {
|
||||
totalBytes += stats.size;
|
||||
}
|
||||
}
|
||||
return totalBytes;
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List all files in a client's isolated directory
|
||||
*/
|
||||
async listFiles(ipv4Address) {
|
||||
const dir = this.getClientDir(ipv4Address);
|
||||
const files = await fs.promises.readdir(dir);
|
||||
|
||||
const result = [];
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = await fs.promises.stat(filePath);
|
||||
if (stats.isFile()) {
|
||||
result.push({
|
||||
name: file,
|
||||
size: stats.size, // Note: Encrypted size includes 16 byte IV + pad
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Encrypt and save a file buffer to disk
|
||||
*/
|
||||
async uploadFile(ipv4Address, privateKey, filename, fileBuffer) {
|
||||
const dir = this.getClientDir(ipv4Address);
|
||||
// Prevent path traversal
|
||||
const safeFilename = path.basename(filename);
|
||||
const filePath = path.join(dir, safeFilename);
|
||||
|
||||
const key = this.getKey(privateKey);
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
|
||||
writeStream.on("error", (err) => reject(err));
|
||||
writeStream.on("finish", () => resolve({ success: true, name: safeFilename }));
|
||||
|
||||
// Write the 16-byte IV first
|
||||
writeStream.write(iv);
|
||||
|
||||
// Pipe the cipher output to the file
|
||||
cipher.pipe(writeStream);
|
||||
|
||||
// Write the actual file buffer into the cipher
|
||||
cipher.write(fileBuffer);
|
||||
cipher.end();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Decrypt a file and pipe it to standard response stream
|
||||
*/
|
||||
async downloadFile(ipv4Address, privateKey, filename, res) {
|
||||
const dir = this.getClientDir(ipv4Address);
|
||||
const safeFilename = path.basename(filename);
|
||||
const filePath = path.join(dir, safeFilename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
|
||||
const key = this.getKey(privateKey);
|
||||
const fileDescriptor = await fs.promises.open(filePath, "r");
|
||||
|
||||
// Read first 16 bytes to extract IV
|
||||
const ivBuffer = Buffer.alloc(16);
|
||||
await fileDescriptor.read(ivBuffer, 0, 16, 0);
|
||||
await fileDescriptor.close();
|
||||
|
||||
// Create a read stream starting AFTER the 16 byte IV
|
||||
const readStream = fs.createReadStream(filePath, { start: 16 });
|
||||
const decipher = crypto.createDecipheriv("aes-256-cbc", key, ivBuffer);
|
||||
|
||||
// Set response headers for download
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${safeFilename}"`);
|
||||
res.setHeader("Content-Type", "application/octet-stream");
|
||||
|
||||
// Catch error in pipeline without crashing the root process
|
||||
readStream.on("error", (err) => {
|
||||
logger.error(`Error reading encrypted file ${safeFilename}: ${err.message}`);
|
||||
if (!res.headersSent) res.status(500).end();
|
||||
});
|
||||
|
||||
decipher.on("error", (err) => {
|
||||
logger.error(`Error decrypting file ${safeFilename}: ${err.message}`);
|
||||
if (!res.headersSent) res.status(500).end();
|
||||
});
|
||||
|
||||
readStream.pipe(decipher).pipe(res);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an encrypted file
|
||||
*/
|
||||
async deleteFile(ipv4Address, filename) {
|
||||
const dir = this.getClientDir(ipv4Address);
|
||||
const safeFilename = path.basename(filename);
|
||||
const filePath = path.join(dir, safeFilename);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.promises.unlink(filePath);
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename an encrypted file (no re-encryption needed, just fs.rename)
|
||||
*/
|
||||
async renameFile(ipv4Address, oldName, newName) {
|
||||
const dir = this.getClientDir(ipv4Address);
|
||||
const safeOld = path.basename(oldName);
|
||||
const safeNew = path.basename(newName);
|
||||
const oldPath = path.join(dir, safeOld);
|
||||
const newPath = path.join(dir, safeNew);
|
||||
|
||||
if (!fs.existsSync(oldPath)) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
if (fs.existsSync(newPath)) {
|
||||
throw new Error("File name already exists");
|
||||
}
|
||||
await fs.promises.rename(oldPath, newPath);
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import fs from "fs";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { global as logger } from "../logger.js";
|
||||
import * as wgHelpers from "../lib/wg-helpers.js";
|
||||
import internalWireguardFs from "./wireguard-fs.js";
|
||||
import internalAuditLog from "./audit-log.js";
|
||||
import { encrypt, decrypt } from "../lib/crypto.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const WG_INTERFACE_NAME = process.env.WG_INTERFACE_NAME || "wg0";
|
||||
const WG_DEFAULT_PORT = Number.parseInt(process.env.WG_PORT || "51820", 10);
|
||||
|
|
@ -13,6 +20,7 @@ const WG_DEFAULT_PERSISTENT_KEEPALIVE = Number.parseInt(process.env.WG_PERSISTEN
|
|||
const WG_CONFIG_DIR = "/etc/wireguard";
|
||||
|
||||
let cronTimer = null;
|
||||
let connectionMemoryMap = {};
|
||||
|
||||
const internalWireguard = {
|
||||
|
||||
|
|
@ -22,112 +30,180 @@ 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,
|
||||
// Seed a default config if it doesn't exist
|
||||
const insertData = {
|
||||
name: "wg0",
|
||||
private_key: encrypt(privateKey),
|
||||
public_key: publicKey,
|
||||
ipv4_cidr: WG_DEFAULT_ADDRESS,
|
||||
listen_port: WG_DEFAULT_PORT,
|
||||
mtu: WG_DEFAULT_MTU,
|
||||
listen_port: 51820,
|
||||
ipv4_cidr: "10.0.0.1/24",
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save WireGuard config to /etc/wireguard/wg0.conf and sync
|
||||
* Render PostUp and PostDown iptables rules based on interface, isolation, and links
|
||||
*/
|
||||
async saveConfig(knex) {
|
||||
const iface = await this.getOrCreateInterface(knex);
|
||||
const clients = await knex("wg_client").where("enabled", true);
|
||||
async renderIptablesRules(knex, iface) {
|
||||
const basePostUp = [];
|
||||
const basePostDown = [];
|
||||
|
||||
// Generate server interface section
|
||||
const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr);
|
||||
const serverAddress = `${parsed.firstHost}/${parsed.prefix}`;
|
||||
// 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");
|
||||
|
||||
let configContent = wgHelpers.generateServerInterface({
|
||||
privateKey: iface.private_key,
|
||||
address: serverAddress,
|
||||
listenPort: iface.listen_port,
|
||||
mtu: iface.mtu,
|
||||
dns: null, // DNS is for clients, not server
|
||||
postUp: iface.post_up,
|
||||
postDown: iface.post_down,
|
||||
});
|
||||
// Server Isolation: drop cross-server traffic by default.
|
||||
// Uses -I to insert at position 1, before the ACCEPT rules above.
|
||||
basePostUp.push("iptables -I FORWARD -i %i -o wg+ -j DROP");
|
||||
basePostDown.push("iptables -D FORWARD -i %i -o wg+ -j DROP");
|
||||
|
||||
// Generate peer sections for each enabled client
|
||||
for (const client of clients) {
|
||||
configContent += "\n\n" + wgHelpers.generateServerPeer({
|
||||
publicKey: client.public_key,
|
||||
preSharedKey: client.pre_shared_key,
|
||||
allowedIps: `${client.ipv4_address}/32`,
|
||||
});
|
||||
// Same-interface rule: inserted AFTER the DROP above so it lands at position 1,
|
||||
// placing it BEFORE the wg+ DROP in the chain.
|
||||
// This ensures client-to-client traffic on this interface is evaluated first.
|
||||
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");
|
||||
} else {
|
||||
basePostUp.push("iptables -I FORWARD -i %i -o %i -j ACCEPT");
|
||||
basePostDown.push("iptables -D FORWARD -i %i -o %i -j ACCEPT");
|
||||
}
|
||||
|
||||
configContent += "\n";
|
||||
// 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);
|
||||
|
||||
// Write config file
|
||||
const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`;
|
||||
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
|
||||
logger.info(`WireGuard config saved to ${configPath}`);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync config
|
||||
try {
|
||||
await wgHelpers.wgSync(iface.name);
|
||||
logger.info("WireGuard config synced");
|
||||
} catch (err) {
|
||||
logger.warn("WireGuard sync failed, may need full restart:", err.message);
|
||||
return {
|
||||
postUp: basePostUp.join("; "),
|
||||
postDown: basePostDown.join("; "),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Save WireGuard config to /etc/wireguard/wgX.conf and sync
|
||||
*/
|
||||
async saveConfig(knex) {
|
||||
await this.getOrCreateInterface(knex); // Ensure at least wg0 exists
|
||||
|
||||
const ifaces = await knex("wg_interface").select("*");
|
||||
const clients = await knex("wg_client").where("enabled", true);
|
||||
|
||||
for (const iface of ifaces) {
|
||||
// 1. Render IPTables Rules dynamically for this interface
|
||||
const { postUp, postDown } = await this.renderIptablesRules(knex, iface);
|
||||
|
||||
// 2. Generate server interface section
|
||||
const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr);
|
||||
const serverAddress = `${parsed.firstHost}/${parsed.prefix}`;
|
||||
|
||||
let configContent = wgHelpers.generateServerInterface({
|
||||
privateKey: decrypt(iface.private_key),
|
||||
address: serverAddress,
|
||||
listenPort: iface.listen_port,
|
||||
mtu: iface.mtu,
|
||||
dns: null, // DNS is for clients, not server
|
||||
postUp: postUp,
|
||||
postDown: postDown,
|
||||
});
|
||||
|
||||
// 3. Generate peer sections for each enabled client ON THIS SERVER
|
||||
const ifaceClients = clients.filter(c => c.interface_id === iface.id);
|
||||
for (const client of ifaceClients) {
|
||||
configContent += "\n\n" + wgHelpers.generateServerPeer({
|
||||
publicKey: client.public_key,
|
||||
preSharedKey: decrypt(client.pre_shared_key),
|
||||
allowedIps: `${client.ipv4_address}/32`,
|
||||
});
|
||||
}
|
||||
|
||||
configContent += "\n";
|
||||
|
||||
// 4. Write config file
|
||||
const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`;
|
||||
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
|
||||
logger.info(`WireGuard config saved to ${configPath}`);
|
||||
|
||||
// 5. Sync config
|
||||
try {
|
||||
await wgHelpers.wgSync(iface.name);
|
||||
logger.info(`WireGuard config synced for ${iface.name}`);
|
||||
|
||||
// 6. Apply iptables isolation rule directly (wg syncconf does not run PostUp/PostDown)
|
||||
await this.syncIptablesRules(iface);
|
||||
|
||||
// 7. Apply traffic control bandwidth partitions non-blocking
|
||||
this.applyBandwidthLimits(knex, iface).catch((e) => logger.warn(`Skipping QoS on ${iface.name}: ${e.message}`));
|
||||
} catch (err) {
|
||||
logger.warn(`WireGuard sync failed for ${iface.name}, may need full restart:`, err.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start WireGuard interface
|
||||
* Start WireGuard interfaces
|
||||
*/
|
||||
async startup(knex) {
|
||||
try {
|
||||
const iface = await this.getOrCreateInterface(knex);
|
||||
await this.getOrCreateInterface(knex); // ensure at least wg0
|
||||
|
||||
// Ensure config dir exists
|
||||
if (!fs.existsSync(WG_CONFIG_DIR)) {
|
||||
fs.mkdirSync(WG_CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Save config first
|
||||
// Save configs first (generates .conf files dynamically for all wg_interfaces)
|
||||
await this.saveConfig(knex);
|
||||
|
||||
// Bring down if already up, then up
|
||||
try {
|
||||
await wgHelpers.wgDown(iface.name);
|
||||
} catch (_) {
|
||||
// Ignore if not up
|
||||
}
|
||||
// Bring down/up all interfaces sequentially
|
||||
const ifaces = await knex("wg_interface").select("name", "listen_port");
|
||||
for (const iface of ifaces) {
|
||||
try {
|
||||
await wgHelpers.wgDown(iface.name);
|
||||
} catch (_) {
|
||||
// Ignore if not up
|
||||
}
|
||||
|
||||
await wgHelpers.wgUp(iface.name);
|
||||
logger.info(`WireGuard interface ${iface.name} started on port ${iface.listen_port}`);
|
||||
try {
|
||||
await wgHelpers.wgUp(iface.name);
|
||||
logger.info(`WireGuard interface ${iface.name} started on port ${iface.listen_port}`);
|
||||
} catch (err) {
|
||||
logger.error(`WireGuard startup failed for ${iface.name}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Start cron job for expiration
|
||||
this.startCronJob(knex);
|
||||
} catch (err) {
|
||||
logger.error("WireGuard startup failed:", err.message);
|
||||
logger.warn("WireGuard features will be unavailable. Ensure the host supports WireGuard kernel module.");
|
||||
logger.error("WireGuard startup failed overall:", err.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shutdown WireGuard interface
|
||||
* Shutdown WireGuard interfaces
|
||||
*/
|
||||
async shutdown(knex) {
|
||||
if (cronTimer) {
|
||||
|
|
@ -135,26 +211,42 @@ const internalWireguard = {
|
|||
cronTimer = null;
|
||||
}
|
||||
try {
|
||||
const iface = await knex("wg_interface").first();
|
||||
if (iface) {
|
||||
await wgHelpers.wgDown(iface.name);
|
||||
logger.info(`WireGuard interface ${iface.name} stopped`);
|
||||
const ifaces = await knex("wg_interface").select("name");
|
||||
for (const iface of ifaces) {
|
||||
try {
|
||||
await wgHelpers.wgDown(iface.name);
|
||||
logger.info(`WireGuard interface ${iface.name} stopped`);
|
||||
} catch (err) {
|
||||
logger.warn(`WireGuard shutdown warning for ${iface.name}:`, err.message);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn("WireGuard shutdown warning:", err.message);
|
||||
logger.error("WireGuard shutdown failed querying DB:", err.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all clients with live status
|
||||
* Get all clients with live status and interface name correlation
|
||||
*/
|
||||
async getClients(knex) {
|
||||
const iface = await this.getOrCreateInterface(knex);
|
||||
const dbClients = await knex("wg_client").orderBy("created_on", "desc");
|
||||
async getClients(knex, access, accessData) {
|
||||
await this.getOrCreateInterface(knex); // Ensure structure exists
|
||||
|
||||
const query = knex("wg_client")
|
||||
.join("wg_interface", "wg_client.interface_id", "=", "wg_interface.id")
|
||||
.select("wg_client.*", "wg_interface.name as interface_name")
|
||||
.orderBy("wg_client.created_on", "desc");
|
||||
|
||||
if (access) {
|
||||
query.andWhere("wg_client.owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
|
||||
const dbClients = await query;
|
||||
|
||||
const clients = dbClients.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
interfaceName: c.interface_name,
|
||||
interfaceId: c.interface_id,
|
||||
enabled: c.enabled === 1 || c.enabled === true,
|
||||
ipv4_address: c.ipv4_address,
|
||||
public_key: c.public_key,
|
||||
|
|
@ -170,20 +262,28 @@ const internalWireguard = {
|
|||
transfer_tx: 0,
|
||||
}));
|
||||
|
||||
// Get live WireGuard status
|
||||
try {
|
||||
const dump = await wgHelpers.wgDump(iface.name);
|
||||
for (const peer of dump) {
|
||||
const client = clients.find((c) => c.public_key === peer.publicKey);
|
||||
if (client) {
|
||||
client.latest_handshake_at = peer.latestHandshakeAt;
|
||||
client.endpoint = peer.endpoint;
|
||||
client.transfer_rx = peer.transferRx;
|
||||
client.transfer_tx = peer.transferTx;
|
||||
// Get live WireGuard status from ALL interfaces
|
||||
const ifaces = await knex("wg_interface").select("name");
|
||||
for (const iface of ifaces) {
|
||||
try {
|
||||
const dump = await wgHelpers.wgDump(iface.name);
|
||||
for (const peer of dump) {
|
||||
const client = clients.find((c) => c.public_key === peer.publicKey);
|
||||
if (client) {
|
||||
client.latest_handshake_at = peer.latestHandshakeAt;
|
||||
client.endpoint = peer.endpoint;
|
||||
client.transfer_rx = peer.transferRx;
|
||||
client.transfer_tx = peer.transferTx;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// WireGuard might be off or particular interface fails
|
||||
}
|
||||
} catch (_) {
|
||||
// WireGuard may not be running
|
||||
}
|
||||
|
||||
// Inject Storage Utilization Metrics
|
||||
for (const client of clients) {
|
||||
client.storage_usage_bytes = await internalWireguardFs.getClientStorageUsage(client.ipv4_address);
|
||||
}
|
||||
|
||||
return clients;
|
||||
|
|
@ -192,8 +292,10 @@ const internalWireguard = {
|
|||
/**
|
||||
* Create a new WireGuard client
|
||||
*/
|
||||
async createClient(knex, data) {
|
||||
const iface = await this.getOrCreateInterface(knex);
|
||||
async createClient(knex, data, access, accessData) {
|
||||
const iface = data.interface_id
|
||||
? await knex("wg_interface").where("id", data.interface_id).first()
|
||||
: await this.getOrCreateInterface(knex);
|
||||
|
||||
// Generate keys
|
||||
const privateKey = await wgHelpers.generatePrivateKey();
|
||||
|
|
@ -201,20 +303,29 @@ const internalWireguard = {
|
|||
const preSharedKey = await wgHelpers.generatePreSharedKey();
|
||||
|
||||
// Allocate IP
|
||||
const existingClients = await knex("wg_client").select("ipv4_address");
|
||||
const existingClients = await knex("wg_client").select("ipv4_address").where("interface_id", iface.id);
|
||||
const allocatedIPs = existingClients.map((c) => c.ipv4_address);
|
||||
const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs);
|
||||
|
||||
if (!ipv4Address) {
|
||||
throw new Error("No available IP addresses remaining in this WireGuard server subnet.");
|
||||
}
|
||||
|
||||
// Scrub any old junk partitions to prevent leakage
|
||||
await internalWireguardFs.deleteClientDir(ipv4Address);
|
||||
|
||||
const clientData = {
|
||||
name: data.name || "Unnamed Client",
|
||||
enabled: true,
|
||||
ipv4_address: ipv4Address,
|
||||
private_key: privateKey,
|
||||
private_key: encrypt(privateKey),
|
||||
public_key: publicKey,
|
||||
pre_shared_key: preSharedKey,
|
||||
pre_shared_key: encrypt(preSharedKey),
|
||||
allowed_ips: data.allowed_ips || WG_DEFAULT_ALLOWED_IPS,
|
||||
persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE,
|
||||
expires_at: data.expires_at || null,
|
||||
interface_id: iface.id,
|
||||
owner_user_id: access ? access.token.getUserId(1) : 1,
|
||||
created_on: knex.fn.now(),
|
||||
modified_on: knex.fn.now(),
|
||||
};
|
||||
|
|
@ -230,13 +341,21 @@ const internalWireguard = {
|
|||
/**
|
||||
* Delete a WireGuard client
|
||||
*/
|
||||
async deleteClient(knex, clientId) {
|
||||
const client = await knex("wg_client").where("id", clientId).first();
|
||||
async deleteClient(knex, clientId, access, accessData) {
|
||||
const query = knex("wg_client").where("id", clientId);
|
||||
if (access) {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
throw new Error("Client not found");
|
||||
}
|
||||
|
||||
await knex("wg_client").where("id", clientId).del();
|
||||
|
||||
// Hard-remove the encrypted partition safely mapped to the ipv4_address since it's deleted
|
||||
await internalWireguardFs.deleteClientDir(client.ipv4_address);
|
||||
|
||||
await this.saveConfig(knex);
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -245,8 +364,12 @@ const internalWireguard = {
|
|||
/**
|
||||
* Toggle a WireGuard client enabled/disabled
|
||||
*/
|
||||
async toggleClient(knex, clientId, enabled) {
|
||||
const client = await knex("wg_client").where("id", clientId).first();
|
||||
async toggleClient(knex, clientId, enabled, access, accessData) {
|
||||
const query = knex("wg_client").where("id", clientId);
|
||||
if (access) {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
throw new Error("Client not found");
|
||||
}
|
||||
|
|
@ -264,8 +387,12 @@ const internalWireguard = {
|
|||
/**
|
||||
* Update a WireGuard client
|
||||
*/
|
||||
async updateClient(knex, clientId, data) {
|
||||
const client = await knex("wg_client").where("id", clientId).first();
|
||||
async updateClient(knex, clientId, data, access, accessData) {
|
||||
const query = knex("wg_client").where("id", clientId);
|
||||
if (access) {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
throw new Error("Client not found");
|
||||
}
|
||||
|
|
@ -287,21 +414,25 @@ const internalWireguard = {
|
|||
* Get client configuration file content
|
||||
*/
|
||||
async getClientConfiguration(knex, clientId) {
|
||||
const iface = await this.getOrCreateInterface(knex);
|
||||
const client = await knex("wg_client").where("id", clientId).first();
|
||||
if (!client) {
|
||||
throw new Error("Client not found");
|
||||
}
|
||||
|
||||
const iface = await knex("wg_interface").where("id", client.interface_id).first();
|
||||
if (!iface) {
|
||||
throw new Error("Interface not found for this client");
|
||||
}
|
||||
|
||||
const endpoint = `${iface.host || "YOUR_SERVER_IP"}:${iface.listen_port}`;
|
||||
|
||||
return wgHelpers.generateClientConfig({
|
||||
clientPrivateKey: client.private_key,
|
||||
clientPrivateKey: decrypt(client.private_key),
|
||||
clientAddress: `${client.ipv4_address}/32`,
|
||||
dns: iface.dns,
|
||||
mtu: iface.mtu,
|
||||
serverPublicKey: iface.public_key,
|
||||
preSharedKey: client.pre_shared_key,
|
||||
preSharedKey: decrypt(client.pre_shared_key),
|
||||
allowedIps: client.allowed_ips,
|
||||
persistentKeepalive: client.persistent_keepalive,
|
||||
endpoint: endpoint,
|
||||
|
|
@ -317,20 +448,264 @@ const internalWireguard = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Get the WireGuard interface info
|
||||
* Create a new WireGuard Interface Endpoint
|
||||
*/
|
||||
async getInterfaceInfo(knex) {
|
||||
const iface = await this.getOrCreateInterface(knex);
|
||||
return {
|
||||
id: iface.id,
|
||||
name: iface.name,
|
||||
public_key: iface.public_key,
|
||||
ipv4_cidr: iface.ipv4_cidr,
|
||||
listen_port: iface.listen_port,
|
||||
mtu: iface.mtu,
|
||||
dns: iface.dns,
|
||||
host: iface.host,
|
||||
async createInterface(knex, data, access, accessData) {
|
||||
const existingIfaces = await knex("wg_interface").select("name", "listen_port");
|
||||
|
||||
if (existingIfaces.length >= 100) {
|
||||
throw new Error("Maximum limit of 100 WireGuard servers reached.");
|
||||
}
|
||||
|
||||
// Find the lowest available index between 0 and 99
|
||||
const usedPorts = new Set(existingIfaces.map(i => i.listen_port));
|
||||
let newIndex = 0;
|
||||
while (usedPorts.has(51820 + newIndex)) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
const name = `wg${newIndex}`;
|
||||
const listen_port = 51820 + newIndex;
|
||||
|
||||
// Attempt to grab /24 subnets, ex 10.8.0.0/24 -> 10.8.1.0/24
|
||||
const ipv4_cidr = `10.8.${newIndex}.1/24`;
|
||||
|
||||
// Generate keys
|
||||
const privateKey = await wgHelpers.generatePrivateKey();
|
||||
const publicKey = await wgHelpers.getPublicKey(privateKey);
|
||||
|
||||
const insertData = {
|
||||
name,
|
||||
private_key: encrypt(privateKey),
|
||||
public_key: publicKey,
|
||||
listen_port,
|
||||
ipv4_cidr,
|
||||
mtu: data.mtu || WG_DEFAULT_MTU,
|
||||
dns: data.dns || WG_DEFAULT_DNS,
|
||||
host: data.host || WG_HOST,
|
||||
isolate_clients: data.isolate_clients || false,
|
||||
owner_user_id: access ? access.token.getUserId(1) : 1,
|
||||
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);
|
||||
|
||||
const newIface = await knex("wg_interface").where("id", id).first();
|
||||
|
||||
// Regenerate config and restart the new interface seamlessly
|
||||
const parsed = wgHelpers.parseCIDR(newIface.ipv4_cidr);
|
||||
let configContent = wgHelpers.generateServerInterface({
|
||||
privateKey: newIface.private_key,
|
||||
address: `${parsed.firstHost}/${parsed.prefix}`,
|
||||
listenPort: newIface.listen_port,
|
||||
mtu: newIface.mtu,
|
||||
dns: null,
|
||||
postUp: newIface.post_up,
|
||||
postDown: newIface.post_down,
|
||||
});
|
||||
|
||||
fs.writeFileSync(`${WG_CONFIG_DIR}/${name}.conf`, configContent, { mode: 0o600 });
|
||||
await wgHelpers.wgUp(name);
|
||||
|
||||
return newIface;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing Interface
|
||||
*/
|
||||
async updateInterface(knex, id, data, access, accessData) {
|
||||
const query = knex("wg_interface").where("id", id);
|
||||
if (access) {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const iface = await query.first();
|
||||
if (!iface) throw new Error("Interface not found");
|
||||
|
||||
const updateData = { modified_on: knex.fn.now() };
|
||||
if (data.host !== undefined) updateData.host = data.host;
|
||||
if (data.dns !== undefined) updateData.dns = data.dns;
|
||||
if (data.mtu !== undefined) updateData.mtu = data.mtu;
|
||||
if (data.isolate_clients !== undefined) updateData.isolate_clients = data.isolate_clients;
|
||||
|
||||
await knex("wg_interface").where("id", id).update(updateData);
|
||||
|
||||
await this.saveConfig(knex); // This will re-render IPTables and sync
|
||||
return knex("wg_interface").where("id", id).first();
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an interface
|
||||
*/
|
||||
async deleteInterface(knex, id, access, accessData) {
|
||||
const query = knex("wg_interface").where("id", id);
|
||||
if (access) {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const iface = await query.first();
|
||||
if (!iface) throw new Error("Interface not found");
|
||||
|
||||
// Prevent deletion of the initial wg0 interface if it's the only one or a critical one
|
||||
if (iface.name === "wg0") {
|
||||
const otherIfaces = await knex("wg_interface").whereNot("id", id);
|
||||
if (otherIfaces.length === 0) {
|
||||
throw new Error("Cannot delete the initial wg0 interface. It is required.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await wgHelpers.wgDown(iface.name);
|
||||
if (fs.existsSync(`${WG_CONFIG_DIR}/${iface.name}.conf`)) {
|
||||
fs.unlinkSync(`${WG_CONFIG_DIR}/${iface.name}.conf`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to teardown WG interface ${iface.name}: ${e.message}`);
|
||||
}
|
||||
|
||||
// Pre-emptively Cascade delete all Clients & Partitions tied to this interface
|
||||
const clients = await knex("wg_client").where("interface_id", iface.id);
|
||||
for (const c of clients) {
|
||||
await internalWireguardFs.deleteClientDir(c.ipv4_address);
|
||||
}
|
||||
await knex("wg_client").where("interface_id", iface.id).del();
|
||||
|
||||
// Cascading deletion handles links in DB schema
|
||||
await knex("wg_interface").where("id", id).del();
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Peering Links between WireGuard Interfaces
|
||||
*/
|
||||
async updateInterfaceLinks(knex, id, linkedServers, access, accessData) {
|
||||
// Verify ownership
|
||||
const query = knex("wg_interface").where("id", id);
|
||||
if (access) {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const iface = await query.first();
|
||||
if (!iface) throw new Error("Interface not found");
|
||||
|
||||
// Clean up existing links where this interface is involved
|
||||
await knex("wg_server_link").where("interface_id_1", id).orWhere("interface_id_2", id).del();
|
||||
|
||||
// Insert new ones
|
||||
for (const peerId of linkedServers) {
|
||||
if (peerId !== Number(id)) {
|
||||
await knex("wg_server_link").insert({
|
||||
interface_id_1: id,
|
||||
interface_id_2: peerId
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.saveConfig(knex);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the WireGuard interfaces info
|
||||
*/
|
||||
async getInterfacesInfo(knex, access, accessData) {
|
||||
const query = knex("wg_interface").select("*");
|
||||
if (access) {
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
}
|
||||
const ifaces = await query;
|
||||
const allLinks = await knex("wg_server_link").select("*");
|
||||
const allClients = await knex("wg_client").select("interface_id", "ipv4_address");
|
||||
|
||||
const result = [];
|
||||
for (const i of ifaces) {
|
||||
const links = allLinks.filter(l => l.interface_id_1 === i.id || l.interface_id_2 === i.id);
|
||||
const client_count = allClients.filter(c => c.interface_id === i.id).length;
|
||||
|
||||
let storage_usage_bytes = 0;
|
||||
for (const c of allClients.filter(c => c.interface_id === i.id)) {
|
||||
storage_usage_bytes += await internalWireguardFs.getClientStorageUsage(c.ipv4_address);
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
public_key: i.public_key,
|
||||
ipv4_cidr: i.ipv4_cidr,
|
||||
listen_port: i.listen_port,
|
||||
mtu: i.mtu,
|
||||
dns: i.dns,
|
||||
host: i.host,
|
||||
isolate_clients: i.isolate_clients,
|
||||
linked_servers: links.map(l => l.interface_id_1 === i.id ? l.interface_id_2 : l.interface_id_1),
|
||||
client_count,
|
||||
storage_usage_bytes
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply or remove the client isolation iptables rule for an interface.
|
||||
* Called after every wg syncconf because PostUp/PostDown are not re-executed by syncconf.
|
||||
*/
|
||||
async syncIptablesRules(iface) {
|
||||
const name = iface.name;
|
||||
// Remove both possible same-interface rules first (idempotent)
|
||||
await execAsync(`iptables -D FORWARD -i ${name} -o ${name} -j REJECT 2>/dev/null || true`);
|
||||
await execAsync(`iptables -D FORWARD -i ${name} -o ${name} -j ACCEPT 2>/dev/null || true`);
|
||||
// Re-insert at position 1 so it appears before the wg+ DROP rule in the chain
|
||||
if (iface.isolate_clients) {
|
||||
await execAsync(`iptables -I FORWARD -i ${name} -o ${name} -j REJECT`);
|
||||
logger.info(`Client isolation enabled for ${name}`);
|
||||
} else {
|
||||
await execAsync(`iptables -I FORWARD -i ${name} -o ${name} -j ACCEPT`);
|
||||
logger.info(`Client isolation disabled for ${name}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Run TC Traffic Control QoS limits on a WireGuard Interface (Bytes per sec)
|
||||
*/
|
||||
async applyBandwidthLimits(knex, iface) {
|
||||
const clients = await knex("wg_client").where("interface_id", iface.id).where("enabled", true);
|
||||
const cmds = [];
|
||||
|
||||
// Detach old qdiscs gracefully allowing error suppression
|
||||
cmds.push(`tc qdisc del dev ${iface.name} root 2>/dev/null || true`);
|
||||
cmds.push(`tc qdisc del dev ${iface.name} ingress 2>/dev/null || true`);
|
||||
|
||||
let hasLimits = false;
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
const client = clients[i];
|
||||
if (client.tx_limit > 0 || client.rx_limit > 0) {
|
||||
if (!hasLimits) {
|
||||
cmds.push(`tc qdisc add dev ${iface.name} root handle 1: htb default 10`);
|
||||
cmds.push(`tc class add dev ${iface.name} parent 1: classid 1:1 htb rate 10gbit`);
|
||||
cmds.push(`tc qdisc add dev ${iface.name} handle ffff: ingress`);
|
||||
hasLimits = true;
|
||||
}
|
||||
|
||||
const mark = i + 10;
|
||||
// client.rx_limit (Server -> Client = Download = root qdisc TX) - Rate is Bytes/sec so mult by 8 -> bits, /1000 -> Kbits
|
||||
if (client.rx_limit > 0) {
|
||||
const rateKbit = Math.floor((client.rx_limit * 8) / 1000);
|
||||
cmds.push(`tc class add dev ${iface.name} parent 1:1 classid 1:${mark} htb rate ${rateKbit}kbit`);
|
||||
cmds.push(`tc filter add dev ${iface.name} protocol ip parent 1:0 prio 1 u32 match ip dst ${client.ipv4_address}/32 flowid 1:${mark}`);
|
||||
}
|
||||
|
||||
// client.tx_limit (Client -> Server = Upload = ingress qdisc RX)
|
||||
if (client.tx_limit > 0) {
|
||||
const rateKbit = Math.floor((client.tx_limit * 8) / 1000);
|
||||
cmds.push(`tc filter add dev ${iface.name} parent ffff: protocol ip u32 match ip src ${client.ipv4_address}/32 police rate ${rateKbit}kbit burst 1m drop flowid :1`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLimits) {
|
||||
await execAsync(cmds.join(" && "));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -356,6 +731,45 @@ const internalWireguard = {
|
|||
if (needsSave) {
|
||||
await this.saveConfig(knex);
|
||||
}
|
||||
|
||||
// Audit Logging Polling
|
||||
const ifaces = await knex("wg_interface").select("name");
|
||||
const allClients = await knex("wg_client").select("id", "public_key", "name", "owner_user_id");
|
||||
|
||||
for (const iface of ifaces) {
|
||||
try {
|
||||
const dump = await wgHelpers.wgDump(iface.name);
|
||||
for (const peer of dump) {
|
||||
const client = allClients.find((c) => c.public_key === peer.publicKey);
|
||||
if (client) {
|
||||
const lastHandshakeTime = new Date(peer.latestHandshakeAt).getTime();
|
||||
const wasConnected = connectionMemoryMap[client.id] || false;
|
||||
const isConnected = lastHandshakeTime > 0 && (Date.now() - lastHandshakeTime < 3 * 60 * 1000);
|
||||
|
||||
if (isConnected && !wasConnected) {
|
||||
connectionMemoryMap[client.id] = true;
|
||||
// Log connection (dummy token signature for audit logic)
|
||||
internalAuditLog.add({ token: { getUserId: () => client.owner_user_id } }, {
|
||||
action: "connected",
|
||||
meta: { message: `WireGuard client ${client.name} came online.` },
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id
|
||||
}).catch(()=>{});
|
||||
} else if (!isConnected && wasConnected) {
|
||||
connectionMemoryMap[client.id] = false;
|
||||
// Log disconnection
|
||||
internalAuditLog.add({ token: { getUserId: () => client.owner_user_id } }, {
|
||||
action: "disconnected",
|
||||
meta: { message: `WireGuard client ${client.name} went offline or drifted past TTL.` },
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id
|
||||
}).catch(()=>{});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.error("WireGuard cron job error:", err.message);
|
||||
}
|
||||
|
|
|
|||
96
backend/lib/crypto.js
Normal file
96
backend/lib/crypto.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Application-layer AES-256-GCM encryption for sensitive DB fields.
|
||||
*
|
||||
* Encrypted format stored in DB:
|
||||
* enc:<base64(iv)>:<base64(authTag)>:<base64(ciphertext)>
|
||||
*
|
||||
* Set DB_ENCRYPTION_KEY to a 64-char hex string (32 bytes):
|
||||
* export DB_ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
*
|
||||
* If DB_ENCRYPTION_KEY is not set, values are stored as plaintext (with warning).
|
||||
*/
|
||||
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
||||
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
const IV_LENGTH = 12; // 96-bit IV recommended for GCM
|
||||
const TAG_LENGTH = 16; // 128-bit auth tag
|
||||
const ENC_PREFIX = "enc:";
|
||||
|
||||
let _key = null;
|
||||
let _warned = false;
|
||||
|
||||
function getKey() {
|
||||
if (_key) return _key;
|
||||
const hex = process.env.DB_ENCRYPTION_KEY;
|
||||
if (!hex) {
|
||||
if (!_warned) {
|
||||
console.warn("[crypto] WARNING: DB_ENCRYPTION_KEY is not set. WireGuard private keys are stored as PLAINTEXT in the database.");
|
||||
_warned = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (hex.length !== 64) {
|
||||
throw new Error("DB_ENCRYPTION_KEY must be a 64-character hex string (32 bytes). Generate with: openssl rand -hex 32");
|
||||
}
|
||||
_key = Buffer.from(hex, "hex");
|
||||
return _key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a plaintext string.
|
||||
* Returns the encrypted string with "enc:" prefix, or the original value if no key is set.
|
||||
* @param {string} plaintext
|
||||
* @returns {string}
|
||||
*/
|
||||
export function encrypt(plaintext) {
|
||||
if (!plaintext) return plaintext;
|
||||
// Already encrypted — idempotent
|
||||
if (plaintext.startsWith(ENC_PREFIX)) return plaintext;
|
||||
|
||||
const key = getKey();
|
||||
if (!key) return plaintext; // passthrough if no key configured
|
||||
|
||||
const iv = randomBytes(IV_LENGTH);
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: TAG_LENGTH });
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(plaintext, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return `${ENC_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an encrypted string.
|
||||
* Returns plaintext, or the original value if it is not encrypted.
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function decrypt(value) {
|
||||
if (!value) return value;
|
||||
// Not encrypted — return as-is (backward compat with plaintext rows)
|
||||
if (!value.startsWith(ENC_PREFIX)) return value;
|
||||
|
||||
const key = getKey();
|
||||
if (!key) {
|
||||
throw new Error("DB_ENCRYPTION_KEY is required to decrypt WireGuard keys but is not set.");
|
||||
}
|
||||
|
||||
const parts = value.slice(ENC_PREFIX.length).split(":");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid encrypted value format in database.");
|
||||
}
|
||||
|
||||
const [ivB64, tagB64, dataB64] = parts;
|
||||
const iv = Buffer.from(ivB64, "base64");
|
||||
const tag = Buffer.from(tagB64, "base64");
|
||||
const data = Buffer.from(dataB64,"base64");
|
||||
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: TAG_LENGTH });
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8");
|
||||
}
|
||||
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");
|
||||
});
|
||||
}
|
||||
35
backend/migrations/20260310000000_wireguard_owner.js
Normal file
35
backend/migrations/20260310000000_wireguard_owner.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Migration to add owner_user_id to WireGuard tables for user-based data isolation
|
||||
*/
|
||||
export async function up(knex) {
|
||||
// 1. Add owner_user_id to wg_interface
|
||||
await knex.schema.alterTable("wg_interface", (table) => {
|
||||
table.integer("owner_user_id").unsigned().nullable();
|
||||
});
|
||||
|
||||
// 2. Add owner_user_id to wg_client
|
||||
await knex.schema.alterTable("wg_client", (table) => {
|
||||
table.integer("owner_user_id").unsigned().nullable();
|
||||
});
|
||||
|
||||
// 3. Backfill existing rows with admin user (id=1)
|
||||
await knex("wg_interface").whereNull("owner_user_id").update({ owner_user_id: 1 });
|
||||
await knex("wg_client").whereNull("owner_user_id").update({ owner_user_id: 1 });
|
||||
|
||||
// 4. Make columns not nullable
|
||||
await knex.schema.alterTable("wg_interface", (table) => {
|
||||
table.integer("owner_user_id").unsigned().notNullable().defaultTo(1).alter();
|
||||
});
|
||||
await knex.schema.alterTable("wg_client", (table) => {
|
||||
table.integer("owner_user_id").unsigned().notNullable().defaultTo(1).alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
await knex.schema.alterTable("wg_client", (table) => {
|
||||
table.dropColumn("owner_user_id");
|
||||
});
|
||||
await knex.schema.alterTable("wg_interface", (table) => {
|
||||
table.dropColumn("owner_user_id");
|
||||
});
|
||||
}
|
||||
18
backend/migrations/20260310000001_wireguard_quotas.js
Normal file
18
backend/migrations/20260310000001_wireguard_quotas.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export const up = function (knex) {
|
||||
return knex.schema.alterTable("wg_client", (table) => {
|
||||
// Traffic Bandwidth Limits (0 = Unlimited)
|
||||
table.bigInteger("tx_limit").notNull().defaultTo(0);
|
||||
table.bigInteger("rx_limit").notNull().defaultTo(0);
|
||||
|
||||
// Disk Partition Ceiling Quota Configuration in Megabytes
|
||||
table.integer("storage_limit_mb").notNull().defaultTo(500);
|
||||
});
|
||||
};
|
||||
|
||||
export const down = function (knex) {
|
||||
return knex.schema.alterTable("wg_client", (table) => {
|
||||
table.dropColumn("tx_limit");
|
||||
table.dropColumn("rx_limit");
|
||||
table.dropColumn("storage_limit_mb");
|
||||
});
|
||||
};
|
||||
61
backend/migrations/20260319000000_wireguard_encrypt_keys.js
Normal file
61
backend/migrations/20260319000000_wireguard_encrypt_keys.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* Data migration: encrypts existing plaintext WireGuard private keys
|
||||
* and pre-shared keys in the database.
|
||||
*
|
||||
* This migration is safe to run multiple times (idempotent):
|
||||
* already-encrypted values (prefix "enc:") are skipped.
|
||||
*
|
||||
* Requires DB_ENCRYPTION_KEY to be set in the environment.
|
||||
* If not set, migration logs a warning and exits without modifying data.
|
||||
*/
|
||||
|
||||
import { encrypt } from "../lib/crypto.js";
|
||||
|
||||
const migrate_name = "wireguard_encrypt_keys";
|
||||
|
||||
export async function up(knex) {
|
||||
const key = process.env.DB_ENCRYPTION_KEY;
|
||||
if (!key) {
|
||||
console.warn(`[${migrate_name}] DB_ENCRYPTION_KEY not set — skipping encryption migration. Keys remain as plaintext.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[${migrate_name}] Encrypting existing WireGuard keys...`);
|
||||
|
||||
// --- wg_interface: encrypt private_key ---
|
||||
const ifaces = await knex("wg_interface").select("id", "private_key");
|
||||
let ifaceCount = 0;
|
||||
for (const iface of ifaces) {
|
||||
if (!iface.private_key || iface.private_key.startsWith("enc:")) continue;
|
||||
await knex("wg_interface").where("id", iface.id).update({
|
||||
private_key: encrypt(iface.private_key),
|
||||
});
|
||||
ifaceCount++;
|
||||
}
|
||||
console.log(`[${migrate_name}] wg_interface: ${ifaceCount} rows encrypted.`);
|
||||
|
||||
// --- wg_client: encrypt private_key + pre_shared_key ---
|
||||
const clients = await knex("wg_client").select("id", "private_key", "pre_shared_key");
|
||||
let clientCount = 0;
|
||||
for (const client of clients) {
|
||||
const updates = {};
|
||||
if (client.private_key && !client.private_key.startsWith("enc:")) {
|
||||
updates.private_key = encrypt(client.private_key);
|
||||
}
|
||||
if (client.pre_shared_key && !client.pre_shared_key.startsWith("enc:")) {
|
||||
updates.pre_shared_key = encrypt(client.pre_shared_key);
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await knex("wg_client").where("id", client.id).update(updates);
|
||||
clientCount++;
|
||||
}
|
||||
}
|
||||
console.log(`[${migrate_name}] wg_client: ${clientCount} rows encrypted.`);
|
||||
console.log(`[${migrate_name}] Done.`);
|
||||
}
|
||||
|
||||
export async function down(knex) {
|
||||
// Intentionally a no-op: decrypting back to plaintext would require the key,
|
||||
// and rolling back encryption is a security risk.
|
||||
console.warn(`[${migrate_name}] down() is a no-op. Keys remain encrypted.`);
|
||||
}
|
||||
5810
backend/package-lock.json
generated
Normal file
5810
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,9 @@
|
|||
"compression": "^1.8.1",
|
||||
"express": "^5.2.1",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"gravatar": "^1.8.2",
|
||||
"helmet": "^8.0.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"knex": "3.1.0",
|
||||
"liquidjs": "10.24.0",
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
"proxy-agent": "^6.5.0",
|
||||
"signale": "1.4.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"systeminformation": "^5.31.3",
|
||||
"temp-write": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
63
backend/routes/api/database.js
Normal file
63
backend/routes/api/database.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import express from "express";
|
||||
import internalDatabase from "../../internal/database.js";
|
||||
import jwtdecode from "../../lib/express/jwt-decode.js";
|
||||
import { debug, express as logger } from "../../logger.js";
|
||||
|
||||
const router = express.Router({
|
||||
caseSensitive: true,
|
||||
strict: true,
|
||||
mergeParams: true,
|
||||
});
|
||||
|
||||
router.use(jwtdecode());
|
||||
|
||||
/**
|
||||
* Middleware to strictly ensure only Super Admins can access this route
|
||||
*/
|
||||
const requireSuperAdmin = async (req, res, next) => {
|
||||
try {
|
||||
const accessData = await res.locals.access.can("proxy_hosts:list");
|
||||
if (!accessData || accessData.permission_visibility !== "all") {
|
||||
return res.status(403).json({ error: { message: "Forbidden: Super Admin only" } });
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
router.use(requireSuperAdmin);
|
||||
|
||||
/**
|
||||
* GET /api/database/tables
|
||||
* List all tables in the database
|
||||
*/
|
||||
router.get("/tables", async (req, res, next) => {
|
||||
try {
|
||||
const tables = await internalDatabase.getTables();
|
||||
res.status(200).json(tables);
|
||||
} catch (err) {
|
||||
debug(logger, `GET ${req.path} error: ${err.message}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/database/tables/:name
|
||||
* Get table schema and data rows
|
||||
*/
|
||||
router.get("/tables/:name", async (req, res, next) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit, 10) || 50;
|
||||
const offset = parseInt(req.query.offset, 10) || 0;
|
||||
const name = req.params.name;
|
||||
|
||||
const data = await internalDatabase.getTableData(name, limit, offset);
|
||||
res.status(200).json(data);
|
||||
} catch (err) {
|
||||
debug(logger, `GET ${req.path} error: ${err.message}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -16,6 +16,7 @@ import tokensRoutes from "./tokens.js";
|
|||
import usersRoutes from "./users.js";
|
||||
import versionRoutes from "./version.js";
|
||||
import wireguardRoutes from "./wireguard.js";
|
||||
import databaseRoutes from "./api/database.js";
|
||||
|
||||
const router = express.Router({
|
||||
caseSensitive: true,
|
||||
|
|
@ -56,6 +57,7 @@ router.use("/nginx/streams", streamsRoutes);
|
|||
router.use("/nginx/access-lists", accessListsRoutes);
|
||||
router.use("/nginx/certificates", certificatesHostsRoutes);
|
||||
router.use("/wireguard", wireguardRoutes);
|
||||
router.use("/database", databaseRoutes);
|
||||
|
||||
/**
|
||||
* API 404 for all other routes
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import si from "systeminformation";
|
||||
import express from "express";
|
||||
import internalReport from "../internal/report.js";
|
||||
import jwtdecode from "../lib/express/jwt-decode.js";
|
||||
|
|
@ -29,4 +30,47 @@ router
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /reports/system
|
||||
*/
|
||||
router
|
||||
.route("/system")
|
||||
.options((_, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
.get(async (req, res, next) => {
|
||||
try {
|
||||
const [cpuTotal, memData, networkStats, fsData] = await Promise.all([
|
||||
si.currentLoad(),
|
||||
si.mem(),
|
||||
si.networkStats("*"),
|
||||
si.fsSize()
|
||||
]);
|
||||
|
||||
// Grab eth0 or the first active interface
|
||||
const activeNet = networkStats.find(n => n.operstate === 'up' && n.iface !== 'lo') || networkStats[0] || {};
|
||||
|
||||
// Summarize all detected physical drives to find total storage and used storage
|
||||
const totalStorage = fsData.reduce((acc, disk) => acc + (disk.size || 0), 0);
|
||||
const usedStorage = fsData.reduce((acc, disk) => acc + (disk.used || 0), 0);
|
||||
const storagePercent = totalStorage > 0 ? Math.round((usedStorage / totalStorage) * 100) : 0;
|
||||
|
||||
res.status(200).json({
|
||||
cpu: Math.round(cpuTotal.currentLoad),
|
||||
memory: Math.round((memData.active / memData.total) * 100),
|
||||
memoryTotal: memData.total,
|
||||
memoryActive: memData.active,
|
||||
storage: storagePercent,
|
||||
storageTotal: totalStorage,
|
||||
storageUsed: usedStorage,
|
||||
networkRx: (activeNet.rx_sec / 1024 / 1024 * 8).toFixed(2), // Mbps
|
||||
networkTx: (activeNet.tx_sec / 1024 / 1024 * 8).toFixed(2), // Mbps
|
||||
});
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
150
backend/routes/wg_public.js
Normal file
150
backend/routes/wg_public.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import express from "express";
|
||||
import internalWireguardFs from "../internal/wireguard-fs.js";
|
||||
import db from "../db.js";
|
||||
|
||||
const router = express.Router({
|
||||
caseSensitive: true,
|
||||
strict: true,
|
||||
mergeParams: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Authenticate WireGuard client by tunnel remote socket IP
|
||||
*/
|
||||
const authenticateWgClientIp = async (req, res, next) => {
|
||||
let clientIp = req.headers["x-forwarded-for"] || req.socket.remoteAddress || req.ip;
|
||||
if (clientIp) {
|
||||
if (clientIp.includes("::ffff:")) {
|
||||
clientIp = clientIp.split("::ffff:")[1];
|
||||
}
|
||||
clientIp = clientIp.split(',')[0].trim();
|
||||
}
|
||||
|
||||
if (!clientIp) {
|
||||
return res.status(401).json({ error: { message: "Unknown remote IP address" } });
|
||||
}
|
||||
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await knex("wg_client").where("ipv4_address", clientIp).first();
|
||||
if (!client) {
|
||||
return res.status(401).json({ error: { message: `Unauthorized: IP ${clientIp} does not match any registered WireGuard Client in the Hub Database` } });
|
||||
}
|
||||
|
||||
req.wgClient = client;
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
router.use(authenticateWgClientIp);
|
||||
|
||||
/**
|
||||
* GET /api/wg-public/me
|
||||
* Returns connection metrics and identity details dynamically mapped to this IP
|
||||
*/
|
||||
router.get("/me", async (req, res, next) => {
|
||||
try {
|
||||
const totalStorageBytes = await internalWireguardFs.getClientStorageUsage(req.wgClient.ipv4_address);
|
||||
res.status(200).json({
|
||||
id: req.wgClient.id,
|
||||
name: req.wgClient.name,
|
||||
ipv4_address: req.wgClient.ipv4_address,
|
||||
enabled: !!req.wgClient.enabled,
|
||||
rx_limit: req.wgClient.rx_limit,
|
||||
tx_limit: req.wgClient.tx_limit,
|
||||
storage_limit_mb: req.wgClient.storage_limit_mb,
|
||||
transfer_rx: req.wgClient.transfer_rx,
|
||||
transfer_tx: req.wgClient.transfer_tx,
|
||||
storage_usage_bytes: totalStorageBytes
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wg-public/files
|
||||
* Fetch encrypted files directory securely
|
||||
*/
|
||||
router.get("/files", async (req, res, next) => {
|
||||
try {
|
||||
const files = await internalWireguardFs.listFiles(req.wgClient.ipv4_address);
|
||||
res.status(200).json(files);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") return res.status(200).json([]);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/wg-public/files
|
||||
* Upload directly into backend AES storage limits
|
||||
*/
|
||||
router.post("/files", async (req, res, next) => {
|
||||
try {
|
||||
if (!req.files || !req.files.file) {
|
||||
return res.status(400).json({ error: { message: "No file provided" } });
|
||||
}
|
||||
const file = req.files.file;
|
||||
|
||||
if (req.wgClient.storage_limit_mb > 0) {
|
||||
const existingStorage = await internalWireguardFs.getClientStorageUsage(req.wgClient.ipv4_address);
|
||||
if (existingStorage + file.size > req.wgClient.storage_limit_mb * 1024 * 1024) {
|
||||
return res.status(413).json({ error: { message: "Storage Quota Exceeded limits assigned by Administrator" } });
|
||||
}
|
||||
}
|
||||
|
||||
await internalWireguardFs.uploadFile(req.wgClient.ipv4_address, req.wgClient.pre_shared_key, file.name, file.data);
|
||||
res.status(200).json({ success: true, message: "File encrypted and saved safely via your Wireguard IP Auth!" });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wg-public/files/:filename
|
||||
* Decrypt stream
|
||||
*/
|
||||
router.get("/files/:filename", async (req, res, next) => {
|
||||
try {
|
||||
const filename = req.params.filename;
|
||||
await internalWireguardFs.downloadFile(req.wgClient.ipv4_address, req.wgClient.pre_shared_key, filename, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/wg-public/files/:filename
|
||||
* Rename an encrypted file
|
||||
*/
|
||||
router.patch("/files/:filename", async (req, res, next) => {
|
||||
try {
|
||||
const oldName = req.params.filename;
|
||||
const newName = req.body?.name?.trim();
|
||||
if (!newName) {
|
||||
return res.status(400).json({ error: { message: "New file name is required" } });
|
||||
}
|
||||
await internalWireguardFs.renameFile(req.wgClient.ipv4_address, oldName, newName);
|
||||
res.status(200).json({ success: true, message: "File renamed successfully" });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/wg-public/files/:filename
|
||||
*/
|
||||
router.delete("/files/:filename", async (req, res, next) => {
|
||||
try {
|
||||
const filename = req.params.filename;
|
||||
await internalWireguardFs.deleteFile(req.wgClient.ipv4_address, filename);
|
||||
res.status(200).json({ success: true, message: "Destroyed safely" });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import express from "express";
|
||||
import archiver from "archiver";
|
||||
import internalWireguard from "../internal/wireguard.js";
|
||||
import internalWireguardFs from "../internal/wireguard-fs.js";
|
||||
import internalAuditLog from "../internal/audit-log.js";
|
||||
import jwtdecode from "../lib/express/jwt-decode.js";
|
||||
import db from "../db.js";
|
||||
|
||||
const router = express.Router({
|
||||
|
|
@ -8,28 +12,180 @@ const router = express.Router({
|
|||
mergeParams: true,
|
||||
});
|
||||
|
||||
// Protect all WireGuard routes
|
||||
router.use(jwtdecode());
|
||||
|
||||
/**
|
||||
* GET /api/wireguard
|
||||
* Get WireGuard interface info
|
||||
* Get WireGuard interfaces info
|
||||
*/
|
||||
router.get("/", async (req, res, next) => {
|
||||
router.get("/", async (_req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const iface = await internalWireguard.getInterfaceInfo(knex);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:list");
|
||||
const ifaces = await internalWireguard.getInterfacesInfo(knex, access, accessData);
|
||||
res.status(200).json(ifaces);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wireguard/dashboard
|
||||
* Aggregated analytics for the main dashboard
|
||||
*/
|
||||
router.get("/dashboard", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:list");
|
||||
|
||||
const clients = await internalWireguard.getClients(knex, access, accessData);
|
||||
|
||||
let totalStorageBytes = 0;
|
||||
let totalTransferRx = 0;
|
||||
let totalTransferTx = 0;
|
||||
let online24h = 0;
|
||||
let online7d = 0;
|
||||
let online30d = 0;
|
||||
|
||||
const now = Date.now();
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
for (const client of clients) {
|
||||
try {
|
||||
totalStorageBytes += await internalWireguardFs.getClientStorageUsage(client.ipv4_address);
|
||||
} catch (_) {}
|
||||
|
||||
totalTransferRx += parseInt(client.transfer_rx || 0, 10);
|
||||
totalTransferTx += parseInt(client.transfer_tx || 0, 10);
|
||||
|
||||
if (client.latest_handshake_at) {
|
||||
const handshakeStr = String(client.latest_handshake_at);
|
||||
let handshakeTime = Date.parse(handshakeStr);
|
||||
|
||||
// Handle 0 or invalid epoch
|
||||
if (handshakeTime > 0) {
|
||||
if (now - handshakeTime <= DAY) online24h++;
|
||||
if (now - handshakeTime <= 7 * DAY) online7d++;
|
||||
if (now - handshakeTime <= 30 * DAY) online30d++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
totalStorageBytes,
|
||||
totalTransferRx,
|
||||
totalTransferTx,
|
||||
online24h,
|
||||
online7d,
|
||||
online30d,
|
||||
totalClients: clients.length
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/wireguard
|
||||
* Create a new WireGuard interface
|
||||
*/
|
||||
router.post("/", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:create");
|
||||
const iface = await internalWireguard.createInterface(knex, req.body, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "created",
|
||||
object_type: "wireguard-server",
|
||||
object_id: iface.id,
|
||||
meta: req.body,
|
||||
});
|
||||
res.status(201).json(iface);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/wireguard/:id
|
||||
* Update a WireGuard interface
|
||||
*/
|
||||
router.put("/:id", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "updated",
|
||||
object_type: "wireguard-server",
|
||||
object_id: iface.id,
|
||||
meta: req.body,
|
||||
});
|
||||
res.status(200).json(iface);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/wireguard/:id
|
||||
* Delete a WireGuard interface
|
||||
*/
|
||||
router.delete("/:id", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:delete");
|
||||
const result = await internalWireguard.deleteInterface(knex, req.params.id, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "deleted",
|
||||
object_type: "wireguard-server",
|
||||
object_id: req.params.id,
|
||||
meta: {},
|
||||
});
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/wireguard/:id/links
|
||||
* Update peering links for a WireGuard interface
|
||||
*/
|
||||
router.post("/:id/links", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || [], access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "updated",
|
||||
object_type: "wireguard-server-links",
|
||||
object_id: req.params.id,
|
||||
meta: req.body,
|
||||
});
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:list");
|
||||
const clients = await internalWireguard.getClients(knex, access, accessData);
|
||||
res.status(200).json(clients);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
@ -43,7 +199,15 @@ router.get("/client", async (req, res, next) => {
|
|||
router.post("/client", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await internalWireguard.createClient(knex, req.body);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:create");
|
||||
const client = await internalWireguard.createClient(knex, req.body, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "created",
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id,
|
||||
meta: req.body,
|
||||
});
|
||||
res.status(201).json(client);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
@ -57,7 +221,13 @@ router.post("/client", async (req, res, next) => {
|
|||
router.get("/client/:id", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await knex("wg_client").where("id", req.params.id).first();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
|
@ -74,7 +244,15 @@ router.get("/client/:id", async (req, res, next) => {
|
|||
router.put("/client/:id", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await internalWireguard.updateClient(knex, req.params.id, req.body);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const client = await internalWireguard.updateClient(knex, req.params.id, req.body, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "updated",
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id,
|
||||
meta: req.body,
|
||||
});
|
||||
res.status(200).json(client);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
@ -88,7 +266,15 @@ router.put("/client/:id", async (req, res, next) => {
|
|||
router.delete("/client/:id", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const result = await internalWireguard.deleteClient(knex, req.params.id);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:delete");
|
||||
const result = await internalWireguard.deleteClient(knex, req.params.id, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "deleted",
|
||||
object_type: "wireguard-client",
|
||||
object_id: req.params.id,
|
||||
meta: {},
|
||||
});
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
@ -102,7 +288,15 @@ router.delete("/client/:id", async (req, res, next) => {
|
|||
router.post("/client/:id/enable", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await internalWireguard.toggleClient(knex, req.params.id, true);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const client = await internalWireguard.toggleClient(knex, req.params.id, true, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "enabled",
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id,
|
||||
meta: {},
|
||||
});
|
||||
res.status(200).json(client);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
@ -116,7 +310,15 @@ router.post("/client/:id/enable", async (req, res, next) => {
|
|||
router.post("/client/:id/disable", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await internalWireguard.toggleClient(knex, req.params.id, false);
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const client = await internalWireguard.toggleClient(knex, req.params.id, false, access, accessData);
|
||||
await internalAuditLog.add(access, {
|
||||
action: "disabled",
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id,
|
||||
meta: {},
|
||||
});
|
||||
res.status(200).json(client);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
|
@ -130,7 +332,13 @@ router.post("/client/:id/disable", async (req, res, next) => {
|
|||
router.get("/client/:id/configuration", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const client = await knex("wg_client").where("id", req.params.id).first();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
|
@ -151,6 +359,16 @@ router.get("/client/:id/configuration", async (req, res, next) => {
|
|||
router.get("/client/:id/qrcode.svg", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
const svg = await internalWireguard.getClientQRCode(knex, req.params.id);
|
||||
res.set("Content-Type", "image/svg+xml");
|
||||
res.status(200).send(svg);
|
||||
|
|
@ -159,4 +377,233 @@ router.get("/client/:id/qrcode.svg", async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wireguard/client/:id/configuration.zip
|
||||
* Download WireGuard client configuration as a ZIP archive
|
||||
*/
|
||||
router.get("/client/:id/configuration.zip", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
const configStr = await internalWireguard.getClientConfiguration(knex, req.params.id);
|
||||
const svgStr = await internalWireguard.getClientQRCode(knex, req.params.id);
|
||||
const safeName = client.name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32);
|
||||
|
||||
res.set("Content-Disposition", `attachment; filename="${safeName}.zip"`);
|
||||
res.set("Content-Type", "application/zip");
|
||||
|
||||
const archive = archiver("zip", { zlib: { level: 9 } });
|
||||
archive.on("error", (err) => next(err));
|
||||
archive.pipe(res);
|
||||
|
||||
archive.append(configStr, { name: `${safeName}.conf` });
|
||||
archive.append(svgStr, { name: `${safeName}-qrcode.svg` });
|
||||
|
||||
await archive.finalize();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wireguard/client/:id/storage
|
||||
* Get storage usage for a client
|
||||
*/
|
||||
router.get("/client/:id/storage", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
const totalBytes = await internalWireguardFs.getClientStorageUsage(client.ipv4_address);
|
||||
res.status(200).json({ totalBytes, limitMb: client.storage_limit_mb });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wireguard/client/:id/logs
|
||||
* Get connection history logs for a client
|
||||
*/
|
||||
router.get("/client/:id/logs", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
const logs = await knex("audit_log")
|
||||
.where("object_type", "wireguard-client")
|
||||
.andWhere("object_id", req.params.id)
|
||||
.orderBy("created_on", "desc")
|
||||
.limit(100);
|
||||
|
||||
res.status(200).json(logs);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wireguard/client/:id/files
|
||||
* List files for a client
|
||||
*/
|
||||
router.get("/client/:id/files", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
const files = await internalWireguardFs.listFiles(client.ipv4_address);
|
||||
res.status(200).json(files);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/wireguard/client/:id/files
|
||||
* Upload an encrypted file for a client
|
||||
*/
|
||||
router.post("/client/:id/files", async (req, res, next) => {
|
||||
try {
|
||||
if (!req.files || !req.files.file) {
|
||||
return res.status(400).json({ error: { message: "No file uploaded" } });
|
||||
}
|
||||
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
const uploadedFile = req.files.file;
|
||||
|
||||
// Enforce Storage Quota if not unlimited (0)
|
||||
if (client.storage_limit_mb > 0) {
|
||||
const currentUsageBytes = await internalWireguardFs.getClientStorageUsage(client.ipv4_address);
|
||||
const requestedSize = uploadedFile.size;
|
||||
const maxBytes = client.storage_limit_mb * 1024 * 1024;
|
||||
|
||||
if (currentUsageBytes + requestedSize > maxBytes) {
|
||||
return res.status(413).json({
|
||||
error: {
|
||||
message: `Storage Quota Exceeded. Maximum allowed: ${client.storage_limit_mb} MB.`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await internalWireguardFs.uploadFile(client.ipv4_address, client.private_key, uploadedFile.name, uploadedFile.data);
|
||||
|
||||
await internalAuditLog.add(access, {
|
||||
action: "uploaded-file",
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id,
|
||||
meta: { filename: uploadedFile.name }
|
||||
});
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/wireguard/client/:id/files/:filename
|
||||
* Download a decrypted file for a client
|
||||
*/
|
||||
router.get("/client/:id/files/:filename", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:get");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
await internalWireguardFs.downloadFile(client.ipv4_address, client.private_key, req.params.filename, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/wireguard/client/:id/files/:filename
|
||||
* Delete a file for a client
|
||||
*/
|
||||
router.delete("/client/:id/files/:filename", async (req, res, next) => {
|
||||
try {
|
||||
const knex = db();
|
||||
const access = res.locals.access;
|
||||
const accessData = await access.can("proxy_hosts:update");
|
||||
const query = knex("wg_client").where("id", req.params.id);
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
||||
}
|
||||
const client = await query.first();
|
||||
if (!client) {
|
||||
return res.status(404).json({ error: { message: "Client not found" } });
|
||||
}
|
||||
|
||||
const result = await internalWireguardFs.deleteFile(client.ipv4_address, req.params.filename);
|
||||
|
||||
await internalAuditLog.add(access, {
|
||||
action: "deleted-file",
|
||||
object_type: "wireguard-client",
|
||||
object_id: client.id,
|
||||
meta: { filename: req.params.filename }
|
||||
});
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
116
blog-starter/.forgejo/workflows/create-post.yml
Normal file
116
blog-starter/.forgejo/workflows/create-post.yml
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
name: Create Blog Post
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
title:
|
||||
description: "Post title"
|
||||
required: true
|
||||
type: string
|
||||
slug:
|
||||
description: "URL slug, for example my-new-post"
|
||||
required: true
|
||||
type: string
|
||||
summary:
|
||||
description: "Short summary"
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
description: "Comma-separated tags"
|
||||
required: false
|
||||
type: string
|
||||
categories:
|
||||
description: "Comma-separated categories"
|
||||
required: false
|
||||
type: string
|
||||
draft:
|
||||
description: "Create as draft"
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
create-post:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate post file
|
||||
env:
|
||||
INPUT_TITLE: ${{ inputs.title }}
|
||||
INPUT_SLUG: ${{ inputs.slug }}
|
||||
INPUT_SUMMARY: ${{ inputs.summary }}
|
||||
INPUT_TAGS: ${{ inputs.tags }}
|
||||
INPUT_CATEGORIES: ${{ inputs.categories }}
|
||||
INPUT_DRAFT: ${{ inputs.draft }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
def split_csv(value: str):
|
||||
if not value:
|
||||
return []
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
|
||||
title = os.environ["INPUT_TITLE"].strip()
|
||||
slug = os.environ["INPUT_SLUG"].strip().lower()
|
||||
summary = os.environ.get("INPUT_SUMMARY", "").strip()
|
||||
draft = os.environ.get("INPUT_DRAFT", "true").strip().lower() == "true"
|
||||
tags = split_csv(os.environ.get("INPUT_TAGS", ""))
|
||||
categories = split_csv(os.environ.get("INPUT_CATEGORIES", ""))
|
||||
|
||||
if not title:
|
||||
raise SystemExit("Title is required")
|
||||
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", slug):
|
||||
raise SystemExit("Slug must use lowercase letters, numbers, and hyphens only")
|
||||
|
||||
content_dir = Path("content/posts")
|
||||
content_dir.mkdir(parents=True, exist_ok=True)
|
||||
target = content_dir / f"{slug}.md"
|
||||
if target.exists():
|
||||
raise SystemExit(f"Post already exists: {target}")
|
||||
|
||||
now = datetime.now(timezone.utc).astimezone().replace(microsecond=0).isoformat()
|
||||
|
||||
def format_array(values):
|
||||
if not values:
|
||||
return "[]"
|
||||
return "[{}]".format(", ".join(f'\"{value}\"' for value in values))
|
||||
|
||||
escaped_title = title.replace('"', '\\"')
|
||||
escaped_summary = summary.replace('"', '\\"')
|
||||
|
||||
body = f"""++++
|
||||
title = \"{escaped_title}\"
|
||||
date = {now}
|
||||
draft = {\"true\" if draft else \"false\"}
|
||||
slug = \"{slug}\"
|
||||
summary = \"{escaped_summary}\"
|
||||
tags = {format_array(tags)}
|
||||
categories = {format_array(categories)}
|
||||
++++
|
||||
|
||||
Write your post here.
|
||||
"""
|
||||
|
||||
target.write_text(body, encoding="utf-8")
|
||||
print(target)
|
||||
PY
|
||||
|
||||
- name: Commit new post
|
||||
env:
|
||||
POST_SLUG: ${{ inputs.slug }}
|
||||
run: |
|
||||
git config user.name "forgejo-actions"
|
||||
git config user.email "forgejo-actions@localhost"
|
||||
git add "content/posts/${POST_SLUG}.md"
|
||||
git commit -m "Create post: ${POST_SLUG}"
|
||||
git push
|
||||
72
blog-starter/.forgejo/workflows/deploy.yml
Normal file
72
blog-starter/.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
name: Deploy Blog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl git rsync openssh-client tar
|
||||
|
||||
- name: Install Hugo Extended
|
||||
env:
|
||||
HUGO_VERSION: "0.145.0"
|
||||
run: |
|
||||
curl -fsSL -o hugo.deb \
|
||||
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
|
||||
sudo dpkg -i hugo.deb
|
||||
hugo version
|
||||
|
||||
- name: Bootstrap LoveIt theme
|
||||
run: |
|
||||
chmod +x scripts/bootstrap-theme.sh
|
||||
./scripts/bootstrap-theme.sh
|
||||
|
||||
- name: Build site
|
||||
run: |
|
||||
hugo --gc --minify
|
||||
test -f public/index.html
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
BLOG_DEPLOY_KEY: ${{ secrets.BLOG_DEPLOY_KEY }}
|
||||
BLOG_DEPLOY_KNOWN_HOSTS: ${{ secrets.BLOG_DEPLOY_KNOWN_HOSTS }}
|
||||
run: |
|
||||
install -d -m 700 ~/.ssh
|
||||
printf '%s\n' "$BLOG_DEPLOY_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
if [ -n "${BLOG_DEPLOY_KNOWN_HOSTS}" ]; then
|
||||
printf '%s\n' "$BLOG_DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
fi
|
||||
|
||||
- name: Deploy to server
|
||||
env:
|
||||
BLOG_DEPLOY_HOST: ${{ secrets.BLOG_DEPLOY_HOST }}
|
||||
BLOG_DEPLOY_PORT: ${{ secrets.BLOG_DEPLOY_PORT }}
|
||||
BLOG_DEPLOY_USER: ${{ secrets.BLOG_DEPLOY_USER }}
|
||||
BLOG_DEPLOY_PATH: ${{ secrets.BLOG_DEPLOY_PATH }}
|
||||
run: |
|
||||
test -n "$BLOG_DEPLOY_HOST"
|
||||
test -n "$BLOG_DEPLOY_PORT"
|
||||
test -n "$BLOG_DEPLOY_USER"
|
||||
test -n "$BLOG_DEPLOY_PATH"
|
||||
SSH_OPTS="-p $BLOG_DEPLOY_PORT"
|
||||
if [ ! -f ~/.ssh/known_hosts ]; then
|
||||
SSH_OPTS="$SSH_OPTS -o StrictHostKeyChecking=accept-new"
|
||||
fi
|
||||
ssh $SSH_OPTS "$BLOG_DEPLOY_USER@$BLOG_DEPLOY_HOST" "mkdir -p '$BLOG_DEPLOY_PATH/public'"
|
||||
rsync -az --delete \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
public/ "$BLOG_DEPLOY_USER@$BLOG_DEPLOY_HOST:$BLOG_DEPLOY_PATH/public/"
|
||||
4
blog-starter/.gitignore
vendored
Normal file
4
blog-starter/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/public/
|
||||
/resources/
|
||||
/themes/LoveIt/
|
||||
.hugo_build.lock
|
||||
86
blog-starter/README.md
Normal file
86
blog-starter/README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Blog Starter
|
||||
|
||||
This folder is a standalone starter for a Hugo blog that uses the LoveIt theme
|
||||
and deploys to the static blog host installed by `install.sh`.
|
||||
|
||||
## What This Starter Includes
|
||||
|
||||
- Hugo site structure
|
||||
- LoveIt bootstrap script
|
||||
- sample content
|
||||
- a helper script to create new posts
|
||||
- a Forgejo Actions workflow that builds and deploys to `/opt/blog/public`
|
||||
|
||||
## Recommended Setup
|
||||
|
||||
Create a separate Git repository for your blog, then copy this starter into that
|
||||
repository root.
|
||||
|
||||
## Local Writing Workflow
|
||||
|
||||
1. Bootstrap the theme:
|
||||
|
||||
```bash
|
||||
./scripts/bootstrap-theme.sh
|
||||
```
|
||||
|
||||
2. Create a new post:
|
||||
|
||||
```bash
|
||||
./scripts/new-post.sh my-first-post
|
||||
```
|
||||
|
||||
3. Start the local preview server:
|
||||
|
||||
```bash
|
||||
hugo server
|
||||
```
|
||||
|
||||
4. Edit the generated file under `content/posts/`.
|
||||
5. When ready to publish, set `draft = false`, commit, and push.
|
||||
|
||||
## Forgejo Web Workflow
|
||||
|
||||
If you want to create posts directly from the Forgejo web UI, use the manual
|
||||
workflow in `.forgejo/workflows/create-post.yml`.
|
||||
|
||||
From the Actions page:
|
||||
|
||||
1. Run `Create Blog Post`
|
||||
2. Fill in:
|
||||
- `title`
|
||||
- `slug`
|
||||
- optional `summary`
|
||||
- optional comma-separated `tags`
|
||||
- optional comma-separated `categories`
|
||||
- `draft`
|
||||
3. The workflow creates `content/posts/<slug>.md`
|
||||
4. The normal deploy workflow publishes the post on the next push
|
||||
|
||||
This keeps you out of front matter for most day-to-day writing.
|
||||
|
||||
## Forgejo Secrets
|
||||
|
||||
The workflow expects these repository secrets:
|
||||
|
||||
- `BLOG_DEPLOY_HOST`: server hostname or IP
|
||||
- `BLOG_DEPLOY_PORT`: SSH port, usually `22`
|
||||
- `BLOG_DEPLOY_USER`: deploy user on the server
|
||||
- `BLOG_DEPLOY_KEY`: private SSH key for the deploy user
|
||||
- `BLOG_DEPLOY_PATH`: target directory, usually `/opt/blog`
|
||||
- `BLOG_DEPLOY_KNOWN_HOSTS`: optional `known_hosts` entry for stricter SSH
|
||||
|
||||
## Expected Server State
|
||||
|
||||
- Blog host installed from this repo's `install.sh`
|
||||
- `d3v-blog` container running
|
||||
- a deploy user created by `install.sh blog-install` or `install.sh blog-update`
|
||||
- `install.sh blog-deploy-info` prints the exact secret values to paste into Forgejo
|
||||
- Nginx Proxy Manager forwards `blog.yourdomain.com` to `d3v-blog:80`
|
||||
|
||||
## Notes
|
||||
|
||||
- This starter fetches LoveIt into `themes/LoveIt` using Git.
|
||||
- The workflow installs Hugo Extended before building.
|
||||
- Deployment uses `rsync --delete` to keep `/opt/blog/public` in sync with the
|
||||
latest generated `public/` output.
|
||||
10
blog-starter/archetypes/default.md
Normal file
10
blog-starter/archetypes/default.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
+++
|
||||
title = "{{ replace .Name "-" " " | title }}"
|
||||
date = {{ .Date }}
|
||||
draft = true
|
||||
slug = "{{ .Name }}"
|
||||
tags = []
|
||||
categories = []
|
||||
+++
|
||||
|
||||
Write your post here.
|
||||
9
blog-starter/content/about/index.md
Normal file
9
blog-starter/content/about/index.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
+++
|
||||
title = "About"
|
||||
date = 2026-03-19T12:00:00+07:00
|
||||
draft = false
|
||||
+++
|
||||
|
||||
This is the About page for your Hugo blog.
|
||||
|
||||
Update this page with your own bio, project information, or contact details.
|
||||
7
blog-starter/content/posts/_index.md
Normal file
7
blog-starter/content/posts/_index.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
+++
|
||||
title = "Posts"
|
||||
date = 2026-03-19T12:00:00+07:00
|
||||
draft = false
|
||||
+++
|
||||
|
||||
Latest posts from the blog.
|
||||
17
blog-starter/content/posts/hello-world.md
Normal file
17
blog-starter/content/posts/hello-world.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
+++
|
||||
title = "Hello World"
|
||||
date = 2026-03-19T12:00:00+07:00
|
||||
draft = false
|
||||
slug = "hello-world"
|
||||
tags = ["welcome", "hugo"]
|
||||
categories = ["general"]
|
||||
+++
|
||||
|
||||
This is the first post published from the Hugo + LoveIt starter.
|
||||
|
||||
When you are ready to write your own content:
|
||||
|
||||
1. Create a new post with `./scripts/new-post.sh your-post-slug`
|
||||
2. Edit the generated Markdown file
|
||||
3. Preview with `hugo server`
|
||||
4. Commit and push to trigger deployment
|
||||
35
blog-starter/hugo.toml
Normal file
35
blog-starter/hugo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
baseURL = "https://blog.example.com/"
|
||||
languageCode = "en-us"
|
||||
title = "D3V Blog"
|
||||
theme = "LoveIt"
|
||||
defaultContentLanguage = "en"
|
||||
enableRobotsTXT = true
|
||||
|
||||
[permalinks]
|
||||
posts = "/posts/:slug/"
|
||||
|
||||
[params]
|
||||
defaultTheme = "auto"
|
||||
dateFormat = "2006-01-02"
|
||||
description = "Notes, updates, and guides."
|
||||
|
||||
[params.author]
|
||||
name = "D3V Team"
|
||||
|
||||
[markup]
|
||||
[markup.goldmark]
|
||||
[markup.goldmark.renderer]
|
||||
unsafe = true
|
||||
|
||||
[menu]
|
||||
[[menu.main]]
|
||||
identifier = "posts"
|
||||
name = "Posts"
|
||||
url = "/posts/"
|
||||
weight = 1
|
||||
|
||||
[[menu.main]]
|
||||
identifier = "about"
|
||||
name = "About"
|
||||
url = "/about/"
|
||||
weight = 2
|
||||
25
blog-starter/scripts/bootstrap-theme.sh
Normal file
25
blog-starter/scripts/bootstrap-theme.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
THEME_DIR="themes/LoveIt"
|
||||
THEME_REPO="https://github.com/dillonzq/LoveIt.git"
|
||||
THEME_REF="${LOVEIT_REF:-v0.3.1}"
|
||||
|
||||
mkdir -p themes
|
||||
|
||||
if [ -d "${THEME_DIR}/.git" ]; then
|
||||
echo "Updating LoveIt theme..."
|
||||
git -C "${THEME_DIR}" fetch --depth 1 origin "${THEME_REF}"
|
||||
git -C "${THEME_DIR}" checkout -f FETCH_HEAD
|
||||
else
|
||||
echo "Cloning LoveIt theme..."
|
||||
git clone --depth 1 --branch "${THEME_REF}" "${THEME_REPO}" "${THEME_DIR}"
|
||||
fi
|
||||
|
||||
if [ -f "${THEME_DIR}/.gitmodules" ]; then
|
||||
echo "Initializing LoveIt submodules..."
|
||||
git -C "${THEME_DIR}" submodule sync --recursive
|
||||
git -C "${THEME_DIR}" submodule update --init --recursive
|
||||
fi
|
||||
|
||||
echo "LoveIt theme is ready in ${THEME_DIR}"
|
||||
11
blog-starter/scripts/new-post.sh
Normal file
11
blog-starter/scripts/new-post.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 my-post-slug" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
slug="$1"
|
||||
hugo new "posts/${slug}.md"
|
||||
echo "Created content/posts/${slug}.md"
|
||||
1
blog-starter/static/.gitkeep
Normal file
1
blog-starter/static/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -76,7 +76,7 @@ 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.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.name="d3v-gateway" \
|
||||
org.label-schema.description="D3V Gateway: Reverse Proxy + WireGuard VPN Manager" \
|
||||
org.label-schema.url="https://src.d3v.ac/d3v/D3V-Server" \
|
||||
org.label-schema.cmd="docker run --rm -ti --cap-add=NET_ADMIN --cap-add=SYS_MODULE src.d3v.ac/d3v/d3v-server:latest"
|
||||
|
|
|
|||
|
|
@ -1,255 +1 @@
|
|||
/*
|
||||
|
||||
How this was generated:
|
||||
1. bring up an empty pdns stack
|
||||
2. use api to create a zone ...
|
||||
|
||||
curl -X POST \
|
||||
'http://npm.dev:8081/api/v1/servers/localhost/zones' \
|
||||
--header 'X-API-Key: npm' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "example.com.",
|
||||
"kind": "Native",
|
||||
"masters": [],
|
||||
"nameservers": [
|
||||
"ns1.pdns.",
|
||||
"ns2.pdns."
|
||||
]
|
||||
}'
|
||||
|
||||
3. Dump sql:
|
||||
|
||||
docker exec -ti npm.pdns.db mysqldump -u pdns -p pdns
|
||||
|
||||
*/
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `comments`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `comments`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `comments` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`domain_id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`type` varchar(10) NOT NULL,
|
||||
`modified_at` int(11) NOT NULL,
|
||||
`account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL,
|
||||
`comment` text CHARACTER SET utf8mb3 NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `comments_name_type_idx` (`name`,`type`),
|
||||
KEY `comments_order_idx` (`domain_id`,`modified_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `comments`
|
||||
--
|
||||
|
||||
LOCK TABLES `comments` WRITE;
|
||||
/*!40000 ALTER TABLE `comments` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `comments` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `cryptokeys`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `cryptokeys`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `cryptokeys` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`domain_id` int(11) NOT NULL,
|
||||
`flags` int(11) NOT NULL,
|
||||
`active` tinyint(1) DEFAULT NULL,
|
||||
`published` tinyint(1) DEFAULT 1,
|
||||
`content` text DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `domainidindex` (`domain_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `cryptokeys`
|
||||
--
|
||||
|
||||
LOCK TABLES `cryptokeys` WRITE;
|
||||
/*!40000 ALTER TABLE `cryptokeys` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `cryptokeys` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `domainmetadata`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `domainmetadata`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `domainmetadata` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`domain_id` int(11) NOT NULL,
|
||||
`kind` varchar(32) DEFAULT NULL,
|
||||
`content` text DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `domainmetadata_idx` (`domain_id`,`kind`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `domainmetadata`
|
||||
--
|
||||
|
||||
LOCK TABLES `domainmetadata` WRITE;
|
||||
/*!40000 ALTER TABLE `domainmetadata` DISABLE KEYS */;
|
||||
INSERT INTO `domainmetadata` VALUES
|
||||
(1,1,'SOA-EDIT-API','DEFAULT');
|
||||
/*!40000 ALTER TABLE `domainmetadata` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `domains`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `domains`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `domains` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`master` varchar(128) DEFAULT NULL,
|
||||
`last_check` int(11) DEFAULT NULL,
|
||||
`type` varchar(8) NOT NULL,
|
||||
`notified_serial` int(10) unsigned DEFAULT NULL,
|
||||
`account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL,
|
||||
`options` varchar(64000) DEFAULT NULL,
|
||||
`catalog` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name_index` (`name`),
|
||||
KEY `catalog_idx` (`catalog`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `domains`
|
||||
--
|
||||
|
||||
LOCK TABLES `domains` WRITE;
|
||||
/*!40000 ALTER TABLE `domains` DISABLE KEYS */;
|
||||
INSERT INTO `domains` VALUES
|
||||
(1,'example.com','',NULL,'NATIVE',NULL,'',NULL,NULL);
|
||||
/*!40000 ALTER TABLE `domains` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `records`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `records`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `records` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`domain_id` int(11) DEFAULT NULL,
|
||||
`name` varchar(255) DEFAULT NULL,
|
||||
`type` varchar(10) DEFAULT NULL,
|
||||
`content` varchar(64000) DEFAULT NULL,
|
||||
`ttl` int(11) DEFAULT NULL,
|
||||
`prio` int(11) DEFAULT NULL,
|
||||
`disabled` tinyint(1) DEFAULT 0,
|
||||
`ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
|
||||
`auth` tinyint(1) DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `nametype_index` (`name`,`type`),
|
||||
KEY `domain_id` (`domain_id`),
|
||||
KEY `ordername` (`ordername`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `records`
|
||||
--
|
||||
|
||||
LOCK TABLES `records` WRITE;
|
||||
/*!40000 ALTER TABLE `records` DISABLE KEYS */;
|
||||
INSERT INTO `records` VALUES
|
||||
(1,1,'example.com','NS','ns1.pdns',1500,0,0,NULL,1),
|
||||
(2,1,'example.com','NS','ns2.pdns',1500,0,0,NULL,1),
|
||||
(3,1,'example.com','SOA','a.misconfigured.dns.server.invalid hostmaster.example.com 2023030501 10800 3600 604800 3600',1500,0,0,NULL,1);
|
||||
/*!40000 ALTER TABLE `records` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `supermasters`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `supermasters`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `supermasters` (
|
||||
`ip` varchar(64) NOT NULL,
|
||||
`nameserver` varchar(255) NOT NULL,
|
||||
`account` varchar(40) CHARACTER SET utf8mb3 NOT NULL,
|
||||
PRIMARY KEY (`ip`,`nameserver`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `supermasters`
|
||||
--
|
||||
|
||||
LOCK TABLES `supermasters` WRITE;
|
||||
/*!40000 ALTER TABLE `supermasters` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `supermasters` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `tsigkeys`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `tsigkeys`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `tsigkeys` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) DEFAULT NULL,
|
||||
`algorithm` varchar(50) DEFAULT NULL,
|
||||
`secret` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `namealgoindex` (`name`,`algorithm`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `tsigkeys`
|
||||
--
|
||||
|
||||
LOCK TABLES `tsigkeys` WRITE;
|
||||
/*!40000 ALTER TABLE `tsigkeys` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `tsigkeys` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
-- MySQL DB Init Script Mock
|
||||
|
|
|
|||
|
|
@ -1,92 +1 @@
|
|||
# WELCOME TO SQUID 6.6
|
||||
# ----------------------------
|
||||
#
|
||||
# This is the documentation for the Squid configuration file.
|
||||
# This documentation can also be found online at:
|
||||
# http://www.squid-cache.org/Doc/config/
|
||||
#
|
||||
# You may wish to look at the Squid home page and wiki for the
|
||||
# FAQ and other documentation:
|
||||
# http://www.squid-cache.org/
|
||||
# https://wiki.squid-cache.org/SquidFaq
|
||||
# https://wiki.squid-cache.org/ConfigExamples
|
||||
#
|
||||
|
||||
# Example rule allowing access from your local networks.
|
||||
# Adapt to list your (internal) IP networks from where browsing
|
||||
# should be allowed
|
||||
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
|
||||
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
|
||||
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
|
||||
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
|
||||
acl localnet src 172.0.0.0/8
|
||||
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
|
||||
acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
|
||||
acl SSL_ports port 443
|
||||
acl Safe_ports port 80 # http
|
||||
acl Safe_ports port 81
|
||||
acl Safe_ports port 443 # https
|
||||
|
||||
#
|
||||
# Recommended minimum Access Permission configuration:
|
||||
#
|
||||
# Deny requests to certain unsafe ports
|
||||
http_access deny !Safe_ports
|
||||
|
||||
# Deny CONNECT to other than secure SSL ports
|
||||
http_access deny CONNECT !SSL_ports
|
||||
|
||||
# Only allow cachemgr access from localhost
|
||||
http_access allow localhost manager
|
||||
http_access deny manager
|
||||
|
||||
# This default configuration only allows localhost requests because a more
|
||||
# permissive Squid installation could introduce new attack vectors into the
|
||||
# network by proxying external TCP connections to unprotected services.
|
||||
http_access allow localhost
|
||||
|
||||
# The two deny rules below are unnecessary in this default configuration
|
||||
# because they are followed by a "deny all" rule. However, they may become
|
||||
# critically important when you start allowing external requests below them.
|
||||
|
||||
# Protect web applications running on the same server as Squid. They often
|
||||
# assume that only local users can access them at "localhost" ports.
|
||||
http_access deny to_localhost
|
||||
|
||||
# Protect cloud servers that provide local users with sensitive info about
|
||||
# their server via certain well-known link-local (a.k.a. APIPA) addresses.
|
||||
http_access deny to_linklocal
|
||||
|
||||
#
|
||||
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
|
||||
#
|
||||
include /etc/squid/conf.d/*.conf
|
||||
|
||||
# For example, to allow access from your local networks, you may uncomment the
|
||||
# following rule (and/or add rules that match your definition of "local"):
|
||||
# http_access allow localnet
|
||||
|
||||
# And finally deny all other access to this proxy
|
||||
http_access deny all
|
||||
|
||||
# Squid normally listens to port 3128
|
||||
http_port 3128
|
||||
|
||||
# Leave coredumps in the first cache dir
|
||||
coredump_dir /var/spool/squid
|
||||
|
||||
#
|
||||
# Add any of your own refresh_pattern entries above these.
|
||||
#
|
||||
refresh_pattern ^ftp: 1440 20% 10080
|
||||
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
|
||||
refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
|
||||
refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
|
||||
refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
|
||||
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
|
||||
# example pattern for deb packages
|
||||
#refresh_pattern (\.deb|\.udeb)$ 129600 100% 129600
|
||||
refresh_pattern . 0 20% 4320
|
||||
|
||||
# Squid config mock
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -26,9 +26,24 @@ const RedirectionHosts = lazy(() => import("src/pages/Nginx/RedirectionHosts"));
|
|||
const DeadHosts = lazy(() => import("src/pages/Nginx/DeadHosts"));
|
||||
const Streams = lazy(() => import("src/pages/Nginx/Streams"));
|
||||
const WireGuard = lazy(() => import("src/pages/WireGuard"));
|
||||
const DatabaseManager = lazy(() => import("src/pages/DatabaseManager"));
|
||||
const WgPublicPortal = lazy(() => import("src/pages/WgPublicPortal"));
|
||||
|
||||
function Router() {
|
||||
const health = useHealth();
|
||||
|
||||
const isPublicWg = window.location.pathname.startsWith("/wg-public");
|
||||
if (isPublicWg) {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<Routes>
|
||||
<Route path="/wg-public/*" element={<WgPublicPortal />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
const { authenticated } = useAuthState();
|
||||
|
||||
if (health.isLoading) {
|
||||
|
|
@ -73,6 +88,7 @@ function Router() {
|
|||
<Route path="/nginx/stream" element={<Streams />} />
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/wireguard" element={<WireGuard />} />
|
||||
<Route path="/database" element={<DatabaseManager />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</SiteContainer>
|
||||
|
|
|
|||
14
frontend/src/api/backend/database.ts
Normal file
14
frontend/src/api/backend/database.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import * as api from "./base";
|
||||
|
||||
export async function getTables(): Promise<string[]> {
|
||||
return await api.get({
|
||||
url: "/database/tables",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTableData(tableName: string, offset: number = 0, limit: number = 50): Promise<any> {
|
||||
return await api.get({
|
||||
url: `/database/tables/${tableName}`,
|
||||
params: { offset, limit },
|
||||
});
|
||||
}
|
||||
|
|
@ -62,3 +62,4 @@ export * from "./uploadCertificate";
|
|||
export * from "./validateCertificate";
|
||||
export * from "./twoFactor";
|
||||
export * from "./wireguard";
|
||||
export * from "./database";
|
||||
|
|
|
|||
|
|
@ -11,10 +11,16 @@ export interface WgClient {
|
|||
createdOn: string;
|
||||
updatedOn: string;
|
||||
expiresAt: string | null;
|
||||
interfaceId: number;
|
||||
interfaceName: string;
|
||||
latestHandshakeAt: string | null;
|
||||
endpoint: string | null;
|
||||
transferRx: number;
|
||||
transferTx: number;
|
||||
txLimit: number;
|
||||
rxLimit: number;
|
||||
storageLimitMb: number;
|
||||
storageUsageBytes?: number;
|
||||
}
|
||||
|
||||
export interface WgInterface {
|
||||
|
|
@ -25,21 +31,45 @@ export interface WgInterface {
|
|||
listenPort: number;
|
||||
mtu: number;
|
||||
dns: string;
|
||||
host: string;
|
||||
host: string | null;
|
||||
isolateClients: boolean;
|
||||
linkedServers: number[];
|
||||
storageUsageBytes?: number;
|
||||
clientCount?: 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; tx_limit?: number; rx_limit?: number; storage_limit_mb?: number; }): Promise<WgClient> {
|
||||
return await api.post({ url: "/wireguard/client", data });
|
||||
}
|
||||
|
||||
export async function updateWgClient(id: number, data: { name?: string; allowed_ips?: string; persistent_keepalive?: number; expires_at?: string; tx_limit?: number; rx_limit?: number; storage_limit_mb?: number; }): Promise<WgClient> {
|
||||
return await api.put({ url: `/wireguard/client/${id}`, data });
|
||||
}
|
||||
|
||||
export async function deleteWgClient(id: number): Promise<boolean> {
|
||||
return await api.del({ url: `/wireguard/client/${id}` });
|
||||
}
|
||||
|
|
@ -59,3 +89,41 @@ export async function getWgClientConfig(id: number): Promise<string> {
|
|||
export function downloadWgConfig(id: number, name: string) {
|
||||
return api.download({ url: `/wireguard/client/${id}/configuration` }, `${name}.conf`);
|
||||
}
|
||||
|
||||
export function downloadWgConfigZip(id: number, name: string) {
|
||||
return api.download({ url: `/wireguard/client/${id}/configuration.zip` }, `${name}.zip`);
|
||||
}
|
||||
|
||||
export async function getWgClientFiles(id: number): Promise<any[]> {
|
||||
return await api.get({ url: `/wireguard/client/${id}/files` });
|
||||
}
|
||||
|
||||
export async function getWgClientStorage(id: number): Promise<{ totalBytes: number; limitMb: number }> {
|
||||
return await api.get({ url: `/wireguard/client/${id}/storage` });
|
||||
}
|
||||
|
||||
export async function getWgDashboardStats(): Promise<any> {
|
||||
return await api.get({ url: `/wireguard/dashboard` });
|
||||
}
|
||||
|
||||
export async function getWgClientLogs(id: number): Promise<any[]> {
|
||||
return await api.get({ url: `/wireguard/client/${id}/logs` });
|
||||
}
|
||||
|
||||
export async function uploadWgClientFile(id: number, file: File): Promise<any> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
return await api.post({
|
||||
url: `/wireguard/client/${id}/files`,
|
||||
data: formData
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadWgClientFile(id: number, filename: string) {
|
||||
return api.download({ url: `/wireguard/client/${id}/files/${encodeURIComponent(filename)}` }, filename);
|
||||
}
|
||||
|
||||
export async function deleteWgClientFile(id: number, filename: string): Promise<boolean> {
|
||||
return await api.del({ url: `/wireguard/client/${id}/files/${encodeURIComponent(filename)}` });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,75 @@
|
|||
import { useCheckVersion, useHealth } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconCpu, IconServer, IconArrowsDownUp, IconDatabase } from "@tabler/icons-react";
|
||||
import * as api from "../api/backend/base";
|
||||
|
||||
export function SiteFooter() {
|
||||
const health = useHealth();
|
||||
const { data: versionData } = useCheckVersion();
|
||||
const [sysStats, setSysStats] = useState({
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
memoryTotal: 0,
|
||||
memoryActive: 0,
|
||||
storage: 0,
|
||||
storageTotal: 0,
|
||||
storageUsed: 0,
|
||||
networkRx: "0.00",
|
||||
networkTx: "0.00"
|
||||
});
|
||||
|
||||
const getVersion = () => {
|
||||
if (!health.data) {
|
||||
return "";
|
||||
}
|
||||
const v = health.data.version;
|
||||
return `v${v.major}.${v.minor}.${v.revision}`;
|
||||
};
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const data = await api.get({ url: "/reports/system" });
|
||||
if (isMounted && data) {
|
||||
setSysStats(data);
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently fail polling to prevent console flood
|
||||
}
|
||||
};
|
||||
|
||||
// Initial fetch
|
||||
fetchStats();
|
||||
|
||||
// Poll every 1 second
|
||||
const interval = setInterval(fetchStats, 1000);
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Convert bytes to GB string
|
||||
const formatGB = (bytes: number) => (bytes / 1024 / 1024 / 1024).toFixed(1);
|
||||
|
||||
return (
|
||||
<footer className="footer d-print-none py-3">
|
||||
<div className="container-xl">
|
||||
<div className="row text-center align-items-center flex-row-reverse">
|
||||
<div className="col-lg-auto ms-lg-auto">
|
||||
<ul className="list-inline list-inline-dots mb-0">
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href="https://github.com/NginxProxyManager/nginx-proxy-manager"
|
||||
target="_blank"
|
||||
className="link-secondary"
|
||||
rel="noopener"
|
||||
>
|
||||
<T id="footer.github-fork" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="col-lg-auto ms-lg-auto d-flex gap-3 align-items-center text-muted small">
|
||||
<div title="CPU Usage" className="d-flex align-items-center gap-1">
|
||||
<IconCpu size={16} />
|
||||
<span>{sysStats.cpu}%</span>
|
||||
</div>
|
||||
<div title={`Memory Usage (${formatGB(sysStats.memoryActive)}GB / ${formatGB(sysStats.memoryTotal)}GB)`} className="d-flex align-items-center gap-1">
|
||||
<IconServer size={16} />
|
||||
<span>{sysStats.memory}% of {Math.round(sysStats.memoryTotal / 1024 / 1024 / 1024)}GB</span>
|
||||
</div>
|
||||
<div title={`Storage Usage (${formatGB(sysStats.storageUsed)}GB / ${formatGB(sysStats.storageTotal)}GB)`} className="d-flex align-items-center gap-1">
|
||||
<IconDatabase size={16} />
|
||||
<span>Free {formatGB(sysStats.storageTotal - sysStats.storageUsed)}GB</span>
|
||||
</div>
|
||||
<div title="Network Bandwidth" className="d-flex align-items-center gap-1">
|
||||
<IconArrowsDownUp size={16} />
|
||||
<span>↓{sysStats.networkRx} ↑{sysStats.networkTx} Mbps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-auto mt-3 mt-lg-0">
|
||||
<ul className="list-inline list-inline-dots mb-0">
|
||||
<li className="list-inline-item">
|
||||
© 2025{" "}
|
||||
<a href="https://jc21.com" rel="noreferrer" target="_blank" className="link-secondary">
|
||||
jc21.com
|
||||
</a>
|
||||
© D3V.AC 2026{" "}
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
Theme by{" "}
|
||||
<a href="https://tabler.io" rel="noreferrer" target="_blank" className="link-secondary">
|
||||
Tabler
|
||||
</a>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${getVersion()}`}
|
||||
className="link-secondary"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{" "}
|
||||
{getVersion()}{" "}
|
||||
</a>
|
||||
</li>
|
||||
{versionData?.updateAvailable && versionData?.latest && (
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${versionData.latest}`}
|
||||
className="link-warning fw-bold"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title={`New version ${versionData.latest} is available`}
|
||||
>
|
||||
<T id="update-available" data={{ latestVersion: versionData.latest }} />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -102,10 +102,23 @@ const menuItems: MenuItem[] = [
|
|||
permissionSection: ADMIN,
|
||||
},
|
||||
{
|
||||
to: "/settings",
|
||||
icon: IconSettings,
|
||||
label: "settings",
|
||||
label: "tools",
|
||||
permissionSection: ADMIN,
|
||||
items: [
|
||||
{
|
||||
to: "/settings",
|
||||
label: "settings",
|
||||
permissionSection: ADMIN,
|
||||
permission: VIEW,
|
||||
},
|
||||
{
|
||||
to: "/database",
|
||||
label: "database-manager",
|
||||
permissionSection: ADMIN,
|
||||
permission: VIEW,
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
|
||||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconNetwork, IconServer, IconShield, IconUser } from "@tabler/icons-react";
|
||||
import cn from "classnames";
|
||||
import type { AuditLog } from "src/api/backend";
|
||||
import { useLocaleState } from "src/context";
|
||||
|
|
@ -17,6 +17,12 @@ const getEventValue = (event: AuditLog) => {
|
|||
return event.meta?.incomingPort || "N/A";
|
||||
case "certificate":
|
||||
return event.meta?.domainNames?.join(", ") || event.meta?.niceName || "N/A";
|
||||
case "wireguard-server":
|
||||
return event.meta?.name || `Server #${event.objectId}`;
|
||||
case "wireguard-client":
|
||||
return event.meta?.name || `Client #${event.objectId}`;
|
||||
case "wireguard-server-links":
|
||||
return `Server #${event.objectId}`;
|
||||
default:
|
||||
return `UNKNOWN EVENT TYPE: ${event.objectType}`;
|
||||
}
|
||||
|
|
@ -58,6 +64,13 @@ const getIcon = (row: AuditLog) => {
|
|||
case "certificate":
|
||||
ico = <IconShield size={16} className={c} />;
|
||||
break;
|
||||
case "wireguard-server":
|
||||
case "wireguard-server-links":
|
||||
ico = <IconServer size={16} className={c} />;
|
||||
break;
|
||||
case "wireguard-client":
|
||||
ico = <IconNetwork size={16} className={c} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return ico;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getWgClients,
|
||||
getWgInterface,
|
||||
getWgInterfaces,
|
||||
createWgInterface,
|
||||
updateWgInterface,
|
||||
deleteWgInterface,
|
||||
updateWgInterfaceLinks,
|
||||
createWgClient,
|
||||
updateWgClient,
|
||||
deleteWgClient,
|
||||
enableWgClient,
|
||||
disableWgClient,
|
||||
|
|
@ -20,19 +25,72 @@ 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; tx_limit?: number; rx_limit?: number; storage_limit_mb?: number; }) => createWgClient(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["wg-clients"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWgClient = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => updateWgClient(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["wg-clients"] });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
# Internationalisation support
|
||||
|
||||
## Before you start
|
||||
|
||||
It's highly recommended that you spin up a development instance of this project
|
||||
on your docker capable server. It's pretty easy:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NginxProxyManager/nginx-proxy-manager.git
|
||||
cd nginx-proxy-manager
|
||||
./scripts/start-dev -f
|
||||
```
|
||||
|
||||
Then after a while, you can access http://yourserverip:3081
|
||||
|
||||
This stack will watch the file system for changes, especially to language files,
|
||||
and reload the site you have open in the browser.
|
||||
|
||||
|
||||
## Adding new translations
|
||||
|
||||
Modify the files in the `src` folder. Follow the conventions already there.
|
||||
|
||||
When the development stack is running, it will sort the locale lang files
|
||||
for you when you save.
|
||||
|
||||
|
||||
## After making changes
|
||||
|
||||
If you're NOT running the development stack, you will need to run
|
||||
`yarn locale-compile` in the `frontend` folder for
|
||||
the new translations to be compiled into the `lang` folder.
|
||||
|
||||
|
||||
## Adding a whole new language
|
||||
|
||||
There's a fair bit you'll need to touch. Here's a list that may
|
||||
not be complete by the time you're reading this:
|
||||
|
||||
- frontend/src/locale/src/[yourlang].json
|
||||
- frontend/src/locale/src/lang-list.json
|
||||
- frontend/src/locale/src/HelpDoc/[yourlang]/*
|
||||
- frontend/src/locale/src/HelpDoc/index.tsx
|
||||
- frontend/src/locale/IntlProvider.tsx
|
||||
- frontend/check-locales.cjs
|
||||
|
||||
|
||||
## Checking for missing translations in languages
|
||||
|
||||
Run `node check-locales.cjs` in this frontend folder.
|
||||
|
|
@ -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."
|
||||
|
|
@ -691,5 +691,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Потребители"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Клиент"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Клиенти"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Клиент} other {WireGuard Клиента}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Сървър"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Сървърни Връзки"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Сървъри"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Сървър} other {WireGuard Сървъра}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -766,5 +766,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Uživatelé"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Klient"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Klienti"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Klient} other {WireGuard Klientů}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Propojení Serverů"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Servery"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Serverů}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -652,5 +652,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Benutzer"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Client"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Clients"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Client} other {WireGuard Clients}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Server-Verknüpfungen"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Server}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -775,5 +775,26 @@
|
|||
},
|
||||
"wireguard": {
|
||||
"defaultMessage": "WireGuard"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Client"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Clients"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Client} other {WireGuard Clients}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Server Links"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Servers"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Servers}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -688,5 +688,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Usuarios"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Cliente WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Clientes WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Cliente WireGuard} other {Clientes WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Servidor WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Enlaces de servidor WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Servidores WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Servidor WireGuard} other {Servidores WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -772,5 +772,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Users"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Klient"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Kliendid"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Klient} other {WireGuard Klienti}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Serveri Lingid"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Serverid"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Serverit}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -643,5 +643,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Utilisateurs"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Client WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Clients WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Client WireGuard} other {Clients WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Serveur WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Liens serveur WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Serveurs WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Serveur WireGuard} other {Serveurs WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -679,5 +679,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Úsáideoirí"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Cliant WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Cliaint WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Cliant WireGuard} other {Cliant WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Freastalaí WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Naisc Fhreastalaí WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Freastalaithe WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Freastalaí WireGuard} other {Freastalaí WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -766,5 +766,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Felhasználók"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Kliens"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Kliensek"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Kliens} other {WireGuard Kliensek}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Szerver"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Szerver Kapcsolatok"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Szerverek"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Szerver} other {WireGuard Szerverek}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -679,5 +679,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Pengguna"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Klien WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Klien WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Klien WireGuard} other {Klien WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Server WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Tautan Server WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Server WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Server WireGuard} other {Server WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -655,5 +655,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Utenti"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Client WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Client WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Client WireGuard} other {Client WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Server WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Link server WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Server WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Server WireGuard} other {Server WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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への接続でエラーが発生し、到達性チェックに失敗しました"
|
||||
|
|
@ -649,5 +649,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "ユーザー"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard クライアント"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard クライアント"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard クライアント} other {WireGuard クライアント}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard サーバー"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard サーバーリンク"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard サーバー"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard サーバー} other {WireGuard サーバー}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다."
|
||||
|
|
@ -691,5 +691,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "사용자"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard 클라이언트"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard 클라이언트"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard 클라이언트} other {WireGuard 클라이언트}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard 서버"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard 서버 링크"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard 서버"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard 서버} other {WireGuard 서버}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,89 @@
|
|||
{
|
||||
"locale-en-US": {
|
||||
"defaultMessage": "English"
|
||||
},
|
||||
"locale-es-ES": {
|
||||
"defaultMessage": "Español"
|
||||
},
|
||||
"locale-et-EE": {
|
||||
"defaultMessage": "Eesti"
|
||||
},
|
||||
"locale-ie-GA": {
|
||||
"defaultMessage": "Gaeilge"
|
||||
},
|
||||
"locale-de-DE": {
|
||||
"defaultMessage": "German"
|
||||
},
|
||||
"locale-pt-PT": {
|
||||
"defaultMessage": "Português (Europeu)"
|
||||
},
|
||||
"locale-fr-FR": {
|
||||
"defaultMessage": "Français"
|
||||
},
|
||||
"locale-id-ID": {
|
||||
"defaultMessage": "Bahasa Indonesia"
|
||||
},
|
||||
"locale-ja-JP": {
|
||||
"defaultMessage": "日本語"
|
||||
},
|
||||
"locale-ru-RU": {
|
||||
"defaultMessage": "Русский"
|
||||
},
|
||||
"locale-sk-SK": {
|
||||
"defaultMessage": "Slovenčina"
|
||||
},
|
||||
"locale-cs-CZ": {
|
||||
"defaultMessage": "Čeština"
|
||||
},
|
||||
"locale-zh-CN": {
|
||||
"defaultMessage": "中文"
|
||||
},
|
||||
"locale-pl-PL": {
|
||||
"defaultMessage": "Polski"
|
||||
},
|
||||
"locale-it-IT": {
|
||||
"defaultMessage": "Italiano"
|
||||
},
|
||||
"locale-vi-VN": {
|
||||
"defaultMessage": "Tiếng Việt"
|
||||
},
|
||||
"locale-nl-NL": {
|
||||
"defaultMessage": "Nederlands"
|
||||
},
|
||||
"locale-ko-KR": {
|
||||
"defaultMessage": "한국어"
|
||||
},
|
||||
"locale-bg-BG": {
|
||||
"defaultMessage": "Български"
|
||||
},
|
||||
"locale-tr-TR": {
|
||||
"defaultMessage": "Türkçe"
|
||||
},
|
||||
"locale-hu-HU": {
|
||||
"defaultMessage": "Magyar"
|
||||
},
|
||||
"locale-no-NO": {
|
||||
"defaultMessage": "Norsk"
|
||||
}
|
||||
"locale-bg-BG": {
|
||||
"defaultMessage": "Български"
|
||||
},
|
||||
"locale-cs-CZ": {
|
||||
"defaultMessage": "Čeština"
|
||||
},
|
||||
"locale-de-DE": {
|
||||
"defaultMessage": "German"
|
||||
},
|
||||
"locale-en-US": {
|
||||
"defaultMessage": "English"
|
||||
},
|
||||
"locale-es-ES": {
|
||||
"defaultMessage": "Español"
|
||||
},
|
||||
"locale-et-EE": {
|
||||
"defaultMessage": "Eesti"
|
||||
},
|
||||
"locale-fr-FR": {
|
||||
"defaultMessage": "Français"
|
||||
},
|
||||
"locale-hu-HU": {
|
||||
"defaultMessage": "Magyar"
|
||||
},
|
||||
"locale-id-ID": {
|
||||
"defaultMessage": "Bahasa Indonesia"
|
||||
},
|
||||
"locale-ie-GA": {
|
||||
"defaultMessage": "Gaeilge"
|
||||
},
|
||||
"locale-it-IT": {
|
||||
"defaultMessage": "Italiano"
|
||||
},
|
||||
"locale-ja-JP": {
|
||||
"defaultMessage": "日本語"
|
||||
},
|
||||
"locale-ko-KR": {
|
||||
"defaultMessage": "한국어"
|
||||
},
|
||||
"locale-nl-NL": {
|
||||
"defaultMessage": "Nederlands"
|
||||
},
|
||||
"locale-no-NO": {
|
||||
"defaultMessage": "Norsk"
|
||||
},
|
||||
"locale-pl-PL": {
|
||||
"defaultMessage": "Polski"
|
||||
},
|
||||
"locale-pt-PT": {
|
||||
"defaultMessage": "Português (Europeu)"
|
||||
},
|
||||
"locale-ru-RU": {
|
||||
"defaultMessage": "Русский"
|
||||
},
|
||||
"locale-sk-SK": {
|
||||
"defaultMessage": "Slovenčina"
|
||||
},
|
||||
"locale-tr-TR": {
|
||||
"defaultMessage": "Türkçe"
|
||||
},
|
||||
"locale-vi-VN": {
|
||||
"defaultMessage": "Tiếng Việt"
|
||||
},
|
||||
"locale-zh-CN": {
|
||||
"defaultMessage": "中文"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Client"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Clients"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Client} other {WireGuard Clients}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Server Links"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Servers"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Servers}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -655,5 +655,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Gebruikers"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Client"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Clients"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Client} other {WireGuard Clients}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Serverkoppelingen"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Servers"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Servers}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -772,5 +772,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Brukere"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Klient"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Klienter"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Klient} other {WireGuard Klienter}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Serverforbindelser"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Servere"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Servere}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -658,5 +658,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Użytkownicy"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Klient WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Klienci WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Klient WireGuard} other {Klientów WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Serwer WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Połączenia serwerów WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Serwery WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Serwer WireGuard} other {Serwerów WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -679,5 +679,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Utilizadores"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "Cliente WireGuard"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "Clientes WireGuard"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Cliente WireGuard} other {Clientes WireGuard}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "Servidor WireGuard"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Links de servidor WireGuard"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "Servidores WireGuard"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {Servidor WireGuard} other {Servidores WireGuard}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -652,5 +652,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Пользователи"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Клиент"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Клиенты"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Клиент} other {WireGuard Клиентов}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Сервер"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "Связи WireGuard Серверов"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Серверы"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Сервер} other {WireGuard Серверов}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -766,5 +766,26 @@
|
|||
},
|
||||
"users": {
|
||||
"defaultMessage": "Používatelia"
|
||||
},
|
||||
"wireguard-client": {
|
||||
"defaultMessage": "WireGuard Klient"
|
||||
},
|
||||
"wireguard-clients": {
|
||||
"defaultMessage": "WireGuard Klienti"
|
||||
},
|
||||
"wireguard-clients.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Klient} other {WireGuard Klientov}}"
|
||||
},
|
||||
"wireguard-server": {
|
||||
"defaultMessage": "WireGuard Server"
|
||||
},
|
||||
"wireguard-server-links": {
|
||||
"defaultMessage": "WireGuard Prepojenia Serverov"
|
||||
},
|
||||
"wireguard-servers": {
|
||||
"defaultMessage": "WireGuard Servery"
|
||||
},
|
||||
"wireguard-servers.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {WireGuard Server} other {WireGuard Serverov}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue