Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da90d67cc0 | |||
| a52da67b38 | |||
| e513090074 | |||
| 2dc8ba7792 | |||
| 4e8cccb4c7 | |||
| e57b0977d3 | |||
| b8be53c3f7 | |||
| 0794413191 | |||
| 261b0d6b8f | |||
| f656d57d20 | |||
| 46074fea1c | |||
| 05c655ede9 | |||
| ebcc42cc49 | |||
| be8d24e492 | |||
| 9682dedea7 | |||
| 5eb80fe3c1 | |||
| dd9da2a067 | |||
| 15111ecd62 | |||
| e9f4b4650b | |||
| e3e4049f8e | |||
| 3fb9adbf6f | |||
| cd9db1563d | |||
| b5c30f5dbe | |||
| 1eb9527175 | |||
| cd890c1f0f | |||
| 2a78b30aeb | |||
| 9a843087c3 | |||
| 483b63bba6 | |||
| 13942c97b2 | |||
| 82c64e506e | |||
| 9b3d919168 | |||
| 3eaf0d01dc | |||
| 414e9a114b | |||
| 2d28d87c5c | |||
| 76c2233e46 | |||
| abedf9f38e | |||
| 25f3fa3c0f | |||
| c3c51f8088 | |||
| 8ea2b635c1 | |||
| bc4dfd03df | |||
| 7cde04de81 | |||
| 5228734c98 | |||
| 47f81dd66a | |||
| e5b2c3d10b | |||
| 059a0ccffc | |||
| 8c9c54c8b2 | |||
| 02ce6f0a65 | |||
| 48682e1abd | |||
| 044dd34dcc | |||
| a469ac6897 | |||
| 1178d23659 | |||
| b3cc1ce839 | |||
| 71aaec93ef | |||
| 7a219b73d4 | |||
| c456c5e7dd | |||
| a5880cc0b8 | |||
| 5fca628ebd | |||
| 7ab297c366 | |||
| c5de8f0f44 | |||
| c0641889cf | |||
| 9141562f91 | |||
| d21af70df1 | |||
| 56ee86cc1c | |||
| f017b4968c | |||
| 6004457666 | |||
| 1a5a0180ea | |||
| c732f149e6 | |||
| 4819480eb0 | |||
| fc7ab0dc21 | |||
| eb6b96c0f1 | |||
| f62fad9c40 | |||
| e9a472c8f7 | |||
| 552e2b8cbf | |||
| a0b4192cbd | |||
| 32a055093a | |||
| 51558c7027 | |||
| b85286f0ab | |||
| 48f3fbd715 | |||
| 458f66818a | |||
| 7964dc7993 | |||
| 0ac5d7d461 | |||
| 826914a4f3 | |||
| 261da431e7 | |||
| 94b33abf5e | |||
| 8325056ccc | |||
| 81a1b2c907 | |||
| fc8f89fb83 | |||
| d846c7745f | |||
| 61c6fd6c02 | |||
| abe5708058 | |||
| 626339bddb | |||
| 381ae23c07 | |||
| 52382d1d7a | |||
| 68e5216d4b | |||
| ceff95b336 | |||
| 782d1c215f | |||
| e8e26cfe10 | |||
| 400d4d992e | |||
| b08df55321 | |||
| b26a8110e0 | |||
| 692f550596 | |||
| badae8b8e4 | |||
| 7f89bde99e | |||
| 326717444b | |||
| 4e4bfc75e3 | |||
| 5c97a8ba73 | |||
| cba7a160ea | |||
| 4a75e1379d | |||
| 10a140d188 | |||
| edc3c5af57 | |||
| 26708305d6 | |||
| 6a282e6ab9 | |||
| a8ba7f7247 | |||
| 502fe718d5 | |||
| 5c7aac9fd2 | |||
| 2f96d9934b | |||
| daff15463d | |||
| 5f68d261c0 | |||
| 013ea6dba9 | |||
| ab9d75757f | |||
| 9be20109af | |||
| 9430b76258 | |||
| 99f1a004d5 | |||
| 2b42b639ea | |||
| 76d5944726 | |||
| 81bd19cfb6 | |||
| 0365ca7fb6 | |||
| 529d65b3fb | |||
| cbbf5d3d25 | |||
| 7b2d234ea5 | |||
| a282ca35f1 | |||
| 0792862c0d | |||
| 6c0d8e91fa | |||
| 8892c43a7d | |||
| 7cfe04286a | |||
| 000513f212 | |||
| 6ca3da1b80 | |||
| fe394ecbe4 | |||
| ec6f0423ca | |||
| e12208af75 | |||
| 2d9c75fd81 | |||
| 0c54b1c3da | |||
| be7943dc9b | |||
| 303c2f1e39 | |||
| 0b32ab899c | |||
| ef463d3d85 | |||
| c10daa2fd4 | |||
| cb8aa45cde | |||
| 54e0a1e886 | |||
| 71a452080e | |||
| 5be7fb3038 | |||
| 59f0c8b0d2 | |||
| e1ed93674d | |||
| 6b65a8099b | |||
| c1dd494d0f | |||
| bf9e8a6e21 | |||
| 371d7617ff | |||
| 0b435d9ed8 | |||
| 07f89d15a9 | |||
| b5318086d2 | |||
| b7f9b7c830 | |||
| 2e4f386f49 | |||
| 9ead985798 | |||
| 6326ee31c4 | |||
| 984dc95550 | |||
| cd0a9b8e33 | |||
| 90b9ba15ec | |||
| 0abc419db7 | |||
| b185d7a63d | |||
| 4bb880c4b7 | |||
| b0ba9e43f9 | |||
| ddb01fb968 | |||
| 22812e0632 | |||
| 4d84e1d9d3 | |||
| 9368b857e8 | |||
| 0f663df7f6 | |||
| 68fde7d165 | |||
| 501a784264 | |||
| 629c184195 | |||
| 76b8818a33 | |||
| 6c52301a64 | |||
| be26db63ca | |||
| 962bfa213f | |||
| ee00e5c914 | |||
| 6343213538 | |||
| 187bdc0836 | |||
| f2520f0481 | |||
| 5e9a73645b | |||
| 783fa3286c | |||
| 77b4f9db65 | |||
| 0f6f07161b | |||
| d75a836de9 | |||
| f79b0fd025 |
@@ -27,17 +27,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -18,10 +18,10 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
os: ubuntu-24.04-arm
|
||||
- platform: linux/arm/v7
|
||||
os: ubuntu-24.04-arm
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
@@ -38,21 +38,21 @@ jobs:
|
||||
latest=false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.arch.platform }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -85,28 +85,36 @@ jobs:
|
||||
needs: docker-build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Codeberg
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: codeberg.org
|
||||
username: ${{ secrets.CODEBERG_USER }}
|
||||
password: ${{ secrets.CODEBERG_PASS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
codeberg.org/wg-easy/wg-easy
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
@@ -130,10 +138,10 @@ jobs:
|
||||
contents: write
|
||||
needs: docker-merge
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11.9
|
||||
cache: "pip"
|
||||
|
||||
@@ -25,10 +25,10 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
os: ubuntu-24.04-arm
|
||||
- platform: linux/arm/v7
|
||||
os: ubuntu-24.04-arm
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: master
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
@@ -47,21 +47,21 @@ jobs:
|
||||
latest=false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.arch.platform }}
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -94,28 +94,36 @@ jobs:
|
||||
needs: docker-build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Codeberg
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: codeberg.org
|
||||
username: ${{ secrets.CODEBERG_USER }}
|
||||
password: ${{ secrets.CODEBERG_PASS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
codeberg.org/wg-easy/wg-easy
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
@@ -139,12 +147,12 @@ jobs:
|
||||
contents: write
|
||||
needs: docker-merge
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11.9
|
||||
cache: "pip"
|
||||
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
os: ubuntu-24.04-arm
|
||||
- platform: linux/arm/v7
|
||||
os: ubuntu-24.04-arm
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -32,20 +32,20 @@ jobs:
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
os: ubuntu-24.04-arm
|
||||
- platform: linux/arm/v7
|
||||
os: ubuntu-24.04-arm
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
@@ -46,21 +46,21 @@ jobs:
|
||||
latest=false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.arch.platform }}
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -95,28 +95,36 @@ jobs:
|
||||
needs: docker-build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Codeberg
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: codeberg.org
|
||||
username: ${{ secrets.CODEBERG_USER }}
|
||||
password: ${{ secrets.CODEBERG_PASS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
codeberg.org/wg-easy/wg-easy
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
@@ -144,10 +152,10 @@ jobs:
|
||||
contents: write
|
||||
needs: docker-merge
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11.9
|
||||
cache: "pip"
|
||||
|
||||
@@ -14,17 +14,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
node-version: "lts/krypton"
|
||||
check-latest: true
|
||||
cache: "pnpm"
|
||||
|
||||
@@ -47,17 +47,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
node-version: "lts/krypton"
|
||||
check-latest: true
|
||||
cache: "pnpm"
|
||||
|
||||
|
||||
@@ -15,13 +15,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wg-easy'
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
# Stale after 30 days of inactivity
|
||||
days-before-issue-stale: 30
|
||||
# Close after 14 days of being stale
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
@@ -32,3 +35,9 @@ jobs:
|
||||
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
operations-per-run: 100
|
||||
# Ignore Feature requests (https://github.com/actions/stale/issues/1293)
|
||||
only-issue-types: "Bug"
|
||||
# Ignore confirmed bugs
|
||||
exempt-issue-labels: "status: confirmed"
|
||||
# Ignore PRs with milestones
|
||||
exempt-all-pr-milestones: true
|
||||
|
||||
Vendored
-2
@@ -3,8 +3,6 @@
|
||||
"aaron-bond.better-comments",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"antfu.goto-alias",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"Nuxtr.nuxtr-vscode",
|
||||
"esbenp.prettier-vscode",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
|
||||
Vendored
-3
@@ -3,9 +3,6 @@
|
||||
"editor.useTabStops": false,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"nuxtr.vueFiles.style.addStyleTag": false,
|
||||
"nuxtr.piniaFiles.defaultTemplate": "setup",
|
||||
"nuxtr.monorepoMode.DirectoryName": "src",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
|
||||
+104
-2
@@ -7,14 +7,116 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- AWG: support for H1-H4 ranges (https://github.com/wg-easy/wg-easy/pull/2480)
|
||||
- Client Firewall (https://github.com/wg-easy/wg-easy/pull/2418)
|
||||
- CLI: Show QR code (https://github.com/wg-easy/wg-easy/pull/2518)
|
||||
- Copy QR code to clipboard / save as png (https://github.com/wg-easy/wg-easy/pull/2521)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add trailing newline to Prometheus metrics output (https://github.com/wg-easy/wg-easy/pull/2573)
|
||||
- Correctly use DEBUG env var (https://github.com/wg-easy/wg-easy/pull/2619)
|
||||
|
||||
### Changed
|
||||
|
||||
- Hooks are now Textareas (https://github.com/wg-easy/wg-easy/pull/2522)
|
||||
- Update to Node Krypton (24) (https://github.com/wg-easy/wg-easy/pull/2536)
|
||||
- Mobile UI (https://github.com/wg-easy/wg-easy/pull/2569)
|
||||
- Prevent enabling client when expired (https://github.com/wg-easy/wg-easy/pull/2594)
|
||||
|
||||
## [15.2.2] - 2026-02-06
|
||||
|
||||
### Added
|
||||
|
||||
- Added Userspace WireGuard support (https://github.com/wg-easy/wg-easy/pull/2419)
|
||||
|
||||
### Fixed
|
||||
|
||||
- LangSelector overlapping with Buttons (https://github.com/wg-easy/wg-easy/pull/2434)
|
||||
- AmnzeziaWG config parameters (https://github.com/wg-easy/wg-easy/pull/2440)
|
||||
- OpenMetrics help string format (https://github.com/wg-easy/wg-easy/pull/2453)
|
||||
- Reset 2fa when resetting admin password (https://github.com/wg-easy/wg-easy/pull/2461)
|
||||
|
||||
### Docs
|
||||
|
||||
- Replace Watchtower with maintained fork (https://github.com/wg-easy/wg-easy/pull/2456)
|
||||
|
||||
## [15.2.1] - 2026-01-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Icon in Searchbar (https://github.com/wg-easy/wg-easy/commit/458f66818a400f181e2c6326ede077c8793d71f2)
|
||||
- Interface save not working (https://github.com/wg-easy/wg-easy/commit/48f3fbd715a889e2425702a8a46332f2752aef91)
|
||||
- Error Messages in Setup (https://github.com/wg-easy/wg-easy/commit/32a055093a76342c40858d8dcf563b0700a8bd48)
|
||||
|
||||
## [15.2.0] - 2026-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- AmneziaWG integration (https://github.com/wg-easy/wg-easy/pull/2102, https://github.com/wg-easy/wg-easy/pull/2226)
|
||||
- Search / filter box (https://github.com/wg-easy/wg-easy/pull/2170)
|
||||
- `INIT_ALLOWED_IPS` env var (https://github.com/wg-easy/wg-easy/pull/2164)
|
||||
- Show client endpoint (https://github.com/wg-easy/wg-easy/pull/2058)
|
||||
- Add option to view and copy config (https://github.com/wg-easy/wg-easy/pull/2289)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix download as conf.txt (https://github.com/wg-easy/wg-easy/pull/2269)
|
||||
- Clean filename for OTL download (https://github.com/wg-easy/wg-easy/pull/2253)
|
||||
- Text color in admin menu in light mode (https://github.com/wg-easy/wg-easy/pull/2307)
|
||||
|
||||
### Changed
|
||||
|
||||
- Allow lower MTU (https://github.com/wg-easy/wg-easy/pull/2228)
|
||||
- Use /32 and /128 for client Cidr (https://github.com/wg-easy/wg-easy/pull/2217)
|
||||
- Return client id on create (https://github.com/wg-easy/wg-easy/pull/2190)
|
||||
- Publish on Codeberg (https://github.com/wg-easy/wg-easy/pull/2160)
|
||||
- Allow empty DNS (https://github.com/wg-easy/wg-easy/pull/2052, https://github.com/wg-easy/wg-easy/pull/2057)
|
||||
- Don't include keys in API responses (https://github.com/wg-easy/wg-easy/pull/2015)
|
||||
- Try all QR ecc levels (https://github.com/wg-easy/wg-easy/pull/2288)
|
||||
- Update OneTimeLink expiry on reuse (https://github.com/wg-easy/wg-easy/pull/2370)
|
||||
- Removed ARMv7 support (https://github.com/wg-easy/wg-easy/pull/2369)
|
||||
|
||||
### Docs
|
||||
|
||||
- Add AdGuard Home (https://github.com/wg-easy/wg-easy/pull/2175)
|
||||
- Add Routed (No NAT) docs (https://github.com/wg-easy/wg-easy/pull/2181, https://github.com/wg-easy/wg-easy/pull/2380)
|
||||
- Add AmneziaWG docs (https://github.com/wg-easy/wg-easy/pull/2108, https://github.com/wg-easy/wg-easy/pull/2292)
|
||||
|
||||
## [15.1.0] - 2025-07-01
|
||||
|
||||
### Added
|
||||
|
||||
- Added Ukrainian language (#1906)
|
||||
- Add French language (#1924)
|
||||
- docs for caddy example (#1939)
|
||||
- add docs on how to add/update translation (be26db6)
|
||||
- Add german translations (#1889)
|
||||
- feat: Add Traditional Chinese (zh-HK) i18n Support (#1988)
|
||||
- Add Chinese Simplified (#1990)
|
||||
- Add option to disable ipv6 (#1951)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updated container launch commands (#1989)
|
||||
- update screenshot (962bfa2)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated dependencies
|
||||
|
||||
## [15.0.0] - 2025-05-28
|
||||
|
||||
We're super excited to announce v15!
|
||||
This update is an entire rewrite to make it even easier to set up your own VPN.
|
||||
|
||||
## Breaking Changes
|
||||
### Breaking Changes
|
||||
|
||||
As the whole setup has changed, we recommend to start from scratch. And import your existing configs.
|
||||
|
||||
## Major Changes
|
||||
### Major Changes
|
||||
|
||||
- Almost all Environment variables removed
|
||||
- New and Improved UI
|
||||
|
||||
+31
-7
@@ -1,4 +1,4 @@
|
||||
FROM docker.io/library/node:lts-alpine AS build
|
||||
FROM docker.io/library/node:krypton-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# update corepack
|
||||
@@ -7,16 +7,29 @@ RUN npm install --global corepack@latest
|
||||
RUN corepack enable pnpm
|
||||
|
||||
# Copy Web UI
|
||||
COPY src/package.json src/pnpm-lock.yaml ./
|
||||
COPY src/package.json src/pnpm-lock.yaml src/pnpm-workspace.yaml ./
|
||||
RUN pnpm install
|
||||
|
||||
# Build UI
|
||||
COPY src ./
|
||||
RUN pnpm build
|
||||
|
||||
# Build amneziawg-tools
|
||||
RUN apk add linux-headers build-base go git && \
|
||||
git clone https://github.com/amnezia-vpn/amneziawg-tools.git && \
|
||||
git clone https://github.com/amnezia-vpn/amneziawg-go && \
|
||||
cd amneziawg-go && \
|
||||
make && \
|
||||
cd ../amneziawg-tools/src && \
|
||||
make
|
||||
|
||||
FROM docker.io/library/node:krypton-alpine AS build-libsql
|
||||
WORKDIR /app
|
||||
RUN npm install --no-save --omit=dev libsql
|
||||
|
||||
# Copy build result to a new image.
|
||||
# This saves a lot of disk space.
|
||||
FROM docker.io/library/node:lts-alpine
|
||||
FROM docker.io/library/node:krypton-alpine
|
||||
WORKDIR /app
|
||||
|
||||
HEALTHCHECK --interval=1m --timeout=5s --retries=3 CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1"
|
||||
@@ -26,12 +39,18 @@ COPY --from=build /app/.output /app
|
||||
# Copy migrations
|
||||
COPY --from=build /app/server/database/migrations /app/server/database/migrations
|
||||
# libsql (https://github.com/nitrojs/nitro/issues/3328)
|
||||
RUN cd /app/server && \
|
||||
npm install --no-save libsql && \
|
||||
npm cache clean --force
|
||||
COPY --from=build-libsql /app/node_modules /app/server/node_modules
|
||||
|
||||
# cli
|
||||
COPY --from=build /app/cli/cli.sh /usr/local/bin/cli
|
||||
RUN chmod +x /usr/local/bin/cli
|
||||
# Copy amneziawg-go
|
||||
COPY --from=build /app/amneziawg-go/amneziawg-go /usr/bin/amneziawg-go
|
||||
RUN chmod +x /usr/bin/amneziawg-go
|
||||
# Copy amneziawg-tools
|
||||
COPY --from=build /app/amneziawg-tools/src/wg /usr/bin/awg
|
||||
COPY --from=build /app/amneziawg-tools/src/wg-quick/linux.bash /usr/bin/awg-quick
|
||||
RUN chmod +x /usr/bin/awg /usr/bin/awg-quick
|
||||
|
||||
# Install Linux packages
|
||||
RUN apk add --no-cache \
|
||||
@@ -42,18 +61,23 @@ RUN apk add --no-cache \
|
||||
nftables \
|
||||
kmod \
|
||||
iptables-legacy \
|
||||
wireguard-go \
|
||||
wireguard-tools
|
||||
|
||||
RUN mkdir -p /etc/amnezia
|
||||
RUN ln -s /etc/wireguard /etc/amnezia/amneziawg
|
||||
|
||||
# Use iptables-legacy
|
||||
RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save
|
||||
RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tables-legacy 10 --slave /usr/sbin/ip6tables-restore ip6tables-restore /usr/sbin/ip6tables-legacy-restore --slave /usr/sbin/ip6tables-save ip6tables-save /usr/sbin/ip6tables-legacy-save
|
||||
|
||||
# Set Environment
|
||||
ENV DEBUG=Server,WireGuard,Database,CMD
|
||||
ENV DEBUG=Server,WireGuard,Database,CMD,Firewall
|
||||
ENV PORT=51821
|
||||
ENV HOST=0.0.0.0
|
||||
ENV INSECURE=false
|
||||
ENV INIT_ENABLED=false
|
||||
ENV DISABLE_IPV6=false
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy
|
||||
|
||||
|
||||
+5
-3
@@ -1,4 +1,4 @@
|
||||
FROM docker.io/library/node:lts-alpine
|
||||
FROM docker.io/library/node:krypton-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# update corepack
|
||||
@@ -16,6 +16,7 @@ RUN apk add --no-cache \
|
||||
ip6tables \
|
||||
kmod \
|
||||
iptables-legacy \
|
||||
wireguard-go \
|
||||
wireguard-tools
|
||||
|
||||
# Use iptables-legacy
|
||||
@@ -23,14 +24,15 @@ RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables
|
||||
RUN update-alternatives --install /usr/sbin/ip6tables ip6tables /usr/sbin/ip6tables-legacy 10 --slave /usr/sbin/ip6tables-restore ip6tables-restore /usr/sbin/ip6tables-legacy-restore --slave /usr/sbin/ip6tables-save ip6tables-save /usr/sbin/ip6tables-legacy-save
|
||||
|
||||
# Set Environment
|
||||
ENV DEBUG=Server,WireGuard,Database,CMD
|
||||
ENV DEBUG=Server,WireGuard,Database,CMD,Firewall
|
||||
ENV PORT=51821
|
||||
ENV HOST=0.0.0.0
|
||||
ENV INSECURE=true
|
||||
ENV INIT_ENABLED=false
|
||||
ENV DISABLE_IPV6=false
|
||||
|
||||
# Install Dependencies
|
||||
COPY src/package.json src/pnpm-lock.yaml ./
|
||||
COPY src/package.json src/pnpm-lock.yaml src/pnpm-workspace.yaml ./
|
||||
RUN pnpm install
|
||||
|
||||
# Copy Project
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
# WireGuard Easy
|
||||
|
||||
[](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml)
|
||||
[](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml)
|
||||
[](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml)
|
||||
[](https://github.com/wg-easy/wg-easy/stargazers)
|
||||
[](LICENSE)
|
||||
[](https://github.com/wg-easy/wg-easy/releases/latest)
|
||||
[](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
|
||||
|
||||
<!-- TODO: remove after release -->
|
||||
|
||||
> [!WARNING]
|
||||
> You are viewing the README of the pre-release of v15.
|
||||
> If you want to setup wg-easy right now. Read the README in the production branch here: [README](https://github.com/wg-easy/wg-easy/tree/production) or here for the last nightly: [README](https://github.com/wg-easy/wg-easy/tree/c6dce0f6fb2e28e7e40ddac1498bd67e9bb17cba)
|
||||
[](https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy)
|
||||
|
||||
You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||
|
||||
<!-- TOOD: update screenshot -->
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/screenshot.png" width="802" />
|
||||
<img src="./assets/screenshot.png" width="802" alt="wg-easy Screenshot" />
|
||||
</p>
|
||||
|
||||
## Features
|
||||
@@ -39,15 +31,11 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||
- IPv6 support
|
||||
- CIDR support
|
||||
- 2FA support
|
||||
- Per-client firewall filtering (requires iptables)
|
||||
|
||||
> [!NOTE]
|
||||
> To better manage documentation for this project, it has its own site here: [https://wg-easy.github.io/wg-easy/latest](https://wg-easy.github.io/wg-easy/latest)
|
||||
|
||||
<!-- TODO: remove after release -->
|
||||
|
||||
> [!WARNING]
|
||||
> As the Docs are still in Pre-release, you can access them here [https://wg-easy.github.io/wg-easy/Pre-release](https://wg-easy.github.io/wg-easy/Pre-release)
|
||||
|
||||
- [Getting Started](https://wg-easy.github.io/wg-easy/latest/getting-started/)
|
||||
- [Basic Installation](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/basic-installation/)
|
||||
- [Caddy](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/caddy/)
|
||||
@@ -79,11 +67,11 @@ And log in again.
|
||||
|
||||
The easiest way to run WireGuard Easy is with Docker Compose.
|
||||
|
||||
Just download [`docker-compose.yml`](docker-compose.yml) and execute `sudo docker compose up -d`.
|
||||
Just follow [these steps](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/basic-installation/) in the detailed documentation.
|
||||
|
||||
Now setup a reverse proxy to be able to access the Web UI securely from the internet.
|
||||
You can also install WireGuard Easy with the [docker run command](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/docker-run/) or via [podman](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/podman-nft/).
|
||||
|
||||
If you want to access the Web UI over HTTP, change the env var `INSECURE` to `true`. This is not recommended. Only use this for testing
|
||||
Now [setup a reverse proxy](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/basic-installation/#setup-reverse-proxy) to be able to access the Web UI securely from the internet. This step is optional, just make sure to follow the guide [here](https://wg-easy.github.io/wg-easy/latest/examples/tutorials/reverse-proxyless/) if you decide not to do it.
|
||||
|
||||
## Donate
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 167 KiB |
@@ -1,44 +0,0 @@
|
||||
volumes:
|
||||
etc_wireguard:
|
||||
|
||||
services:
|
||||
wg-easy:
|
||||
#environment:
|
||||
# Optional:
|
||||
# - PORT=51821
|
||||
# - HOST=0.0.0.0
|
||||
# - INSECURE=false
|
||||
|
||||
image: ghcr.io/wg-easy/wg-easy:15
|
||||
container_name: wg-easy
|
||||
networks:
|
||||
wg:
|
||||
ipv4_address: 10.42.42.42
|
||||
ipv6_address: fdcc:ad94:bacf:61a3::2a
|
||||
volumes:
|
||||
- etc_wireguard:/etc/wireguard
|
||||
- /lib/modules:/lib/modules:ro
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
- "51821:51821/tcp"
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
# - NET_RAW # ⚠️ Uncomment if using Podman Compose
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
- net.ipv6.conf.all.forwarding=1
|
||||
- net.ipv6.conf.default.forwarding=1
|
||||
|
||||
networks:
|
||||
wg:
|
||||
driver: bridge
|
||||
enable_ipv6: true
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.42.42.0/24
|
||||
- subnet: fdcc:ad94:bacf:61a3::/64
|
||||
+24
-25
@@ -3,35 +3,21 @@ volumes:
|
||||
|
||||
services:
|
||||
wg-easy:
|
||||
environment:
|
||||
# Change Language:
|
||||
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi)
|
||||
- LANG=de
|
||||
# ⚠️ Required:
|
||||
# Change this to your host's public address
|
||||
- WG_HOST=raspberrypi.local
|
||||
#environment:
|
||||
# Optional:
|
||||
# - PORT=51821
|
||||
# - HOST=0.0.0.0
|
||||
# - INSECURE=false
|
||||
|
||||
# Optional:
|
||||
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG # (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
|
||||
# - PORT=51821
|
||||
# - WG_PORT=51820
|
||||
# - WG_CONFIG_PORT=92820
|
||||
# - WG_DEFAULT_ADDRESS=10.8.0.x
|
||||
# - WG_DEFAULT_DNS=1.1.1.1
|
||||
# - WG_MTU=1420
|
||||
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
|
||||
# - WG_PERSISTENT_KEEPALIVE=25
|
||||
# - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt
|
||||
# - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt
|
||||
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
|
||||
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
|
||||
# - UI_TRAFFIC_STATS=true
|
||||
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
|
||||
|
||||
image: ghcr.io/wg-easy/wg-easy:14
|
||||
image: ghcr.io/wg-easy/wg-easy:15
|
||||
container_name: wg-easy
|
||||
networks:
|
||||
wg:
|
||||
ipv4_address: 10.42.42.42
|
||||
ipv6_address: fdcc:ad94:bacf:61a3::2a
|
||||
volumes:
|
||||
- etc_wireguard:/etc/wireguard
|
||||
- /lib/modules:/lib/modules:ro
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
- "51821:51821/tcp"
|
||||
@@ -43,3 +29,16 @@ services:
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
- net.ipv6.conf.all.forwarding=1
|
||||
- net.ipv6.conf.default.forwarding=1
|
||||
|
||||
networks:
|
||||
wg:
|
||||
driver: bridge
|
||||
enable_ipv6: true
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.42.42.0/24
|
||||
- subnet: fdcc:ad94:bacf:61a3::/64
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: AmneziaWG
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
**AmneziaWG** is a modified version of the WireGuard protocol with enhanced traffic obfuscation capabilities. AmneziaWG's primary goal is to counter deep packet inspection (DPI) systems and bypass VPN blocking.
|
||||
|
||||
AmneziaWG adds multi-level transport-layer obfuscation by:
|
||||
|
||||
- Modifying packet headers
|
||||
- Randomizing handshake message sizes
|
||||
- Disguising traffic to resemble popular UDP protocols
|
||||
|
||||
These measures make it harder for third parties to analyze or identify your traffic, enhancing both privacy and security.
|
||||
|
||||
## Activating AmneziaWG
|
||||
|
||||
You must install the [AmneziaWG kernel module](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module) on the host system.
|
||||
|
||||
Experimental support for AmneziaWG can be enabled by setting the `EXPERIMENTAL_AWG` environment variable to `true`. Starting from wg-easy version 16, this setting will be enabled by default. This feature is still under development and may change in future releases.
|
||||
|
||||
When enabled, wg-easy will automatically detect whether the AmneziaWG kernel module is available. If it is not, the system will fall back to the standard WireGuard module.
|
||||
|
||||
To override this automatic detection, set the `OVERRIDE_AUTO_AWG` environment variable. By default, this variable is unset.
|
||||
|
||||
Possible values:
|
||||
|
||||
- `awg` — Force use of AmneziaWG
|
||||
- `wg` — Force use of standard WireGuard
|
||||
|
||||
## AmneziaWG Parameters
|
||||
|
||||
Parameter descriptions can be found in the [AmneziaWG documentation](https://docs.amnezia.org/documentation/amnezia-wg) and on the [kernel module page](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module).
|
||||
|
||||
All parameters except I1-I5 will be set at first startup. For information on how to set I1-I5 parameters, refer to the [AmneziaWG documentation](https://docs.amnezia.org/documentation/instructions/new-amneziawg-selfhosted/#how-to-extract-a-protocol-signature-for-amneziawg-15-manually).
|
||||
|
||||
If a parameter is not set, it will not be added to the configuration. If all AmneziaWG-specific parameters are absent, AmneziaWG will be fully compatible with standard WireGuard.
|
||||
|
||||
### Parameter Compatibility Table
|
||||
|
||||
| Parameter | Can differ between server and client | Configurable on server | Configurable on client |
|
||||
| --------- | ------------------------------------ | ---------------------- | ----------------------- |
|
||||
| Jc | ✅ Yes | ✅ | ✅ |
|
||||
| Jmin | ✅ Yes | ✅ | ✅ |
|
||||
| Jmax | ✅ Yes | ✅ | ✅ |
|
||||
| S1-S4 | ❌ No, must match | ✅ | ❌ (copied from server) |
|
||||
| H1-H4 | ❌ No, must match | ✅ | ❌ (copied from server) |
|
||||
| I1-I5 | ✅ Yes | ✅ | ✅ |
|
||||
|
||||
## Client Applications
|
||||
|
||||
To be able to connect to wg-easy if AmneziaWG is enabled, you must have an AmneziaWG-compatible client. Where an AmneziaWG app is available for your platform, it is recommended to use it rather than Amnezia VPN.
|
||||
|
||||
Android:
|
||||
|
||||
- [AmneziaWG](https://play.google.com/store/apps/details?id=org.amnezia.awg) - AmneziaWG Official Client
|
||||
- [WG Tunnel](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel) - Third Party Client
|
||||
- [Amnezia VPN](https://play.google.com/store/apps/details?id=org.amnezia.vpn) - Amnezia VPN Official Client
|
||||
|
||||
iOS and macOS:
|
||||
|
||||
- [AmneziaWG](https://apps.apple.com/us/app/amneziawg/id6478942365) - AmneziaWG Official Client
|
||||
- [Amnezia VPN](https://apps.apple.com/us/app/amneziavpn/id1600529900) - Amnezia VPN Official Client
|
||||
|
||||
Windows:
|
||||
|
||||
- [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-windows-client/releases) - AmneziaWG Official Client (Requires building from source code)
|
||||
- [Amnezia VPN](https://amnezia.org/downloads) - Amnezia VPN Official Client
|
||||
|
||||
Linux:
|
||||
|
||||
- [Amnezia VPN](https://amnezia.org/downloads) - Amnezia VPN Official Client
|
||||
- [amneziawg-tools](https://github.com/amnezia-vpn/amneziawg-tools) - AmneziaWG Tools
|
||||
|
||||
OpenWRT:
|
||||
|
||||
- [AmneziaWG OpenWRT](https://github.com/Slava-Shchipunov/awg-openwrt) - AmneziaWG OpenWRT Packages
|
||||
- [AmneziaWG OpenWRT](https://github.com/lolo6oT/awg-openwrt) - AmneziaWG OpenWRT Packages
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Experimental Configuration
|
||||
---
|
||||
|
||||
There are several experimental features that can be enabled by setting the appropriate environment variables. These features are not guaranteed to be stable and may change in future releases.
|
||||
|
||||
| Env | Default | Example | Description | Notes | More Info |
|
||||
| ---------------- | ------- | ------- | -------------------------------------- | --------------------------------------- | ------------------------ |
|
||||
| EXPERIMENTAL_AWG | false | true | Enables experimental AmneziaWG support | Planned to be enabled by default in v16 | [See here](./amnezia.md) |
|
||||
@@ -4,8 +4,19 @@ title: Optional Configuration
|
||||
|
||||
You can set these environment variables to configure the container. They are not required, but can be useful in some cases.
|
||||
|
||||
| Env | Default | Example | Description |
|
||||
| ---------- | --------- | ----------- | ------------------------------ |
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `INSECURE` | `false` | `true` | If access over http is allowed |
|
||||
| Env | Default | Example | Description |
|
||||
| -------------- | --------- | ----------- | ---------------------------------- |
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `INSECURE` | `false` | `true` | If access over http is allowed |
|
||||
| `DISABLE_IPV6` | `false` | `true` | If IPv6 support should be disabled |
|
||||
|
||||
/// note | IPv6 Caveats
|
||||
|
||||
Disabling IPv6 will disable the creation of the default IPv6 firewall rules and won't add a IPv6 address to the interface and clients.
|
||||
|
||||
You will however still see a IPv6 address in the Web UI, but it won't be used.
|
||||
|
||||
This option can be removed in the future, as more devices support IPv6.
|
||||
|
||||
///
|
||||
|
||||
@@ -6,18 +6,19 @@ If you want to run the setup without any user interaction, e.g. with a tool like
|
||||
|
||||
These will only be used during the first start of the container. After that, the setup will be disabled.
|
||||
|
||||
| Env | Example | Description | Group |
|
||||
| ---------------- | ----------------- | --------------------------------------------------------- | ----- |
|
||||
| `INIT_ENABLED` | `true` | Enables the below env vars | 0 |
|
||||
| `INIT_USERNAME` | `admin` | Sets admin username | 1 |
|
||||
| `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 |
|
||||
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 |
|
||||
| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 |
|
||||
| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 |
|
||||
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 |
|
||||
| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 |
|
||||
| Env | Example | Description | Group |
|
||||
| ------------------ | ---------------------------- | --------------------------------------------------------- | ----- |
|
||||
| `INIT_ENABLED` | `true` | Enables the below env vars | 0 |
|
||||
| `INIT_USERNAME` | `admin` | Sets admin username | 1 |
|
||||
| `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 |
|
||||
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 |
|
||||
| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 |
|
||||
| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 |
|
||||
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 |
|
||||
| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 |
|
||||
| `INIT_ALLOWED_IPS` | `10.8.0.0/24,2001:0DB8::/32` | Sets global Allowed IPs | 4 |
|
||||
|
||||
/// warning | Variables have to be used together
|
||||
/// warning | Variables have to be used together
|
||||
|
||||
If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`.
|
||||
|
||||
|
||||
@@ -6,22 +6,24 @@ This guide will help you migrate from `v14` to version `v15` of `wg-easy`.
|
||||
|
||||
## Changes
|
||||
|
||||
- This is a complete rewrite of the `wg-easy` project. Therefore the configuration files and the way you interact with the project have changed.
|
||||
- If you use armv6, you unfortunately won't be able to migrate to `v15`.
|
||||
- If you are connecting to the web ui via HTTP, you need to set the `INSECURE` environment variable to `true` in the new container.
|
||||
- This is a complete rewrite of the `wg-easy` project, therefore the configuration files and the way you interact with the project have changed.
|
||||
- If you use armv6 or armv7, you unfortunately won't be able to migrate to `v15`.
|
||||
- If you are connecting to the Web UI via HTTP, you need to set the `INSECURE` environment variable to `true` in the new container.
|
||||
|
||||
## Migration
|
||||
|
||||
### Backup
|
||||
|
||||
Before you start the migration, make sure to backup your existing configuration files.
|
||||
Before you start the migration, make sure to back up your existing configuration files.
|
||||
|
||||
Go into the Web Ui and click the Backup button, this should download a `wg0.json` file.
|
||||
Go into the Web UI and click the Backup button, this should download a `wg0.json` file.
|
||||
|
||||
Or download the `wg0.json` file from your container volume to your pc.
|
||||
|
||||
You will need this file for the migration
|
||||
|
||||
You will also need to back up the old environment variables you set for the container, as they will not be automatically migrated.
|
||||
|
||||
### Remove old container
|
||||
|
||||
1. Stop the running container
|
||||
@@ -32,10 +34,10 @@ If you are using `docker run`
|
||||
docker stop wg-easy
|
||||
```
|
||||
|
||||
If you are using `docker-compose`
|
||||
If you are using `docker compose`
|
||||
|
||||
```shell
|
||||
docker-compose down
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Start new container
|
||||
@@ -47,6 +49,10 @@ In the setup wizard, select that you already have a configuration file and uploa
|
||||
[docs-getting-started]: ../../getting-started.md
|
||||
[docs-examples]: ../../examples/tutorials/basic-installation.md
|
||||
|
||||
### Environment Variables
|
||||
|
||||
v15 does not use the same environment variables as v14, most of them have been moved to the Admin Panel in the Web UI.
|
||||
|
||||
### Done
|
||||
|
||||
You have now successfully migrated to `v15` of `wg-easy`.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Translation
|
||||
---
|
||||
|
||||
This project supports multiple languages. If you would like to contribute a translation, please follow these steps:
|
||||
|
||||
## Add new Translation
|
||||
|
||||
Create a new file in `src/i18n/locales`. Name it `<locale_code>.json` (e.g. `fr.json` for French).
|
||||
|
||||
Import and add the newly created file in `src/i18n/i18n.config.ts`.
|
||||
|
||||
Add your language in the `src/nuxt.config.ts` file. You have to specify code, language and name.
|
||||
|
||||
`code` is the name of the translation file without the extension (e.g. `fr` for `fr.json`).
|
||||
|
||||
`language` is the BCP 47 language tag with region (e.g. `fr-FR` for French). See [www.lingoes.net](http://www.lingoes.net/en/translator/langcode.htm) for a list of language codes.
|
||||
|
||||
`name` is the display name of the language (e.g. `Français` for French).
|
||||
|
||||
## Update existing Translation
|
||||
|
||||
If you need to update an existing translation, simply edit the corresponding `<locale_code>.json` file in `src/i18n/locales`.
|
||||
|
||||
## Contribute changes
|
||||
|
||||
See [Pull Requests](./issues-and-pull-requests.md#pull-requests) on how to contribute your translation.
|
||||
@@ -2,8 +2,176 @@
|
||||
title: AdGuard Home
|
||||
---
|
||||
|
||||
It seems like the Docs on how to setup AdGuard Home are not available yet.
|
||||
This tutorial is a follow-up to the official [Traefik tutorial](./traefik.md). It will guide you through integrating AdGuard Home with your existing `wg-easy` and Traefik setup to provide network-wide DNS ad-blocking.
|
||||
|
||||
Feel free to create a PR and add them here.
|
||||
## Prerequisites
|
||||
|
||||
<!-- TODO -->
|
||||
- A working [wg-easy](./basic-installation.md) and [Traefik](./traefik.md) setup from the previous guides.
|
||||
|
||||
/// warning | Important: Following this guide will reset your WireGuard configuration.
|
||||
The process involves re-creating the `wg-easy` container and its data, which means **all existing WireGuard clients and settings will be deleted.**
|
||||
|
||||
You will need to create your clients again after completing this guide.
|
||||
///
|
||||
|
||||
## Add `adguard` configuration
|
||||
|
||||
1. Create a directory for the configuration files:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /etc/docker/containers/adguard
|
||||
```
|
||||
|
||||
2. Create volumes for persistent data:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /etc/docker/volumes/adguard/adguard_work
|
||||
sudo mkdir -p /etc/docker/volumes/adguard/adguard_conf
|
||||
sudo chmod -R 700 /etc/docker/volumes/adguard
|
||||
```
|
||||
|
||||
3. Create the `docker-compose.yml` file.
|
||||
|
||||
File: `/etc/docker/containers/adguard/docker-compose.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
adguard:
|
||||
image: adguard/adguardhome:v0.107.64
|
||||
container_name: adguard
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /etc/docker/volumes/adguard/adguard_work:/opt/adguardhome/work
|
||||
- /etc/docker/volumes/adguard/adguard_conf:/opt/adguardhome/conf
|
||||
networks:
|
||||
wg:
|
||||
interface_name: eth0
|
||||
ipv4_address: 10.42.42.43
|
||||
ipv6_address: fdcc:ad94:bacf:61a3::2b
|
||||
traefik:
|
||||
interface_name: eth1
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.adguard.rule=Host(`adguard.$example.com$`)'
|
||||
- 'traefik.http.routers.adguard.entrypoints=websecure'
|
||||
- 'traefik.http.routers.adguard.service=adguard'
|
||||
- 'traefik.http.services.adguard.loadbalancer.server.port=3000'
|
||||
- 'traefik.docker.network=traefik'
|
||||
|
||||
networks:
|
||||
wg:
|
||||
external: true
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Update `wg-easy` configuration
|
||||
|
||||
Modify the corresponding sections of your existing `wg-easy` compose file to match the updated version below.
|
||||
|
||||
File: `/etc/docker/containers/wg-easy/docker-compose.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-easy:
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
...
|
||||
networks:
|
||||
wg:
|
||||
interface_name: eth0
|
||||
...
|
||||
traefik:
|
||||
interface_name: eth1
|
||||
...
|
||||
...
|
||||
environment:
|
||||
# Unattended Setup
|
||||
- INIT_ENABLED=true
|
||||
# Replace $username$ with your username
|
||||
- INIT_USERNAME=$username$
|
||||
# Replace $password$ with your unhashed password
|
||||
- INIT_PASSWORD=$password$
|
||||
# Replace $example.com$ with your domain
|
||||
- INIT_HOST=wg-easy.$example.com$
|
||||
- INIT_PORT=51820
|
||||
- INIT_DNS=10.42.42.43,fdcc:ad94:bacf:61a3::2b
|
||||
- INIT_IPV4_CIDR=10.8.0.0/24
|
||||
- INIT_IPV6_CIDR=fd42:42:42::/64
|
||||
...
|
||||
|
||||
networks:
|
||||
wg:
|
||||
# Prevents Docker Compose from prefixing the network name.
|
||||
name: wg
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
## Setup Wireguard
|
||||
|
||||
1. Restart `wg-easy`:
|
||||
|
||||
```shell
|
||||
cd /etc/docker/containers/wg-easy
|
||||
sudo docker compose down -v
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
2. Edit Wireguard's Hooks.
|
||||
|
||||
In the Admin Panel of your WireGuard server, go to the Hooks tab and replace it with:
|
||||
|
||||
**_PostUp_**
|
||||
|
||||
```shell
|
||||
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43; iptables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43; ip6tables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; ip6tables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE;
|
||||
```
|
||||
|
||||
**_PostDown_**
|
||||
|
||||
```shell
|
||||
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; iptables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; iptables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; ip6tables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; ip6tables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; iptables -D FORWARD -i wg0 -j ACCEPT || true; iptables -D FORWARD -o wg0 -j ACCEPT || true; ip6tables -D FORWARD -i wg0 -j ACCEPT || true; ip6tables -D FORWARD -o wg0 -j ACCEPT || true; iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE || true; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE || true;
|
||||
```
|
||||
|
||||
3. Restart `wg-easy` to apply changes:
|
||||
|
||||
```shell
|
||||
sudo docker restart wg-easy
|
||||
```
|
||||
|
||||
## Setup Adguard Home
|
||||
|
||||
1. Start `adguard` service:
|
||||
|
||||
```shell
|
||||
cd /etc/docker/containers/adguard
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
2. Navigate to `https://adguard.$example.com$` to begin the AdGuard Home setup.
|
||||
|
||||
/// warning | Important: Configure AdGuard Home Admin Web Interface Port
|
||||
During the initial AdGuard Home setup on the `Step 2/5` page, you **must** set the **Admin Web Interface Port** to **3000**. Do not use the default port 80, as it will not work with the Traefik configuration.
|
||||
|
||||
After completing the setup, the AdGuard UI might appear unresponsive. This is expected. **Simply reload the page**, and the panel will display correctly.
|
||||
///
|
||||
|
||||
> If you accidentally left it default (80), you will need to manually edit the `docker-compose.yml` file for AdGuard Home (`/etc/docker/containers/adguard/docker-compose.yml`) and change the line `traefik.http.services.adguard.loadbalancer.server.port=3000` to `traefik.http.services.adguard.loadbalancer.server.port=80`. After making this change, restart AdGuard Home by navigating to `/etc/docker/containers/adguard` and running `sudo docker compose up -d`.
|
||||
|
||||
## Final System Checks
|
||||
|
||||
### Firewall
|
||||
|
||||
Ensure the ports `80/tcp`, `443/tcp`, `443/udp`, and `51820/udp` are open.
|
||||
|
||||
### Optional: Optimizing UDP Buffer Sizes
|
||||
|
||||
AdGuard Home, as a DNS server, handles a large volume of UDP packets. To ensure optimal performance, it is recommended to increase the system's UDP buffer sizes. You can apply these settings using your system's `sysctl` configuration (e.g., by creating a file in `/etc/sysctl.d/`).
|
||||
|
||||
```shell
|
||||
net.core.rmem_max = 7500000
|
||||
net.core.wmem_max = 7500000
|
||||
```
|
||||
|
||||
After adding these settings, remember to apply them (e.g., by running `sudo sysctl --system` or rebooting)
|
||||
|
||||
@@ -20,7 +20,7 @@ File: `/etc/docker/containers/watchtower/docker-compose.yml`
|
||||
```yaml
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower:latest
|
||||
image: nickfedor/watchtower:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env_file:
|
||||
|
||||
@@ -8,7 +8,7 @@ title: Basic Installation
|
||||
|
||||
1. You need to have a host that you can manage
|
||||
2. You need to have a domain name or a public IP address
|
||||
3. You need a supported architecture (x86_64, arm64, armv7)
|
||||
3. You need a supported architecture (x86_64, arm64)
|
||||
4. You need curl installed on your host
|
||||
|
||||
## Install Docker
|
||||
@@ -33,7 +33,7 @@ Follow the Docs here: <https://docs.docker.com/engine/install/> and install Dock
|
||||
|
||||
```shell
|
||||
cd /etc/docker/containers/wg-easy
|
||||
sudo docker-compose up -d
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
## Setup Firewall
|
||||
@@ -56,8 +56,8 @@ To update `wg-easy` to the latest version, run:
|
||||
|
||||
```shell
|
||||
cd /etc/docker/containers/wg-easy
|
||||
sudo docker-compose pull
|
||||
sudo docker-compose up -d
|
||||
sudo docker compose pull
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
## Auto Update
|
||||
|
||||
@@ -2,8 +2,101 @@
|
||||
title: Caddy
|
||||
---
|
||||
|
||||
It seems like the Docs on how to setup Caddy are not available yet.
|
||||
/// note | Opinionated
|
||||
|
||||
Feel free to create a PR and add them here.
|
||||
This guide is opinionated. If you use other conventions or folder layouts, feel free to change the commands and paths.
|
||||
///
|
||||
|
||||
<!-- TODO -->
|
||||
We're using [Caddy](https://caddyserver.com/) here as reverse proxy to serve `wg-easy` on [https://wg-easy.example.com](https://wg-easy.example.com) via TLS.
|
||||
|
||||
## Create a docker composition for `caddy`
|
||||
|
||||
```txt
|
||||
.
|
||||
├── compose.yml
|
||||
└── Caddyfile
|
||||
|
||||
1 directory, 2 files
|
||||
```
|
||||
|
||||
```yaml
|
||||
# compose.yml
|
||||
|
||||
services:
|
||||
caddy:
|
||||
container_name: caddy
|
||||
image: caddy:2.10.0-alpine
|
||||
# publish everything you deem necessary
|
||||
ports:
|
||||
- '80:80/tcp'
|
||||
- '443:443/tcp'
|
||||
- '443:443/udp'
|
||||
networks:
|
||||
- caddy
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- './Caddyfile:/etc/caddy/Caddyfile:ro'
|
||||
- config:/config
|
||||
- data:/data
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
name: caddy
|
||||
|
||||
volumes:
|
||||
config:
|
||||
data:
|
||||
```
|
||||
|
||||
```txt
|
||||
# Caddyfile
|
||||
|
||||
{
|
||||
# setup your email address
|
||||
email mail@example.com
|
||||
}
|
||||
|
||||
wg-easy.example.com {
|
||||
# since the container will share the network with wg-easy
|
||||
# we can use the proper container name
|
||||
reverse_proxy wg-easy:80
|
||||
tls internal
|
||||
}
|
||||
```
|
||||
|
||||
...and start it with:
|
||||
|
||||
```shell
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
## Adapt the docker composition of `wg-easy`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-easy:
|
||||
# sync container name and port according to Caddyfile
|
||||
container_name: wg-easy
|
||||
environment:
|
||||
- PORT=80
|
||||
# no need to publish the HTTP server anymore
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
# add to caddy network
|
||||
networks:
|
||||
caddy:
|
||||
...
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
...
|
||||
```
|
||||
|
||||
...and restart it with:
|
||||
|
||||
```shell
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
You can now access `wg-easy` at [https://wg-easy.example.com](https://wg-easy.example.com) and start the setup.
|
||||
|
||||
@@ -7,9 +7,9 @@ To setup the IPv6 Network, simply run once:
|
||||
```shell
|
||||
docker network create \
|
||||
-d bridge --ipv6 \
|
||||
-d default \
|
||||
--subnet 10.42.42.0/24 \
|
||||
--subnet fdcc:ad94:bacf:61a3::/64 wg \
|
||||
--subnet fdcc:ad94:bacf:61a3::/64 \
|
||||
wg
|
||||
```
|
||||
|
||||
<!-- ref: major version -->
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Routed setup (No NAT)
|
||||
---
|
||||
|
||||
This guide shows how to run **wg-easy** with a routed setup, so packets are forwarded instead of NATed.
|
||||
|
||||
In a routed design, each WireGuard client keeps its own IPv4/IPv6 address. That means you can identify clients by their real addresses instead of seeing everything as the WireGuard server’s IP.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. You know how to add static routes on your router to the WireGuard server.
|
||||
|
||||
## Docker setup
|
||||
|
||||
To make use of our own IPv4/IPv6 addresses, run the container with the `network_mode: host` option.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-easy:
|
||||
image: ghcr.io/wg-easy/wg-easy:15
|
||||
container_name: wg-easy
|
||||
network_mode: 'host'
|
||||
volumes:
|
||||
- ./config:/etc/wireguard
|
||||
- /lib/modules:/lib/modules:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Because we’re on the host network, remove any `ports:` and container `sysctls:` you might have had before.
|
||||
|
||||
## Kernel parameters (on the host)
|
||||
|
||||
With host networking, system sysctls must be set on the **host**. On your host, create `/etc/sysctl.d/90-wireguard.conf`:
|
||||
|
||||
```txt
|
||||
net.ipv4.ip_forward=1
|
||||
net.ipv4.conf.all.src_valid_mark=1
|
||||
net.ipv6.conf.all.disable_ipv6=0
|
||||
net.ipv6.conf.all.forwarding=1
|
||||
net.ipv6.conf.default.forwarding=1
|
||||
```
|
||||
|
||||
Apply and verify:
|
||||
|
||||
```shell
|
||||
sysctl -p /etc/sysctl.d/90-wireguard.conf
|
||||
sysctl -n net.ipv4.ip_forward # should print 1
|
||||
```
|
||||
|
||||
## Add static routes on your router
|
||||
|
||||
Pick an IPv4 and IPv6 subnet for your clients and add static routes on your router, pointing to the WireGuard server's LAN addresses.
|
||||
|
||||
### Example
|
||||
|
||||
/// note | 2001:db8::/32
|
||||
|
||||
The _documentation prefix_ `2001:db8::/32` (RFC 3849) used in this example is not meant for production use, replace it with your own ISP-assigned IPv6 prefix (GUA) or local prefix (ULA)
|
||||
///
|
||||
|
||||
I want my WireGuard clients in `192.168.0.0/24` and `2001:db8:abc:0::/64`.
|
||||
|
||||
- Routed IPv4 subnet: `192.168.0.0/24`
|
||||
- Routed IPv6 prefix: `2001:db8:abc:0::/64`
|
||||
- WireGuard server IPs: `192.168.10.118` and `2001:db8:abc:10:216:3eff:fedb:949e`
|
||||
|
||||
On your router:
|
||||
|
||||
- Route `192.168.0.0/24` → next hop `192.168.10.118`
|
||||
- Route `2001:db8:abc:0::/64` → next hop `2001:db8:abc:10:216:3eff:fedb:949e`
|
||||
|
||||
Don't forget to create the necessary firewall rules to allow these subnets to travel across your LAN. Some routers or servers may require specific Outbound NAT rules for the chosen IPv4 and IPv6 subnets to allow traffic to traverse your LAN.
|
||||
|
||||
## `wg-easy` configuration
|
||||
|
||||
In the Web UI → Admin → Interface, click Change CIDR and set the IPv4/IPv6 routed subnets you chose above. Save.
|
||||
|
||||
Then go to Admin → Hooks and add:
|
||||
|
||||
PostUp
|
||||
|
||||
```shell
|
||||
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT
|
||||
```
|
||||
|
||||
PostDown
|
||||
|
||||
```shell
|
||||
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT
|
||||
```
|
||||
|
||||
/// warning | Important: When using nftables use the following hooks instead.
|
||||
|
||||
PostUp
|
||||
|
||||
```shell
|
||||
nft add chain ip filter WG_EASY; nft add rule ip filter DOCKER-USER jump WG_EASY; nft add rule ip filter WG_EASY iifname {{device}} accept; nft add rule ip filter WG_EASY oifname {{device}} accept; nft add chain ip6 filter WG_EASY; nft add rule ip6 filter DOCKER-USER jump WG_EASY; nft add rule ip6 filter WG_EASY iifname {{device}} accept; nft add rule ip6 filter WG_EASY oifname {{device}} accept;
|
||||
```
|
||||
|
||||
PostDown
|
||||
|
||||
```shell
|
||||
nft delete rule ip filter DOCKER-USER handle $(nft -a list chain ip filter DOCKER-USER | awk '/jump WG_EASY/ {print $NF}'); nft flush chain ip filter WG_EASY; nft delete chain ip filter WG_EASY; nft delete rule ip6 filter DOCKER-USER handle $(nft -a list chain ip6 filter DOCKER-USER | awk '/jump WG_EASY/ {print $NF}'); nft flush chain ip6 filter WG_EASY; nft delete chain ip6 filter WG_EASY
|
||||
```
|
||||
|
||||
///
|
||||
@@ -141,10 +141,10 @@ sudo docker network create traefik
|
||||
## Start traefik
|
||||
|
||||
```shell
|
||||
sudo docker-compose up -d
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
You can no access the Traefik dashboard at `https://traefik.$example.com$` with the credentials you set in `traefik_dynamic.yml`.
|
||||
You can now access the Traefik dashboard at `https://traefik.$example.com$` with the credentials you set in `traefik_dynamic.yml`.
|
||||
|
||||
## Add Labels to `wg-easy`
|
||||
|
||||
@@ -166,6 +166,7 @@ services:
|
||||
- "traefik.http.routers.wg-easy.entrypoints=websecure"
|
||||
- "traefik.http.routers.wg-easy.service=wg-easy"
|
||||
- "traefik.http.services.wg-easy.loadbalancer.server.port=51821"
|
||||
- "traefik.docker.network=traefik"
|
||||
...
|
||||
|
||||
networks:
|
||||
@@ -178,7 +179,7 @@ networks:
|
||||
|
||||
```shell
|
||||
cd /etc/docker/containers/wg-easy
|
||||
sudo docker-compose up -d
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
You can now access `wg-easy` at `https://wg-easy.$example.com$` and start the setup.
|
||||
|
||||
@@ -6,6 +6,20 @@ hide:
|
||||
|
||||
Here are some frequently asked questions or errors about `wg-easy`. If you have a question that is not answered here, please feel free to open a discussion on GitHub.
|
||||
|
||||
## How do I restrict client access to specific networks or servers?
|
||||
|
||||
Use the **Per-Client Firewall** feature to enforce server-side restrictions on what each client can access.
|
||||
|
||||
**Requirements:** This feature requires `iptables` (and `ip6tables` for IPv6) to be installed on the host system.
|
||||
|
||||
1. Enable "Per-Client Firewall" in **Admin Panel → Interface**
|
||||
2. Edit a client and configure "Firewall Allowed IPs"
|
||||
3. Specify which destinations the client should be allowed to access
|
||||
|
||||
Unlike "Allowed IPs" which only controls client-side routing, firewall rules are enforced by the server and cannot be bypassed.
|
||||
|
||||
See the [Admin Panel Guide](./guides/admin.md#per-client-firewall) and [Client Guide](./guides/clients.md#firewall-allowed-ips) for detailed configuration.
|
||||
|
||||
## Error: WireGuard exited with the error: Cannot find device "wg0"
|
||||
|
||||
This error indicates that the WireGuard interface `wg0` does not exist. This can happen if the WireGuard kernel module is not loaded or if the interface was not created properly.
|
||||
@@ -95,3 +109,31 @@ To resolve this issue, you can try the following steps:
|
||||
```shell
|
||||
echo "ip6table_filter" | sudo tee -a /etc/modules
|
||||
```
|
||||
|
||||
## Clients lose connectivity after restarting the container when using multiple networks?
|
||||
|
||||
When you attach multiple Docker networks (e.g., `wg` and a reverse proxy network like `traefik` or `nginx`) to the `wg-easy` container, Docker might assign the network interfaces randomly (e.g., swapping `eth0` and `eth1`). Since `wg-easy` expects the wireguard interface to act as `eth0` and configures `POSTROUTING` rules for it, connectivity will break if the interfaces are swapped upon container restart.
|
||||
|
||||
To solve this, specify the `interface_name` and `gw_priority` explicitly in your `docker-compose.yml` file to guarantee that the `wg` network always binds to `eth0` and acts as the default gateway.
|
||||
|
||||
**Example `docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-easy:
|
||||
# ... other configuration ...
|
||||
networks:
|
||||
wg:
|
||||
interface_name: eth0
|
||||
gw_priority: 1
|
||||
ipv4_address: 10.42.42.42
|
||||
nginx:
|
||||
interface_name: eth1
|
||||
gw_priority: 0
|
||||
|
||||
networks:
|
||||
wg:
|
||||
# ... wg network config ...
|
||||
nginx:
|
||||
external: true
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ Before you can get started with deploying your own VPN, there are some requireme
|
||||
|
||||
1. You need to have a host that you can manage
|
||||
2. You need to have a domain name or a public IP address
|
||||
3. You need a supported architecture (x86_64, arm64, armv7)
|
||||
3. You need a supported architecture (x86_64, arm64)
|
||||
|
||||
### Host Setup
|
||||
|
||||
@@ -38,24 +38,26 @@ If you're using podman, make sure to read the related [documentation][docs-podma
|
||||
To understand which tags you should use, read this section carefully. [Our CI][github-ci] will automatically build, test and push new images to the following container registry:
|
||||
|
||||
1. GitHub Container Registry ([`ghcr.io/wg-easy/wg-easy`][ghcr-image])
|
||||
2. Codeberg Container Registry ([`codeberg.org/wg-easy/wg-easy`][codeberg-image]) (IPv6 support)
|
||||
|
||||
All workflows are using the tagging convention listed below. It is subsequently applied to all images.
|
||||
|
||||
| tag | Type | Example | Description |
|
||||
| ------------- | ------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `15` | latest minor for that major tag | `ghcr.io/wg-easy/wg-easy:15` | latest features for specific major versions, no breaking changes, recommended |
|
||||
| `latest` | latest tag | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | points to latest release, can include breaking changes |
|
||||
| `15.0` | latest patch for that minor tag | `ghcr.io/wg-easy/wg-easy:15.0` | latest patches for specific minor version |
|
||||
| `15.0.0` | specific tag | `ghcr.io/wg-easy/wg-easy:15.0.0` | specific release, no updates |
|
||||
| `edge` | push to `master` | `ghcr.io/wg-easy/wg-easy:edge` | mostly unstable, gets frequent package and code updates |
|
||||
| `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs |
|
||||
| `latest` | latest tag | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | points to the v14 release, should be avoided |
|
||||
|
||||
<!-- ref: major version -->
|
||||
<!-- ref: major version (check links too) -->
|
||||
|
||||
When publishing a tag we follow the [Semantic Versioning][semver] specification. The `latest` tag is always pointing to the latest stable release. If you want to avoid breaking changes, use the major version tag (e.g. `15`).
|
||||
When publishing a tag we follow the [Semantic Versioning][semver] specification. Pin to the latest major version to avoid breaking changes (e.g. `15`), avoid using the `latest` tag.
|
||||
|
||||
[github-ci]: https://github.com/wg-easy/wg-easy/actions
|
||||
[ghcr-image]: https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy
|
||||
[codeberg-image]: https://codeberg.org/wg-easy/-/packages/container/wg-easy/15
|
||||
[semver]: https://semver.org/
|
||||
|
||||
### Follow tutorials
|
||||
|
||||
@@ -2,4 +2,42 @@
|
||||
title: Admin Panel
|
||||
---
|
||||
|
||||
TODO
|
||||
## Interface Settings
|
||||
|
||||
### Per-Client Firewall
|
||||
|
||||
Enable server-side firewall filtering to enforce network access restrictions per client.
|
||||
|
||||
When enabled, each client can have custom "Firewall Allowed IPs" configured that restrict which destinations they can access through the VPN. These restrictions are enforced by the server using iptables/ip6tables and cannot be bypassed by the client.
|
||||
|
||||
/// warning | Experimental Feature
|
||||
|
||||
This feature is currently experimental. While functional, it should be thoroughly tested in your environment before relying on it for production security requirements. Always verify that firewall rules are working as expected using test traffic or by manually inspecting the rules.
|
||||
|
||||
///
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- `iptables` must be installed on the host system
|
||||
- `ip6tables` must be installed if IPv6 is enabled (default)
|
||||
- The feature cannot be enabled if these tools are not available
|
||||
|
||||
/// note
|
||||
Most Linux distributions include iptables by default. If you're running in a minimal container environment, you may need to install the `iptables` package on the host system.
|
||||
///
|
||||
|
||||
**Enable this feature if you want to:**
|
||||
|
||||
- Restrict certain clients to only access specific servers or networks
|
||||
- Prevent clients from accessing the internet while allowing LAN access
|
||||
- Enforce port-based restrictions (e.g., only allow HTTP/HTTPS)
|
||||
- Separate routing configuration from security enforcement
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Enable "Per-Client Firewall" in Admin Panel → Interface
|
||||
2. Edit any client to see the new "Firewall Allowed IPs" field
|
||||
3. Specify allowed destinations (IPs, subnets, ports) for that client
|
||||
4. Server enforces these rules automatically
|
||||
|
||||
See [Edit Client → Firewall Allowed IPs](./clients.md#firewall-allowed-ips) for detailed configuration syntax and examples.
|
||||
|
||||
@@ -41,3 +41,31 @@ docker compose exec -it wg-easy cli db:admin:reset --password <new_password>
|
||||
```
|
||||
|
||||
This will reset the password for the admin user to the new password you provided. If you include special characters in the password, make sure to escape them properly.
|
||||
|
||||
### Show Clients
|
||||
|
||||
List all clients that are currently configured with details such as client ID, Name, Public Key, and enabled status.
|
||||
|
||||
```shell
|
||||
cli clients:list
|
||||
```
|
||||
|
||||
### Show Client QR Code
|
||||
|
||||
Display the QR code for a specific client, which can be scanned by a compatible app to import the client's configuration.
|
||||
|
||||
```shell
|
||||
cli clients:qr <client_id>
|
||||
```
|
||||
|
||||
Replace `<client_id>` with the actual client ID you want to show the QR code for.
|
||||
|
||||
/// warning | IPv6 Support
|
||||
|
||||
IPv6 support is enabled by default, even if you disabled it using environment variables. To disable it pass the `--no-ipv6` flag when running the CLI.
|
||||
|
||||
```shell
|
||||
cli clients:qr <client_id> --no-ipv6
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
@@ -19,7 +19,58 @@ Which IPs will be routed through the VPN.
|
||||
|
||||
This will not prevent the user from modifying it locally and accessing IP ranges that they should not be able to access.
|
||||
|
||||
Use firewall rules to prevent access to IP ranges that the user should not be able to access.
|
||||
Use the Firewall Allowed IPs feature to prevent access to IP ranges that the user should not be able to access.
|
||||
|
||||
## Firewall Allowed IPs
|
||||
|
||||
/// note | Attention
|
||||
|
||||
This field only appears when **Per-Client Firewall** is enabled in the Admin Panel → Interface settings.
|
||||
|
||||
///
|
||||
|
||||
Server-side firewall rules that restrict which destinations the client can access, regardless of their local configuration.
|
||||
|
||||
Unlike "Allowed IPs" which only controls routing on the client side, these rules are enforced by the server using iptables/ip6tables and cannot be bypassed by the client.
|
||||
|
||||
**Supported Formats:**
|
||||
|
||||
- `10.10.0.3`, `2001:db8::1` - Allow access to a single IP address
|
||||
- `10.10.0.0/24`, `2001:db8::/32` - Allow access to an entire subnet
|
||||
- `192.168.1.5:443` - Allow access to specific port (TCP+UDP)
|
||||
- `192.168.1.5:443/tcp` - Allow access to specific port (TCP only)
|
||||
- `192.168.1.5:443/udp` - Allow access to specific port (UDP only)
|
||||
- `10.10.0.0/24:443` - Allow access to an entire subnet on a specific port (TCP+UDP)
|
||||
- `10.10.0.0/24:443/tcp` - Allow access to an entire subnet on a specific port (TCP only)
|
||||
- `10.10.0.0/24:443/udp` - Allow access to an entire subnet on a specific port (UDP only)
|
||||
- `[2001:db8::1]:443` - IPv6 address with port (brackets required)
|
||||
- `[2001:db8::/32]:443/tcp` - IPv6 CIDR with port and protocol
|
||||
|
||||
/// warning | Invalid Formats
|
||||
|
||||
Protocol specifiers (`/tcp` or `/udp`) require a port number. The following formats are **not supported** and will result in an error:
|
||||
|
||||
- `10.10.0.3/tcp` (use `10.10.0.3:443/tcp` instead)
|
||||
- `10.10.0.0/24/udp` (use `10.10.0.0/24:53/udp` instead)
|
||||
|
||||
///
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- **Empty**: Falls back to the client's "Allowed IPs" setting
|
||||
- **Specified**: Only listed destinations are accessible (allow-only, everything else is blocked)
|
||||
- **Disable for specific client**: To disable firewall filtering for a single client while keeping it enabled for others, add `0.0.0.0/0, ::/0` to allow all traffic
|
||||
|
||||
/// note
|
||||
To allow clients to reach the VPN server itself (e.g. for DNS), include the server's VPN address in the firewall allowed IPs.
|
||||
///
|
||||
|
||||
**Use Case Examples**:
|
||||
|
||||
- Allow only specific servers: `10.10.0.5`
|
||||
- Allow only internal network: `10.10.0.0/24, 192.168.1.0/24`
|
||||
- Allow only web browsing: `0.0.0.0/0:80, 0.0.0.0/0:443, [::/0]:80, [::/0]:443`
|
||||
- Block internet, allow LAN: Leave "Allowed IPs" as `0.0.0.0/0, ::/0` but set Firewall IPs to `10.0.0.0/8, 192.168.0.0/16`
|
||||
|
||||
## Server Allowed IPs
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ hide:
|
||||
|
||||
/// info | This Documentation is Versioned
|
||||
|
||||
**Make sure** to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the `:latest` image tag - [the most recent stable release][docs-tagging].
|
||||
**Make sure** to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to [the most recent stable release][docs-tagging].
|
||||
///
|
||||
|
||||
This documentation provides you not only with the basic setup and configuration of `wg-easy` but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.
|
||||
|
||||
+3
-2
@@ -7,10 +7,11 @@
|
||||
"build": "docker build -t wg-easy .",
|
||||
"docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs squidfunk/mkdocs-material serve -a 0.0.0.0:8080",
|
||||
"scripts:version": "bash scripts/version.sh",
|
||||
"scripts:i18n": "bash scripts/i18n.sh",
|
||||
"format:check:docs": "prettier --check docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
"prettier": "^3.8.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0"
|
||||
"packageManager": "pnpm@11.1.2"
|
||||
}
|
||||
|
||||
Generated
+5
-5
@@ -9,16 +9,16 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
specifier: ^3.8.3
|
||||
version: 3.8.3
|
||||
|
||||
packages:
|
||||
|
||||
prettier@3.5.3:
|
||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||
prettier@3.8.3:
|
||||
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
prettier@3.5.3: {}
|
||||
prettier@3.8.3: {}
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
folder="src/i18n/locales"
|
||||
base_file="$folder/en.json"
|
||||
|
||||
# Get all leaf keys from the English base file
|
||||
base_keys=$(jq -r 'paths(scalars) | map(tostring) | join(".")' "$base_file")
|
||||
total=$(echo "$base_keys" | wc -l)
|
||||
|
||||
# Loop through all JSON files in the folder
|
||||
for file in "$folder"/*.json; do
|
||||
name=$(basename "$file" .json)
|
||||
translated_keys=$(jq -r 'paths(scalars) | map(tostring) | join(".")' "$file")
|
||||
done=$(comm -12 <(echo "$base_keys" | sort) <(echo "$translated_keys" | sort) | wc -l)
|
||||
percent=$((100 * done / total))
|
||||
check="[ ]"
|
||||
[ "$percent" -eq 100 ] && check="[x]"
|
||||
printf "%s %s (%d%%)\n" "- $check" "$name" "$percent"
|
||||
done
|
||||
@@ -30,6 +30,7 @@ echo "Updated package.json to version $new_version"
|
||||
|
||||
echo "----"
|
||||
echo "If you changed the major version, remember to update the docker-compose.yml file and docs (search for: ref: major version)"
|
||||
echo "Make sure to stage any changes before proceeding (e.g. Changelog updates)."
|
||||
echo "----"
|
||||
|
||||
echo "If you did everything press 'y' to commit the changes and create a new tag"
|
||||
|
||||
@@ -23,4 +23,6 @@ logs
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
coverage/
|
||||
|
||||
wg-easy.db
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
setups.@nuxt/test-utils="4.0.3"
|
||||
@@ -0,0 +1,7 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -11,10 +11,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { VueApexChartsComponent } from 'vue3-apexcharts';
|
||||
import type { VueApexChartsComponentProps } from 'vue3-apexcharts';
|
||||
|
||||
defineProps<{
|
||||
options: VueApexChartsComponent['options'];
|
||||
series: VueApexChartsComponent['series'];
|
||||
options: VueApexChartsComponentProps['options'];
|
||||
series: VueApexChartsComponentProps['series'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="overflow-x-auto rounded border-2 border-red-800 py-2">
|
||||
<pre
|
||||
class="mx-2 inline-block"
|
||||
@click="selectCode"
|
||||
><code ref="codeBlock">{{ code }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
code: string;
|
||||
}>();
|
||||
|
||||
const codeBlock = useTemplateRef('codeBlock');
|
||||
|
||||
function selectCode() {
|
||||
// TODO: keyboard support?
|
||||
if (codeBlock.value) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(codeBlock.value);
|
||||
const sel = window.getSelection();
|
||||
if (sel) {
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
>
|
||||
<slot name="description" />
|
||||
</DialogDescription>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<div class="mt-6 flex flex-wrap justify-end gap-2">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<textarea
|
||||
v-model="data"
|
||||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const data = defineModel<string>();
|
||||
</script>
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ApexOptions } from 'apexcharts';
|
||||
import type { ApexChart, ApexOptions } from 'apexcharts';
|
||||
|
||||
defineProps<{
|
||||
client: LocalClient;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ClientCardCharts :client="client" />
|
||||
<div
|
||||
class="relative z-10 flex flex-col justify-between gap-3 px-3 py-3 sm:flex-row md:py-5"
|
||||
class="relative flex flex-col justify-between gap-3 px-3 py-3 sm:flex-row md:py-5"
|
||||
>
|
||||
<div class="flex w-full items-center gap-3 md:gap-4">
|
||||
<ClientCardAvatar :client="client" />
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="flex flex-grow flex-col gap-1">
|
||||
<ClientCardName :client="client" />
|
||||
<div
|
||||
class="flex flex-col pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400"
|
||||
class="flex flex-col text-xs text-gray-500 dark:text-neutral-400"
|
||||
>
|
||||
<div>
|
||||
<ClientCardAddress :client="client" />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="block pb-1 text-xs text-gray-500 md:inline-block md:pb-0 dark:text-neutral-400"
|
||||
>
|
||||
<div class="block text-xs text-gray-500 dark:text-neutral-400">
|
||||
<span class="inline-block">{{ expiredDateFormat(client.expiresAt) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="text-sm text-gray-700 md:text-base dark:text-neutral-200"
|
||||
class="break-all text-sm text-gray-700 md:text-base dark:text-neutral-200"
|
||||
:title="$t('client.createdOn') + $d(new Date(client.createdAt))"
|
||||
>
|
||||
<span class="border-b-2 border-t-2 border-transparent">
|
||||
{{ client.name }}
|
||||
</span>
|
||||
{{ client.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<BaseDialog :trigger-class="triggerClass">
|
||||
<template #trigger>
|
||||
<slot />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ $t('client.config') }}
|
||||
</template>
|
||||
<template #description>
|
||||
<div v-if="status === 'success'">
|
||||
<BaseCodeBlock :code="config ?? ''" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<span>{{ $t('general.loading') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<DialogClose as-child>
|
||||
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
|
||||
</DialogClose>
|
||||
<DialogClose as-child>
|
||||
<BasePrimaryButton @click="copyCode">
|
||||
{{ $t('copy.copy') }}
|
||||
</BasePrimaryButton>
|
||||
</DialogClose>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ triggerClass?: string; clientId: number }>();
|
||||
|
||||
const toast = useToast();
|
||||
const { copied, copy, isSupported } = useClipboard({
|
||||
// fallback does not work
|
||||
legacy: false,
|
||||
});
|
||||
|
||||
const { data: config, status } = useFetch(
|
||||
`/api/client/${props.clientId}/configuration`,
|
||||
{
|
||||
responseType: 'text',
|
||||
server: false,
|
||||
}
|
||||
);
|
||||
|
||||
async function copyCode() {
|
||||
if (status.value !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSupported.value) {
|
||||
toast.showToast({
|
||||
type: 'error',
|
||||
message: $t('copy.notSupported'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await copy(config.value ?? '');
|
||||
|
||||
if (copied.value) {
|
||||
toast.showToast({
|
||||
type: 'success',
|
||||
message: $t('copy.copied'),
|
||||
});
|
||||
} else {
|
||||
toast.showToast({
|
||||
type: 'error',
|
||||
message: $t('copy.failed'),
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ $t('client.empty') }}<br /><br />
|
||||
<ClientsCreateDialog>
|
||||
<BaseSecondaryButton as="span">
|
||||
<IconsPlus class="w-4 md:mr-2" />
|
||||
<IconsPlus class="mr-2 w-4" />
|
||||
<span class="text-sm">{{ $t('client.new') }}</span>
|
||||
</BaseSecondaryButton>
|
||||
</ClientsCreateDialog>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<ClientsCreateDialog>
|
||||
<BaseSecondaryButton as="span">
|
||||
<IconsPlus class="w-4 md:mr-2" />
|
||||
<span class="text-sm max-md:hidden">{{ $t('client.newShort') }}</span>
|
||||
<IconsPlus class="mr-2 w-4" />
|
||||
<span class="text-sm">{{ $t('client.newShort') }}</span>
|
||||
</BaseSecondaryButton>
|
||||
</ClientsCreateDialog>
|
||||
</template>
|
||||
|
||||
@@ -5,11 +5,25 @@
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="bg-white">
|
||||
<img :src="qrCode" />
|
||||
<img ref="img" :src="qrCode" />
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<DialogClose>
|
||||
<BaseSecondaryButton
|
||||
class="flex items-center gap-2"
|
||||
:title="$t('client.copyPng')"
|
||||
@click="copyPng"
|
||||
>
|
||||
<IconsCopy class="size-5" /> PNG
|
||||
</BaseSecondaryButton>
|
||||
<BaseSecondaryButton
|
||||
class="flex items-center gap-2"
|
||||
:title="$t('client.downloadPng')"
|
||||
@click="downloadPng"
|
||||
>
|
||||
<IconsDownload class="size-5" /> PNG
|
||||
</BaseSecondaryButton>
|
||||
<DialogClose as-child>
|
||||
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
|
||||
</DialogClose>
|
||||
</template>
|
||||
@@ -18,4 +32,87 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ qrCode: string }>();
|
||||
|
||||
const toast = useToast();
|
||||
const img = useTemplateRef('img');
|
||||
|
||||
async function svgToPng() {
|
||||
if (!img.value || !img.value.complete || img.value.naturalWidth === 0) {
|
||||
throw new Error('image is not loaded');
|
||||
}
|
||||
|
||||
const width = 1000;
|
||||
const height = 1000;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('was not able to create 2d context');
|
||||
}
|
||||
ctx.drawImage(img.value!, 0, 0, width, height);
|
||||
|
||||
return new Promise<Blob>((res, rej) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
return rej(new Error('was not able to create blob'));
|
||||
}
|
||||
return res(blob);
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadPng() {
|
||||
try {
|
||||
const blob = await svgToPng();
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'client-config.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error('failed to download png', e);
|
||||
toast.showToast({
|
||||
type: 'error',
|
||||
message: $t('toast.unknown'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPng() {
|
||||
const blob = await svgToPng().catch((e) => {
|
||||
console.error('failed to convert svg to png', e);
|
||||
toast.showToast({
|
||||
type: 'error',
|
||||
message: $t('toast.unknown'),
|
||||
});
|
||||
});
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
}),
|
||||
]);
|
||||
|
||||
toast.showToast({
|
||||
type: 'success',
|
||||
message: $t('copy.copied'),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('failed to copy png', e);
|
||||
toast.showToast({
|
||||
type: 'error',
|
||||
message: $t('copy.failed'),
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="relative flex h-full items-center">
|
||||
<IconsMagnifyingGlass
|
||||
class="absolute left-2.5 h-4 w-4 text-gray-400 dark:text-neutral-500"
|
||||
/>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="$t('client.search')"
|
||||
class="w-full rounded bg-white px-8 py-2 text-sm text-gray-900 shadow-sm ring-1 ring-gray-300 transition-all placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-red-600 dark:bg-neutral-800 dark:text-white dark:ring-neutral-700 dark:placeholder:text-neutral-500 dark:focus:ring-red-700"
|
||||
@input="updateSearch"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
class="absolute right-2 flex h-5 w-5 items-center justify-center rounded-full bg-gray-200 text-gray-600 hover:bg-gray-300 hover:text-gray-800 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600 dark:hover:text-neutral-100"
|
||||
aria-label="Clear search"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<IconsClose class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const clientsStore = useClientsStore();
|
||||
const searchQuery = ref('');
|
||||
|
||||
const updateSearch = useDebounceFn(() => {
|
||||
clientsStore.setSearchQuery(searchQuery.value);
|
||||
}, 300);
|
||||
|
||||
function clearSearch() {
|
||||
searchQuery.value = '';
|
||||
clientsStore.setSearchQuery('');
|
||||
}
|
||||
</script>
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<BasePrimaryButton @click="toggleSort">
|
||||
<IconsArrowDown
|
||||
v-if="globalStore.sortClient === true"
|
||||
class="w-4 md:mr-2"
|
||||
/>
|
||||
<IconsArrowUp v-else class="w-4 md:mr-2" />
|
||||
<span class="text-sm max-md:hidden"> {{ $t('client.sort') }}</span>
|
||||
<IconsArrowDown v-if="globalStore.sortClient === true" class="mr-2 w-4" />
|
||||
<IconsArrowUp v-else class="mr-2 w-4" />
|
||||
<span class="text-sm">{{ $t('client.sort') }}</span>
|
||||
</BasePrimaryButton>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -12,23 +12,15 @@
|
||||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
|
||||
@input="update($event, i)"
|
||||
/>
|
||||
<BaseSecondaryButton
|
||||
as="input"
|
||||
type="button"
|
||||
class="rounded-lg"
|
||||
value="-"
|
||||
@click="del(i)"
|
||||
/>
|
||||
<BaseSecondaryButton type="button" class="rounded-lg" @click="del(i)">
|
||||
{{ '-' }}
|
||||
</BaseSecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<BasePrimaryButton
|
||||
as="input"
|
||||
type="button"
|
||||
class="rounded-lg"
|
||||
:value="$t('form.add')"
|
||||
@click="add"
|
||||
/>
|
||||
<BasePrimaryButton type="button" class="rounded-lg" @click="add">
|
||||
{{ $t('form.add') }}
|
||||
</BasePrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<h4 class="col-span-full flex items-center py-6 text-2xl">
|
||||
<h3 class="col-span-full flex items-center py-6 text-2xl">
|
||||
<slot />
|
||||
<BaseTooltip v-if="description" :text="description">
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</h4>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
<BasePrimaryButton as="span">
|
||||
<div class="flex items-center gap-3">
|
||||
<IconsSparkles class="w-4" />
|
||||
<span>{{ $t('admin.config.suggest') }}</span>
|
||||
<span class="whitespace-nowrap">
|
||||
{{ $t('admin.config.suggest') }}
|
||||
</span>
|
||||
</div>
|
||||
</BasePrimaryButton>
|
||||
</AdminSuggestDialog>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<FormLabel :for="id">
|
||||
{{ label }}
|
||||
</FormLabel>
|
||||
<BaseTooltip v-if="description" :text="description">
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<span :id="id" class="flex flex-col justify-center">{{ data }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
data?: string;
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<RLabel :for="props.for" class="md:align-middle md:leading-10"
|
||||
><slot
|
||||
/></RLabel>
|
||||
<RLabel :for="props.for" class="md:leading-[2.75rem]">
|
||||
<slot />
|
||||
</RLabel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -12,23 +12,15 @@
|
||||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
|
||||
@input="update($event, i)"
|
||||
/>
|
||||
<BaseSecondaryButton
|
||||
as="input"
|
||||
type="button"
|
||||
class="rounded-lg"
|
||||
value="-"
|
||||
@click="del(i)"
|
||||
/>
|
||||
<BaseSecondaryButton type="button" class="rounded-lg" @click="del(i)">
|
||||
{{ '-' }}
|
||||
</BaseSecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<BasePrimaryButton
|
||||
as="input"
|
||||
type="button"
|
||||
class="rounded-lg"
|
||||
:value="$t('form.add')"
|
||||
@click="add"
|
||||
/>
|
||||
<BasePrimaryButton type="button" class="rounded-lg" @click="add">
|
||||
{{ $t('form.add') }}
|
||||
</BasePrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<FormLabel :for="id">
|
||||
{{ label }}
|
||||
</FormLabel>
|
||||
<BaseTooltip v-if="description" :text="description">
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<BaseInput :id="id" v-model.number="data" :name="id" type="number" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ id: string; label: string; description?: string }>();
|
||||
|
||||
const data = defineModel<number | null>({
|
||||
set(value) {
|
||||
const temp = value ?? null;
|
||||
if (temp === 0) {
|
||||
return null;
|
||||
}
|
||||
if ((temp as string | null) === '') {
|
||||
return null;
|
||||
}
|
||||
return temp;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -7,7 +7,9 @@
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<BaseSwitch :id="id" v-model="data" />
|
||||
<div class="my-auto">
|
||||
<BaseSwitch :id="id" v-model="data" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<FormLabel :for="id">
|
||||
{{ label }}
|
||||
</FormLabel>
|
||||
<BaseTooltip v-if="description" :text="description">
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<BaseTextArea
|
||||
:id="id"
|
||||
v-model.trim="data"
|
||||
:name="id"
|
||||
:autocomplete="autocomplete"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
autocomplete?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const data = defineModel<string>();
|
||||
</script>
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<Toggle
|
||||
:pressed="globalStore.uiShowCharts"
|
||||
class="group inline-flex h-8 w-8 cursor-pointer items-center justify-center whitespace-nowrap rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
class="group flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
:title="$t('layout.toggleCharts')"
|
||||
@update:pressed="globalStore.toggleCharts"
|
||||
>
|
||||
<IconsChart
|
||||
class="h-5 w-5 fill-gray-400 transition group-data-[state=on]:fill-gray-600 dark:fill-neutral-600 dark:group-data-[state=on]:fill-neutral-400"
|
||||
class="h-5 w-5 transition group-data-[state=on]:fill-gray-600 dark:text-neutral-400 dark:group-data-[state=on]:fill-gray-300"
|
||||
/>
|
||||
</Toggle>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<NuxtLink to="/" class="mb-4">
|
||||
<NuxtLink to="/" class="max-sm:mb-4">
|
||||
<h1 class="text-4xl font-medium dark:text-neutral-200">
|
||||
<img
|
||||
src="/logo.png"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
authStore.userData &&
|
||||
hasPermissions(authStore.userData, 'admin', 'any')
|
||||
"
|
||||
class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600"
|
||||
class="font-small rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600"
|
||||
:title="`v${globalStore.information.currentRelease} → v${globalStore.information.latestRelease.version}`"
|
||||
>
|
||||
<div class="container mx-auto flex flex-auto flex-row items-center">
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<ClipboardDocumentIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ClipboardDocumentIcon from '@heroicons/vue/24/outline/esm/ClipboardDocumentIcon';
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<MagnifyingGlassIcon />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MagnifyingGlassIcon from '@heroicons/vue/24/outline/esm/MagnifyingGlassIcon';
|
||||
</script>
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="container mx-auto max-w-3xl overflow-hidden rounded-lg bg-white px-3 text-gray-700 shadow-md md:px-0 dark:bg-neutral-700 dark:text-neutral-200"
|
||||
>
|
||||
<slot />
|
||||
<div class="container mx-auto max-w-3xl">
|
||||
<div
|
||||
class="mx-3 overflow-hidden rounded-lg bg-white text-gray-700 shadow-md dark:bg-neutral-700 dark:text-neutral-200"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-shrink-0 space-x-1 md:block">
|
||||
<div class="flex flex-shrink-0 flex-col items-center gap-2 sm:flex-row">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-auto flex-grow flex-row items-center border-b-2 border-gray-100 p-3 px-5 dark:border-neutral-600"
|
||||
class="flex flex-col items-center gap-2 border-b-2 border-gray-100 p-3 px-5 sm:flex-row dark:border-neutral-600"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<h2 class="flex-1 text-2xl font-medium">
|
||||
{{ text }}
|
||||
<h2 class="flex-1 break-all text-2xl font-medium">
|
||||
<slot />
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { text } = defineProps<{
|
||||
text: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
href="https://github.com/wg-easy/wg-easy"
|
||||
>WireGuard Easy</a
|
||||
>
|
||||
({{ globalStore.information?.currentRelease }}) © 2021-2025 by
|
||||
({{ globalStore.information?.currentRelease }}) © 2021-2026 by
|
||||
<a
|
||||
class="hover:underline"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<header class="mx-auto mt-4 flex max-w-3xl flex-col justify-center">
|
||||
<header
|
||||
class="mx-auto my-4 flex max-w-3xl flex-col justify-center max-md:px-3"
|
||||
>
|
||||
<div
|
||||
class="mb-5 w-full"
|
||||
:class="
|
||||
@@ -17,7 +19,7 @@
|
||||
<UiUserMenu v-if="loggedIn" />
|
||||
</div>
|
||||
</div>
|
||||
<HeaderUpdate class="mt-4" />
|
||||
<HeaderUpdate class="my-4" />
|
||||
</header>
|
||||
<slot />
|
||||
<UiFooter />
|
||||
|
||||
@@ -4,25 +4,27 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = useRequestEvent();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const userData = await authStore.getSession();
|
||||
authStore.userData = await authStore.getSession(event);
|
||||
|
||||
// skip login if already logged in
|
||||
if (to.path === '/login') {
|
||||
if (userData?.username) {
|
||||
if (authStore.userData?.username) {
|
||||
return navigateTo('/', { redirectCode: 302 });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Require auth for every page other than Login
|
||||
if (!userData?.username) {
|
||||
if (!authStore.userData?.username) {
|
||||
return navigateTo('/login', { redirectCode: 302 });
|
||||
}
|
||||
|
||||
// Check for admin access
|
||||
if (to.path.startsWith('/admin')) {
|
||||
if (!hasPermissions(userData, 'admin', 'any')) {
|
||||
if (!hasPermissions(authStore.userData, 'admin', 'any')) {
|
||||
return abortNavigation('Not allowed to access Admin Panel');
|
||||
}
|
||||
}
|
||||
|
||||
+40
-29
@@ -2,34 +2,47 @@
|
||||
<div>
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="flex flex-col gap-4 lg:flex-row">
|
||||
<div class="rounded-lg bg-white p-4 lg:w-64 dark:bg-neutral-700">
|
||||
<NuxtLink to="/admin">
|
||||
<h2 class="mb-4 text-xl font-bold dark:text-neutral-200">
|
||||
{{ t('pages.admin.panel') }}
|
||||
</h2>
|
||||
</NuxtLink>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<NuxtLink
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="index"
|
||||
:to="`/admin/${item.id}`"
|
||||
active-class="bg-red-800 rounded"
|
||||
>
|
||||
<BaseSecondaryButton
|
||||
as="span"
|
||||
class="w-full cursor-pointer rounded p-2 font-medium transition-colors duration-200 hover:bg-red-800 dark:text-neutral-200"
|
||||
<div
|
||||
class="overflow-hidden rounded-lg bg-white text-gray-700 shadow-md lg:w-64 dark:bg-neutral-700 dark:text-neutral-200"
|
||||
>
|
||||
<PanelHead>
|
||||
<PanelHeadTitle>
|
||||
<NuxtLink to="/admin">
|
||||
{{ t('pages.admin.panel') }}
|
||||
</NuxtLink>
|
||||
</PanelHeadTitle>
|
||||
</PanelHead>
|
||||
<PanelBody>
|
||||
<nav class="flex flex-col gap-2">
|
||||
<NuxtLink
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="index"
|
||||
:to="`/admin/${item.id}`"
|
||||
class="group rounded"
|
||||
active-class="bg-red-800 active"
|
||||
>
|
||||
{{ item.name }}
|
||||
</BaseSecondaryButton>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<BaseSecondaryButton
|
||||
as="span"
|
||||
class="w-full font-medium group-[.active]:text-white"
|
||||
>
|
||||
{{ item.name }}
|
||||
</BaseSecondaryButton>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
</PanelBody>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 rounded-lg bg-white p-6 dark:bg-neutral-700 dark:text-neutral-200"
|
||||
class="flex-1 overflow-hidden rounded-lg bg-white text-gray-700 shadow-md dark:bg-neutral-700 dark:text-neutral-200"
|
||||
>
|
||||
<h1 class="mb-6 text-3xl font-bold">{{ activeMenuItem.name }}</h1>
|
||||
<NuxtPage />
|
||||
<PanelHead>
|
||||
<PanelHeadTitle>
|
||||
{{ activeMenuItem.name }}
|
||||
</PanelHeadTitle>
|
||||
</PanelHead>
|
||||
<PanelBody>
|
||||
<NuxtPage />
|
||||
</PanelBody>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,25 +50,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const authStore = useAuthStore();
|
||||
authStore.update();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const menuItems = [
|
||||
const menuItems = computed(() => [
|
||||
{ id: 'general', name: t('pages.admin.general') },
|
||||
{ id: 'config', name: t('pages.admin.config') },
|
||||
{ id: 'interface', name: t('pages.admin.interface') },
|
||||
{ id: 'hooks', name: t('pages.admin.hooks') },
|
||||
];
|
||||
]);
|
||||
|
||||
const defaultItem = { id: '', name: t('pages.admin.panel') };
|
||||
|
||||
const activeMenuItem = computed(() => {
|
||||
return (
|
||||
menuItems.find((item) => route.path === `/admin/${item.id}`) ?? defaultItem
|
||||
menuItems.value.find((item) => route.path === `/admin/${item.id}`) ??
|
||||
defaultItem
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -47,6 +47,61 @@
|
||||
:description="$t('admin.config.persistentKeepaliveDesc')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup v-if="globalStore.information?.isAwg">
|
||||
<FormHeading>{{ $t('awg.obfuscationParameters') }}</FormHeading>
|
||||
|
||||
<FormNullNumberField
|
||||
id="jC"
|
||||
v-model="data.defaultJC"
|
||||
:label="$t('awg.jCLabel')"
|
||||
:description="$t('awg.jCDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="jMin"
|
||||
v-model="data.defaultJMin"
|
||||
:label="$t('awg.jMinLabel')"
|
||||
:description="$t('awg.jMinDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="jMax"
|
||||
v-model="data.defaultJMax"
|
||||
:label="$t('awg.jMaxLabel')"
|
||||
:description="$t('awg.jMaxDescription')"
|
||||
/>
|
||||
|
||||
<div class="col-span-full text-sm">* {{ $t('awg.mtuNote') }}</div>
|
||||
|
||||
<FormNullTextField
|
||||
id="i1"
|
||||
v-model="data.defaultI1"
|
||||
:label="$t('awg.i1Label')"
|
||||
:description="$t('awg.i1Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i2"
|
||||
v-model="data.defaultI2"
|
||||
:label="$t('awg.i2Label')"
|
||||
:description="$t('awg.i2Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i3"
|
||||
v-model="data.defaultI3"
|
||||
:label="$t('awg.i3Label')"
|
||||
:description="$t('awg.i3Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i4"
|
||||
v-model="data.defaultI4"
|
||||
:label="$t('awg.i4Label')"
|
||||
:description="$t('awg.i4Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i5"
|
||||
v-model="data.defaultI5"
|
||||
:label="$t('awg.i5Label')"
|
||||
:description="$t('awg.i5Description')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('form.actions') }}</FormHeading>
|
||||
<FormPrimaryActionField type="submit" :label="$t('form.save')" />
|
||||
@@ -57,6 +112,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, {
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
<main v-if="data">
|
||||
<FormElement @submit.prevent="submit">
|
||||
<FormGroup>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PreUp"
|
||||
v-model="data.preUp"
|
||||
:label="$t('hooks.preUp')"
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PostUp"
|
||||
v-model="data.postUp"
|
||||
:label="$t('hooks.postUp')"
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PreDown"
|
||||
v-model="data.preDown"
|
||||
:label="$t('hooks.preDown')"
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PostDown"
|
||||
v-model="data.postDown"
|
||||
:label="$t('hooks.postDown')"
|
||||
|
||||
@@ -21,6 +21,118 @@
|
||||
:description="$t('admin.interface.deviceDesc')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup v-if="globalStore.information?.isAwg">
|
||||
<FormHeading>{{ $t('awg.obfuscationParameters') }}</FormHeading>
|
||||
|
||||
<FormNullNumberField
|
||||
id="jC"
|
||||
v-model="data.jC"
|
||||
:label="$t('awg.jCLabel')"
|
||||
:description="$t('awg.jCDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="jMin"
|
||||
v-model="data.jMin"
|
||||
:label="$t('awg.jMinLabel')"
|
||||
:description="$t('awg.jMinDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="jMax"
|
||||
v-model="data.jMax"
|
||||
:label="$t('awg.jMaxLabel')"
|
||||
:description="$t('awg.jMaxDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="s1"
|
||||
v-model="data.s1"
|
||||
:label="$t('awg.s1Label')"
|
||||
:description="$t('awg.s1Description')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="s2"
|
||||
v-model="data.s2"
|
||||
:label="$t('awg.s2Label')"
|
||||
:description="$t('awg.s2Description')"
|
||||
/>
|
||||
|
||||
<div class="col-span-full text-sm">* {{ $t('awg.mtuNote') }}</div>
|
||||
|
||||
<FormNullNumberField
|
||||
id="s3"
|
||||
v-model="data.s3"
|
||||
:label="$t('awg.s3Label')"
|
||||
:description="$t('awg.s3Description')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="s4"
|
||||
v-model="data.s4"
|
||||
:label="$t('awg.s4Label')"
|
||||
:description="$t('awg.s4Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="h1"
|
||||
v-model="data.h1"
|
||||
:label="$t('awg.h1Label')"
|
||||
:description="$t('awg.h1Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="h2"
|
||||
v-model="data.h2"
|
||||
:label="$t('awg.h2Label')"
|
||||
:description="$t('awg.h2Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="h3"
|
||||
v-model="data.h3"
|
||||
:label="$t('awg.h3Label')"
|
||||
:description="$t('awg.h3Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="h4"
|
||||
v-model="data.h4"
|
||||
:label="$t('awg.h4Label')"
|
||||
:description="$t('awg.h4Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i1"
|
||||
v-model="data.i1"
|
||||
:label="$t('awg.i1Label')"
|
||||
:description="$t('awg.i1Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i2"
|
||||
v-model="data.i2"
|
||||
:label="$t('awg.i2Label')"
|
||||
:description="$t('awg.i2Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i3"
|
||||
v-model="data.i3"
|
||||
:label="$t('awg.i3Label')"
|
||||
:description="$t('awg.i3Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i4"
|
||||
v-model="data.i4"
|
||||
:label="$t('awg.i4Label')"
|
||||
:description="$t('awg.i4Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i5"
|
||||
v-model="data.i5"
|
||||
:label="$t('awg.i5Label')"
|
||||
:description="$t('awg.i5Description')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('admin.interface.firewall') }}</FormHeading>
|
||||
<FormSwitchField
|
||||
id="firewallEnabled"
|
||||
v-model="data.firewallEnabled"
|
||||
:label="$t('admin.interface.firewallEnabled')"
|
||||
:description="$t('admin.interface.firewallEnabledDesc')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('form.actions') }}</FormHeading>
|
||||
<FormPrimaryActionField type="submit" :label="$t('form.save')" />
|
||||
@@ -53,6 +165,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
|
||||
@@ -66,7 +180,15 @@ const _submit = useSubmit(
|
||||
{
|
||||
method: 'post',
|
||||
},
|
||||
{ revert }
|
||||
{
|
||||
revert: async (success) => {
|
||||
await revert();
|
||||
if (success) {
|
||||
// Refresh global store information after successful save
|
||||
await globalStore.refreshInformation();
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function submit() {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<main v-if="data">
|
||||
<Panel>
|
||||
<PanelHead>
|
||||
<PanelHeadTitle :text="data.name" />
|
||||
<PanelHeadTitle>
|
||||
{{ data.name }}
|
||||
</PanelHeadTitle>
|
||||
</PanelHead>
|
||||
<PanelBody>
|
||||
<FormElement @submit.prevent="submit">
|
||||
@@ -39,6 +41,12 @@
|
||||
v-model="data.ipv6Address"
|
||||
label="IPv6"
|
||||
/>
|
||||
<FormInfoField
|
||||
id="endpoint"
|
||||
:data="data.endpoint ?? $t('client.notConnected')"
|
||||
:label="$t('client.endpoint')"
|
||||
:description="$t('client.endpointDesc')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading :description="$t('client.allowedIpsDesc')">
|
||||
@@ -55,6 +63,12 @@
|
||||
name="serverAllowedIps"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup v-if="globalStore.information?.firewallEnabled">
|
||||
<FormHeading :description="$t('client.firewallIpsDesc')">
|
||||
{{ $t('client.firewallIps') }}
|
||||
</FormHeading>
|
||||
<FormNullArrayField v-model="data.firewallIps" name="firewallIps" />
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading :description="$t('client.dnsDesc')">
|
||||
{{ $t('general.dns') }}
|
||||
@@ -76,29 +90,84 @@
|
||||
:label="$t('general.persistentKeepalive')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup v-if="globalStore.information?.isAwg">
|
||||
<FormHeading>{{ $t('awg.obfuscationParameters') }}</FormHeading>
|
||||
|
||||
<FormNullNumberField
|
||||
id="jC"
|
||||
v-model="data.jC"
|
||||
:label="$t('awg.jCLabel')"
|
||||
:description="$t('awg.jCDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="Jmin"
|
||||
v-model="data.jMin"
|
||||
:label="$t('awg.jMinLabel')"
|
||||
:description="$t('awg.jMinDescription')"
|
||||
/>
|
||||
<FormNullNumberField
|
||||
id="Jmax"
|
||||
v-model="data.jMax"
|
||||
:label="$t('awg.jMaxLabel')"
|
||||
:description="$t('awg.jMaxDescription')"
|
||||
/>
|
||||
|
||||
<div class="col-span-full text-sm">* {{ $t('awg.mtuNote') }}</div>
|
||||
|
||||
<FormNullTextField
|
||||
id="i1"
|
||||
v-model="data.i1"
|
||||
:label="$t('awg.i1Label')"
|
||||
:description="$t('awg.i1Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i2"
|
||||
v-model="data.i2"
|
||||
:label="$t('awg.i2Label')"
|
||||
:description="$t('awg.i2Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i3"
|
||||
v-model="data.i3"
|
||||
:label="$t('awg.i3Label')"
|
||||
:description="$t('awg.i3Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i4"
|
||||
v-model="data.i4"
|
||||
:label="$t('awg.i4Label')"
|
||||
:description="$t('awg.i4Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="i5"
|
||||
v-model="data.i5"
|
||||
:label="$t('awg.i5Label')"
|
||||
:description="$t('awg.i5Description')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading :description="$t('client.hooksDescription')">
|
||||
{{ $t('client.hooks') }}
|
||||
</FormHeading>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PreUp"
|
||||
v-model="data.preUp"
|
||||
:description="$t('client.hooksLeaveEmpty')"
|
||||
:label="$t('hooks.preUp')"
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PostUp"
|
||||
v-model="data.postUp"
|
||||
:description="$t('client.hooksLeaveEmpty')"
|
||||
:label="$t('hooks.postUp')"
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PreDown"
|
||||
v-model="data.preDown"
|
||||
:description="$t('client.hooksLeaveEmpty')"
|
||||
:label="$t('hooks.preDown')"
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextArea
|
||||
id="PostDown"
|
||||
v-model="data.postDown"
|
||||
:description="$t('client.hooksLeaveEmpty')"
|
||||
@@ -118,13 +187,25 @@
|
||||
@delete="deleteClient"
|
||||
>
|
||||
<FormSecondaryActionField
|
||||
label="Delete"
|
||||
:label="$t('client.delete')"
|
||||
class="w-full"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
as="span"
|
||||
/>
|
||||
</ClientsDeleteDialog>
|
||||
<ClientsConfigDialog
|
||||
trigger-class="col-span-2"
|
||||
:client-id="data.id"
|
||||
>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('client.viewConfig')"
|
||||
class="w-full"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
as="span"
|
||||
/>
|
||||
</ClientsConfigDialog>
|
||||
</FormGroup>
|
||||
</FormElement>
|
||||
</PanelBody>
|
||||
@@ -133,8 +214,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const authStore = useAuthStore();
|
||||
authStore.update();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const route = useRoute();
|
||||
const id = route.params.id as string;
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
<main>
|
||||
<Panel>
|
||||
<PanelHead>
|
||||
<PanelHeadTitle :text="$t('pages.clients')" />
|
||||
<PanelHeadTitle>
|
||||
{{ $t('pages.clients') }}
|
||||
</PanelHeadTitle>
|
||||
<PanelHeadBoat>
|
||||
<ClientsSort />
|
||||
<ClientsNew />
|
||||
<ClientsSearch />
|
||||
<div class="flex gap-2">
|
||||
<ClientsSort />
|
||||
<ClientsNew />
|
||||
</div>
|
||||
</PanelHeadBoat>
|
||||
</PanelHead>
|
||||
|
||||
@@ -28,9 +33,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const authStore = useAuthStore();
|
||||
authStore.update();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const clientsStore = useClientsStore();
|
||||
|
||||
|
||||
@@ -67,9 +67,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const authStore = useAuthStore();
|
||||
authStore.update();
|
||||
|
||||
const toast = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<main>
|
||||
<Panel>
|
||||
<PanelHead>
|
||||
<PanelHeadTitle :text="$t('pages.me')" />
|
||||
<PanelHeadTitle>
|
||||
{{ $t('pages.me') }}
|
||||
</PanelHeadTitle>
|
||||
</PanelHead>
|
||||
<PanelBody class="dark:text-neutral-200">
|
||||
<FormElement @submit.prevent="submit">
|
||||
@@ -120,7 +122,6 @@
|
||||
import { encodeQR } from 'qr';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
authStore.update();
|
||||
|
||||
const name = ref(authStore.userData?.name);
|
||||
const email = ref(authStore.userData?.email);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(VueApexCharts);
|
||||
// https://github.com/apexcharts/vue3-apexcharts/issues/141
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
nuxtApp.vueApp.use(VueApexCharts as any);
|
||||
});
|
||||
|
||||
+14
-7
@@ -1,18 +1,25 @@
|
||||
export const useAuthStore = defineStore('Auth', () => {
|
||||
const { data: userData, refresh: update } = useFetch('/api/session', {
|
||||
method: 'get',
|
||||
});
|
||||
import type { H3Event } from 'h3';
|
||||
import type { SharedPublicUser } from '~~/shared/utils/permissions';
|
||||
|
||||
async function getSession() {
|
||||
export const useAuthStore = defineStore('Auth', () => {
|
||||
const userData = useState<SharedPublicUser | null>('user-data', () => null);
|
||||
|
||||
async function getSession(event?: H3Event) {
|
||||
const fetch = event?.$fetch || $fetch;
|
||||
try {
|
||||
const { data } = await useFetch('/api/session', {
|
||||
const data = await fetch('/api/session', {
|
||||
method: 'get',
|
||||
});
|
||||
return data.value;
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const data = await getSession();
|
||||
userData.value = data;
|
||||
}
|
||||
|
||||
return { userData, update, getSession };
|
||||
});
|
||||
|
||||
@@ -31,8 +31,13 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
const clients = ref<null | LocalClient[]>(null);
|
||||
const clientsPersist = ref<Record<string, ClientPersist>>({});
|
||||
|
||||
const searchParams = ref({
|
||||
filter: undefined as string | undefined,
|
||||
});
|
||||
|
||||
const { data: _clients, refresh: _refresh } = useFetch('/api/client', {
|
||||
method: 'get',
|
||||
params: searchParams,
|
||||
});
|
||||
|
||||
// TODO: rewrite
|
||||
@@ -61,10 +66,11 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
const clientPersist = clientsPersist.value[client.id]!;
|
||||
|
||||
// Debug
|
||||
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
||||
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
||||
// client.latestHandshakeAt = new Date();
|
||||
// this.requiresPassword = true;
|
||||
/* client.transferRx =
|
||||
clientPersist.transferRxPrevious + Math.random() * 1000;
|
||||
client.transferTx =
|
||||
clientPersist.transferTxPrevious + Math.random() * 1000;
|
||||
client.latestHandshakeAt = new Date().toISOString(); */
|
||||
|
||||
clientPersist.transferRxCurrent =
|
||||
(client.transferRx ?? 0) - clientPersist.transferRxPrevious;
|
||||
@@ -120,6 +126,7 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move sort to backend
|
||||
if (transformedClients !== undefined) {
|
||||
transformedClients = sortByProperty(
|
||||
transformedClients,
|
||||
@@ -130,5 +137,11 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
|
||||
clients.value = transformedClients ?? null;
|
||||
}
|
||||
return { clients, clientsPersist, refresh, _clients };
|
||||
|
||||
function setSearchQuery(filter: string) {
|
||||
clients.value = null;
|
||||
searchParams.value.filter = filter || undefined;
|
||||
}
|
||||
|
||||
return { clients, clientsPersist, refresh, _clients, setSearchQuery };
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export const useGlobalStore = defineStore('Global', () => {
|
||||
const { data: information } = useFetch('/api/information', {
|
||||
method: 'get',
|
||||
});
|
||||
const { data: information, refresh: refreshInformation } = useFetch(
|
||||
'/api/information',
|
||||
{
|
||||
method: 'get',
|
||||
}
|
||||
);
|
||||
|
||||
const sortClient = ref(true); // Sort clients by name, true = asc, false = desc
|
||||
|
||||
@@ -22,6 +25,7 @@ export const useGlobalStore = defineStore('Global', () => {
|
||||
return {
|
||||
sortClient,
|
||||
information,
|
||||
refreshInformation,
|
||||
uiShowCharts,
|
||||
toggleCharts,
|
||||
uiChartType,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defineCommand } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db, schema } from '../db';
|
||||
import { hashPassword } from '../../server/utils/password';
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'db:admin:reset',
|
||||
description: 'Reset the admin user password and TOTP settings',
|
||||
},
|
||||
args: {
|
||||
password: {
|
||||
type: 'string',
|
||||
description: 'New password for the admin user',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
async run(ctx) {
|
||||
let password = ctx.args.password || undefined;
|
||||
if (!password) {
|
||||
password = await consola.prompt('Please enter a new password:', {
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
if (!password) {
|
||||
consola.error('Password is required');
|
||||
return;
|
||||
}
|
||||
if (password.length < 12) {
|
||||
consola.error('Password must be at least 12 characters long');
|
||||
return;
|
||||
}
|
||||
consola.info('Setting new password for admin user...');
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
const user = await db.transaction(async (tx) => {
|
||||
const user = await tx
|
||||
.select()
|
||||
.from(schema.user)
|
||||
.where(eq(schema.user.id, 1))
|
||||
.get();
|
||||
|
||||
if (!user) {
|
||||
consola.error('Admin user not found');
|
||||
return;
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(schema.user)
|
||||
.set({
|
||||
password: hash,
|
||||
totpVerified: false,
|
||||
totpKey: null,
|
||||
})
|
||||
.where(eq(schema.user.id, 1));
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
consola.error('Failed to update admin user');
|
||||
return;
|
||||
}
|
||||
|
||||
consola.success(
|
||||
`Successfully updated admin user ${user.id} (${user.username})`
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { defineCommand } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
|
||||
import { db } from '../db';
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'clients:list',
|
||||
description: 'List all clients',
|
||||
},
|
||||
async run() {
|
||||
consola.info('Listing all clients...');
|
||||
const clients = await db.query.client.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
publicKey: true,
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (clients.length === 0) {
|
||||
consola.info('No clients found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.table(clients);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defineCommand } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { wg } from '../../server/utils/wgHelper';
|
||||
import { encodeQRCodeTerm } from '../../server/utils/qr';
|
||||
import { db, schema } from '../db';
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'clients:qr',
|
||||
description: 'Generate QR code for a client',
|
||||
},
|
||||
args: {
|
||||
id: {
|
||||
required: true,
|
||||
type: 'positional',
|
||||
},
|
||||
ipv6: {
|
||||
required: false,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
async run(ctx) {
|
||||
const clientId = Number(ctx.args.id);
|
||||
const enableIpv6 = ctx.args.ipv6;
|
||||
|
||||
if (Number.isNaN(clientId)) {
|
||||
consola.error('Invalid client ID');
|
||||
return;
|
||||
}
|
||||
|
||||
consola.info('Generating QR code for client...');
|
||||
|
||||
const wgInterface = await db.query.wgInterface.findFirst({
|
||||
where: eq(schema.wgInterface.name, 'wg0'),
|
||||
});
|
||||
if (!wgInterface) {
|
||||
consola.error('WireGuard interface not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const userConfig = await db.query.userConfig.findFirst({
|
||||
where: eq(schema.userConfig.id, 'wg0'),
|
||||
});
|
||||
if (!userConfig) {
|
||||
consola.error('User config not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await db.query.client.findFirst({
|
||||
where: eq(schema.client.id, clientId),
|
||||
});
|
||||
if (!client) {
|
||||
consola.error(`Client with ID ${clientId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const clientConfig = wg.generateClientConfig(
|
||||
wgInterface,
|
||||
userConfig,
|
||||
client,
|
||||
{
|
||||
enableIpv6,
|
||||
}
|
||||
);
|
||||
|
||||
consola.log(encodeQRCodeTerm(clientConfig));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
|
||||
import * as schema from '../server/database/schema';
|
||||
|
||||
//const client = createClient({ url: 'file:../data/wg-easy.db' });
|
||||
const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' });
|
||||
export const db = drizzle({ client, schema });
|
||||
|
||||
export { schema };
|
||||
+26
-72
@@ -1,82 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ! Auto Imports are not supported in this file
|
||||
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import { createClient } from '@libsql/client';
|
||||
import type { Resolvable, SubCommandsDef } from 'citty';
|
||||
import { defineCommand, runMain } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import packageJson from '../package.json';
|
||||
import * as schema from '../server/database/schema';
|
||||
import { hashPassword } from '../server/utils/password';
|
||||
|
||||
const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' });
|
||||
const db = drizzle({ client, schema });
|
||||
// Commands
|
||||
import dbAdminReset from './admin/reset';
|
||||
import clientsList from './clients/list';
|
||||
import clientsQr from './clients/qr';
|
||||
const subCommands = [dbAdminReset, clientsList, clientsQr] as const;
|
||||
|
||||
const dbAdminReset = defineCommand({
|
||||
meta: {
|
||||
name: 'db:admin:reset',
|
||||
description: 'Reset the admin user',
|
||||
},
|
||||
args: {
|
||||
password: {
|
||||
type: 'string',
|
||||
description: 'New password for the admin user',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
async run(ctx) {
|
||||
let password = ctx.args.password || undefined;
|
||||
if (!password) {
|
||||
password = await consola.prompt('Please enter a new password:', {
|
||||
type: 'text',
|
||||
});
|
||||
// from citty
|
||||
function resolveValue<T>(input: Resolvable<T>): T | Promise<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return typeof input === 'function' ? (input as any)() : input;
|
||||
}
|
||||
|
||||
async function generateSubCommands(): Promise<SubCommandsDef> {
|
||||
const subCommandsMap: Record<string, SubCommandsDef[string]> = {};
|
||||
|
||||
for (const cmd of subCommands) {
|
||||
const cmdMeta = await resolveValue(cmd.meta || {});
|
||||
if (!cmdMeta.name) {
|
||||
console.warn('Skipping command without name:', cmd);
|
||||
continue;
|
||||
}
|
||||
if (!password) {
|
||||
consola.error('Password is required');
|
||||
return;
|
||||
}
|
||||
if (password.length < 12) {
|
||||
consola.error('Password must be at least 12 characters long');
|
||||
return;
|
||||
}
|
||||
console.info('Setting new password for admin user...');
|
||||
const hash = await hashPassword(password);
|
||||
subCommandsMap[cmdMeta.name] = cmd;
|
||||
}
|
||||
|
||||
const user = await db.transaction(async (tx) => {
|
||||
const user = await tx
|
||||
.select()
|
||||
.from(schema.user)
|
||||
.where(eq(schema.user.id, 1))
|
||||
.get();
|
||||
return subCommandsMap;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
consola.error('Admin user not found');
|
||||
return;
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(schema.user)
|
||||
.set({
|
||||
password: hash,
|
||||
})
|
||||
.where(eq(schema.user.id, 1));
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
consola.error('Failed to update admin user');
|
||||
return;
|
||||
}
|
||||
|
||||
consola.success(
|
||||
`Successfully updated admin user ${user.id} (${user.username})`
|
||||
);
|
||||
},
|
||||
});
|
||||
const subCommandsMap = await generateSubCommands();
|
||||
|
||||
const main = defineCommand({
|
||||
meta: {
|
||||
@@ -84,9 +40,7 @@ const main = defineCommand({
|
||||
version: packageJson.version,
|
||||
description: 'Command Line Interface',
|
||||
},
|
||||
subCommands: {
|
||||
'db:admin:reset': dbAdminReset,
|
||||
},
|
||||
subCommands: subCommandsMap,
|
||||
});
|
||||
|
||||
runMain(main);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "es2024",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1,9 +1,51 @@
|
||||
import en from './locales/en.json';
|
||||
import pl from './locales/pl.json';
|
||||
import uk from './locales/uk.json';
|
||||
import fr from './locales/fr.json';
|
||||
import de from './locales/de.json';
|
||||
import it from './locales/it.json';
|
||||
import ru from './locales/ru.json';
|
||||
import zhhk from './locales/zh-HK.json';
|
||||
import zhcn from './locales/zh-CN.json';
|
||||
import zhtw from './locales/zh-TW.json';
|
||||
import ko from './locales/ko.json';
|
||||
import es from './locales/es.json';
|
||||
import ptbr from './locales/pt-BR.json';
|
||||
import tr from './locales/tr.json';
|
||||
import bn from './locales/bn.json';
|
||||
import id from './locales/id.json';
|
||||
import nl from './locales/nl.json';
|
||||
import nb from './locales/nb.json';
|
||||
import bg from './locales/bg.json';
|
||||
import gl from './locales/gl.json';
|
||||
import cs from './locales/cs.json';
|
||||
import vi from './locales/vi.json';
|
||||
|
||||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en,
|
||||
pl,
|
||||
uk,
|
||||
fr,
|
||||
de,
|
||||
it,
|
||||
ru,
|
||||
'zh-HK': zhhk,
|
||||
'zh-CN': zhcn,
|
||||
'zh-TW': zhtw,
|
||||
ko,
|
||||
es,
|
||||
'pt-BR': ptbr,
|
||||
tr,
|
||||
bn,
|
||||
id,
|
||||
nl,
|
||||
nb,
|
||||
bg,
|
||||
gl,
|
||||
cs,
|
||||
vi,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
{
|
||||
"pages": {
|
||||
"me": "Профил",
|
||||
"clients": "Клиенти",
|
||||
"admin": {
|
||||
"panel": "Админ Панел",
|
||||
"general": "Общи",
|
||||
"config": "Конфигурация",
|
||||
"interface": "Интерфейс",
|
||||
"hooks": "Hooks"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "Имейл"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "Текуща парола",
|
||||
"enable2fa": "Активирай двуфакторна автентикация",
|
||||
"enable2faDesc": "Сканирай QR кода с твоето приложение за автентикатор или въведи ключа ръчно.",
|
||||
"2faKey": "TOTP ключ",
|
||||
"2faCodeDesc": "Въведи кода от твоето приложение за автентикатор.",
|
||||
"disable2fa": "Деактивирай двуфакторна автентикация",
|
||||
"disable2faDesc": "Въведи паролата си, за да деактивираш двуфакторната автентикация."
|
||||
},
|
||||
"general": {
|
||||
"name": "Име",
|
||||
"username": "Потребителско име",
|
||||
"password": "Парола",
|
||||
"newPassword": "Нова парола",
|
||||
"updatePassword": "Обнови парола",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "Разрешени IP-та",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "Постоянно поддържане на връзката",
|
||||
"logout": "Изход",
|
||||
"continue": "Продължи",
|
||||
"host": "Хост",
|
||||
"port": "Порт",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
"confirmPassword": "Потвърди парола",
|
||||
"loading": "Зареждане...",
|
||||
"2fa": "Двуфакторна автентикация",
|
||||
"2faCode": "TOTP код"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Добре дошъл в първоначалната настройка на wg-easy",
|
||||
"welcomeDesc": "Откри най-лесния начин да инсталираш и управляваш WireGuard на всеки Linux сървър",
|
||||
"existingSetup": "Имаш ли вече съществуваща инсталация?",
|
||||
"createAdminDesc": "Моля, първо въведи администраторско потребителско име и силна сигурна парола. Тези данни ще се използват за вход в административния панел.",
|
||||
"setupConfigDesc": "Моля, въведи хост и порт. Тази информация ще се използва при генериране на клиентски конфигурации за WireGuard.",
|
||||
"setupMigrationDesc": "Ако желаеш да мигрираш данните от предишна версия на wg-easy, качи резервното копие.",
|
||||
"upload": "Качи",
|
||||
"migration": "Възстанови от резервно копие:",
|
||||
"createAccount": "Създай акаунт",
|
||||
"successful": "Настройката е успешна",
|
||||
"hostDesc": "Публично име/адрес, към който клиентите ще се свързват",
|
||||
"portDesc": "Публичен UDP порт, на който клиентите ще се свързват и на който WireGuard слуша"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "Налична е актуализация!",
|
||||
"update": "Актуализирай"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Тъмна тема",
|
||||
"light": "Светла тема",
|
||||
"system": "Системна тема"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "Покажи/скрий графики",
|
||||
"donate": "Дарение"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Вход",
|
||||
"rememberMe": "Запомни ме",
|
||||
"rememberMeDesc": "Остани влязъл след затваряне на браузъра",
|
||||
"insecure": "Не можеш да влезеш през несигурна връзка. Използвай HTTPS.",
|
||||
"2faRequired": "Изисква се двуфакторна автентикация",
|
||||
"2faWrong": "Грешен код за двуфакторна автентикация"
|
||||
},
|
||||
"client": {
|
||||
"empty": "Все още няма клиенти.",
|
||||
"newShort": "Нов",
|
||||
"sort": "Сортирай",
|
||||
"create": "Създай клиент",
|
||||
"created": "Клиентът е създаден",
|
||||
"new": "Нов клиент",
|
||||
"name": "Име",
|
||||
"expireDate": "Дата на изтичане",
|
||||
"expireDateDesc": "Дата, след която клиентът ще бъде деактивиран. Празно = постоянен",
|
||||
"delete": "Изтрий",
|
||||
"deleteClient": "Изтрий клиент",
|
||||
"deleteDialog1": "Сигурен ли си, че искаш да изтриеш",
|
||||
"deleteDialog2": "Това действие е необратимо.",
|
||||
"enabled": "Активен",
|
||||
"address": "Адрес",
|
||||
"serverAllowedIps": "Разрешени IP-та от сървъра",
|
||||
"otlDesc": "Генерирай кратък еднократен линк",
|
||||
"permanent": "Постоянно",
|
||||
"createdOn": "Създаден на ",
|
||||
"lastSeen": "Последно видян на ",
|
||||
"totalDownload": "Общо изтеглени: ",
|
||||
"totalUpload": "Общо качени: ",
|
||||
"newClient": "Нов клиент",
|
||||
"disableClient": "Деактивирай клиент",
|
||||
"enableClient": "Активирай клиент",
|
||||
"noPrivKey": "Този клиент няма известен частен ключ. Не може да се създаде конфигурация.",
|
||||
"showQR": "Покажи QR код",
|
||||
"downloadConfig": "Изтегли конфигурация",
|
||||
"allowedIpsDesc": "Кои IP-та ще се насочват през VPN (замества глобалната настройка)",
|
||||
"serverAllowedIpsDesc": "Кои IP-та сървърът ще насочва към клиента",
|
||||
"mtuDesc": "Задава максималния размер на пакета (MTU) за VPN тунела",
|
||||
"persistentKeepaliveDesc": "Интервал (в секунди) за изпращане на keep-alive пакети. 0 = изключено",
|
||||
"hooks": "Hooks",
|
||||
"hooksDescription": "Hooks работят само с wg-quick",
|
||||
"hooksLeaveEmpty": "Само за wg-quick. В противен случай остави празно",
|
||||
"dnsDesc": "DNS сървър, който клиентите ще използват (замества глобалната настройка)",
|
||||
"notConnected": "Клиентът не е свързан",
|
||||
"endpoint": "Крайна точка",
|
||||
"endpointDesc": "IP адресът на клиента, от който е установена WireGuard връзката",
|
||||
"search": "Търси клиенти...",
|
||||
"config": "Конфигурация",
|
||||
"viewConfig": "Прегледай конфигурацията"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "Промени",
|
||||
"cancel": "Отказ",
|
||||
"create": "Създай"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Успех",
|
||||
"saved": "Запазено",
|
||||
"error": "Грешка"
|
||||
},
|
||||
"form": {
|
||||
"actions": "Действия",
|
||||
"save": "Запази",
|
||||
"revert": "Отмени промените",
|
||||
"sectionGeneral": "Общи",
|
||||
"sectionAdvanced": "Разширени",
|
||||
"noItems": "Няма елементи",
|
||||
"nullNoItems": "Няма елементи. Използва се глобалната конфигурация",
|
||||
"add": "Добави"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "Време на сесията",
|
||||
"sessionTimeoutDesc": "Продължителност на сесията при „Запомни ме“ (в секунди)",
|
||||
"metrics": "Метрики",
|
||||
"metricsPassword": "Парола",
|
||||
"metricsPasswordDesc": "Bearer парола за достъп до metrics ендпойнт (парола или argon2 хеш)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "Път за метрики в JSON формат",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Път за Prometheus метрики"
|
||||
},
|
||||
"config": {
|
||||
"connection": "Връзка",
|
||||
"hostDesc": "Публично име/адрес за клиентите (инвалидира конфигурациите)",
|
||||
"portDesc": "Публичен UDP порт за клиентите (инвалидира конфигурациите; вероятно искаш да смениш и порта на интерфейса)",
|
||||
"allowedIpsDesc": "Разрешени IP-та за клиентите (глобална настройка)",
|
||||
"dnsDesc": "DNS сървър за клиентите (глобална настройка)",
|
||||
"mtuDesc": "MTU, който ще ползват клиентите (само за нови клиенти)",
|
||||
"persistentKeepaliveDesc": "Интервал в секунди за keep-alive към сървъра. 0 = изключено (само за нови клиенти)",
|
||||
"suggest": "Предложи",
|
||||
"suggestDesc": "Избери IP адрес или хост за полето Host"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR променен",
|
||||
"device": "Устройство",
|
||||
"deviceDesc": "Мрежово устройство, през което да се препраща WireGuard трафикът",
|
||||
"mtuDesc": "MTU, който ще ползва WireGuard",
|
||||
"portDesc": "UDP порт, на който слуша WireGuard (вероятно искаш да смениш и порта в Config)",
|
||||
"changeCidr": "Смени CIDR",
|
||||
"restart": "Рестартирай интерфейс",
|
||||
"restartDesc": "Рестартиране на WireGuard интерфейса",
|
||||
"restartWarn": "Сигурен ли си, че искаш да рестартираш интерфейса? Всички клиенти ще бъдат изключени.",
|
||||
"restartSuccess": "Интерфейсът е рестартиран"
|
||||
},
|
||||
"introText": "Добре дошъл в административния панел.\n\nТук можеш да управляваш общите настройки, конфигурацията, настройките на интерфейса и hooks.\n\nЗапочни, като избереш някоя от секциите в страничното меню."
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{0} е задължително",
|
||||
"validNumber": "{0} трябва да е валидно число",
|
||||
"validString": "{0} трябва да е валиден текст",
|
||||
"validBoolean": "{0} трябва да е валидна булева стойност",
|
||||
"validArray": "{0} трябва да е валиден масив",
|
||||
"stringMin": "{0} трябва да съдържа поне {1} символа",
|
||||
"numberMin": "{0} трябва да е поне {1}"
|
||||
},
|
||||
"client": {
|
||||
"id": "ID на клиента",
|
||||
"name": "Име",
|
||||
"expiresAt": "Изтича на",
|
||||
"address4": "IPv4 адрес",
|
||||
"address6": "IPv6 адрес",
|
||||
"serverAllowedIps": "Разрешени IP-та от сървъра"
|
||||
},
|
||||
"user": {
|
||||
"username": "Потребителско име",
|
||||
"password": "Парола",
|
||||
"remember": "Запомни",
|
||||
"name": "Име",
|
||||
"email": "Имейл",
|
||||
"emailInvalid": "Имейлът трябва да е валиден",
|
||||
"passwordMatch": "Паролите трябва да съвпадат",
|
||||
"totpEnable": "Активиране на TOTP",
|
||||
"totpEnableTrue": "TOTP Enable трябва да е true",
|
||||
"totpCode": "TOTP код"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "Хост"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "Време на сесията",
|
||||
"metricsEnabled": "Метрики",
|
||||
"metricsPassword": "Парола за метрики"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "Устройство",
|
||||
"cidrValid": "CIDR трябва да е валиден"
|
||||
},
|
||||
"otl": "Еднократен линк",
|
||||
"stringMalformed": "Невалиден формат на низа",
|
||||
"body": "Тялото трябва да е валиден обект",
|
||||
"hook": "Hook",
|
||||
"enabled": "Активиран",
|
||||
"mtu": "MTU",
|
||||
"port": "Порт",
|
||||
"persistentKeepalive": "Постоянно поддържане на връзката",
|
||||
"address": "IP адрес",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "Разрешени IP-та",
|
||||
"file": "Файл"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "Копиране не се поддържа",
|
||||
"copied": "Копирано!",
|
||||
"failed": "Копирането неуспешно",
|
||||
"copy": "Копирай"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "Брой junk пакети (Jc)",
|
||||
"jCDescription": "Брой junk пакети за изпращане (1–128, препоръчително: 4–12)",
|
||||
"jMinLabel": "Минимален размер на junk пакети (Jmin)",
|
||||
"jMinDescription": "Минимален размер на junk пакетите (0–1279*, препоръчително: 8, трябва да е < Jmax)",
|
||||
"jMaxLabel": "Максимален размер на junk пакети (Jmax)",
|
||||
"jMaxDescription": "Максимален размер на junk пакетите (1–1280*, препоръчително: 80, трябва да е > Jmin)",
|
||||
"s1Label": "Размер на junk в init пакета (S1)",
|
||||
"s1Description": "Размер на junk в init пакета (0–1132 [1280*−148=1132], препоръчително: 15–150, S1+56 ≠ S2)",
|
||||
"s2Label": "Размер на junk в отговорния пакет (S2)",
|
||||
"s2Description": "Размер на junk в response пакета (0–1188 [1280*−92=1188], препоръчително: 15–150)",
|
||||
"s3Label": "Размер на junk в cookie reply пакета (S3)",
|
||||
"s3Description": "Размер на junk в cookie reply пакета",
|
||||
"s4Label": "Размер на junk в транспортния пакет (S4)",
|
||||
"s4Description": "Размер на junk в транспортния пакет",
|
||||
"h1Label": "Init magic header (H1)",
|
||||
"h1Description": "Стойност на хедера в init пакета (5–2147483647, уникална спрямо H2–H4)",
|
||||
"h2Label": "Response magic header (H2)",
|
||||
"h2Description": "Стойност на хедера в response пакета (5–2147483647, уникална спрямо H1, H3, H4)",
|
||||
"h3Label": "Cookie reply magic header (H3)",
|
||||
"h3Description": "Стойност на хедера в cookie reply пакета (5–2147483647, уникална спрямо H1, H2, H4)",
|
||||
"h4Label": "Transport magic header (H4)",
|
||||
"h4Description": "Стойност на хедера в транспортния пакет (5–2147483647, уникална спрямо H1–H3)",
|
||||
"i1Label": "Специален junk пакет 1 (I1)",
|
||||
"i1Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
|
||||
"i2Label": "Специален junk пакет 2 (I2)",
|
||||
"i2Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
|
||||
"i3Label": "Специален junk пакет 3 (I3)",
|
||||
"i3Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
|
||||
"i4Label": "Специален junk пакет 4 (I4)",
|
||||
"i4Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
|
||||
"i5Label": "Специален junk пакет 5 (I5)",
|
||||
"i5Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
|
||||
"mtuNote": "Стойностите зависят от MTU",
|
||||
"obfuscationParameters": "Параметри за обфускация на AmneziaWG"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"pages": {
|
||||
"me": "অ্যাকাউন্ট",
|
||||
"clients": "ক্লায়েন্টস",
|
||||
"admin": {
|
||||
"panel": "অ্যাডমিন প্যানেল",
|
||||
"general": "সাধারণ",
|
||||
"config": "কনফিগ",
|
||||
"interface": "ইন্টারফেস",
|
||||
"hooks": "হুকস"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "ই-মেইল"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "বর্তমান পাসওয়ার্ড",
|
||||
"enable2fa": "দুই স্তরের যাচাইকরণ চালু করুন",
|
||||
"enable2faDesc": "আপনার অথেন্টিকেটর অ্যাপ দিয়ে QR কোড স্ক্যান করুন অথবা কীটি ম্যানুয়ালি প্রবেশ করুন।",
|
||||
"2faKey": "TOTP কী",
|
||||
"2faCodeDesc": "আপনার অথেন্টিকেটর অ্যাপ থেকে কোডটি প্রবেশ করুন।",
|
||||
"disable2fa": "দুই স্তরের যাচাইকরণ বন্ধ করুন",
|
||||
"disable2faDesc": "দুই স্তরের যাচাইকরণ বন্ধ করতে আপনার পাসওয়ার্ড দিন।"
|
||||
},
|
||||
"general": {
|
||||
"name": "নাম",
|
||||
"username": "ইউজারনেম",
|
||||
"password": "পাসওয়ার্ড",
|
||||
"newPassword": "নতুন পাসওয়ার্ড",
|
||||
"updatePassword": "পাসওয়ার্ড আপডেট করুন",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "অনুমোদিত IPs",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "স্থায়ী কিপ-এলাইভ",
|
||||
"logout": "লগআউট",
|
||||
"continue": "চালিয়ে যান",
|
||||
"host": "হোস্ট",
|
||||
"port": "পোর্ট",
|
||||
"yes": "হ্যাঁ",
|
||||
"no": "না",
|
||||
"confirmPassword": "পাসওয়ার্ড নিশ্চিত করুন",
|
||||
"loading": "লোড হচ্ছে...",
|
||||
"2fa": "দুই স্তরের যাচাইকরণ",
|
||||
"2faCode": "TOTP কোড"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "wg-easy এর প্রথম সেটআপে স্বাগতম",
|
||||
"welcomeDesc": "আপনি WireGuard ইনস্টল ও পরিচালনার সবচেয়ে সহজ উপায় পেয়েছেন",
|
||||
"existingSetup": "আপনার কি পূর্ববর্তী সেটআপ আছে?",
|
||||
"createAdminDesc": "প্রথমে একটি অ্যাডমিন ইউজারনেম এবং শক্তিশালী পাসওয়ার্ড দিন। এই তথ্য অ্যাডমিন প্যানেলে লগইন করতে ব্যবহৃত হবে।",
|
||||
"setupConfigDesc": "হোস্ট এবং পোর্ট তথ্য দিন। এটি ক্লায়েন্ট কনফিগারেশনের জন্য ব্যবহৃত হবে যখন WireGuard তাদের ডিভাইসে সেটআপ করা হবে।",
|
||||
"setupMigrationDesc": "আপনার পূর্ববর্তী wg-easy থেকে নতুন সেটআপে ডেটা স্থানান্তর করতে চাইলে ব্যাকআপ ফাইল দিন।",
|
||||
"upload": "আপলোড",
|
||||
"migration": "ব্যাকআপ পুনরুদ্ধার করুন:",
|
||||
"createAccount": "অ্যাকাউন্ট তৈরি করুন",
|
||||
"successful": "সেটআপ সফল হয়েছে",
|
||||
"hostDesc": "পাবলিক হোস্টনেম ক্লায়েন্টরা সংযোগ করবে",
|
||||
"portDesc": "পাবলিক UDP পোর্ট ক্লায়েন্টরা সংযোগ করবে এবং WireGuard শুনবে"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "একটি আপডেট উপলব্ধ!",
|
||||
"update": "আপডেট"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "ডার্ক থিম",
|
||||
"light": "লাইট থিম",
|
||||
"system": "সিস্টেম থিম"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "চার্ট দেখান/লুকান",
|
||||
"donate": "ডোনেট"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "সাইন ইন",
|
||||
"rememberMe": "মনে রাখুন",
|
||||
"rememberMeDesc": "ব্রাউজার বন্ধ করার পরেও লগইন থাকুন",
|
||||
"insecure": "অনিরাপদ সংযোগে লগইন করা যাবে না। HTTPS ব্যবহার করুন।",
|
||||
"2faRequired": "দুই স্তরের যাচাইকরণ প্রয়োজন",
|
||||
"2faWrong": "দুই স্তরের যাচাইকরণ ভুল"
|
||||
},
|
||||
"client": {
|
||||
"empty": "এখনো কোনো ক্লায়েন্ট নেই।",
|
||||
"newShort": "নতুন",
|
||||
"sort": "সাজান",
|
||||
"create": "ক্লায়েন্ট তৈরি করুন",
|
||||
"created": "ক্লায়েন্ট তৈরি হয়েছে",
|
||||
"new": "নতুন ক্লায়েন্ট",
|
||||
"name": "নাম",
|
||||
"expireDate": "মেয়াদ শেষের তারিখ",
|
||||
"expireDateDesc": "যেদিন ক্লায়েন্ট নিষ্ক্রিয় হবে। স্থায়ী করতে ফাঁকা রাখুন",
|
||||
"deleteClient": "ক্লায়েন্ট মুছে ফেলুন",
|
||||
"deleteDialog1": "আপনি কি নিশ্চিতভাবে মুছে ফেলতে চান",
|
||||
"deleteDialog2": "এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
|
||||
"enabled": "সক্রিয়",
|
||||
"address": "ঠিকানা",
|
||||
"serverAllowedIps": "সার্ভার অনুমোদিত IPs",
|
||||
"otlDesc": "সংক্ষিপ্ত একবারের লিঙ্ক তৈরি করুন",
|
||||
"permanent": "স্থায়ী",
|
||||
"createdOn": "তৈরি হয়েছে",
|
||||
"lastSeen": "শেষ দেখা হয়েছে",
|
||||
"totalDownload": "মোট ডাউনলোড: ",
|
||||
"totalUpload": "মোট আপলোড: ",
|
||||
"newClient": "নতুন ক্লায়েন্ট",
|
||||
"disableClient": "ক্লায়েন্ট নিষ্ক্রিয় করুন",
|
||||
"enableClient": "ক্লায়েন্ট সক্রিয় করুন",
|
||||
"noPrivKey": "এই ক্লায়েন্টের কোনো ব্যক্তিগত কী নেই। কনফিগারেশন তৈরি করা যাবে না।",
|
||||
"showQR": "QR কোড দেখান",
|
||||
"downloadConfig": "কনফিগারেশন ডাউনলোড করুন",
|
||||
"allowedIpsDesc": "কোন IPs VPN এর মাধ্যমে রাউট হবে (গ্লোবাল কনফিগ ওভাররাইড করবে)",
|
||||
"serverAllowedIpsDesc": "কোন IPs সার্ভার ক্লায়েন্টে রাউট করবে",
|
||||
"mtuDesc": "VPN টানেলের জন্য সর্বাধিক প্যাকেট সাইজ নির্ধারণ করে",
|
||||
"persistentKeepaliveDesc": "কিপ-এলাইভ প্যাকেটের জন্য ইন্টারভাল (সেকেন্ডে)। 0 হলে বন্ধ",
|
||||
"hooks": "হুকস",
|
||||
"hooksDescription": "হুকস শুধুমাত্র wg-quick এর সাথে কাজ করে",
|
||||
"hooksLeaveEmpty": "শুধুমাত্র wg-quick এর জন্য। অন্যথায় ফাঁকা রাখুন",
|
||||
"dnsDesc": "DNS সার্ভার ক্লায়েন্টরা ব্যবহার করবে (গ্লোবাল কনফিগ ওভাররাইড করবে)",
|
||||
"notConnected": "ক্লায়েন্ট সংযুক্ত নয়",
|
||||
"endpoint": "এন্ডপয়েন্ট",
|
||||
"endpointDesc": "ক্লায়েন্টের IP যেখান থেকে WireGuard সংযোগ স্থাপন করা হয়েছে"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "পরিবর্তন করুন",
|
||||
"cancel": "বাতিল করুন",
|
||||
"create": "তৈরি করুন"
|
||||
},
|
||||
"toast": {
|
||||
"success": "সফলতা",
|
||||
"saved": "সংরক্ষিত",
|
||||
"error": "ত্রুটি"
|
||||
},
|
||||
"form": {
|
||||
"actions": "কর্মসমূহ",
|
||||
"save": "সংরক্ষণ করুন",
|
||||
"revert": "পূর্বাবস্থায় ফেরান",
|
||||
"sectionGeneral": "সাধারণ",
|
||||
"sectionAdvanced": "উন্নত",
|
||||
"noItems": "কোনো আইটেম নেই",
|
||||
"nullNoItems": "কোনো আইটেম নেই। গ্লোবাল কনফিগ ব্যবহার হচ্ছে",
|
||||
"add": "যোগ করুন"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "সেশন টাইমআউট",
|
||||
"sessionTimeoutDesc": "মনে রাখুনের জন্য সেশন সময়কাল (সেকেন্ডে)",
|
||||
"metrics": "মেট্রিক্স",
|
||||
"metricsPassword": "পাসওয়ার্ড",
|
||||
"metricsPasswordDesc": "মেট্রিক্স এন্ডপয়েন্টের জন্য বেয়ারার পাসওয়ার্ড (পাসওয়ার্ড বা argon2 হ্যাশ)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "JSON ফরম্যাটে মেট্রিক্সের রুট",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Prometheus মেট্রিক্সের রুট"
|
||||
},
|
||||
"config": {
|
||||
"connection": "সংযোগ",
|
||||
"hostDesc": "পাবলিক হোস্টনেম ক্লায়েন্টরা সংযোগ করবে (কনফিগ বাতিল হবে)",
|
||||
"portDesc": "পাবলিক UDP পোর্ট ক্লায়েন্টরা সংযোগ করবে (কনফিগ বাতিল হবে, আপনি সম্ভবত ইন্টারফেস পোর্টও পরিবর্তন করতে চাইবেন)",
|
||||
"allowedIpsDesc": "অনুমোদিত IPs ক্লায়েন্টরা ব্যবহার করবে (গ্লোবাল কনফিগ)",
|
||||
"dnsDesc": "DNS সার্ভার ক্লায়েন্টরা ব্যবহার করবে (গ্লোবাল কনফিগ)",
|
||||
"mtuDesc": "MTU ক্লায়েন্টরা ব্যবহার করবে (শুধুমাত্র নতুন ক্লায়েন্টের জন্য)",
|
||||
"persistentKeepaliveDesc": "সার্ভারে কিপ-এলাইভ পাঠানোর ইন্টারভাল (সেকেন্ডে)। 0 = বন্ধ (শুধুমাত্র নতুন ক্লায়েন্টের জন্য)",
|
||||
"suggest": "পরামর্শ",
|
||||
"suggestDesc": "হোস্ট ফিল্ডের জন্য একটি IP-ঠিকানা বা হোস্টনেম নির্বাচন করুন"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR পরিবর্তন হয়েছে",
|
||||
"device": "ডিভাইস",
|
||||
"deviceDesc": "ইথারনেট ডিভাইস যার মাধ্যমে wireguard ট্রাফিক ফরওয়ার্ড হবে",
|
||||
"mtuDesc": "WireGuard ব্যবহার করবে এমন MTU",
|
||||
"portDesc": "UDP পোর্ট WireGuard শুনবে (আপনি সম্ভবত কনফিগ পোর্টও পরিবর্তন করতে চাইবেন)",
|
||||
"changeCidr": "CIDR পরিবর্তন করুন",
|
||||
"restart": "ইন্টারফেস রিস্টার্ট করুন",
|
||||
"restartDesc": "WireGuard ইন্টারফেস রিস্টার্ট করুন",
|
||||
"restartWarn": "আপনি কি নিশ্চিতভাবে ইন্টারফেস রিস্টার্ট করতে চান? এতে সব ক্লায়েন্ট সংযোগ বিচ্ছিন্ন হবে।",
|
||||
"restartSuccess": "ইন্টারফেস রিস্টার্ট হয়েছে"
|
||||
},
|
||||
"introText": "অ্যাডমিন প্যানেলে স্বাগতম।\n\nএখানে আপনি সাধারণ সেটিংস, কনফিগারেশন, ইন্টারফেস সেটিংস এবং হুকস পরিচালনা করতে পারবেন।\n\nসাইডবার থেকে একটি বিভাগ নির্বাচন করে শুরু করুন।"
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{0} আবশ্যক",
|
||||
"validNumber": "{0} একটি বৈধ সংখ্যা হতে হবে",
|
||||
"validString": "{0} একটি বৈধ স্ট্রিং হতে হবে",
|
||||
"validBoolean": "{0} একটি বৈধ বুলিয়ান হতে হবে",
|
||||
"validArray": "{0} একটি বৈধ অ্যারে হতে হবে",
|
||||
"stringMin": "{0} কমপক্ষে {1} অক্ষর হতে হবে",
|
||||
"numberMin": "{0} কমপক্ষে {1} হতে হবে"
|
||||
},
|
||||
"client": {
|
||||
"id": "ক্লায়েন্ট আইডি",
|
||||
"name": "নাম",
|
||||
"expiresAt": "মেয়াদ শেষ",
|
||||
"address4": "IPv4 ঠিকানা",
|
||||
"address6": "IPv6 ঠিকানা",
|
||||
"serverAllowedIps": "সার্ভার অনুমোদিত IPs"
|
||||
},
|
||||
"user": {
|
||||
"username": "ইউজারনেম",
|
||||
"password": "পাসওয়ার্ড",
|
||||
"remember": "মনে রাখুন",
|
||||
"name": "নাম",
|
||||
"email": "ই-মেইল",
|
||||
"emailInvalid": "ই-মেইল একটি বৈধ ই-মেইল হতে হবে",
|
||||
"passwordMatch": "পাসওয়ার্ড মিলতে হবে",
|
||||
"totpEnable": "TOTP চালু",
|
||||
"totpEnableTrue": "TOTP চালু সত্য হতে হবে",
|
||||
"totpCode": "TOTP কোড"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "হোস্ট"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "সেশন টাইমআউট",
|
||||
"metricsEnabled": "মেট্রিক্স",
|
||||
"metricsPassword": "মেট্রিক্স পাসওয়ার্ড"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "ডিভাইস",
|
||||
"cidrValid": "CIDR বৈধ হতে হবে"
|
||||
},
|
||||
"otl": "একবারের লিঙ্ক",
|
||||
"stringMalformed": "স্ট্রিং বিকৃত",
|
||||
"body": "বডি একটি বৈধ অবজেক্ট হতে হবে",
|
||||
"hook": "হুক",
|
||||
"enabled": "সক্রিয়",
|
||||
"mtu": "MTU",
|
||||
"port": "পোর্ট",
|
||||
"persistentKeepalive": "স্থায়ী কিপ-এলাইভ",
|
||||
"address": "IP ঠিকানা",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "অনুমোদিত IPs",
|
||||
"file": "ফাইল"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
{
|
||||
"pages": {
|
||||
"me": "Účet",
|
||||
"clients": "Klienti",
|
||||
"admin": {
|
||||
"panel": "Administrace",
|
||||
"general": "Obecné",
|
||||
"config": "Konfigurace",
|
||||
"interface": "Rozhraní",
|
||||
"hooks": "Nastavení reakcí"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "E-mail"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "Aktuální heslo",
|
||||
"enable2fa": "Zapnout dvoufázové ověření (2FA)",
|
||||
"enable2faDesc": "Naskenujte QR kód ve své autentizační aplikaci nebo zadejte klíč ručně.",
|
||||
"2faKey": "TOTP klíč",
|
||||
"2faCodeDesc": "Zadejte kód z vaší autentizační aplikace.",
|
||||
"disable2fa": "Vypnout dvoufázové ověření",
|
||||
"disable2faDesc": "Pro vypnutí dvoufázového ověření zadejte své heslo."
|
||||
},
|
||||
"general": {
|
||||
"name": "Jméno",
|
||||
"username": "Uživatelské jméno",
|
||||
"password": "Heslo",
|
||||
"newPassword": "Nové heslo",
|
||||
"updatePassword": "Aktualizovat heslo",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "Povolené IP adresy",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "Persistent Keepalive",
|
||||
"logout": "Odhlásit se",
|
||||
"continue": "Pokračovat",
|
||||
"host": "Hostitel",
|
||||
"port": "Port",
|
||||
"yes": "Ano",
|
||||
"no": "Ne",
|
||||
"confirmPassword": "Potvrdit heslo",
|
||||
"loading": "Načítání...",
|
||||
"2fa": "Dvoufázové ověření",
|
||||
"2faCode": "TOTP kód"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Vítejte u první instalace wg-easy",
|
||||
"welcomeDesc": "Našli jste nejjednodušší způsob, jak instalovat a spravovat WireGuard na jakémkoliv Linuxovém hostiteli",
|
||||
"existingSetup": "Máte již existující nastavení?",
|
||||
"createAdminDesc": "Nejprve zadejte uživatelské jméno administrátora a silné heslo. Tyto údaje budou použity pro přihlášení do administrace.",
|
||||
"setupConfigDesc": "Zadejte údaje o hostiteli a portu. Tyto informace budou použity pro konfiguraci klientů při nastavování WireGuard na jejich zařízeních.",
|
||||
"setupMigrationDesc": "Pokud chcete migrovat data z předchozí verze wg-easy do nové instalace, nahrajte soubor se zálohou.",
|
||||
"upload": "Nahrát",
|
||||
"migration": "Obnovit zálohu:",
|
||||
"createAccount": "Vytvořit účet",
|
||||
"successful": "Nastavení bylo úspěšné",
|
||||
"hostDesc": "Veřejný název hostitele, ke kterému se budou klienti připojovat",
|
||||
"portDesc": "Veřejný UDP port, ke kterému se budou klienti připojovat a na kterém bude WireGuard naslouchat"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "Je k dispozici aktualizace!",
|
||||
"update": "Aktualizovat"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Tmavý režim",
|
||||
"light": "Světlý režim",
|
||||
"system": "Systémové nastavení"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "Zobrazit/skrýt grafy",
|
||||
"donate": "Přispět"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Přihlásit se",
|
||||
"rememberMe": "Zapamatovat si mě",
|
||||
"rememberMeDesc": "Zůstat přihlášen i po zavření prohlížeče",
|
||||
"insecure": "Nemůžete se přihlásit přes nezabezpečené připojení. Použijte HTTPS.",
|
||||
"2faRequired": "Je vyžadováno dvoufázové ověření",
|
||||
"2faWrong": "Neplatný kód dvoufázového ověření"
|
||||
},
|
||||
"client": {
|
||||
"empty": "Zatím zde nejsou žádní klienti.",
|
||||
"newShort": "Nový",
|
||||
"sort": "Seřadit",
|
||||
"create": "Vytvořit klienta",
|
||||
"created": "Klient vytvořen",
|
||||
"new": "Nový klient",
|
||||
"name": "Jméno",
|
||||
"expireDate": "Datum vypršení",
|
||||
"expireDateDesc": "Datum, kdy bude klient deaktivován. Ponechte prázdné pro trvalý přístup.",
|
||||
"delete": "Smazat",
|
||||
"deleteClient": "Smazat klienta",
|
||||
"deleteDialog1": "Opravdu chcete smazat uživatele",
|
||||
"deleteDialog2": "Tuto akci nelze vzít zpět.",
|
||||
"enabled": "Aktivní",
|
||||
"address": "Adresa",
|
||||
"serverAllowedIps": "Povolené IP adresy serveru",
|
||||
"otlDesc": "Generovat krátký jednorázový odkaz",
|
||||
"permanent": "Trvalý",
|
||||
"createdOn": "Vytvořeno dne ",
|
||||
"lastSeen": "Naposledy viděn ",
|
||||
"totalDownload": "Celkem staženo: ",
|
||||
"totalUpload": "Celkem nahráno: ",
|
||||
"newClient": "Nový klient",
|
||||
"disableClient": "Deaktivovat klienta",
|
||||
"enableClient": "Aktivovat klienta",
|
||||
"noPrivKey": "Tento klient nemá známý soukromý klíč. Nelze vytvořit konfiguraci.",
|
||||
"showQR": "Zobrazit QR kód",
|
||||
"downloadConfig": "Stáhnout konfiguraci",
|
||||
"allowedIpsDesc": "Které IP adresy budou směrovány přes VPN (přebíjí globální nastavení)",
|
||||
"serverAllowedIpsDesc": "Které IP adresy bude server směrovat ke klientovi",
|
||||
"mtuDesc": "Nastavuje maximální velikost přenášeného paketu (MTU) pro VPN tunel",
|
||||
"persistentKeepaliveDesc": "Nastavuje interval (v sekundách) pro udržovací pakety. 0 pro vypnutí",
|
||||
"hooks": "Hooky",
|
||||
"hooksDescription": "Hooky fungují pouze s wg-quick",
|
||||
"hooksLeaveEmpty": "Pouze pro wg-quick. Jinak ponechte prázdné",
|
||||
"dnsDesc": "DNS server, který budou klienti používat (přebíjí globální nastavení)",
|
||||
"notConnected": "Klient není připojen",
|
||||
"endpoint": "Koncový bod",
|
||||
"endpointDesc": "IP adresa klienta, ze které je navázáno spojení WireGuard",
|
||||
"search": "Hledat klienty...",
|
||||
"config": "Konfigurace",
|
||||
"viewConfig": "Zobrazit konfiguraci"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "Změnit",
|
||||
"cancel": "Zrušit",
|
||||
"create": "Vytvořit"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Úspěch",
|
||||
"saved": "Uloženo",
|
||||
"error": "Chyba"
|
||||
},
|
||||
"form": {
|
||||
"actions": "Akce",
|
||||
"save": "Uložit",
|
||||
"revert": "Vrátit změny",
|
||||
"sectionGeneral": "Obecné",
|
||||
"sectionAdvanced": "Pokročilé",
|
||||
"noItems": "Žádné položky",
|
||||
"nullNoItems": "Žádné položky. Používá se globální konfigurace",
|
||||
"add": "Přidat"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "Vypršení relace",
|
||||
"sessionTimeoutDesc": "Doba trvání relace pro 'Zapamatovat si mě' (sekundy)",
|
||||
"metrics": "Metriky",
|
||||
"metricsPassword": "Heslo",
|
||||
"metricsPasswordDesc": "Bearer heslo pro koncový bod metrik (heslo nebo argon2 hash)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "Cesta pro metriky ve formátu JSON",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Cesta pro metriky Prometheus"
|
||||
},
|
||||
"config": {
|
||||
"connection": "Připojení",
|
||||
"hostDesc": "Veřejný název hostitele, ke kterému se klienti připojují (zneplatní stávající konfigurace)",
|
||||
"portDesc": "Veřejný UDP port, ke kterému se klienti připojují (zneplatní stávající konfigurace, pravděpodobně budete chtít změnit i Port rozhraní)",
|
||||
"allowedIpsDesc": "Povolené IP adresy, které budou klienti používat (globální nastavení)",
|
||||
"dnsDesc": "DNS server, který budou klienti používat (globální nastavení)",
|
||||
"mtuDesc": "MTU, které budou klienti používat (pouze pro nové klienty)",
|
||||
"persistentKeepaliveDesc": "Interval v sekundách pro odesílání udržovacích paketů na server. 0 = vypnuto (pouze pro nové klienty)",
|
||||
"suggest": "Navrhnout",
|
||||
"suggestDesc": "Vyberte IP adresu nebo název hostitele pro pole Hostitel"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR změněn",
|
||||
"device": "Zařízení",
|
||||
"deviceDesc": "Ethernetové zařízení, přes které má být provoz WireGuard přeposílán",
|
||||
"mtuDesc": "MTU, které bude WireGuard používat",
|
||||
"portDesc": "UDP port, na kterém bude WireGuard naslouchat (pravděpodobně budete chtít změnit i Konfigurační port)",
|
||||
"changeCidr": "Změnit CIDR",
|
||||
"restart": "Restartovat rozhraní",
|
||||
"restartDesc": "Restartovat rozhraní WireGuard",
|
||||
"restartWarn": "Opravdu chcete restartovat rozhraní? Dojde k odpojení všech klientů.",
|
||||
"restartSuccess": "Rozhraní bylo restartováno"
|
||||
},
|
||||
"introText": "Vítejte v administraci.\n\nZde můžete spravovat obecná nastavení, konfiguraci, nastavení rozhraní a hooky.\n\nZačněte výběrem jedné ze sekcí v bočním panelu."
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "Pole {0} je povinné",
|
||||
"validNumber": "{0} musí být platné číslo",
|
||||
"validNumberRange": "{0} musí být platné číslo nebo rozsah čísel",
|
||||
"validString": "{0} musí být platný řetězec",
|
||||
"validBoolean": "{0} musí být platná logická hodnota",
|
||||
"validArray": "{0} musí být platné pole",
|
||||
"stringMin": "{0} musí mít alespoň {1} znak(ů)",
|
||||
"numberMin": "{0} musí být alespoň {1}"
|
||||
},
|
||||
"client": {
|
||||
"id": "ID klienta",
|
||||
"name": "Jméno",
|
||||
"expiresAt": "Vyprší dne",
|
||||
"address4": "IPv4 adresa",
|
||||
"address6": "IPv6 adresa",
|
||||
"serverAllowedIps": "Povolené IP adresy serveru"
|
||||
},
|
||||
"user": {
|
||||
"username": "Uživatelské jméno",
|
||||
"password": "Heslo",
|
||||
"remember": "Pamatovat si",
|
||||
"name": "Jméno",
|
||||
"email": "E-mail",
|
||||
"emailInvalid": "E-mail musí být platná e-mailová adresa",
|
||||
"passwordMatch": "Hesla se musí shodovat",
|
||||
"totpEnable": "Zapnout TOTP",
|
||||
"totpEnableTrue": "Zapnutí TOTP musí být potvrzeno",
|
||||
"totpCode": "TOTP kód"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "Hostitel"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "Vypršení relace",
|
||||
"metricsEnabled": "Metriky",
|
||||
"metricsPassword": "Heslo k metrikám"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "Zařízení",
|
||||
"cidrValid": "CIDR musí být platný"
|
||||
},
|
||||
"otl": "Jednorázový odkaz",
|
||||
"stringMalformed": "Řetězec má nesprávný formát",
|
||||
"body": "Tělo požadavku musí být platný objekt",
|
||||
"hook": "Hook",
|
||||
"enabled": "Aktivní",
|
||||
"mtu": "MTU",
|
||||
"port": "Port",
|
||||
"persistentKeepalive": "Persistent Keepalive",
|
||||
"address": "IP adresa",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "Povolené IP adresy",
|
||||
"file": "Soubor"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "Kopírování není podporováno",
|
||||
"copied": "Zkopírováno!",
|
||||
"failed": "Kopírování selhalo",
|
||||
"copy": "Kopírovat"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "Počet junk paketů (Jc)",
|
||||
"jCDescription": "Počet odesílaných junk paketů (1-128, doporučeno: 4-12)",
|
||||
"jMinLabel": "Min. velikost junk paketu (Jmin)",
|
||||
"jMinDescription": "Minimální velikost junk paketů (0-1279*, doporučeno: 8, musí být < Jmax)",
|
||||
"jMaxLabel": "Max. velikost junk paketu (Jmax)",
|
||||
"jMaxDescription": "Maximální velikost junk paketů (1-1280*, doporučeno: 80, musí být > Jmin)",
|
||||
"s1Label": "Velikost junk dat u Init paketu (S1)",
|
||||
"s1Description": "Velikost junk dat u Init paketu (0-1132, doporučeno: 15-150, S1+56 ≠ S2)",
|
||||
"s2Label": "Velikost junk dat u Response paketu (S2)",
|
||||
"s2Description": "Velikost junk dat u Response paketu (0-1188, doporučeno: 15-150)",
|
||||
"s3Label": "Velikost junk dat u Cookie reply paketu (S3)",
|
||||
"s3Description": "Velikost junk dat u paketu s odpovědí na cookie",
|
||||
"s4Label": "Velikost junk dat u Transport paketu (S4)",
|
||||
"s4Description": "Velikost junk dat u transportního paketu",
|
||||
"h1Label": "Init magic header (H1)",
|
||||
"h1Description": "Hodnota nebo rozsah hlavičky Init paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
|
||||
"h2Label": "Response magic header (H2)",
|
||||
"h2Description": "Hodnota nebo rozsah hlavičky Response paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
|
||||
"h3Label": "Cookie reply magic header (H3)",
|
||||
"h3Description": "Hodnota nebo rozsah hlavičky Cookie reply paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
|
||||
"h4Label": "Transport magic header (H4)",
|
||||
"h4Description": "Hodnota nebo rozsah hlavičky Transport paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
|
||||
"i1Label": "Speciální junk paket 1 (I1)",
|
||||
"i1Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
|
||||
"i2Label": "Speciální junk paket 2 (I2)",
|
||||
"i2Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
|
||||
"i3Label": "Speciální junk paket 3 (I3)",
|
||||
"i3Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
|
||||
"i4Label": "Speciální junk paket 4 (I4)",
|
||||
"i4Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
|
||||
"i5Label": "Speciální junk paket 5 (I5)",
|
||||
"i5Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
|
||||
"mtuNote": "Hodnoty závisí na nastavení MTU",
|
||||
"obfuscationParameters": "AmneziaWG parametry obfuskace"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"pages": {
|
||||
"me": "Konto",
|
||||
"clients": "Clients",
|
||||
"admin": {
|
||||
"panel": "Admin-Konsole",
|
||||
"general": "Allgemein",
|
||||
"config": "Konfiguration",
|
||||
"interface": "Oberfläche",
|
||||
"hooks": "Hooks"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "E-Mail"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "Aktuelles Passwort",
|
||||
"enable2fa": "Zwei-Faktor-Authentifizierung aktivieren",
|
||||
"enable2faDesc": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App oder geben Sie den Schlüssel manuell ein.",
|
||||
"2faKey": "TOTP-Schlüssel",
|
||||
"2faCodeDesc": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein.",
|
||||
"disable2fa": "Zwei-Faktor-Authentifizierung deaktivieren",
|
||||
"disable2faDesc": "Geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren."
|
||||
},
|
||||
"general": {
|
||||
"name": "Name",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"newPassword": "Neues Passwort",
|
||||
"updatePassword": "Passwort aktualisieren",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "Erlaubte IP-Adressen",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "Dauerhaftes Keepalive",
|
||||
"logout": "Abmelden",
|
||||
"continue": "Weiter",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"confirmPassword": "Passwort bestätigen",
|
||||
"loading": "Laden...",
|
||||
"2fa": "Zwei-Faktor-Authentifizierung",
|
||||
"2faCode": "TOTP-Code"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Willkommen zur Ersteinrichtung von wg-easy",
|
||||
"welcomeDesc": "Sie haben den einfachsten Weg gefunden, WireGuard auf jedem Linux-Server zu installieren und zu verwalten.",
|
||||
"existingSetup": "Haben Sie eine bestehende Einrichtung?",
|
||||
"createAdminDesc": "Bitte geben Sie zuerst einen Admin-Benutzernamen sowie ein starkes, sicheres Passwort ein. Diese Anmeldedaten benötigen Sie, um sich in der Admin-Konsole anzumelden.",
|
||||
"setupConfigDesc": "Bitte geben Sie die Host- und Portinformationen ein. Diese werden für die Client-Konfiguration verwendet, wenn Sie WireGuard auf Ihren Geräten einrichten.",
|
||||
"setupMigrationDesc": "Bitte halten Sie die Sicherungsdatei bereit, wenn Sie Ihre Daten von Ihrer vorherigen wg-easy Version auf ihre neue Einrichtung migrieren möchten.",
|
||||
"upload": "Hochladen",
|
||||
"migration": "Backup wiederherstellen:",
|
||||
"createAccount": "Konto erstellen",
|
||||
"successful": "Einrichtung erfolgreich",
|
||||
"hostDesc": "Öffentlicher Hostname mit dem sich die Clients verbinden",
|
||||
"portDesc": "Öffentlicher UDP-Port an dem sich die Clients verbinden und auf dem WireGuard läuft"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "Ein neues Update ist verfügbar!",
|
||||
"update": "Aktualisieren"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Dunkles Thema",
|
||||
"light": "Helles Thema",
|
||||
"system": "System Thema"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "Statistiken ein-/ausblenden",
|
||||
"donate": "Spenden"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Anmelden",
|
||||
"rememberMe": "Angemeldet bleiben",
|
||||
"rememberMeDesc": "Bleiben Sie auch nach dem Schließen des Browsers angemeldet",
|
||||
"insecure": "Sie können sich nicht über eine unsichere Verbindung anmelden. Bitte benutzen Sie HTTPS.",
|
||||
"2faRequired": "Zwei-Faktor-Authentifizierung wird benötigt",
|
||||
"2faWrong": "Zwei-Faktor-Authentifizierung ist fehlgeschlagen"
|
||||
},
|
||||
"client": {
|
||||
"empty": "Es gibt noch keine Clients.",
|
||||
"newShort": "Neu",
|
||||
"sort": "Sortieren",
|
||||
"create": "Client erstellen",
|
||||
"created": "Client wurde erstellt",
|
||||
"new": "Neuer Client",
|
||||
"name": "Name",
|
||||
"expireDate": "Ablaufdatum",
|
||||
"expireDateDesc": "Datum, an dem der Client deaktiviert wird. Leer lassen für dauerhaft aktiv.",
|
||||
"delete": "Löschen",
|
||||
"deleteClient": "Client löschen",
|
||||
"deleteDialog1": "Sind Sie sicher, dass Sie diesen Client löschen möchten",
|
||||
"deleteDialog2": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"enabled": "Aktiviert",
|
||||
"address": "Adresse",
|
||||
"serverAllowedIps": "Serverseitig erlaubte IP-Adressen",
|
||||
"otlDesc": "Einen kurzen Einmal-Link erzeugen",
|
||||
"permanent": "Dauerhaft",
|
||||
"createdOn": "Angelegt am ",
|
||||
"lastSeen": "Zuletzt verbunden am ",
|
||||
"totalDownload": "Gesamt-Download: ",
|
||||
"totalUpload": "Gesamt-Upload: ",
|
||||
"newClient": "Neuer Client",
|
||||
"disableClient": "Client deaktivieren",
|
||||
"enableClient": "Client aktivieren",
|
||||
"noPrivKey": "Dieser Client hat keinen bekannten privaten Schlüssel, weshalb keine Konfiguration angelegt werden kann.",
|
||||
"showQR": "QR-Code anzeigen",
|
||||
"downloadConfig": "Konfiguration herunterladen",
|
||||
"allowedIpsDesc": "Welche IP-Adressen durch das VPN geleitet werden (überschreibt die globale Konfiguration)",
|
||||
"serverAllowedIpsDesc": "Welche IP-Adressen der Server zum Client leiten wird",
|
||||
"mtuDesc": "Setzt die maximale Übertragungsgröße (Paketgröße) für den VPN-Tunnel",
|
||||
"persistentKeepaliveDesc": "Legt das Intervall (in Sekunden) für Keepalive-Pakete fest. 0 deaktiviert es",
|
||||
"hooks": "Hooks",
|
||||
"hooksDescription": "Hooks funktionieren nur mit wg-quick",
|
||||
"hooksLeaveEmpty": "Nur für wg-quick. Andernfalls leer lassen",
|
||||
"dnsDesc": "DNS-Server, den die Clients benutzen (überschreibt die globale Konfiguration)",
|
||||
"notConnected": "Client nicht verbunden",
|
||||
"endpoint": "Endpunkt",
|
||||
"endpointDesc": "IP-Adresse des Clients, von dem aus die WireGuard-Verbindung hergestellt wird",
|
||||
"search": "Suche Clients...",
|
||||
"config": "Konfiguration",
|
||||
"viewConfig": "Konfiguration anzeigen",
|
||||
"firewallIps": "Firewall erlaubte IPs",
|
||||
"firewallIpsDesc": "Ziel-IPs/CIDRs, auf die dieser Client zugreifen darf (serverseitig erzwingen). Lassen Sie das Feld leer, um die Liste der zugelassenen IPs zu verwenden. Unterstützt optionale Port- und Protokollfilterung. Die Syntax finden Sie in der Dokumentation.",
|
||||
"downloadPng": "Herunterladen PNG",
|
||||
"copyPng": "Kopieren PNG"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "Ändern",
|
||||
"cancel": "Abbrechen",
|
||||
"create": "Erstellen"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Erfolg",
|
||||
"saved": "Gespeichert",
|
||||
"error": "Fehler",
|
||||
"unknown": "Unbekannter Fehler. Weitere Informationen finden Sie in der Konsole."
|
||||
},
|
||||
"form": {
|
||||
"actions": "Aktionen",
|
||||
"save": "Speichern",
|
||||
"revert": "Rückgängig machen",
|
||||
"sectionGeneral": "Allgemein",
|
||||
"sectionAdvanced": "Erweitert",
|
||||
"noItems": "Keine Einträge",
|
||||
"nullNoItems": "Keine Einträge. Die globale Konfiguration wird benutzt",
|
||||
"add": "Hinzufügen"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "Sitzungszeitüberschreitung",
|
||||
"sessionTimeoutDesc": "Sitzungsdauer für \"Angemeldet bleiben\" (Sekunden)",
|
||||
"metrics": "Statistiken",
|
||||
"metricsPassword": "Passwort",
|
||||
"metricsPasswordDesc": "Bearer-Passwort für den Statistik-Endpunkt (Passwort oder Argon2-Hash)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "Pfad zu den Statistiken als JSON",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Pfad zu den Prometheus-Statistiken"
|
||||
},
|
||||
"config": {
|
||||
"connection": "Verbindung",
|
||||
"hostDesc": "Öffentlicher Hostname mit dem sich die Clients verbinden (überschreibt die Konfiguration)",
|
||||
"portDesc": "Öffentlicher UDP-Port an dem sich die Clients verbinden (überschreibt die Konfiguration, vermutlich wollen Sie auch den Interface-Port ändern)",
|
||||
"allowedIpsDesc": "Erlaubte IP-Adressen, die die Clients nutzen werden (Globale Konfiguration)",
|
||||
"dnsDesc": "DNS-Server, den die Clients nutzen werden (Globale Konfiguration)",
|
||||
"mtuDesc": "MTU, den die Clients benutzen werden (nur für neue Clients)",
|
||||
"persistentKeepaliveDesc": "Intervall in Sekunden, in dem Keepalive-Pakete an den Server gesendet werden. 0 = deaktiviert (nur für neue Clients)",
|
||||
"suggest": "Vorschlagen",
|
||||
"suggestDesc": "Wählen Sie eine IP-Adresse oder einen Hostnamen für das Host-Feld aus"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR wurde geändert",
|
||||
"device": "Gerät",
|
||||
"deviceDesc": "Ethernet-Gerät, durch das der WireGuard-Datenverkehr geleitet werden soll",
|
||||
"mtuDesc": "MTU, den WireGuard benutzen wird",
|
||||
"portDesc": "UDP-Port, auf dem WireGuard lauschen wird (Sie wollen wahrscheinlich auch den Interface-Port ändern)",
|
||||
"changeCidr": "CIDR ändern",
|
||||
"restart": "Interface neu starten",
|
||||
"restartDesc": "Das WireGuard-Interface neu starten",
|
||||
"restartWarn": "Sind Sie sicher, dass Sie das Interface neu starten möchten? Dies wird die Verbindungen aller Clients trennen.",
|
||||
"restartSuccess": "Interface neu gestartet",
|
||||
"firewall": "Datenverkehr Filterung",
|
||||
"firewallEnabled": "Firewall pro Client aktivieren",
|
||||
"firewallEnabledDesc": "Beschränken Sie den Client-Datenverkehr mithilfe von iptables auf bestimmte Ziel-IP-Adressen. Bei Aktivierung kann für jeden Client eine Liste der zulässigen Ziele konfiguriert werden."
|
||||
},
|
||||
"introText": "Willkommen in der Admin-Konsole.\n\nHier können Sie die allgemeinen Einstellungen, die Konfiguration, die Schnittstelleneinstellungen und die Hooks verwalten.\n\nBeginnen Sie, indem Sie einen der Bereiche in der Seitenleiste auswählen."
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{0} ist erforderlich",
|
||||
"validNumber": "{0} muss eine Zahl sein",
|
||||
"validNumberRange": "{0} muss eine gültige Zahl oder ein gültiger Zahlenbereich sein",
|
||||
"validString": "{0} muss eine Zeichenkette sein",
|
||||
"validBoolean": "{0} muss ein Wahrheitswert sein",
|
||||
"validArray": "{0} muss eine Liste sein",
|
||||
"stringMin": "{0} muss mindestens {1} Zeichen lang sein",
|
||||
"numberMin": "{0} muss mindestens {1} sein"
|
||||
},
|
||||
"client": {
|
||||
"id": "Client-ID",
|
||||
"name": "Name",
|
||||
"expiresAt": "Läuft ab am",
|
||||
"address4": "IPv4-Adresse",
|
||||
"address6": "IPv6-Adresse",
|
||||
"serverAllowedIps": "Serverseitig erlaubte IP-Adressen",
|
||||
"firewallIps": "Von der Firewall zugelassene IP-Adressen",
|
||||
"firewallIpsInvalid": "Ungültiger IP-Eintrag in der Firewall. Informationen zur unterstützten Syntax finden Sie in der Dokumentation."
|
||||
},
|
||||
"user": {
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"remember": "Merken",
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"emailInvalid": "Die E-Mail-Adresse muss gültig sein",
|
||||
"passwordMatch": "Die Passwörter müssen übereinstimmen",
|
||||
"totpEnable": "TOTP aktivieren",
|
||||
"totpEnableTrue": "\"TOTP aktivieren\" muss ausgewählt sein",
|
||||
"totpCode": "TOTP-Code"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "Host"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "Sitzungszeitüberschreitung",
|
||||
"metricsEnabled": "Statistiken",
|
||||
"metricsPassword": "Passwort für Statistiken"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "Gerät",
|
||||
"cidrValid": "CIDR muss gültig sein"
|
||||
},
|
||||
"otl": "Einmal-Link",
|
||||
"stringMalformed": "Zeichenkette ist fehlerhaft",
|
||||
"body": "Body muss ein gültiges Objekt sein",
|
||||
"hook": "Hook",
|
||||
"enabled": "Aktiviert",
|
||||
"mtu": "MTU",
|
||||
"port": "Port",
|
||||
"persistentKeepalive": "Dauerhaftes Keepalive",
|
||||
"address": "IP-Adresse",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "Erlaubte IP-Adressen",
|
||||
"file": "Datei"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "Kopieren ist nicht unterstützt",
|
||||
"copied": "Kopiert!",
|
||||
"failed": "Kopieren fehlgeschlagen",
|
||||
"copy": "Kopieren"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "Anzahl der Junk-Pakete (Jc)",
|
||||
"jCDescription": "Anzahl der zu sendenden Junk-Pakete (1-128, empfohlen: 4-12)",
|
||||
"jMinLabel": "Minimale Junk-Paketgröße (Jmin)",
|
||||
"jMinDescription": "Mindestgröße von Junk-Paketen (0-1279*, empfohlen: 8, muss < Jmax sein)",
|
||||
"jMaxLabel": "Maximale Junk-Paketgröße (Jmax)",
|
||||
"jMaxDescription": "Maximalgröße von Junk-Paketen (1-1280*, empfohlen: 80, muss > Jmin sein)",
|
||||
"s1Label": "Junk-Paketgröße des Init-Pakets (S1)",
|
||||
"s1Description": "Junk-Paketgröße des Init-Pakets (0-1132[1280* - 148 = 1132], empfohlen: 15-150, S1+56 ≠ S2)",
|
||||
"s2Label": "Junk-Paketgröße des Antwort-Pakets (S2)",
|
||||
"s2Description": "Junk-Paketgröße des Antwort-Pakets (0-1188[1280* - 92 = 1188], empfohlen: 15-150)",
|
||||
"s3Label": "Junk-Paketgröße des Cookie-Antwort-Pakets (S3)",
|
||||
"s3Description": "Junk-Paketgröße des Cookie-Antwort-Pakets",
|
||||
"s4Label": "Junk-Paketgröße des Transport-Pakets (S4)",
|
||||
"s4Description": "Junk-Paketgröße des Transport-Pakets",
|
||||
"h1Label": "Init-Magic-Header (H1)",
|
||||
"h1Description": "Wert des Init-Paket-Headers (5-2147483647, muss eindeutig zu H2-H4 sein)",
|
||||
"h2Label": "Antwort-Magic-Header (H2)",
|
||||
"h2Description": "Wert des Antwort-Paket-Headers (5-2147483647, muss eindeutig zu H1, H3, H4 sein)",
|
||||
"h3Label": "Cookie-Antwort-Magic-Header (H3)",
|
||||
"h3Description": "Wert des Cookie-Antwort-Paket-Headers (5-2147483647, muss eindeutig zu H1, H2, H4 sein)",
|
||||
"h4Label": "Transport-Magic-Header (H4)",
|
||||
"h4Description": "Wert des Transport-Paket-Headers (5-2147483647, muss eindeutig zu H1-H3 sein)",
|
||||
"i1Label": "Spezial-Junk-Paket 1 (I1)",
|
||||
"i1Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
|
||||
"i2Label": "Spezial-Junk-Paket 2 (I2)",
|
||||
"i2Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
|
||||
"i3Label": "Spezial-Junk-Paket 3 (I3)",
|
||||
"i3Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
|
||||
"i4Label": "Spezial-Junk-Paket 4 (I4)",
|
||||
"i4Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
|
||||
"i5Label": "Spezial-Junk-Paket 5 (I5)",
|
||||
"i5Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
|
||||
"mtuNote": "Werte hängen von der MTU ab",
|
||||
"obfuscationParameters": "AmneziaWG Verschleierungsparameter"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user