Compare commits
311 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 380ff5c2f1 | |||
| ae540593fe | |||
| 42adeb391c | |||
| 02589a3ce9 | |||
| 0a6645b526 | |||
| a18a715f6f | |||
| ec202d8575 | |||
| 9dd7f256ba | |||
| 33e95bac5e | |||
| 72fe64385e | |||
| 2b7c846823 | |||
| 9275cf611a | |||
| 95934c6008 | |||
| babb9983aa | |||
| c9ff248011 | |||
| f9edec0ac1 | |||
| eb0fa90cd0 | |||
| 1607fd1562 | |||
| 5938474bf8 | |||
| ff9c1b49b6 | |||
| 72562dc660 | |||
| 4ffa6b37de | |||
| 1416613cc6 | |||
| a3c5cf359f | |||
| d8a48aef29 | |||
| 5dad038796 | |||
| 63f49a20ed | |||
| 39949d2704 | |||
| 13fcccb2f2 | |||
| 200332df4b | |||
| 3d0070f3f6 | |||
| 14fd01f4d0 | |||
| 52bcfb056a | |||
| d5b8d707ef | |||
| caad2e4162 | |||
| 3d376e542f | |||
| 74372dc05d | |||
| b46018efd8 | |||
| 2ef264d06f | |||
| d23c5f7d01 | |||
| 73f2ad4ac3 | |||
| 5a075683c4 | |||
| c28e5befa6 | |||
| 42ad29b494 | |||
| 53dad56bb6 | |||
| f5d93f6c5a | |||
| a9c798deda | |||
| 781d56d0ff | |||
| 883ca34182 | |||
| cc5d45b833 | |||
| 8bfcb5d502 | |||
| 9a19430dc8 | |||
| 62ea932d33 | |||
| be8a592072 | |||
| 9371b78a21 | |||
| 4ba638c09c | |||
| 3ad2607515 | |||
| 7f05448a5d | |||
| c5b3bcd31d | |||
| 10d24fa04b | |||
| 3dc83f9c25 | |||
| 1a54a0b016 | |||
| a0ed35fd76 | |||
| 8421e313b5 | |||
| ce20bb7fcb | |||
| e3ee09b755 | |||
| 03b7d8e537 | |||
| e7d4bbc12c | |||
| 45087a9683 | |||
| 39d32b0a1c | |||
| f47e740861 | |||
| ba85c085ab | |||
| 9aafbd73d2 | |||
| 9efac11680 | |||
| e5131fb707 | |||
| abdf96011e | |||
| 8b2706e3c2 | |||
| 6fe197f4fd | |||
| e4a7ff08c6 | |||
| 43b193b76d | |||
| 4deca34faf | |||
| 72ba79b5f2 | |||
| 94d87681c3 | |||
| e3315d92c4 | |||
| a0c495ddad | |||
| c73c6c7291 | |||
| 04d1ca18b7 | |||
| 438fc7ccf9 | |||
| a2c758dbcb | |||
| 6a588ee3fa | |||
| 378464a424 | |||
| 7ecf7b08b1 | |||
| cea9e9302d | |||
| 11f5122c39 | |||
| ee117ddb91 | |||
| d3e8e627e9 | |||
| 864bb00d0b | |||
| 7608f91913 | |||
| 4d849fc508 | |||
| 436ccac824 | |||
| b4ca454ec5 | |||
| 8044c53815 | |||
| e7e374cfd7 | |||
| 9afd549273 | |||
| 6b67f20b6a | |||
| d31524a531 | |||
| 34c28805c4 | |||
| 9b6ac7cd8d | |||
| 3613d26d4a | |||
| ccacc5ea87 | |||
| bfd7ef9e46 | |||
| 2f956248db | |||
| e0e2a6deba | |||
| 304506d26d | |||
| e0775c0d2e | |||
| 8598a167ef | |||
| 6659785514 | |||
| 34ae8e42f3 | |||
| eaa4b1ebaa | |||
| 72fbf1baf6 | |||
| 2a102eea93 | |||
| 85913b71ed | |||
| 7d0e2729b6 | |||
| b5372f0dbc | |||
| 390b72c94a | |||
| de22768079 | |||
| 00acd1a07e | |||
| a082a40bf6 | |||
| 3d1e42c722 | |||
| e6d2d95340 | |||
| 1370141f03 | |||
| ac552e0384 | |||
| ca737f9452 | |||
| b60f0e9668 | |||
| 0e1ad23f17 | |||
| 3638e81718 | |||
| 44417d3db6 | |||
| e5e63c43e6 | |||
| f06b7e00aa | |||
| 70d59d0fdd | |||
| 332039de56 | |||
| 13616a2f1e | |||
| 5e015bfdb5 | |||
| d2d15fca2a | |||
| c26b536b65 | |||
| 859dd2f25b | |||
| e80ff54ebc | |||
| 4bfef3c0c0 | |||
| 93d9f0b6fe | |||
| 54236eb8b4 | |||
| 8249b92a34 | |||
| 4cd5d5459a | |||
| 678cf5bffb | |||
| ad80017846 | |||
| 975c61df6d | |||
| 69726cba75 | |||
| 211e0b6aa2 | |||
| 8df1b6ff54 | |||
| cf94b98482 | |||
| 3844d04569 | |||
| 68c6f6252e | |||
| 519f4efa20 | |||
| c6dd456a07 | |||
| a43d2201fc | |||
| 86146ccc68 | |||
| 191dd74b0c | |||
| e2eb7bc362 | |||
| b60461e917 | |||
| fb628bcb89 | |||
| 195e307ff5 | |||
| e46efd6088 | |||
| 66bb13ed30 | |||
| 34fdc313ae | |||
| 9b5b8c77c3 | |||
| 7d05a82dae | |||
| a25d28755c | |||
| cada04ad0e | |||
| 6b2f57f2f1 | |||
| 488e3c32b3 | |||
| 4cc07c5312 | |||
| c5d328be2a | |||
| 155541fbdb | |||
| 3f763d2607 | |||
| f9656d0bfe | |||
| 83408f6a9b | |||
| 4911082a34 | |||
| a3a69654fc | |||
| 4f8ee27d77 | |||
| 1e2da39a87 | |||
| e153998d29 | |||
| 6b284cb27c | |||
| e6db5e8b7f | |||
| 6e7f2f730e | |||
| c8224f34f9 | |||
| 3032163814 | |||
| 2eec97ff25 | |||
| ae60493414 | |||
| e43688a091 | |||
| c29ba35d41 | |||
| a05b71c652 | |||
| e6b5f2e33c | |||
| 187888e078 | |||
| f9daa3e5be | |||
| c2482f494a | |||
| ed369ff199 | |||
| 2a3acdcad5 | |||
| 9be036aefa | |||
| 9507454d3f | |||
| 9e925c2ebb | |||
| 31c34367b2 | |||
| 990a7ae548 | |||
| 6dc75b7f1e | |||
| 4868f32f1e | |||
| 61b57a885c | |||
| 074b3548d2 | |||
| 2dafeae54d | |||
| 2d7460f35a | |||
| 4981b72d00 | |||
| 33634211a9 | |||
| 6045c2ada5 | |||
| bf214fb4d3 | |||
| 9332e8b663 | |||
| dbbfdd5357 | |||
| cb63d5c67f | |||
| 3fce6e8d1d | |||
| 2f9364aa31 | |||
| 479c51d741 | |||
| 196cb63c6e | |||
| d024018bba | |||
| 5cdacd6cc3 | |||
| fe7d77e481 | |||
| 6c0049770e | |||
| f134a3671a | |||
| 8d00c5456a | |||
| c2829d79e0 | |||
| ac47789561 | |||
| 5afb701013 | |||
| 81cae5e231 | |||
| 6c567d0082 | |||
| 62703ffbd6 | |||
| 19589e7ee7 | |||
| d40536c3fb | |||
| f979d23704 | |||
| 99c8081fe9 | |||
| 1c98e466c6 | |||
| 953a67bbdd | |||
| 5fbfb26937 | |||
| 74f3e2f320 | |||
| c107920df2 | |||
| e666a14612 | |||
| a7ecb2a067 | |||
| 88b1b20e48 | |||
| ac6a05f9be | |||
| 262318df27 | |||
| 98a5daf458 | |||
| 71c208133d | |||
| aedb691b2b | |||
| ed0e46788a | |||
| 90bcdd1259 | |||
| 81ccf8762d | |||
| 90431ff9c5 | |||
| 32fc78589a | |||
| f3a8ff6490 | |||
| f7bd362538 | |||
| 44cc5683d4 | |||
| 4d5a5c9e0d | |||
| 12b72cf389 | |||
| ccde2fdfd3 | |||
| 166a58a685 | |||
| 73242c61c3 | |||
| 3d11730926 | |||
| 8ce2d44c08 | |||
| 3cb83dc53a | |||
| e2242daef1 | |||
| ee5a2c6c5a | |||
| 68187e07a1 | |||
| 7596385d5a | |||
| 9a35b56f5c | |||
| 10f246ea59 | |||
| 3f2495a0ea | |||
| b7c2c81cc7 | |||
| b3306bee48 | |||
| 064e264ce8 | |||
| 08c412fd14 | |||
| 8c771b054c | |||
| e9480a5ed5 | |||
| 1a3cff855e | |||
| f0ec8e45a0 | |||
| d91f08eb1b | |||
| 17fdb3396f | |||
| 2f5878d406 | |||
| 4dc439f041 | |||
| f69aa9dc65 | |||
| 11a56ffdc5 | |||
| bad7bff98e | |||
| a4c3bf0291 | |||
| 2ffb68eeb2 | |||
| a36ab8891e | |||
| 5ee284b973 | |||
| 754b5f29af | |||
| 84b2b61d63 | |||
| 63faf4c507 | |||
| 76a3d7f81d | |||
| bbc919608c | |||
| 2f89765112 | |||
| cb45bc1c43 | |||
| c4d4da38e7 | |||
| 89415a2258 | |||
| 577af9947d | |||
| 7efdbf38e4 | |||
| e8a160b14f |
@@ -0,0 +1 @@
|
||||
/src/node_modules
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Emile Nijssen (WeeJeWel)
|
||||
# Founder and Codeowner of WireGuard Easy (wg-easy)
|
||||
# Maintained by Bernd Storath (kaaax0815)
|
||||
* @WeeJeWel
|
||||
* @kaaax0815
|
||||
@@ -24,13 +24,13 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- OS: [e.g. macOS 12.1]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- OS: [e.g. iOS 8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Motivation and Context
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
## How has this been tested?
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, tests ran to see how -->
|
||||
<!--- your change affects other areas of the code, etc. -->
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
|
||||
## Types of changes
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
|
||||
## Checklist:
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
@@ -1,41 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: "15 0 * * *"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wg-easy'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript-typescript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Build & Publish Nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wg-easy'
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: production
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
tags: ghcr.io/wg-easy/wg-easy:nightly
|
||||
@@ -1,7 +1,6 @@
|
||||
name: Build & Publish Development
|
||||
name: Build Pull Request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -14,8 +13,6 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: production
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -30,9 +27,9 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
tags: ghcr.io/wg-easy/wg-easy:development
|
||||
tags: ghcr.io/wg-easy/wg-easy:pr
|
||||
@@ -1,23 +1,24 @@
|
||||
name: Build & Publish Latest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
- v14
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wg-easy'
|
||||
if: |
|
||||
github.repository_owner == 'wg-easy' &&
|
||||
!contains(github.event.head_commit.message, '!skipci')
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: production
|
||||
ref: v14
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -32,12 +33,9 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set environment variables
|
||||
run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV
|
||||
|
||||
- name: Build & Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
tags: ghcr.io/wg-easy/wg-easy:latest, ghcr.io/wg-easy/wg-easy:${{ env.RELEASE }}
|
||||
tags: ghcr.io/wg-easy/wg-easy:latest, ghcr.io/wg-easy/wg-easy:14
|
||||
|
||||
@@ -3,8 +3,7 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- production
|
||||
- v14
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -18,12 +17,9 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
src/package-lock.json
|
||||
|
||||
- name: npm run lint
|
||||
run: |
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
name: NPM Update Bot 🤖
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
|
||||
jobs:
|
||||
npmupbot:
|
||||
name: NPM Update Bot 🤖
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wg-easy'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: wg-easy/wg-easy
|
||||
ref: master
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
src/package-lock.json
|
||||
|
||||
- name: Bot 🤖 "Updating NPM Packages..."
|
||||
run: |
|
||||
npm install -g --silent npm-check-updates
|
||||
ncu -u
|
||||
npm update
|
||||
cd src
|
||||
ncu -u
|
||||
npm update
|
||||
npm run buildcss
|
||||
git config --global user.name 'NPM Update Bot'
|
||||
git config --global user.email 'npmupbot@users.noreply.github.com'
|
||||
git add .
|
||||
git commit -am "npm: package updates" || true
|
||||
git push
|
||||
@@ -1,35 +0,0 @@
|
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '*/5 * * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'wg-easy'
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
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."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 14
|
||||
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity."
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
/config
|
||||
/wg0.conf
|
||||
/wg0.json
|
||||
/src/node_modules
|
||||
.DS_Store
|
||||
*.swp
|
||||
|
||||
+13
-18
@@ -1,17 +1,20 @@
|
||||
# There's an issue with node:20-alpine.
|
||||
# Docker deployment is canceled after 25< minutes.
|
||||
# As a workaround we have to build on nodejs 18
|
||||
# nodejs 20 hangs on build with armv6/armv7
|
||||
FROM docker.io/library/node:lts-alpine AS build_node_modules
|
||||
|
||||
FROM docker.io/library/node:18-alpine AS build_node_modules
|
||||
# Update npm to latest
|
||||
RUN npm install -g npm@latest
|
||||
|
||||
# Copy Web UI
|
||||
COPY src/ /app/
|
||||
COPY src /app
|
||||
WORKDIR /app
|
||||
RUN npm ci --omit=dev &&\
|
||||
mv node_modules /node_modules
|
||||
|
||||
# Copy build result to a new image.
|
||||
# This saves a lot of disk space.
|
||||
FROM docker.io/library/node:18-alpine
|
||||
FROM docker.io/library/node:lts-alpine
|
||||
HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3
|
||||
COPY --from=build_node_modules /app /app
|
||||
|
||||
# Move node_modules one directory up, so during development
|
||||
@@ -23,13 +26,9 @@ COPY --from=build_node_modules /app /app
|
||||
# than what runs inside of docker.
|
||||
COPY --from=build_node_modules /node_modules /node_modules
|
||||
|
||||
RUN \
|
||||
# Enable this to run `npm run serve`
|
||||
npm i -g nodemon &&\
|
||||
# Workaround CVE-2023-42282
|
||||
npm uninstall -g ip &&\
|
||||
# Delete unnecessary files
|
||||
npm cache clean --force && rm -rf ~/.npm
|
||||
# Copy the needed wg-password scripts
|
||||
COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw
|
||||
RUN chmod +x /bin/wgpw
|
||||
|
||||
# Install Linux packages
|
||||
RUN apk add --no-cache \
|
||||
@@ -40,15 +39,11 @@ RUN apk add --no-cache \
|
||||
wireguard-tools
|
||||
|
||||
# Use iptables-legacy
|
||||
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save
|
||||
|
||||
# Expose Ports
|
||||
EXPOSE 51820/udp
|
||||
EXPOSE 51821/tcp
|
||||
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
|
||||
|
||||
# Set Environment
|
||||
ENV DEBUG=Server,WireGuard
|
||||
|
||||
# Run Web UI
|
||||
WORKDIR /app
|
||||
CMD ["/usr/bin/dumb-init", "node", "server.js"]
|
||||
CMD ["/usr/bin/dumb-init", "node", "server.js"]
|
||||
@@ -0,0 +1,45 @@
|
||||
# Generating bcrypt-hashed password
|
||||
|
||||
With version 14 of wg-easy, a password hashed with bcrypt is needed instead of the plain-text password string. This doc explains how to generate the hash based on a plain-text password.
|
||||
|
||||
## Using Docker + node
|
||||
|
||||
- You are using docker compose
|
||||
|
||||
The easiest way to generate a bcrypt password hash with wgpw is using docker and node:
|
||||
|
||||
```sh
|
||||
docker run ghcr.io/wg-easy/wg-easy:14 node -e 'const bcrypt = require("bcryptjs"); const hash = bcrypt.hashSync("YOUR_PASSWORD", 10); console.log(hash.replace(/\$/g, "$$$$"));'
|
||||
```
|
||||
|
||||
The hashed password will get printed on your terminal. Copy it and use on the `PASSWORD_HASH` environment variable in your docker compose.
|
||||
|
||||
- You are using `docker run`
|
||||
|
||||
If you are using `docker run` for running wg-easy, you must enclose the hash string in single quotes (`'...'`). You can use this command:
|
||||
|
||||
```sh
|
||||
docker run --rm ghcr.io/wg-easy/wg-easy:14 node -e "const bcrypt = require('bcryptjs'); const hash = bcrypt.hashSync('YOUR_PASSWORD', 10); console.log('\'' + hash + '\'');"
|
||||
```
|
||||
|
||||
The hashed password will get printed on your terminal. Copy it and use on the `PASSWORD_HASH` environment variable in your docker run command.
|
||||
|
||||
## Using Docker + wgpw
|
||||
|
||||
`wg-password` (wgpw) is a script that generates bcrypt password hashes. You can use it with docker:
|
||||
|
||||
```sh
|
||||
docker run ghcr.io/wg-easy/wg-easy:14 wgpw YOUR_PASSWORD
|
||||
```
|
||||
|
||||
You will see an output similar to this:
|
||||
|
||||
```sh
|
||||
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW'
|
||||
```
|
||||
|
||||
In this example, the `$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW` string is your hashed password. For using it with docker-compose, you need to escape each `$` characters by adding another `$` before them, or they will get interpreted as variables. The final password you can use in docker-compose will look like this:
|
||||
|
||||
```sh
|
||||
$$2b$$12$$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW
|
||||
```
|
||||
@@ -30,6 +30,11 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||
* A host with a kernel that supports WireGuard (all modern kernels).
|
||||
* A host with Docker installed.
|
||||
|
||||
## Versions
|
||||
|
||||
This branch is only for the v14 release of WireGuard Easy.
|
||||
For newer versions, please refer to the [master branch](https://github.com/wg-easy/wg-easy/tree/master).
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Docker
|
||||
@@ -37,9 +42,9 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||
If you haven't installed Docker yet, install it by running:
|
||||
|
||||
```bash
|
||||
$ curl -sSL https://get.docker.com | sh
|
||||
$ sudo usermod -aG docker $(whoami)
|
||||
$ exit
|
||||
curl -sSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker $(whoami)
|
||||
exit
|
||||
```
|
||||
|
||||
And log in again.
|
||||
@@ -48,12 +53,14 @@ And log in again.
|
||||
|
||||
To automatically install & run wg-easy, simply run:
|
||||
|
||||
<pre>
|
||||
$ docker run -d \
|
||||
```
|
||||
docker run -d \
|
||||
--name=wg-easy \
|
||||
-e LANG=de \
|
||||
-e WG_HOST=<b>🚨YOUR_SERVER_IP</b> \
|
||||
-e PASSWORD=<b>🚨YOUR_ADMIN_PASSWORD</b> \
|
||||
-e WG_HOST=<🚨YOUR_SERVER_IP> \
|
||||
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
|
||||
-e PORT=51821 \
|
||||
-e WG_PORT=51820 \
|
||||
-v ~/.wg-easy:/etc/wireguard \
|
||||
-p 51820:51820/udp \
|
||||
-p 51821:51821/tcp \
|
||||
@@ -62,17 +69,21 @@ $ docker run -d \
|
||||
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
|
||||
--sysctl="net.ipv4.ip_forward=1" \
|
||||
--restart unless-stopped \
|
||||
ghcr.io/wg-easy/wg-easy
|
||||
</pre>
|
||||
ghcr.io/wg-easy/wg-easy:14
|
||||
```
|
||||
|
||||
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
|
||||
>
|
||||
> 💡 Replace `YOUR_ADMIN_PASSWORD` with a password to log in on the Web UI.
|
||||
> 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI. See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash.
|
||||
|
||||
The Web UI will now be available on `http://0.0.0.0:51821`.
|
||||
|
||||
> 💡 Your configuration files will be saved in `~/.wg-easy`
|
||||
|
||||
WireGuard Easy can be launched with Docker Compose as well - just download
|
||||
[`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
|
||||
execute `docker compose up --detach`.
|
||||
|
||||
### 3. Sponsor
|
||||
|
||||
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
||||
@@ -81,25 +92,27 @@ Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/W
|
||||
|
||||
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
|
||||
|
||||
| Env | Default | Example | Description |
|
||||
| - | - | - | - |
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `PASSWORD` | - | `foobar123` | When set, requires a password when logging in to the Web UI. |
|
||||
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
|
||||
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
|
||||
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will always listen on 51820 inside the Docker container. |
|
||||
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
|
||||
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
|
||||
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
|
||||
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
|
||||
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
|
||||
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
|
||||
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
|
||||
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
|
||||
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
|
||||
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th). |
|
||||
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
||||
| Env | Default | Example | Description |
|
||||
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
|
||||
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
|
||||
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
|
||||
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
|
||||
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
|
||||
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
|
||||
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
|
||||
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
|
||||
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
|
||||
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
|
||||
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
|
||||
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
|
||||
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
|
||||
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
|
||||
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
|
||||
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
||||
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
|
||||
|
||||
> If you change `WG_PORT`, make sure to also change the exposed port.
|
||||
|
||||
@@ -110,14 +123,15 @@ To update to the latest version, simply run:
|
||||
```bash
|
||||
docker stop wg-easy
|
||||
docker rm wg-easy
|
||||
docker pull ghcr.io/wg-easy/wg-easy
|
||||
docker pull ghcr.io/wg-easy/wg-easy:14
|
||||
```
|
||||
|
||||
And then run the `docker run -d \ ...` command above again.
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
* [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole)
|
||||
* [Using WireGuard-Easy with nginx/SSL](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-nginx-SSL)
|
||||
|
||||
For less common or specific edge-case scenarios, please refer to the detailed information provided in the [Wiki](https://github.com/wg-easy/wg-easy/wiki).
|
||||
With Docker Compose WireGuard Easy can be updated with a single command:
|
||||
`docker compose up --detach --pull always` (if an image tag is specified in the
|
||||
Compose file and it is not `latest`, make sure that it is changed to the desired
|
||||
one; by default it is omitted and
|
||||
[defaults to `latest`](https://docs.docker.com/engine/reference/run/#image-references)). \
|
||||
The WireGuard Easy container will be automatically recreated if a newer image
|
||||
was pulled.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
+10
-3
@@ -1,10 +1,17 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
wg-easy:
|
||||
image: wg-easy
|
||||
build:
|
||||
dockerfile: ./Dockerfile
|
||||
command: npm run serve
|
||||
volumes:
|
||||
- ./src/:/app/
|
||||
# - ./data/:/etc/wireguard
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
- "51821:51821/tcp"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
environment:
|
||||
# - PASSWORD=p
|
||||
# - PASSWORD_HASH=p
|
||||
- WG_HOST=192.168.1.233
|
||||
|
||||
+8
-5
@@ -1,4 +1,3 @@
|
||||
version: "3.8"
|
||||
volumes:
|
||||
etc_wireguard:
|
||||
|
||||
@@ -6,15 +5,17 @@ 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)
|
||||
# (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
|
||||
|
||||
# Optional:
|
||||
# - PASSWORD=foobar123
|
||||
# - 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
|
||||
@@ -24,9 +25,10 @@ services:
|
||||
# - 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_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
|
||||
image: ghcr.io/wg-easy/wg-easy:14
|
||||
container_name: wg-easy
|
||||
volumes:
|
||||
- etc_wireguard:/etc/wireguard
|
||||
@@ -37,6 +39,7 @@ services:
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
# - NET_RAW # ⚠️ Uncomment if using Podman
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"1": "Initial version. Enjoy!",
|
||||
"2": "You can now rename a client, and update the address. Enjoy!",
|
||||
"2": "You can now rename a client & update the address. Enjoy!",
|
||||
"3": "Many improvements and small changes. Enjoy!",
|
||||
"4": "Now with pretty charts for client's network speed. Enjoy!",
|
||||
"5": "Many small improvements & feature requests. Enjoy!",
|
||||
@@ -9,6 +9,8 @@
|
||||
"8": "Updated to Node.js v18.",
|
||||
"9": "Fixed issue running on devices with older kernels.",
|
||||
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
||||
"11": "Multilanguage Support & various bugfixes",
|
||||
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more."
|
||||
"11": "Multilanguage Support & various bugfixes.",
|
||||
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more.",
|
||||
"13": "New framework (h3), UI_CHART_TYPE, some bugfixes & more.",
|
||||
"14": "Home Assistent support, PASSWORD_HASH (inc. Helper), translation updates bugfixes & more."
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .",
|
||||
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
|
||||
"serve": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||
"sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy",
|
||||
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/node_modules
|
||||
+8
-4
@@ -1,15 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
const { release } = require('./package.json');
|
||||
const { release: { version } } = require('./package.json');
|
||||
|
||||
module.exports.RELEASE = release;
|
||||
module.exports.RELEASE = version;
|
||||
module.exports.PORT = process.env.PORT || '51821';
|
||||
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
|
||||
/** This is only kept for migration purpose. DO NOT USE! */
|
||||
module.exports.PASSWORD = process.env.PASSWORD;
|
||||
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
|
||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
||||
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
|
||||
module.exports.WG_HOST = process.env.WG_HOST;
|
||||
module.exports.WG_PORT = process.env.WG_PORT || '51820';
|
||||
module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820';
|
||||
module.exports.WG_MTU = process.env.WG_MTU || null;
|
||||
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0';
|
||||
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x';
|
||||
@@ -21,7 +24,7 @@ module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0';
|
||||
module.exports.WG_PRE_UP = process.env.WG_PRE_UP || '';
|
||||
module.exports.WG_POST_UP = process.env.WG_POST_UP || `
|
||||
iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE;
|
||||
iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT;
|
||||
iptables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT;
|
||||
iptables -A FORWARD -i wg0 -j ACCEPT;
|
||||
iptables -A FORWARD -o wg0 -j ACCEPT;
|
||||
`.split('\n').join(' ');
|
||||
@@ -29,9 +32,10 @@ iptables -A FORWARD -o wg0 -j ACCEPT;
|
||||
module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || '';
|
||||
module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || `
|
||||
iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE;
|
||||
iptables -D INPUT -p udp -m udp --dport 51820 -j ACCEPT;
|
||||
iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT;
|
||||
iptables -D FORWARD -i wg0 -j ACCEPT;
|
||||
iptables -D FORWARD -o wg0 -j ACCEPT;
|
||||
`.split('\n').join(' ');
|
||||
module.exports.LANG = process.env.LANG || 'en';
|
||||
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
||||
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
|
||||
|
||||
+225
-86
@@ -1,15 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('node:crypto');
|
||||
const { createServer } = require('node:http');
|
||||
const { stat, readFile } = require('node:fs/promises');
|
||||
const { resolve, sep } = require('node:path');
|
||||
|
||||
const express = require('express');
|
||||
const expressSession = require('express-session');
|
||||
const debug = require('debug')('Server');
|
||||
|
||||
const Util = require('./Util');
|
||||
const ServerError = require('./ServerError');
|
||||
const {
|
||||
createApp,
|
||||
createError,
|
||||
createRouter,
|
||||
defineEventHandler,
|
||||
fromNodeMiddleware,
|
||||
getRouterParam,
|
||||
toNodeListener,
|
||||
readBody,
|
||||
setHeader,
|
||||
serveStatic,
|
||||
} = require('h3');
|
||||
|
||||
const WireGuard = require('../services/WireGuard');
|
||||
|
||||
const {
|
||||
@@ -17,43 +29,74 @@ const {
|
||||
WEBUI_HOST,
|
||||
RELEASE,
|
||||
PASSWORD,
|
||||
PASSWORD_HASH,
|
||||
LANG,
|
||||
UI_TRAFFIC_STATS,
|
||||
UI_CHART_TYPE,
|
||||
} = require('../config');
|
||||
|
||||
const requiresPassword = !!PASSWORD_HASH;
|
||||
|
||||
/**
|
||||
* Checks if `password` matches the PASSWORD_HASH.
|
||||
*
|
||||
* If environment variable is not set, the password is always invalid.
|
||||
*
|
||||
* @param {string} password String to test
|
||||
* @returns {boolean} true if matching environment, otherwise false
|
||||
*/
|
||||
const isPasswordValid = (password) => {
|
||||
if (typeof password !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PASSWORD_HASH) {
|
||||
return bcrypt.compareSync(password, PASSWORD_HASH);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = class Server {
|
||||
|
||||
constructor() {
|
||||
// Express
|
||||
this.app = express()
|
||||
.disable('etag')
|
||||
.use('/', express.static(path.join(__dirname, '..', 'www')))
|
||||
.use(express.json())
|
||||
.use(expressSession({
|
||||
secret: crypto.randomBytes(256).toString('hex'),
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
},
|
||||
const app = createApp();
|
||||
this.app = app;
|
||||
|
||||
app.use(fromNodeMiddleware(expressSession({
|
||||
secret: crypto.randomBytes(256).toString('hex'),
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
})));
|
||||
|
||||
const router = createRouter();
|
||||
app.use(router);
|
||||
|
||||
router
|
||||
.get('/api/release', defineEventHandler((event) => {
|
||||
setHeader(event, 'Content-Type', 'application/json');
|
||||
return RELEASE;
|
||||
}))
|
||||
|
||||
.get('/api/release', (Util.promisify(async () => {
|
||||
return RELEASE;
|
||||
})))
|
||||
.get('/api/lang', defineEventHandler((event) => {
|
||||
setHeader(event, 'Content-Type', 'application/json');
|
||||
return `"${LANG}"`;
|
||||
}))
|
||||
|
||||
.get('/api/lang', (Util.promisify(async () => {
|
||||
return LANG;
|
||||
})))
|
||||
.get('/api/ui-traffic-stats', (Util.promisify(async () => {
|
||||
return UI_TRAFFIC_STATS === 'true';
|
||||
})))
|
||||
.get('/api/ui-traffic-stats', defineEventHandler((event) => {
|
||||
setHeader(event, 'Content-Type', 'application/json');
|
||||
return `"${UI_TRAFFIC_STATS}"`;
|
||||
}))
|
||||
|
||||
// Authentication
|
||||
.get('/api/session', Util.promisify(async (req) => {
|
||||
const requiresPassword = !!process.env.PASSWORD;
|
||||
.get('/api/ui-chart-type', defineEventHandler((event) => {
|
||||
setHeader(event, 'Content-Type', 'application/json');
|
||||
return `"${UI_CHART_TYPE}"`;
|
||||
}))
|
||||
|
||||
// Authentication
|
||||
.get('/api/session', defineEventHandler((event) => {
|
||||
const authenticated = requiresPassword
|
||||
? !!(req.session && req.session.authenticated)
|
||||
? !!(event.node.req.session && event.node.req.session.authenticated)
|
||||
: true;
|
||||
|
||||
return {
|
||||
@@ -61,28 +104,37 @@ module.exports = class Server {
|
||||
authenticated,
|
||||
};
|
||||
}))
|
||||
.post('/api/session', Util.promisify(async (req) => {
|
||||
const {
|
||||
password,
|
||||
} = req.body;
|
||||
.post('/api/session', defineEventHandler(async (event) => {
|
||||
const { password } = await readBody(event);
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
throw new ServerError('Missing: Password', 401);
|
||||
if (!requiresPassword) {
|
||||
// if no password is required, the API should never be called.
|
||||
// Do not automatically authenticate the user.
|
||||
throw createError({
|
||||
status: 401,
|
||||
message: 'Invalid state',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== PASSWORD) {
|
||||
throw new ServerError('Incorrect Password', 401);
|
||||
if (!isPasswordValid(password)) {
|
||||
throw createError({
|
||||
status: 401,
|
||||
message: 'Incorrect Password',
|
||||
});
|
||||
}
|
||||
|
||||
req.session.authenticated = true;
|
||||
req.session.save();
|
||||
event.node.req.session.authenticated = true;
|
||||
event.node.req.session.save();
|
||||
|
||||
debug(`New Session: ${req.session.id}`);
|
||||
}))
|
||||
debug(`New Session: ${event.node.req.session.id}`);
|
||||
|
||||
return { success: true };
|
||||
}));
|
||||
|
||||
// WireGuard
|
||||
.use((req, res, next) => {
|
||||
if (!PASSWORD) {
|
||||
app.use(
|
||||
fromNodeMiddleware((req, res, next) => {
|
||||
if (!requiresPassword || !req.url.startsWith('/api/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -90,8 +142,8 @@ module.exports = class Server {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.path.startsWith('/api/') && req.headers['authorization']) {
|
||||
if (bcrypt.compareSync(req.headers['authorization'], bcrypt.hashSync(PASSWORD, 10))) {
|
||||
if (req.url.startsWith('/api/') && req.headers['authorization']) {
|
||||
if (isPasswordValid(req.headers['authorization'])) {
|
||||
return next();
|
||||
}
|
||||
return res.status(401).json({
|
||||
@@ -102,25 +154,32 @@ module.exports = class Server {
|
||||
return res.status(401).json({
|
||||
error: 'Not Logged In',
|
||||
});
|
||||
})
|
||||
.delete('/api/session', Util.promisify(async (req) => {
|
||||
const sessionId = req.session.id;
|
||||
}),
|
||||
);
|
||||
|
||||
req.session.destroy();
|
||||
const router2 = createRouter();
|
||||
app.use(router2);
|
||||
|
||||
router2
|
||||
.delete('/api/session', defineEventHandler((event) => {
|
||||
const sessionId = event.node.req.session.id;
|
||||
|
||||
event.node.req.session.destroy();
|
||||
|
||||
debug(`Deleted Session: ${sessionId}`);
|
||||
return { success: true };
|
||||
}))
|
||||
.get('/api/wireguard/client', Util.promisify(async (req) => {
|
||||
.get('/api/wireguard/client', defineEventHandler(() => {
|
||||
return WireGuard.getClients();
|
||||
}))
|
||||
.get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
.get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
||||
res.header('Content-Type', 'image/svg+xml');
|
||||
res.send(svg);
|
||||
setHeader(event, 'Content-Type', 'image/svg+xml');
|
||||
return svg;
|
||||
}))
|
||||
.get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
.get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
const client = await WireGuard.getClient({ clientId });
|
||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||
const configName = client.name
|
||||
@@ -128,52 +187,132 @@ module.exports = class Server {
|
||||
.replace(/(-{2,}|-$)/g, '-')
|
||||
.replace(/-$/, '')
|
||||
.substring(0, 32);
|
||||
res.header('Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
|
||||
res.header('Content-Type', 'text/plain');
|
||||
res.send(config);
|
||||
setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
|
||||
setHeader(event, 'Content-Type', 'text/plain');
|
||||
return config;
|
||||
}))
|
||||
.post('/api/wireguard/client', Util.promisify(async (req) => {
|
||||
const { name } = req.body;
|
||||
return WireGuard.createClient({ name });
|
||||
.post('/api/wireguard/client', defineEventHandler(async (event) => {
|
||||
const { name } = await readBody(event);
|
||||
await WireGuard.createClient({ name });
|
||||
return { success: true };
|
||||
}))
|
||||
.delete('/api/wireguard/client/:clientId', Util.promisify(async (req) => {
|
||||
const { clientId } = req.params;
|
||||
return WireGuard.deleteClient({ clientId });
|
||||
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
await WireGuard.deleteClient({ clientId });
|
||||
return { success: true };
|
||||
}))
|
||||
.post('/api/wireguard/client/:clientId/enable', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
.post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||
res.end(403);
|
||||
throw createError({ status: 403 });
|
||||
}
|
||||
return WireGuard.enableClient({ clientId });
|
||||
await WireGuard.enableClient({ clientId });
|
||||
return { success: true };
|
||||
}))
|
||||
.post('/api/wireguard/client/:clientId/disable', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||
res.end(403);
|
||||
throw createError({ status: 403 });
|
||||
}
|
||||
return WireGuard.disableClient({ clientId });
|
||||
await WireGuard.disableClient({ clientId });
|
||||
return { success: true };
|
||||
}))
|
||||
.put('/api/wireguard/client/:clientId/name', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
.put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||
res.end(403);
|
||||
throw createError({ status: 403 });
|
||||
}
|
||||
const { name } = req.body;
|
||||
return WireGuard.updateClientName({ clientId, name });
|
||||
const { name } = await readBody(event);
|
||||
await WireGuard.updateClientName({ clientId, name });
|
||||
return { success: true };
|
||||
}))
|
||||
.put('/api/wireguard/client/:clientId/address', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
.put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => {
|
||||
const clientId = getRouterParam(event, 'clientId');
|
||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||
res.end(403);
|
||||
throw createError({ status: 403 });
|
||||
}
|
||||
const { address } = req.body;
|
||||
return WireGuard.updateClientAddress({ clientId, address });
|
||||
}))
|
||||
const { address } = await readBody(event);
|
||||
await WireGuard.updateClientAddress({ clientId, address });
|
||||
return { success: true };
|
||||
}));
|
||||
|
||||
.listen(PORT, WEBUI_HOST, () => {
|
||||
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
|
||||
const safePathJoin = (base, target) => {
|
||||
// Manage web root (edge case)
|
||||
if (target === '/') {
|
||||
return `${base}${sep}`;
|
||||
}
|
||||
|
||||
// Prepend './' to prevent absolute paths
|
||||
const targetPath = `.${sep}${target}`;
|
||||
|
||||
// Resolve the absolute path
|
||||
const resolvedPath = resolve(base, targetPath);
|
||||
|
||||
// Check if resolvedPath is a subpath of base
|
||||
if (resolvedPath.startsWith(`${base}${sep}`)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
throw createError({
|
||||
status: 400,
|
||||
message: 'Bad Request',
|
||||
});
|
||||
};
|
||||
|
||||
// backup_restore
|
||||
const router3 = createRouter();
|
||||
app.use(router3);
|
||||
|
||||
router3
|
||||
.get('/api/wireguard/backup', defineEventHandler(async (event) => {
|
||||
const config = await WireGuard.backupConfiguration();
|
||||
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
|
||||
setHeader(event, 'Content-Type', 'text/json');
|
||||
return config;
|
||||
}))
|
||||
.put('/api/wireguard/restore', defineEventHandler(async (event) => {
|
||||
const { file } = await readBody(event);
|
||||
await WireGuard.restoreConfiguration(file);
|
||||
return { success: true };
|
||||
}));
|
||||
|
||||
// Static assets
|
||||
const publicDir = '/app/www';
|
||||
app.use(
|
||||
defineEventHandler((event) => {
|
||||
return serveStatic(event, {
|
||||
getContents: (id) => {
|
||||
return readFile(safePathJoin(publicDir, id));
|
||||
},
|
||||
getMeta: async (id) => {
|
||||
const filePath = safePathJoin(publicDir, id);
|
||||
|
||||
const stats = await stat(filePath).catch(() => {});
|
||||
if (!stats || !stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id.endsWith('.html')) setHeader(event, 'Content-Type', 'text/html');
|
||||
if (id.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript');
|
||||
if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json');
|
||||
if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css');
|
||||
if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png');
|
||||
|
||||
return {
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs,
|
||||
};
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (PASSWORD) {
|
||||
throw new Error('DO NOT USE PASSWORD ENVIRONMENT VARIABLE. USE PASSWORD_HASH INSTEAD.\nSee https://github.com/wg-easy/wg-easy/blob/v14/How_to_generate_an_bcrypt_hash.md');
|
||||
}
|
||||
|
||||
createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST);
|
||||
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
+80
-51
@@ -1,10 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
const debug = require('debug')('WireGuard');
|
||||
const uuid = require('uuid');
|
||||
const crypto = require('node:crypto');
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
const Util = require('./Util');
|
||||
@@ -14,6 +13,7 @@ const {
|
||||
WG_PATH,
|
||||
WG_HOST,
|
||||
WG_PORT,
|
||||
WG_CONFIG_PORT,
|
||||
WG_MTU,
|
||||
WG_DEFAULT_DNS,
|
||||
WG_DEFAULT_ADDRESS,
|
||||
@@ -27,54 +27,60 @@ const {
|
||||
|
||||
module.exports = class WireGuard {
|
||||
|
||||
async __buildConfig() {
|
||||
this.__configPromise = Promise.resolve().then(async () => {
|
||||
if (!WG_HOST) {
|
||||
throw new Error('WG_HOST Environment Variable Not Set!');
|
||||
}
|
||||
|
||||
debug('Loading configuration...');
|
||||
let config;
|
||||
try {
|
||||
config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
|
||||
config = JSON.parse(config);
|
||||
debug('Configuration loaded.');
|
||||
} catch (err) {
|
||||
const privateKey = await Util.exec('wg genkey');
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
|
||||
|
||||
config = {
|
||||
server: {
|
||||
privateKey,
|
||||
publicKey,
|
||||
address,
|
||||
},
|
||||
clients: {},
|
||||
};
|
||||
debug('Configuration generated.');
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
return this.__configPromise;
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
if (!this.__configPromise) {
|
||||
this.__configPromise = Promise.resolve().then(async () => {
|
||||
if (!WG_HOST) {
|
||||
throw new Error('WG_HOST Environment Variable Not Set!');
|
||||
const config = await this.__buildConfig();
|
||||
|
||||
await this.__saveConfig(config);
|
||||
await Util.exec('wg-quick down wg0').catch(() => {});
|
||||
await Util.exec('wg-quick up wg0').catch((err) => {
|
||||
if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
|
||||
throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
|
||||
}
|
||||
|
||||
debug('Loading configuration...');
|
||||
let config;
|
||||
try {
|
||||
config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
|
||||
config = JSON.parse(config);
|
||||
debug('Configuration loaded.');
|
||||
} catch (err) {
|
||||
const privateKey = await Util.exec('wg genkey');
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
|
||||
|
||||
config = {
|
||||
server: {
|
||||
privateKey,
|
||||
publicKey,
|
||||
address,
|
||||
},
|
||||
clients: {},
|
||||
};
|
||||
debug('Configuration generated.');
|
||||
}
|
||||
|
||||
await this.__saveConfig(config);
|
||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
||||
await Util.exec('wg-quick up wg0').catch((err) => {
|
||||
if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
|
||||
throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
// await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`);
|
||||
// await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
|
||||
await this.__syncConfig();
|
||||
|
||||
return config;
|
||||
throw err;
|
||||
});
|
||||
// await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`);
|
||||
// await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
|
||||
await this.__syncConfig();
|
||||
}
|
||||
|
||||
return this.__configPromise;
|
||||
@@ -95,7 +101,7 @@ module.exports = class WireGuard {
|
||||
[Interface]
|
||||
PrivateKey = ${config.server.privateKey}
|
||||
Address = ${config.server.address}/24
|
||||
ListenPort = 51820
|
||||
ListenPort = ${WG_PORT}
|
||||
PreUp = ${WG_PRE_UP}
|
||||
PostUp = ${WG_POST_UP}
|
||||
PreDown = ${WG_PRE_DOWN}
|
||||
@@ -208,7 +214,7 @@ PublicKey = ${config.server.publicKey}
|
||||
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
|
||||
}AllowedIPs = ${WG_ALLOWED_IPS}
|
||||
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
|
||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
|
||||
}
|
||||
|
||||
async getClientQRCodeSVG({ clientId }) {
|
||||
@@ -227,7 +233,9 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
const config = await this.getConfig();
|
||||
|
||||
const privateKey = await Util.exec('wg genkey');
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
const preSharedKey = await Util.exec('wg genpsk');
|
||||
|
||||
// Calculate next IP
|
||||
@@ -248,7 +256,7 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
}
|
||||
|
||||
// Create Client
|
||||
const id = uuid.v4();
|
||||
const id = crypto.randomUUID();
|
||||
const client = {
|
||||
id,
|
||||
name,
|
||||
@@ -319,9 +327,30 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
async __reloadConfig() {
|
||||
await this.__buildConfig();
|
||||
await this.__syncConfig();
|
||||
}
|
||||
|
||||
async restoreConfiguration(config) {
|
||||
debug('Starting configuration restore process.');
|
||||
const _config = JSON.parse(config);
|
||||
await this.__saveConfig(_config);
|
||||
await this.__reloadConfig();
|
||||
debug('Configuration restore process completed.');
|
||||
}
|
||||
|
||||
async backupConfiguration() {
|
||||
debug('Starting configuration backup.');
|
||||
const config = await this.getConfig();
|
||||
const backup = JSON.stringify(config, null, 2);
|
||||
debug('Configuration backup completed.');
|
||||
return backup;
|
||||
}
|
||||
|
||||
// Shutdown wireguard
|
||||
async Shutdown() {
|
||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
||||
await Util.exec('wg-quick down wg0').catch(() => {});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Generated
+1013
-954
File diff suppressed because it is too large
Load Diff
+11
-9
@@ -1,28 +1,30 @@
|
||||
{
|
||||
"release": "12",
|
||||
"release": {
|
||||
"version": "14"
|
||||
},
|
||||
"name": "wg-easy",
|
||||
"version": "1.0.1",
|
||||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"serve": "DEBUG=Server,WireGuard nodemon server.js",
|
||||
"serve": "DEBUG=Server,WireGuard npx nodemon server.js",
|
||||
"serve-with-password": "PASSWORD=wg npm run serve",
|
||||
"lint": "eslint .",
|
||||
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
|
||||
},
|
||||
"author": "Emile Nijssen",
|
||||
"license": "GPL",
|
||||
"license": "CC BY-NC-SA 4.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"debug": "^4.3.4",
|
||||
"express": "^4.18.3",
|
||||
"debug": "^4.3.6",
|
||||
"express-session": "^1.18.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"uuid": "^9.0.1"
|
||||
"h3": "^1.12.0",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-athom": "^3.1.3",
|
||||
"tailwindcss": "^3.4.1"
|
||||
"nodemon": "^3.1.4",
|
||||
"tailwindcss": "^3.4.9"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
@@ -30,6 +32,6 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": "18"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ process.on('SIGTERM', async () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Handle interupt signal
|
||||
// Handle interrupt signal
|
||||
process.on('SIGINT', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SIGINT signal received.');
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
darkMode: 'media',
|
||||
darkMode: 'selector',
|
||||
content: ['./www/**/*.{html,js}'],
|
||||
theme: {
|
||||
screens: {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
// Import needed libraries
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Function to generate hash
|
||||
const generateHash = async (password) => {
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`PASSWORD_HASH='${hash}'`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate hash : ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to compare password with hash
|
||||
const comparePassword = async (password, hash) => {
|
||||
try {
|
||||
const match = await bcrypt.compare(password, hash);
|
||||
if (match) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Password matches the hash !');
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Password does not match the hash.');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to compare password and hash : ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Retrieve command line arguments
|
||||
const args = process.argv.slice(2); // Ignore the first two arguments
|
||||
if (args.length > 2) {
|
||||
throw new Error('Usage : wgpw YOUR_PASSWORD [HASH]');
|
||||
}
|
||||
|
||||
const [password, hash] = args;
|
||||
if (password && hash) {
|
||||
await comparePassword(password, hash);
|
||||
} else if (password) {
|
||||
await generateHash(password);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
# This script is intended to be run only inside a docker container, not on the development host machine
|
||||
set -e
|
||||
# proxy command
|
||||
node /app/wgpw.mjs "$@"
|
||||
+364
-219
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.4.9 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -211,6 +211,8 @@ textarea {
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
letter-spacing: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
@@ -234,9 +236,9 @@ select {
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
input:where([type='button']),
|
||||
input:where([type='reset']),
|
||||
input:where([type='submit']) {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
@@ -492,6 +494,10 @@ video {
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
@@ -542,6 +548,10 @@ video {
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -590,6 +600,18 @@ video {
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -692,8 +714,8 @@ video {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-5 {
|
||||
@@ -712,10 +734,6 @@ video {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.mt-0\.5 {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
@@ -732,6 +750,10 @@ video {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-5 {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
@@ -776,6 +798,11 @@ video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.size-6 {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-1 {
|
||||
height: 0.25rem;
|
||||
}
|
||||
@@ -804,14 +831,26 @@ video {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.h-6 {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
@@ -876,6 +915,10 @@ video {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.grow-0 {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
@@ -921,10 +964,18 @@ video {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-col-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -957,6 +1008,10 @@ video {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.self-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -991,6 +1046,16 @@ video {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-l-full {
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
}
|
||||
|
||||
.rounded-r-full {
|
||||
border-top-right-radius: 9999px;
|
||||
border-bottom-right-radius: 9999px;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -1087,6 +1152,14 @@ video {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
|
||||
.fill-gray-400 {
|
||||
fill: #9ca3af;
|
||||
}
|
||||
|
||||
.fill-gray-600 {
|
||||
fill: #4b5563;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
@@ -1141,11 +1214,6 @@ video {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.pb-1 {
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
@@ -1276,6 +1344,11 @@ video {
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-neutral-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||
@@ -1396,6 +1469,10 @@ video {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.hover\:cursor-pointer:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover\:border-red-800:hover {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||
@@ -1463,10 +1540,43 @@ video {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.peer:checked ~ .peer-checked\:fill-gray-600 {
|
||||
fill: #4b5563;
|
||||
}
|
||||
|
||||
@media not all and (min-width: 768px) {
|
||||
.max-md\:hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.max-md\:border-x-0 {
|
||||
border-left-width: 0px;
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
.max-md\:border-l-0 {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.max-md\:border-r-0 {
|
||||
border-right-width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.xxs\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xxs\:self-center {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.xs\:mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@@ -1580,6 +1690,14 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.md\:block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.md\:inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -1588,15 +1706,28 @@ video {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.md\:flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.md\:gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.md\:rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.md\:px-0 {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.md\:py-5 {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.md\:pb-0 {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
@@ -1607,208 +1738,222 @@ video {
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-neutral-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-neutral-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-neutral-800 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-red-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-black {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-400 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 229 229 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(212 212 212 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-50 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(250 250 250 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(82 82 82 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-400::-moz-placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-400::placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-500::-moz-placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-500::placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:border-neutral-600:hover {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:border-red-600:hover {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-neutral-500:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-neutral-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-neutral-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(64 64 64 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-100:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-neutral-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-red-800:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:dark\:border-neutral-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||
}
|
||||
.dark\:border-neutral-500:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-neutral-600:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-neutral-800:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-red-600:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-black:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-400:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-500:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-600:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-700:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-neutral-800:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-100:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-600:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-800:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:fill-neutral-400:where(.dark, .dark *) {
|
||||
fill: #a3a3a3;
|
||||
}
|
||||
|
||||
.dark\:fill-neutral-600:where(.dark, .dark *) {
|
||||
fill: #525252;
|
||||
}
|
||||
|
||||
.dark\:text-gray-500:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-200:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 229 229 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-300:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(212 212 212 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-400:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-50:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(250 250 250 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-500:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-neutral-600:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(82 82 82 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-300:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-600:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:opacity-50:where(.dark, .dark *) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-400:where(.dark, .dark *)::-moz-placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-400:where(.dark, .dark *)::placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-500:where(.dark, .dark *)::-moz-placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder\:text-neutral-500:where(.dark, .dark *)::placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:border-neutral-600:hover:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:border-red-600:hover:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-neutral-500:hover:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-neutral-600:hover:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-600:hover:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-700:hover:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-800:hover:where(.dark, .dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-neutral-700:hover:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(64 64 64 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-100:hover:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover:where(.dark, .dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-neutral-500:focus:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-red-800:focus:where(.dark, .dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:dark\:border-neutral-500:where(.dark, .dark *):focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.group:hover .group-hover\:dark\:fill-neutral-500:where(.dark, .dark *) {
|
||||
fill: #737373;
|
||||
}
|
||||
|
||||
.peer:checked ~ .peer-checked\:dark\:fill-neutral-400:where(.dark, .dark *) {
|
||||
fill: #a3a3a3;
|
||||
}
|
||||
|
||||
+84
-30
@@ -3,40 +3,76 @@
|
||||
|
||||
<head>
|
||||
<title>WireGuard</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link href="./css/app.css" rel="stylesheet">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="icon" type="image/png" href="./img/favicon.png">
|
||||
<link rel="apple-touch-icon" href="./img/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
</head>
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
.line-chart .apexcharts-svg{
|
||||
transform: translateY(3px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-neutral-800">
|
||||
<div id="app">
|
||||
<div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0">
|
||||
<div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6">
|
||||
<div v-if="authenticated === true">
|
||||
<span v-if="requiresPassword"
|
||||
class="text-sm text-gray-400 dark:text-neutral-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right"
|
||||
@click="logout">
|
||||
{{$t("logout")}}
|
||||
|
||||
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</span>
|
||||
<h1 class="text-4xl dark:text-neutral-200 font-medium mt-2 mb-2">
|
||||
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg" />
|
||||
<span class="align-middle">WireGuard</span>
|
||||
</h1>
|
||||
<h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2>
|
||||
|
||||
<div class="flex flex-col-reverse xxs:flex-row flex-auto items-center items-end gap-3">
|
||||
<h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4">
|
||||
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg mr-2" /><span class="align-middle">WireGuard</span>
|
||||
</h1>
|
||||
<div class="flex items-center grow-0 gap-3 items-end self-end xxs:self-center">
|
||||
<!-- Dark / light theme -->
|
||||
<button @click="toggleTheme"
|
||||
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition" :title="$t(`theme.${uiTheme}`)">
|
||||
<svg v-if="uiTheme === 'light'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
|
||||
</svg>
|
||||
<svg v-else-if="uiTheme === 'dark'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-5 h-5 text-neutral-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
|
||||
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400">
|
||||
<path
|
||||
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M3.8,12c0-4.5,3.7-8.2,8.2-8.2v16.5C7.5,20.2,3.8,16.5,3.8,12z" />
|
||||
</svg>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Show / hide charts -->
|
||||
<label v-if="uiChartType > 0" class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group" :title="$t('toggleCharts')">
|
||||
<input type="checkbox" value="" class="sr-only peer" v-model="uiShowCharts" @change="toggleCharts">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" fill="currentColor"
|
||||
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition">
|
||||
<path
|
||||
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z" />
|
||||
</svg>
|
||||
</label>
|
||||
<span v-if="requiresPassword"
|
||||
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline"
|
||||
@click="logout">
|
||||
{{$t("logout")}}
|
||||
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5"></div>
|
||||
<div v-if="latestRelease"
|
||||
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg"
|
||||
:title="`v${currentRelease} → v${latestRelease.version}`">
|
||||
@@ -58,15 +94,33 @@
|
||||
<div class="flex-grow">
|
||||
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex md:block md:flex-shrink-0">
|
||||
<!-- Restore configuration -->
|
||||
<label for="inputRC" :title="$t('titleRestoreConfig')"
|
||||
class="hover:cursor-pointer hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-r-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-l-full md:rounded inline-flex items-center transition">
|
||||
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path>
|
||||
</svg>
|
||||
<span class="max-md:hidden text-sm">{{$t("restore")}}</span>
|
||||
<input id="inputRC" type="file" name="configurationfile" accept="text/*,.json" @change="restoreConfig" class="hidden"/>
|
||||
</label>
|
||||
<!-- Backup configuration -->
|
||||
<a href="./api/wireguard/backup" :title="$t('titleBackupConfig')"
|
||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition">
|
||||
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"></path>
|
||||
</svg>
|
||||
<span class="max-md:hidden text-sm">{{$t("backup")}}</span>
|
||||
</a>
|
||||
<!-- New client -->
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded inline-flex items-center transition">
|
||||
<svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-r-full md:rounded inline-flex items-center transition">
|
||||
<svg class="w-4 md:mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">{{$t("new")}}</span>
|
||||
<span class="max-md:hidden text-sm">{{$t("new")}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,17 +131,17 @@
|
||||
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid">
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%;">
|
||||
<apexchart width="100%" height="100%" :options="client.chartOptions" :series="client.transferTxSeries">
|
||||
<div v-if="uiChartType" :class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" >
|
||||
<apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries">
|
||||
</apexchart>
|
||||
</div>
|
||||
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%;">
|
||||
<apexchart width="100%" height="100%" :options="client.chartOptions" :series="client.transferRxSeries"
|
||||
<div v-if="uiChartType" :class="`absolute z-0 top-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" >
|
||||
<apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries"
|
||||
style="transform: scaleY(-1);">
|
||||
</apexchart>
|
||||
</div>
|
||||
|
||||
<div class="relative py-5 px-3 z-10 flex flex-col sm:flex-row justify-between gap-3">
|
||||
<div class="relative py-3 md:py-5 px-3 z-10 flex flex-col sm:flex-row justify-between gap-3">
|
||||
<div class="flex gap-3 md:gap-4 w-full items-center ">
|
||||
|
||||
<!-- Avatar -->
|
||||
@@ -506,7 +560,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password" :placeholder="$t('password')" v-model="password"
|
||||
<input type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
|
||||
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
|
||||
|
||||
<button v-if="authenticating"
|
||||
@@ -542,7 +596,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <a class="hover:underline" target="_blank"
|
||||
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <a class="hover:underline" target="_blank"
|
||||
href="https://github.com/wg-easy/wg-easy">WireGuard Easy</a> © 2021-2024 by <a class="hover:underline" target="_blank"
|
||||
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> is licensed under <a class="hover:underline" target="_blank"
|
||||
href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> · <a class="hover:underline"
|
||||
@@ -561,4 +615,4 @@
|
||||
<script src="./js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -50,6 +50,13 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
async getChartType() {
|
||||
return this.call({
|
||||
method: 'get',
|
||||
path: '/ui-chart-type',
|
||||
});
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
return this.call({
|
||||
method: 'get',
|
||||
@@ -131,4 +138,12 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
async restoreConfiguration(file) {
|
||||
return this.call({
|
||||
method: 'put',
|
||||
path: '/wireguard/restore',
|
||||
body: { file },
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+142
-34
@@ -29,8 +29,24 @@ const i18n = new VueI18n({
|
||||
messages,
|
||||
});
|
||||
|
||||
const UI_CHART_TYPES = [
|
||||
{ type: false, strokeWidth: 0 },
|
||||
{ type: 'line', strokeWidth: 3 },
|
||||
{ type: 'area', strokeWidth: 0 },
|
||||
{ type: 'bar', strokeWidth: 0 },
|
||||
];
|
||||
|
||||
const CHART_COLORS = {
|
||||
rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' },
|
||||
tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' },
|
||||
gradient: { light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'] },
|
||||
};
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
components: {
|
||||
apexchart: VueApexCharts,
|
||||
},
|
||||
i18n,
|
||||
data: {
|
||||
authenticated: null,
|
||||
@@ -52,13 +68,16 @@ new Vue({
|
||||
currentRelease: null,
|
||||
latestRelease: null,
|
||||
|
||||
isDark: null,
|
||||
uiTrafficStats: false,
|
||||
|
||||
uiChartType: 0,
|
||||
uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
|
||||
uiTheme: localStorage.theme || 'auto',
|
||||
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
|
||||
|
||||
chartOptions: {
|
||||
chart: {
|
||||
background: 'transparent',
|
||||
type: 'bar',
|
||||
stacked: false,
|
||||
toolbar: {
|
||||
show: false,
|
||||
@@ -66,11 +85,27 @@ new Vue({
|
||||
animations: {
|
||||
enabled: false,
|
||||
},
|
||||
parentHeightOffset: 0,
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
colors: [],
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shade: 'dark',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
gradientToColors: CHART_COLORS.gradient[this.theme],
|
||||
inverseColors: false,
|
||||
opacityTo: 0,
|
||||
stops: [0, 100],
|
||||
},
|
||||
},
|
||||
colors: [
|
||||
'#DDDDDD', // rx
|
||||
'#EEEEEE', // tx
|
||||
],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
@@ -84,10 +119,10 @@ new Vue({
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: true,
|
||||
show: false,
|
||||
},
|
||||
axisBorder: {
|
||||
show: true,
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
@@ -153,27 +188,42 @@ new Vue({
|
||||
// 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;
|
||||
|
||||
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
||||
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
||||
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
||||
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
||||
|
||||
if (updateCharts) {
|
||||
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
||||
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
||||
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
||||
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
||||
|
||||
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
|
||||
this.clientsPersist[client.id].transferRxHistory.shift();
|
||||
|
||||
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
|
||||
this.clientsPersist[client.id].transferTxHistory.shift();
|
||||
|
||||
this.clientsPersist[client.id].transferTxSeries = [{
|
||||
name: 'Tx',
|
||||
data: this.clientsPersist[client.id].transferTxHistory,
|
||||
}];
|
||||
|
||||
this.clientsPersist[client.id].transferRxSeries = [{
|
||||
name: 'Rx',
|
||||
data: this.clientsPersist[client.id].transferRxHistory,
|
||||
}];
|
||||
|
||||
client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory;
|
||||
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
|
||||
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory);
|
||||
|
||||
client.transferTxSeries = this.clientsPersist[client.id].transferTxSeries;
|
||||
client.transferRxSeries = this.clientsPersist[client.id].transferRxSeries;
|
||||
}
|
||||
|
||||
client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent;
|
||||
client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent;
|
||||
|
||||
client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory;
|
||||
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
|
||||
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory);
|
||||
|
||||
client.hoverTx = this.clientsPersist[client.id].hoverTx;
|
||||
client.hoverRx = this.clientsPersist[client.id].hoverRx;
|
||||
|
||||
@@ -249,15 +299,42 @@ new Vue({
|
||||
.catch((err) => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
},
|
||||
toggleTheme() {
|
||||
if (this.isDark) {
|
||||
localStorage.theme = 'light';
|
||||
document.documentElement.classList.remove('dark');
|
||||
restoreConfig(e) {
|
||||
e.preventDefault();
|
||||
const file = e.currentTarget.files.item(0);
|
||||
if (file) {
|
||||
file.text()
|
||||
.then((content) => {
|
||||
this.api.restoreConfiguration(content)
|
||||
.then((_result) => alert('The configuration was updated.'))
|
||||
.catch((err) => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
})
|
||||
.catch((err) => alert(err.message || err.toString()));
|
||||
} else {
|
||||
localStorage.theme = 'dark';
|
||||
document.documentElement.classList.add('dark');
|
||||
alert('Failed to load your file!');
|
||||
}
|
||||
this.isDark = !this.isDark;
|
||||
},
|
||||
toggleTheme() {
|
||||
const themes = ['light', 'dark', 'auto'];
|
||||
const currentIndex = themes.indexOf(this.uiTheme);
|
||||
const newIndex = (currentIndex + 1) % themes.length;
|
||||
this.uiTheme = themes[newIndex];
|
||||
localStorage.theme = this.uiTheme;
|
||||
this.setTheme(this.uiTheme);
|
||||
},
|
||||
setTheme(theme) {
|
||||
const { classList } = document.documentElement;
|
||||
const shouldAddDarkClass = theme === 'dark' || (theme === 'auto' && this.prefersDarkScheme.matches);
|
||||
classList.toggle('dark', shouldAddDarkClass);
|
||||
},
|
||||
handlePrefersChange(e) {
|
||||
if (localStorage.theme === 'auto') {
|
||||
this.setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
},
|
||||
toggleCharts() {
|
||||
localStorage.setItem('uiShowCharts', this.uiShowCharts ? 1 : 0);
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
@@ -267,10 +344,8 @@ new Vue({
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isDark = false;
|
||||
if (localStorage.theme === 'dark') {
|
||||
this.isDark = true;
|
||||
}
|
||||
this.prefersDarkScheme.addListener(this.handlePrefersChange);
|
||||
this.setTheme(this.uiTheme);
|
||||
|
||||
this.api = new API();
|
||||
this.api.getSession()
|
||||
@@ -278,7 +353,7 @@ new Vue({
|
||||
this.authenticated = session.authenticated;
|
||||
this.requiresPassword = session.requiresPassword;
|
||||
this.refresh({
|
||||
updateCharts: true,
|
||||
updateCharts: this.updateCharts,
|
||||
}).catch((err) => {
|
||||
alert(err.message || err.toString());
|
||||
});
|
||||
@@ -289,7 +364,7 @@ new Vue({
|
||||
|
||||
setInterval(() => {
|
||||
this.refresh({
|
||||
updateCharts: true,
|
||||
updateCharts: this.updateCharts,
|
||||
}).catch(console.error);
|
||||
}, 1000);
|
||||
|
||||
@@ -298,10 +373,17 @@ new Vue({
|
||||
this.uiTrafficStats = res;
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('Failed to get ui-traffic-stats');
|
||||
this.uiTrafficStats = false;
|
||||
});
|
||||
|
||||
this.api.getChartType()
|
||||
.then((res) => {
|
||||
this.uiChartType = parseInt(res, 10);
|
||||
})
|
||||
.catch(() => {
|
||||
this.uiChartType = 0;
|
||||
});
|
||||
|
||||
Promise.resolve().then(async () => {
|
||||
const lang = await this.api.getLang();
|
||||
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
|
||||
@@ -324,13 +406,39 @@ new Vue({
|
||||
return releasesArray[0];
|
||||
});
|
||||
|
||||
console.log(`Current Release: ${currentRelease}`);
|
||||
console.log(`Latest Release: ${latestRelease.version}`);
|
||||
|
||||
if (currentRelease >= latestRelease.version) return;
|
||||
|
||||
this.currentRelease = currentRelease;
|
||||
this.latestRelease = latestRelease;
|
||||
}).catch((err) => console.error(err));
|
||||
},
|
||||
computed: {
|
||||
chartOptionsTX() {
|
||||
const opts = {
|
||||
...this.chartOptions,
|
||||
colors: [CHART_COLORS.tx[this.theme]],
|
||||
};
|
||||
opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false;
|
||||
opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth;
|
||||
return opts;
|
||||
},
|
||||
chartOptionsRX() {
|
||||
const opts = {
|
||||
...this.chartOptions,
|
||||
colors: [CHART_COLORS.rx[this.theme]],
|
||||
};
|
||||
opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false;
|
||||
opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth;
|
||||
return opts;
|
||||
},
|
||||
updateCharts() {
|
||||
return this.uiChartType > 0 && this.uiShowCharts;
|
||||
},
|
||||
theme() {
|
||||
if (this.uiTheme === 'auto') {
|
||||
return this.prefersDarkScheme.matches ? 'dark' : 'light';
|
||||
}
|
||||
return this.uiTheme;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+89
-17
@@ -28,6 +28,12 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Download Configuration',
|
||||
madeBy: 'Made by',
|
||||
donate: 'Donate',
|
||||
toggleCharts: 'Show/hide Charts',
|
||||
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
|
||||
restore: 'Restore',
|
||||
backup: 'Backup',
|
||||
titleRestoreConfig: 'Restore your configuration',
|
||||
titleBackupConfig: 'Backup your configuration',
|
||||
},
|
||||
ua: {
|
||||
name: 'Ім`я',
|
||||
@@ -51,10 +57,17 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
disableClient: 'Вимкнути клієнта',
|
||||
enableClient: 'Увімкнути клієнта',
|
||||
noClients: 'Ще немає клієнтів.',
|
||||
noPrivKey: 'У цього клієнта немає відомого приватного ключа. Неможливо створити конфігурацію.',
|
||||
showQR: 'Показати QR-код',
|
||||
downloadConfig: 'Завантажити конфігурацію',
|
||||
madeBy: 'Зроблено',
|
||||
donate: 'Пожертвувати',
|
||||
toggleCharts: 'Показати/сховати діаграми',
|
||||
theme: { dark: 'Темна тема', light: 'Світла тема', auto: 'Автоматична тема' },
|
||||
restore: 'Відновити',
|
||||
backup: 'Резервна копія',
|
||||
titleRestoreConfig: 'Відновити конфігурацію',
|
||||
titleBackupConfig: 'Створити резервну копію конфігурації',
|
||||
},
|
||||
ru: {
|
||||
name: 'Имя',
|
||||
@@ -78,10 +91,17 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
disableClient: 'Выключить клиента',
|
||||
enableClient: 'Включить клиента',
|
||||
noClients: 'Пока нет клиентов.',
|
||||
noPrivKey: 'Невозможно создать конфигурацию: у клиента нет известного приватного ключа.',
|
||||
showQR: 'Показать QR-код',
|
||||
downloadConfig: 'Скачать конфигурацию',
|
||||
madeBy: 'Автор',
|
||||
donate: 'Поблагодарить',
|
||||
toggleCharts: 'Показать/скрыть графики',
|
||||
theme: { dark: 'Темная тема', light: 'Светлая тема', auto: 'Как в системе' },
|
||||
restore: 'Восстановить',
|
||||
backup: 'Резервная копия',
|
||||
titleRestoreConfig: 'Восстановить конфигурацию',
|
||||
titleBackupConfig: 'Создать резервную копию конфигурации',
|
||||
},
|
||||
tr: { // Müslüm Barış Korkmazer @babico
|
||||
name: 'İsim',
|
||||
@@ -97,19 +117,25 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
deleteDialog2: 'Bu işlem geri alınamaz.',
|
||||
cancel: 'İptal',
|
||||
create: 'Oluştur',
|
||||
createdAt: 'Şu saatte oluşturuldu: ',
|
||||
createdOn: 'Şu saatte oluşturuldu: ',
|
||||
lastSeen: 'Son görülme tarihi: ',
|
||||
totalDownload: 'Toplam İndirme: ',
|
||||
totalUpload: 'Toplam Yükleme: ',
|
||||
newClient: 'Yeni Kullanıcı',
|
||||
disableClient: 'İstemciyi Devre Dışı Bırak',
|
||||
enableClient: 'İstemciyi Etkinleştir',
|
||||
disableClient: 'Kullanıcıyı Devre Dışı Bırak',
|
||||
enableClient: 'Kullanıcıyı Etkinleştir',
|
||||
noClients: 'Henüz kullanıcı yok.',
|
||||
noPrivKey: 'Bu istemcinin bilinen bir özel anahtarı yok. Yapılandırma oluşturulamıyor.',
|
||||
showQR: 'QR Kodunu Göster',
|
||||
downloadConfig: 'Yapılandırmayı İndir',
|
||||
madeBy: 'Yapan Kişi: ',
|
||||
donate: 'Bağış Yap',
|
||||
changeLang: 'Dil Değiştir',
|
||||
toggleCharts: 'Grafiği göster/gizle',
|
||||
theme: { dark: 'Karanlık tema', light: 'Açık tema', auto: 'Otomatik tema' },
|
||||
restore: 'Geri yükle',
|
||||
backup: 'Yedekle',
|
||||
titleRestoreConfig: 'Yapılandırmanızı geri yükleyin',
|
||||
titleBackupConfig: 'Yapılandırmanızı yedekleyin',
|
||||
},
|
||||
no: { // github.com/digvalley
|
||||
name: 'Navn',
|
||||
@@ -191,6 +217,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Télécharger la configuration',
|
||||
madeBy: 'Développé par',
|
||||
donate: 'Soutenir',
|
||||
restore: 'Restaurer',
|
||||
backup: 'Sauvegarder',
|
||||
titleRestoreConfig: 'Restaurer votre configuration',
|
||||
titleBackupConfig: 'Sauvegarder votre configuration',
|
||||
},
|
||||
de: { // github.com/florian-asche
|
||||
name: 'Name',
|
||||
@@ -219,6 +249,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Konfiguration herunterladen',
|
||||
madeBy: 'Erstellt von',
|
||||
donate: 'Spenden',
|
||||
restore: 'Wiederherstellen',
|
||||
backup: 'Sichern',
|
||||
titleRestoreConfig: 'Stelle deine Konfiguration wieder her',
|
||||
titleBackupConfig: 'Sichere deine Konfiguration',
|
||||
},
|
||||
ca: { // github.com/guillembonet
|
||||
name: 'Nom',
|
||||
@@ -273,6 +307,12 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Descargar configuración',
|
||||
madeBy: 'Hecho por',
|
||||
donate: 'Donar',
|
||||
toggleCharts: 'Mostrar/Ocultar gráficos',
|
||||
theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' },
|
||||
restore: 'Restaurar',
|
||||
backup: 'Realizar copia de seguridad',
|
||||
titleRestoreConfig: 'Restaurar su configuración',
|
||||
titleBackupConfig: 'Realizar copia de seguridad de su configuración',
|
||||
},
|
||||
ko: {
|
||||
name: '이름',
|
||||
@@ -441,27 +481,27 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
password: '密碼',
|
||||
signIn: '登入',
|
||||
logout: '登出',
|
||||
updateAvailable: '有新版本可用!',
|
||||
updateAvailable: '有新版本可以使用!',
|
||||
update: '更新',
|
||||
clients: '客戶',
|
||||
new: '新建',
|
||||
deleteClient: '刪除客戶',
|
||||
clients: '使用者',
|
||||
new: '建立',
|
||||
deleteClient: '刪除使用者',
|
||||
deleteDialog1: '您確定要刪除',
|
||||
deleteDialog2: '此操作無法撤銷。',
|
||||
deleteDialog2: '此作業無法復原。',
|
||||
cancel: '取消',
|
||||
create: '建立',
|
||||
createdOn: '建立於 ',
|
||||
lastSeen: '最後訪問於 ',
|
||||
lastSeen: '最後存取於 ',
|
||||
totalDownload: '總下載: ',
|
||||
totalUpload: '總上傳: ',
|
||||
newClient: '新客戶',
|
||||
disableClient: '禁用客戶',
|
||||
enableClient: '啟用客戶',
|
||||
noClients: '目前沒有客戶。',
|
||||
showQR: '顯示二維碼',
|
||||
downloadConfig: '下載配置',
|
||||
newClient: '新用戶',
|
||||
disableClient: '停用使用者',
|
||||
enableClient: '啟用使用者',
|
||||
noClients: '目前沒有使用者。',
|
||||
showQR: '顯示 QR Code',
|
||||
downloadConfig: '下載 Config 檔',
|
||||
madeBy: '由',
|
||||
donate: '捐贈',
|
||||
donate: '抖內',
|
||||
},
|
||||
it: {
|
||||
name: 'Nome',
|
||||
@@ -489,6 +529,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Scarica configurazione',
|
||||
madeBy: 'Realizzato da',
|
||||
donate: 'Donazione',
|
||||
restore: 'Ripristina',
|
||||
backup: 'Backup',
|
||||
titleRestoreConfig: 'Ripristina la tua configurazione',
|
||||
titleBackupConfig: 'Esegui il backup della tua configurazione',
|
||||
},
|
||||
th: {
|
||||
name: 'ชื่อ',
|
||||
@@ -517,4 +561,32 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
madeBy: 'สร้างโดย',
|
||||
donate: 'บริจาค',
|
||||
},
|
||||
hi: { // github.com/rahilarious
|
||||
name: 'नाम',
|
||||
password: 'पासवर्ड',
|
||||
signIn: 'लॉगिन',
|
||||
logout: 'लॉगआउट',
|
||||
updateAvailable: 'अपडेट उपलब्ध है!',
|
||||
update: 'अपडेट',
|
||||
clients: 'उपयोगकर्ताये',
|
||||
new: 'नया',
|
||||
deleteClient: 'उपयोगकर्ता हटाएँ',
|
||||
deleteDialog1: 'क्या आपको पक्का हटाना है',
|
||||
deleteDialog2: 'यह निर्णय पलट नहीं सकता।',
|
||||
cancel: 'कुछ ना करें',
|
||||
create: 'बनाएं',
|
||||
createdOn: 'सर्जन तारीख ',
|
||||
lastSeen: 'पिछली बार देखे गए थे ',
|
||||
totalDownload: 'कुल डाउनलोड: ',
|
||||
totalUpload: 'कुल अपलोड: ',
|
||||
newClient: 'नया उपयोगकर्ता',
|
||||
disableClient: 'उपयोगकर्ता स्थगित कीजिये',
|
||||
enableClient: 'उपयोगकर्ता शुरू कीजिये',
|
||||
noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।',
|
||||
noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।',
|
||||
showQR: 'क्यू आर कोड देखिये',
|
||||
downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन',
|
||||
madeBy: 'सर्जक',
|
||||
donate: 'दान करें',
|
||||
},
|
||||
};
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user