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.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. iOS]
|
- OS: [e.g. macOS 12.1]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser [e.g. chrome, safari]
|
||||||
- Version [e.g. 22]
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6]
|
- Device: [e.g. iPhone6]
|
||||||
- OS: [e.g. iOS8.1]
|
- OS: [e.g. iOS 8.1]
|
||||||
- Browser [e.g. stock browser, safari]
|
- Browser [e.g. stock browser, safari]
|
||||||
- Version [e.g. 22]
|
- 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:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,8 +13,6 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: production
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -30,9 +27,9 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build & Publish Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: false
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
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
|
name: Build & Publish Latest
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- production
|
- v14
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Build & Deploy
|
name: Build & Deploy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository_owner == 'wg-easy'
|
if: |
|
||||||
|
github.repository_owner == 'wg-easy' &&
|
||||||
|
!contains(github.event.head_commit.message, '!skipci')
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: production
|
ref: v14
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -32,12 +33,9 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
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
|
- name: Build & Publish Docker Image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- v14
|
||||||
- production
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -18,12 +17,9 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '20'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: |
|
|
||||||
package-lock.json
|
|
||||||
src/package-lock.json
|
|
||||||
|
|
||||||
- name: npm run lint
|
- name: npm run lint
|
||||||
run: |
|
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
|
/config
|
||||||
/wg0.conf
|
/wg0.conf
|
||||||
/wg0.json
|
/wg0.json
|
||||||
|
/src/node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
+12
-17
@@ -1,17 +1,20 @@
|
|||||||
# There's an issue with node:20-alpine.
|
# As a workaround we have to build on nodejs 18
|
||||||
# Docker deployment is canceled after 25< minutes.
|
# 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 Web UI
|
||||||
COPY src/ /app/
|
COPY src /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN npm ci --omit=dev &&\
|
RUN npm ci --omit=dev &&\
|
||||||
mv node_modules /node_modules
|
mv node_modules /node_modules
|
||||||
|
|
||||||
# Copy build result to a new image.
|
# Copy build result to a new image.
|
||||||
# This saves a lot of disk space.
|
# 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
|
COPY --from=build_node_modules /app /app
|
||||||
|
|
||||||
# Move node_modules one directory up, so during development
|
# 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.
|
# than what runs inside of docker.
|
||||||
COPY --from=build_node_modules /node_modules /node_modules
|
COPY --from=build_node_modules /node_modules /node_modules
|
||||||
|
|
||||||
RUN \
|
# Copy the needed wg-password scripts
|
||||||
# Enable this to run `npm run serve`
|
COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw
|
||||||
npm i -g nodemon &&\
|
RUN chmod +x /bin/wgpw
|
||||||
# Workaround CVE-2023-42282
|
|
||||||
npm uninstall -g ip &&\
|
|
||||||
# Delete unnecessary files
|
|
||||||
npm cache clean --force && rm -rf ~/.npm
|
|
||||||
|
|
||||||
# Install Linux packages
|
# Install Linux packages
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
@@ -40,11 +39,7 @@ RUN apk add --no-cache \
|
|||||||
wireguard-tools
|
wireguard-tools
|
||||||
|
|
||||||
# Use iptables-legacy
|
# 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
|
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
|
||||||
|
|
||||||
# Expose Ports
|
|
||||||
EXPOSE 51820/udp
|
|
||||||
EXPOSE 51821/tcp
|
|
||||||
|
|
||||||
# Set Environment
|
# Set Environment
|
||||||
ENV DEBUG=Server,WireGuard
|
ENV DEBUG=Server,WireGuard
|
||||||
|
|||||||
@@ -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 a kernel that supports WireGuard (all modern kernels).
|
||||||
* A host with Docker installed.
|
* 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
|
## Installation
|
||||||
|
|
||||||
### 1. Install Docker
|
### 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:
|
If you haven't installed Docker yet, install it by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ curl -sSL https://get.docker.com | sh
|
curl -sSL https://get.docker.com | sh
|
||||||
$ sudo usermod -aG docker $(whoami)
|
sudo usermod -aG docker $(whoami)
|
||||||
$ exit
|
exit
|
||||||
```
|
```
|
||||||
|
|
||||||
And log in again.
|
And log in again.
|
||||||
@@ -48,12 +53,14 @@ And log in again.
|
|||||||
|
|
||||||
To automatically install & run wg-easy, simply run:
|
To automatically install & run wg-easy, simply run:
|
||||||
|
|
||||||
<pre>
|
```
|
||||||
$ docker run -d \
|
docker run -d \
|
||||||
--name=wg-easy \
|
--name=wg-easy \
|
||||||
-e LANG=de \
|
-e LANG=de \
|
||||||
-e WG_HOST=<b>🚨YOUR_SERVER_IP</b> \
|
-e WG_HOST=<🚨YOUR_SERVER_IP> \
|
||||||
-e PASSWORD=<b>🚨YOUR_ADMIN_PASSWORD</b> \
|
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
|
||||||
|
-e PORT=51821 \
|
||||||
|
-e WG_PORT=51820 \
|
||||||
-v ~/.wg-easy:/etc/wireguard \
|
-v ~/.wg-easy:/etc/wireguard \
|
||||||
-p 51820:51820/udp \
|
-p 51820:51820/udp \
|
||||||
-p 51821:51821/tcp \
|
-p 51821:51821/tcp \
|
||||||
@@ -62,17 +69,21 @@ $ docker run -d \
|
|||||||
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
|
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
|
||||||
--sysctl="net.ipv4.ip_forward=1" \
|
--sysctl="net.ipv4.ip_forward=1" \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
ghcr.io/wg-easy/wg-easy
|
ghcr.io/wg-easy/wg-easy:14
|
||||||
</pre>
|
```
|
||||||
|
|
||||||
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
|
> 💡 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`.
|
The Web UI will now be available on `http://0.0.0.0:51821`.
|
||||||
|
|
||||||
> 💡 Your configuration files will be saved in `~/.wg-easy`
|
> 💡 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
|
### 3. Sponsor
|
||||||
|
|
||||||
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
||||||
@@ -82,13 +93,14 @@ 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.
|
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
|
||||||
|
|
||||||
| Env | Default | Example | Description |
|
| Env | Default | Example | Description |
|
||||||
| - | - | - | - |
|
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||||
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
| `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. |
|
| `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_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_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_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_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_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_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
|
||||||
@@ -98,8 +110,9 @@ These options can be configured by setting environment variables using `-e KEY="
|
|||||||
| `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_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_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. |
|
| `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). |
|
| `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_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.
|
> 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
|
```bash
|
||||||
docker stop wg-easy
|
docker stop wg-easy
|
||||||
docker rm 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.
|
And then run the `docker run -d \ ...` command above again.
|
||||||
|
|
||||||
## Common Use Cases
|
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
|
||||||
* [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole)
|
Compose file and it is not `latest`, make sure that it is changed to the desired
|
||||||
* [Using WireGuard-Easy with nginx/SSL](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-nginx-SSL)
|
one; by default it is omitted and
|
||||||
|
[defaults to `latest`](https://docs.docker.com/engine/reference/run/#image-references)). \
|
||||||
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).
|
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:
|
services:
|
||||||
wg-easy:
|
wg-easy:
|
||||||
image: wg-easy
|
build:
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
command: npm run serve
|
command: npm run serve
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/:/app/
|
- ./src/:/app/
|
||||||
|
# - ./data/:/etc/wireguard
|
||||||
|
ports:
|
||||||
|
- "51820:51820/udp"
|
||||||
|
- "51821:51821/tcp"
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_MODULE
|
||||||
environment:
|
environment:
|
||||||
# - PASSWORD=p
|
# - PASSWORD_HASH=p
|
||||||
- WG_HOST=192.168.1.233
|
- WG_HOST=192.168.1.233
|
||||||
|
|||||||
+7
-4
@@ -1,4 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
volumes:
|
volumes:
|
||||||
etc_wireguard:
|
etc_wireguard:
|
||||||
|
|
||||||
@@ -6,15 +5,17 @@ services:
|
|||||||
wg-easy:
|
wg-easy:
|
||||||
environment:
|
environment:
|
||||||
# Change Language:
|
# 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
|
- LANG=de
|
||||||
# ⚠️ Required:
|
# ⚠️ Required:
|
||||||
# Change this to your host's public address
|
# Change this to your host's public address
|
||||||
- WG_HOST=raspberrypi.local
|
- WG_HOST=raspberrypi.local
|
||||||
|
|
||||||
# Optional:
|
# 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_PORT=51820
|
||||||
|
# - WG_CONFIG_PORT=92820
|
||||||
# - WG_DEFAULT_ADDRESS=10.8.0.x
|
# - WG_DEFAULT_ADDRESS=10.8.0.x
|
||||||
# - WG_DEFAULT_DNS=1.1.1.1
|
# - WG_DEFAULT_DNS=1.1.1.1
|
||||||
# - WG_MTU=1420
|
# - WG_MTU=1420
|
||||||
@@ -25,8 +26,9 @@ services:
|
|||||||
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
|
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
|
||||||
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-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
|
container_name: wg-easy
|
||||||
volumes:
|
volumes:
|
||||||
- etc_wireguard:/etc/wireguard
|
- etc_wireguard:/etc/wireguard
|
||||||
@@ -37,6 +39,7 @@ services:
|
|||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
|
# - NET_RAW # ⚠️ Uncomment if using Podman
|
||||||
sysctls:
|
sysctls:
|
||||||
- net.ipv4.ip_forward=1
|
- net.ipv4.ip_forward=1
|
||||||
- net.ipv4.conf.all.src_valid_mark=1
|
- net.ipv4.conf.all.src_valid_mark=1
|
||||||
|
|||||||
+5
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"1": "Initial version. Enjoy!",
|
"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!",
|
"3": "Many improvements and small changes. Enjoy!",
|
||||||
"4": "Now with pretty charts for client's network speed. Enjoy!",
|
"4": "Now with pretty charts for client's network speed. Enjoy!",
|
||||||
"5": "Many small improvements & feature requests. Enjoy!",
|
"5": "Many small improvements & feature requests. Enjoy!",
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
"8": "Updated to Node.js v18.",
|
"8": "Updated to Node.js v18.",
|
||||||
"9": "Fixed issue running on devices with older kernels.",
|
"9": "Fixed issue running on devices with older kernels.",
|
||||||
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
||||||
"11": "Multilanguage Support & various bugfixes",
|
"11": "Multilanguage Support & various bugfixes.",
|
||||||
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more."
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .",
|
||||||
"build": "DOCKER_BUILDKIT=1 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"
|
"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';
|
'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.PORT = process.env.PORT || '51821';
|
||||||
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
|
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 = process.env.PASSWORD;
|
||||||
|
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
|
||||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
||||||
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
|
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
|
||||||
module.exports.WG_HOST = process.env.WG_HOST;
|
module.exports.WG_HOST = process.env.WG_HOST;
|
||||||
module.exports.WG_PORT = process.env.WG_PORT || '51820';
|
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_MTU = process.env.WG_MTU || null;
|
||||||
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0';
|
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';
|
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_PRE_UP = process.env.WG_PRE_UP || '';
|
||||||
module.exports.WG_POST_UP = process.env.WG_POST_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 -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 -i wg0 -j ACCEPT;
|
||||||
iptables -A FORWARD -o wg0 -j ACCEPT;
|
iptables -A FORWARD -o wg0 -j ACCEPT;
|
||||||
`.split('\n').join(' ');
|
`.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_PRE_DOWN = process.env.WG_PRE_DOWN || '';
|
||||||
module.exports.WG_POST_DOWN = process.env.WG_POST_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 -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 -i wg0 -j ACCEPT;
|
||||||
iptables -D FORWARD -o wg0 -j ACCEPT;
|
iptables -D FORWARD -o wg0 -j ACCEPT;
|
||||||
`.split('\n').join(' ');
|
`.split('\n').join(' ');
|
||||||
module.exports.LANG = process.env.LANG || 'en';
|
module.exports.LANG = process.env.LANG || 'en';
|
||||||
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
||||||
|
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
|
||||||
|
|||||||
+221
-82
@@ -1,15 +1,27 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const crypto = require('node:crypto');
|
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 expressSession = require('express-session');
|
||||||
const debug = require('debug')('Server');
|
const debug = require('debug')('Server');
|
||||||
|
|
||||||
const Util = require('./Util');
|
const {
|
||||||
const ServerError = require('./ServerError');
|
createApp,
|
||||||
|
createError,
|
||||||
|
createRouter,
|
||||||
|
defineEventHandler,
|
||||||
|
fromNodeMiddleware,
|
||||||
|
getRouterParam,
|
||||||
|
toNodeListener,
|
||||||
|
readBody,
|
||||||
|
setHeader,
|
||||||
|
serveStatic,
|
||||||
|
} = require('h3');
|
||||||
|
|
||||||
const WireGuard = require('../services/WireGuard');
|
const WireGuard = require('../services/WireGuard');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -17,43 +29,74 @@ const {
|
|||||||
WEBUI_HOST,
|
WEBUI_HOST,
|
||||||
RELEASE,
|
RELEASE,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
|
PASSWORD_HASH,
|
||||||
LANG,
|
LANG,
|
||||||
UI_TRAFFIC_STATS,
|
UI_TRAFFIC_STATS,
|
||||||
|
UI_CHART_TYPE,
|
||||||
} = require('../config');
|
} = 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 {
|
module.exports = class Server {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Express
|
const app = createApp();
|
||||||
this.app = express()
|
this.app = app;
|
||||||
.disable('etag')
|
|
||||||
.use('/', express.static(path.join(__dirname, '..', 'www')))
|
app.use(fromNodeMiddleware(expressSession({
|
||||||
.use(express.json())
|
|
||||||
.use(expressSession({
|
|
||||||
secret: crypto.randomBytes(256).toString('hex'),
|
secret: crypto.randomBytes(256).toString('hex'),
|
||||||
resave: true,
|
resave: true,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
cookie: {
|
})));
|
||||||
httpOnly: 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 () => {
|
.get('/api/lang', defineEventHandler((event) => {
|
||||||
return RELEASE;
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
})))
|
return `"${LANG}"`;
|
||||||
|
}))
|
||||||
|
|
||||||
.get('/api/lang', (Util.promisify(async () => {
|
.get('/api/ui-traffic-stats', defineEventHandler((event) => {
|
||||||
return LANG;
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
})))
|
return `"${UI_TRAFFIC_STATS}"`;
|
||||||
.get('/api/ui-traffic-stats', (Util.promisify(async () => {
|
}))
|
||||||
return UI_TRAFFIC_STATS === 'true';
|
|
||||||
})))
|
.get('/api/ui-chart-type', defineEventHandler((event) => {
|
||||||
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
return `"${UI_CHART_TYPE}"`;
|
||||||
|
}))
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
.get('/api/session', Util.promisify(async (req) => {
|
.get('/api/session', defineEventHandler((event) => {
|
||||||
const requiresPassword = !!process.env.PASSWORD;
|
|
||||||
const authenticated = requiresPassword
|
const authenticated = requiresPassword
|
||||||
? !!(req.session && req.session.authenticated)
|
? !!(event.node.req.session && event.node.req.session.authenticated)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -61,28 +104,37 @@ module.exports = class Server {
|
|||||||
authenticated,
|
authenticated,
|
||||||
};
|
};
|
||||||
}))
|
}))
|
||||||
.post('/api/session', Util.promisify(async (req) => {
|
.post('/api/session', defineEventHandler(async (event) => {
|
||||||
const {
|
const { password } = await readBody(event);
|
||||||
password,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (typeof password !== 'string') {
|
if (!requiresPassword) {
|
||||||
throw new ServerError('Missing: Password', 401);
|
// 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) {
|
if (!isPasswordValid(password)) {
|
||||||
throw new ServerError('Incorrect Password', 401);
|
throw createError({
|
||||||
|
status: 401,
|
||||||
|
message: 'Incorrect Password',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.authenticated = true;
|
event.node.req.session.authenticated = true;
|
||||||
req.session.save();
|
event.node.req.session.save();
|
||||||
|
|
||||||
debug(`New Session: ${req.session.id}`);
|
debug(`New Session: ${event.node.req.session.id}`);
|
||||||
}))
|
|
||||||
|
return { success: true };
|
||||||
|
}));
|
||||||
|
|
||||||
// WireGuard
|
// WireGuard
|
||||||
.use((req, res, next) => {
|
app.use(
|
||||||
if (!PASSWORD) {
|
fromNodeMiddleware((req, res, next) => {
|
||||||
|
if (!requiresPassword || !req.url.startsWith('/api/')) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +142,8 @@ module.exports = class Server {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.path.startsWith('/api/') && req.headers['authorization']) {
|
if (req.url.startsWith('/api/') && req.headers['authorization']) {
|
||||||
if (bcrypt.compareSync(req.headers['authorization'], bcrypt.hashSync(PASSWORD, 10))) {
|
if (isPasswordValid(req.headers['authorization'])) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
@@ -102,25 +154,32 @@ module.exports = class Server {
|
|||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
error: 'Not Logged In',
|
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}`);
|
debug(`Deleted Session: ${sessionId}`);
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.get('/api/wireguard/client', Util.promisify(async (req) => {
|
.get('/api/wireguard/client', defineEventHandler(() => {
|
||||||
return WireGuard.getClients();
|
return WireGuard.getClients();
|
||||||
}))
|
}))
|
||||||
.get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => {
|
.get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
||||||
res.header('Content-Type', 'image/svg+xml');
|
setHeader(event, 'Content-Type', 'image/svg+xml');
|
||||||
res.send(svg);
|
return svg;
|
||||||
}))
|
}))
|
||||||
.get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => {
|
.get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
const client = await WireGuard.getClient({ clientId });
|
const client = await WireGuard.getClient({ clientId });
|
||||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||||
const configName = client.name
|
const configName = client.name
|
||||||
@@ -128,52 +187,132 @@ module.exports = class Server {
|
|||||||
.replace(/(-{2,}|-$)/g, '-')
|
.replace(/(-{2,}|-$)/g, '-')
|
||||||
.replace(/-$/, '')
|
.replace(/-$/, '')
|
||||||
.substring(0, 32);
|
.substring(0, 32);
|
||||||
res.header('Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
|
setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
|
||||||
res.header('Content-Type', 'text/plain');
|
setHeader(event, 'Content-Type', 'text/plain');
|
||||||
res.send(config);
|
return config;
|
||||||
}))
|
}))
|
||||||
.post('/api/wireguard/client', Util.promisify(async (req) => {
|
.post('/api/wireguard/client', defineEventHandler(async (event) => {
|
||||||
const { name } = req.body;
|
const { name } = await readBody(event);
|
||||||
return WireGuard.createClient({ name });
|
await WireGuard.createClient({ name });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.delete('/api/wireguard/client/:clientId', Util.promisify(async (req) => {
|
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
return WireGuard.deleteClient({ clientId });
|
await WireGuard.deleteClient({ clientId });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.post('/api/wireguard/client/:clientId/enable', Util.promisify(async (req, res) => {
|
.post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
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) => {
|
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
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) => {
|
.put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
res.end(403);
|
throw createError({ status: 403 });
|
||||||
}
|
}
|
||||||
const { name } = req.body;
|
const { name } = await readBody(event);
|
||||||
return WireGuard.updateClientName({ clientId, name });
|
await WireGuard.updateClientName({ clientId, name });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.put('/api/wireguard/client/:clientId/address', Util.promisify(async (req, res) => {
|
.put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
res.end(403);
|
throw createError({ status: 403 });
|
||||||
}
|
}
|
||||||
const { address } = req.body;
|
const { address } = await readBody(event);
|
||||||
return WireGuard.updateClientAddress({ clientId, address });
|
await WireGuard.updateClientAddress({ clientId, address });
|
||||||
}))
|
return { success: true };
|
||||||
|
}));
|
||||||
|
|
||||||
.listen(PORT, WEBUI_HOST, () => {
|
const safePathJoin = (base, target) => {
|
||||||
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
|
// 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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
+43
-14
@@ -1,10 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
const fs = require('node:fs/promises');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const debug = require('debug')('WireGuard');
|
const debug = require('debug')('WireGuard');
|
||||||
const uuid = require('uuid');
|
const crypto = require('node:crypto');
|
||||||
const QRCode = require('qrcode');
|
const QRCode = require('qrcode');
|
||||||
|
|
||||||
const Util = require('./Util');
|
const Util = require('./Util');
|
||||||
@@ -14,6 +13,7 @@ const {
|
|||||||
WG_PATH,
|
WG_PATH,
|
||||||
WG_HOST,
|
WG_HOST,
|
||||||
WG_PORT,
|
WG_PORT,
|
||||||
|
WG_CONFIG_PORT,
|
||||||
WG_MTU,
|
WG_MTU,
|
||||||
WG_DEFAULT_DNS,
|
WG_DEFAULT_DNS,
|
||||||
WG_DEFAULT_ADDRESS,
|
WG_DEFAULT_ADDRESS,
|
||||||
@@ -27,8 +27,7 @@ const {
|
|||||||
|
|
||||||
module.exports = class WireGuard {
|
module.exports = class WireGuard {
|
||||||
|
|
||||||
async getConfig() {
|
async __buildConfig() {
|
||||||
if (!this.__configPromise) {
|
|
||||||
this.__configPromise = Promise.resolve().then(async () => {
|
this.__configPromise = Promise.resolve().then(async () => {
|
||||||
if (!WG_HOST) {
|
if (!WG_HOST) {
|
||||||
throw new Error('WG_HOST Environment Variable Not Set!');
|
throw new Error('WG_HOST Environment Variable Not Set!');
|
||||||
@@ -58,8 +57,18 @@ module.exports = class WireGuard {
|
|||||||
debug('Configuration generated.');
|
debug('Configuration generated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.__configPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig() {
|
||||||
|
if (!this.__configPromise) {
|
||||||
|
const config = await this.__buildConfig();
|
||||||
|
|
||||||
await this.__saveConfig(config);
|
await this.__saveConfig(config);
|
||||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
await Util.exec('wg-quick down wg0').catch(() => {});
|
||||||
await Util.exec('wg-quick up wg0').catch((err) => {
|
await Util.exec('wg-quick up wg0').catch((err) => {
|
||||||
if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
|
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 new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
|
||||||
@@ -72,9 +81,6 @@ module.exports = class WireGuard {
|
|||||||
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
|
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
|
||||||
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
|
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
|
||||||
await this.__syncConfig();
|
await this.__syncConfig();
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.__configPromise;
|
return this.__configPromise;
|
||||||
@@ -95,7 +101,7 @@ module.exports = class WireGuard {
|
|||||||
[Interface]
|
[Interface]
|
||||||
PrivateKey = ${config.server.privateKey}
|
PrivateKey = ${config.server.privateKey}
|
||||||
Address = ${config.server.address}/24
|
Address = ${config.server.address}/24
|
||||||
ListenPort = 51820
|
ListenPort = ${WG_PORT}
|
||||||
PreUp = ${WG_PRE_UP}
|
PreUp = ${WG_PRE_UP}
|
||||||
PostUp = ${WG_POST_UP}
|
PostUp = ${WG_POST_UP}
|
||||||
PreDown = ${WG_PRE_DOWN}
|
PreDown = ${WG_PRE_DOWN}
|
||||||
@@ -208,7 +214,7 @@ PublicKey = ${config.server.publicKey}
|
|||||||
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
|
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
|
||||||
}AllowedIPs = ${WG_ALLOWED_IPS}
|
}AllowedIPs = ${WG_ALLOWED_IPS}
|
||||||
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
|
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
|
||||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClientQRCodeSVG({ clientId }) {
|
async getClientQRCodeSVG({ clientId }) {
|
||||||
@@ -227,7 +233,9 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
|||||||
const config = await this.getConfig();
|
const config = await this.getConfig();
|
||||||
|
|
||||||
const privateKey = await Util.exec('wg genkey');
|
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');
|
const preSharedKey = await Util.exec('wg genpsk');
|
||||||
|
|
||||||
// Calculate next IP
|
// Calculate next IP
|
||||||
@@ -248,7 +256,7 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Client
|
// Create Client
|
||||||
const id = uuid.v4();
|
const id = crypto.randomUUID();
|
||||||
const client = {
|
const client = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -319,9 +327,30 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
|||||||
await this.saveConfig();
|
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
|
// Shutdown wireguard
|
||||||
async Shutdown() {
|
async Shutdown() {
|
||||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
await Util.exec('wg-quick down wg0').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+1014
-955
File diff suppressed because it is too large
Load Diff
+11
-9
@@ -1,28 +1,30 @@
|
|||||||
{
|
{
|
||||||
"release": "12",
|
"release": {
|
||||||
|
"version": "14"
|
||||||
|
},
|
||||||
"name": "wg-easy",
|
"name": "wg-easy",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "DEBUG=Server,WireGuard nodemon server.js",
|
"serve": "DEBUG=Server,WireGuard npx nodemon server.js",
|
||||||
"serve-with-password": "PASSWORD=wg npm run serve",
|
"serve-with-password": "PASSWORD=wg npm run serve",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
|
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
|
||||||
},
|
},
|
||||||
"author": "Emile Nijssen",
|
"author": "Emile Nijssen",
|
||||||
"license": "GPL",
|
"license": "CC BY-NC-SA 4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.6",
|
||||||
"express": "^4.18.3",
|
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"qrcode": "^1.5.3",
|
"h3": "^1.12.0",
|
||||||
"uuid": "^9.0.1"
|
"qrcode": "^1.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-athom": "^3.1.3",
|
"eslint-config-athom": "^3.1.3",
|
||||||
"tailwindcss": "^3.4.1"
|
"nodemon": "^3.1.4",
|
||||||
|
"tailwindcss": "^3.4.9"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
@@ -30,6 +32,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ process.on('SIGTERM', async () => {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle interupt signal
|
// Handle interrupt signal
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('SIGINT signal received.');
|
console.log('SIGINT signal received.');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: 'media',
|
darkMode: 'selector',
|
||||||
content: ['./www/**/*.{html,js}'],
|
content: ['./www/**/*.{html,js}'],
|
||||||
theme: {
|
theme: {
|
||||||
screens: {
|
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 "$@"
|
||||||
+243
-98
@@ -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 */
|
/* 1 */
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
|
letter-spacing: inherit;
|
||||||
|
/* 1 */
|
||||||
color: inherit;
|
color: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -234,9 +236,9 @@ select {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
[type='button'],
|
input:where([type='button']),
|
||||||
[type='reset'],
|
input:where([type='reset']),
|
||||||
[type='submit'] {
|
input:where([type='submit']) {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -492,6 +494,10 @@ video {
|
|||||||
--tw-backdrop-opacity: ;
|
--tw-backdrop-opacity: ;
|
||||||
--tw-backdrop-saturate: ;
|
--tw-backdrop-saturate: ;
|
||||||
--tw-backdrop-sepia: ;
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
}
|
}
|
||||||
|
|
||||||
::backdrop {
|
::backdrop {
|
||||||
@@ -542,6 +548,10 @@ video {
|
|||||||
--tw-backdrop-opacity: ;
|
--tw-backdrop-opacity: ;
|
||||||
--tw-backdrop-saturate: ;
|
--tw-backdrop-saturate: ;
|
||||||
--tw-backdrop-sepia: ;
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.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 {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -692,8 +714,8 @@ video {
|
|||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-2 {
|
.mb-4 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-5 {
|
.mb-5 {
|
||||||
@@ -712,10 +734,6 @@ video {
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-0 {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-0\.5 {
|
.mt-0\.5 {
|
||||||
margin-top: 0.125rem;
|
margin-top: 0.125rem;
|
||||||
}
|
}
|
||||||
@@ -732,6 +750,10 @@ video {
|
|||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-5 {
|
.mt-5 {
|
||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
}
|
}
|
||||||
@@ -776,6 +798,11 @@ video {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.size-6 {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-1 {
|
.h-1 {
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -804,14 +831,26 @@ video {
|
|||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-5 {
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-6 {
|
.h-6 {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-8 {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-h-screen {
|
.min-h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-1 {
|
||||||
|
width: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-10 {
|
.w-10 {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
}
|
}
|
||||||
@@ -876,6 +915,10 @@ video {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grow-0 {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.transform {
|
.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));
|
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-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-col-reverse {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-wrap {
|
.flex-wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.items-end {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -957,6 +1008,10 @@ video {
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.self-end {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -991,6 +1046,16 @@ video {
|
|||||||
border-radius: 0.375rem;
|
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 {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
@@ -1087,6 +1152,14 @@ video {
|
|||||||
--tw-bg-opacity: 0.5;
|
--tw-bg-opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-gray-400 {
|
||||||
|
fill: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-gray-600 {
|
||||||
|
fill: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
.p-1 {
|
.p-1 {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -1141,11 +1214,6 @@ video {
|
|||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.py-5 {
|
|
||||||
padding-top: 1.25rem;
|
|
||||||
padding-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pb-1 {
|
.pb-1 {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -1276,6 +1344,11 @@ video {
|
|||||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
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 {
|
.text-red-600 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||||
@@ -1396,6 +1469,10 @@ video {
|
|||||||
border-bottom-width: 0px;
|
border-bottom-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:cursor-pointer:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:border-red-800:hover {
|
.hover\:border-red-800:hover {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||||
@@ -1463,10 +1540,43 @@ video {
|
|||||||
opacity: 1;
|
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) {
|
@media (min-width: 450px) {
|
||||||
.xxs\:flex-row {
|
.xxs\:flex-row {
|
||||||
flex-direction: 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) {
|
@media (min-width: 640px) {
|
||||||
@@ -1580,6 +1690,14 @@ video {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
.md\:mr-2 {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md\:block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:inline-block {
|
.md\:inline-block {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -1588,15 +1706,28 @@ video {
|
|||||||
min-width: 6rem;
|
min-width: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md\:flex-shrink-0 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:gap-4 {
|
.md\:gap-4 {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md\:rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:px-0 {
|
.md\:px-0 {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md\:py-5 {
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:pb-0 {
|
.md\:pb-0 {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
@@ -1607,208 +1738,222 @@ video {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.dark\:border-neutral-500:where(.dark, .dark *) {
|
||||||
.dark\:border-neutral-500 {
|
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:border-neutral-600 {
|
.dark\:border-neutral-600:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:border-neutral-800 {
|
.dark\:border-neutral-800:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:border-red-600 {
|
.dark\:border-red-600:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-black {
|
.dark\:bg-black:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-400 {
|
.dark\:bg-neutral-400:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-500 {
|
.dark\:bg-neutral-500:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-600 {
|
.dark\:bg-neutral-600:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-700 {
|
.dark\:bg-neutral-700:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
|
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-800 {
|
.dark\:bg-neutral-800:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
|
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-red-100 {
|
.dark\:bg-red-100:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-red-600 {
|
.dark\:bg-red-600:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-red-800 {
|
.dark\:bg-red-800:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-gray-500 {
|
.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;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-200 {
|
.dark\:text-neutral-200:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(229 229 229 / var(--tw-text-opacity));
|
color: rgb(229 229 229 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-300 {
|
.dark\:text-neutral-300:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(212 212 212 / var(--tw-text-opacity));
|
color: rgb(212 212 212 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-400 {
|
.dark\:text-neutral-400:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-50 {
|
.dark\:text-neutral-50:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(250 250 250 / var(--tw-text-opacity));
|
color: rgb(250 250 250 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-500 {
|
.dark\:text-neutral-500:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-600 {
|
.dark\:text-neutral-600:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(82 82 82 / var(--tw-text-opacity));
|
color: rgb(82 82 82 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-red-300 {
|
.dark\:text-red-300:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-red-600 {
|
.dark\:text-red-600:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-white {
|
.dark\:text-white:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:opacity-50 {
|
.dark\:opacity-50:where(.dark, .dark *) {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-400::-moz-placeholder {
|
.dark\:placeholder\:text-neutral-400:where(.dark, .dark *)::-moz-placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-400::placeholder {
|
.dark\:placeholder\:text-neutral-400:where(.dark, .dark *)::placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-500::-moz-placeholder {
|
.dark\:placeholder\:text-neutral-500:where(.dark, .dark *)::-moz-placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-500::placeholder {
|
.dark\:placeholder\:text-neutral-500:where(.dark, .dark *)::placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:border-neutral-600:hover {
|
.dark\:hover\:border-neutral-600:hover:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:border-red-600:hover {
|
.dark\:hover\:border-red-600:hover:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-neutral-500:hover {
|
.dark\:hover\:bg-neutral-500:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-neutral-600:hover {
|
.dark\:hover\:bg-neutral-600:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-600:hover {
|
.dark\:hover\:bg-red-600:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-700:hover {
|
.dark\:hover\:bg-red-700:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-800:hover {
|
.dark\:hover\:bg-red-800:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-neutral-700:hover {
|
.dark\:hover\:text-neutral-700:hover:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(64 64 64 / var(--tw-text-opacity));
|
color: rgb(64 64 64 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-red-100:hover {
|
.dark\:hover\:text-red-100:hover:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-white:hover {
|
.dark\:hover\:text-white:hover:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:focus\:border-neutral-500:focus {
|
.dark\:focus\:border-neutral-500:focus:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:focus\:border-red-800:focus {
|
.dark\:focus\:border-red-800:focus:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus\:dark\:border-neutral-500:focus {
|
.focus\:dark\:border-neutral-500:where(.dark, .dark *):focus {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-21
@@ -3,40 +3,76 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>WireGuard</title>
|
<title>WireGuard</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<link rel="manifest" href="./manifest.json">
|
<link rel="manifest" href="./manifest.json">
|
||||||
<link rel="icon" type="image/png" href="./img/favicon.png">
|
<link rel="icon" type="image/png" href="./img/favicon.png">
|
||||||
<link rel="apple-touch-icon" href="./img/apple-touch-icon.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="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-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
</head>
|
</head>
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.line-chart .apexcharts-svg{
|
||||||
|
transform: translateY(3px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<body class="bg-gray-50 dark:bg-neutral-800">
|
<body class="bg-gray-50 dark:bg-neutral-800">
|
||||||
<div id="app">
|
<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">
|
<div v-if="authenticated === true">
|
||||||
|
<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"
|
<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"
|
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline"
|
||||||
@click="logout">
|
@click="logout">
|
||||||
{{$t("logout")}}
|
{{$t("logout")}}
|
||||||
|
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<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"
|
<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" />
|
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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<h1 class="text-4xl dark:text-neutral-200 font-medium mt-2 mb-2">
|
</div>
|
||||||
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg" />
|
</div>
|
||||||
<span class="align-middle">WireGuard</span>
|
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5"></div>
|
||||||
</h1>
|
|
||||||
<h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2>
|
|
||||||
|
|
||||||
<div v-if="latestRelease"
|
<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"
|
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}`">
|
:title="`v${currentRelease} → v${latestRelease.version}`">
|
||||||
@@ -58,15 +94,33 @@
|
|||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
|
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
|
||||||
</div>
|
</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 = '';"
|
<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">
|
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 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
<svg class="w-4 md:mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
stroke="currentColor">
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-sm">{{$t("new")}}</span>
|
<span class="max-md:hidden text-sm">{{$t("new")}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid">
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%;">
|
<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="client.chartOptions" :series="client.transferTxSeries">
|
<apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries">
|
||||||
</apexchart>
|
</apexchart>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%;">
|
<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="client.chartOptions" :series="client.transferRxSeries"
|
<apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries"
|
||||||
style="transform: scaleY(-1);">
|
style="transform: scaleY(-1);">
|
||||||
</apexchart>
|
</apexchart>
|
||||||
</div>
|
</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 ">
|
<div class="flex gap-3 md:gap-4 w-full items-center ">
|
||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
@@ -506,7 +560,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</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" />
|
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"
|
<button v-if="authenticating"
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ class API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChartType() {
|
||||||
|
return this.call({
|
||||||
|
method: 'get',
|
||||||
|
path: '/ui-chart-type',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getSession() {
|
async getSession() {
|
||||||
return this.call({
|
return this.call({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -131,4 +138,12 @@ class API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreConfiguration(file) {
|
||||||
|
return this.call({
|
||||||
|
method: 'put',
|
||||||
|
path: '/wireguard/restore',
|
||||||
|
body: { file },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+137
-29
@@ -29,8 +29,24 @@ const i18n = new VueI18n({
|
|||||||
messages,
|
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({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
i18n,
|
i18n,
|
||||||
data: {
|
data: {
|
||||||
authenticated: null,
|
authenticated: null,
|
||||||
@@ -52,13 +68,16 @@ new Vue({
|
|||||||
currentRelease: null,
|
currentRelease: null,
|
||||||
latestRelease: null,
|
latestRelease: null,
|
||||||
|
|
||||||
isDark: null,
|
|
||||||
uiTrafficStats: false,
|
uiTrafficStats: false,
|
||||||
|
|
||||||
|
uiChartType: 0,
|
||||||
|
uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
|
||||||
|
uiTheme: localStorage.theme || 'auto',
|
||||||
|
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
|
||||||
|
|
||||||
chartOptions: {
|
chartOptions: {
|
||||||
chart: {
|
chart: {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
type: 'bar',
|
|
||||||
stacked: false,
|
stacked: false,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -66,11 +85,27 @@ new Vue({
|
|||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
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: {
|
dataLabels: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
@@ -84,10 +119,10 @@ new Vue({
|
|||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
axisTicks: {
|
axisTicks: {
|
||||||
show: true,
|
show: false,
|
||||||
},
|
},
|
||||||
axisBorder: {
|
axisBorder: {
|
||||||
show: true,
|
show: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
@@ -153,27 +188,42 @@ new Vue({
|
|||||||
// Debug
|
// Debug
|
||||||
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
||||||
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
||||||
|
// client.latestHandshakeAt = new Date();
|
||||||
|
// this.requiresPassword = true;
|
||||||
|
|
||||||
if (updateCharts) {
|
|
||||||
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
||||||
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
||||||
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
||||||
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
||||||
|
|
||||||
|
if (updateCharts) {
|
||||||
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
|
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
|
||||||
this.clientsPersist[client.id].transferRxHistory.shift();
|
this.clientsPersist[client.id].transferRxHistory.shift();
|
||||||
|
|
||||||
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
|
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
|
||||||
this.clientsPersist[client.id].transferTxHistory.shift();
|
this.clientsPersist[client.id].transferTxHistory.shift();
|
||||||
}
|
|
||||||
|
|
||||||
client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent;
|
this.clientsPersist[client.id].transferTxSeries = [{
|
||||||
client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent;
|
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.transferTxHistory = this.clientsPersist[client.id].transferTxHistory;
|
||||||
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
|
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
|
||||||
client.transferMax = Math.max(...client.transferTxHistory, ...client.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.hoverTx = this.clientsPersist[client.id].hoverTx;
|
client.hoverTx = this.clientsPersist[client.id].hoverTx;
|
||||||
client.hoverRx = this.clientsPersist[client.id].hoverRx;
|
client.hoverRx = this.clientsPersist[client.id].hoverRx;
|
||||||
|
|
||||||
@@ -249,15 +299,42 @@ new Vue({
|
|||||||
.catch((err) => alert(err.message || err.toString()))
|
.catch((err) => alert(err.message || err.toString()))
|
||||||
.finally(() => this.refresh().catch(console.error));
|
.finally(() => this.refresh().catch(console.error));
|
||||||
},
|
},
|
||||||
toggleTheme() {
|
restoreConfig(e) {
|
||||||
if (this.isDark) {
|
e.preventDefault();
|
||||||
localStorage.theme = 'light';
|
const file = e.currentTarget.files.item(0);
|
||||||
document.documentElement.classList.remove('dark');
|
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 {
|
} else {
|
||||||
localStorage.theme = 'dark';
|
alert('Failed to load your file!');
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
}
|
}
|
||||||
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: {
|
filters: {
|
||||||
@@ -267,10 +344,8 @@ new Vue({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.isDark = false;
|
this.prefersDarkScheme.addListener(this.handlePrefersChange);
|
||||||
if (localStorage.theme === 'dark') {
|
this.setTheme(this.uiTheme);
|
||||||
this.isDark = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api = new API();
|
this.api = new API();
|
||||||
this.api.getSession()
|
this.api.getSession()
|
||||||
@@ -278,7 +353,7 @@ new Vue({
|
|||||||
this.authenticated = session.authenticated;
|
this.authenticated = session.authenticated;
|
||||||
this.requiresPassword = session.requiresPassword;
|
this.requiresPassword = session.requiresPassword;
|
||||||
this.refresh({
|
this.refresh({
|
||||||
updateCharts: true,
|
updateCharts: this.updateCharts,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
alert(err.message || err.toString());
|
alert(err.message || err.toString());
|
||||||
});
|
});
|
||||||
@@ -289,7 +364,7 @@ new Vue({
|
|||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.refresh({
|
this.refresh({
|
||||||
updateCharts: true,
|
updateCharts: this.updateCharts,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
@@ -298,10 +373,17 @@ new Vue({
|
|||||||
this.uiTrafficStats = res;
|
this.uiTrafficStats = res;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log('Failed to get ui-traffic-stats');
|
|
||||||
this.uiTrafficStats = false;
|
this.uiTrafficStats = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.api.getChartType()
|
||||||
|
.then((res) => {
|
||||||
|
this.uiChartType = parseInt(res, 10);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.uiChartType = 0;
|
||||||
|
});
|
||||||
|
|
||||||
Promise.resolve().then(async () => {
|
Promise.resolve().then(async () => {
|
||||||
const lang = await this.api.getLang();
|
const lang = await this.api.getLang();
|
||||||
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
|
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
|
||||||
@@ -324,13 +406,39 @@ new Vue({
|
|||||||
return releasesArray[0];
|
return releasesArray[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Current Release: ${currentRelease}`);
|
|
||||||
console.log(`Latest Release: ${latestRelease.version}`);
|
|
||||||
|
|
||||||
if (currentRelease >= latestRelease.version) return;
|
if (currentRelease >= latestRelease.version) return;
|
||||||
|
|
||||||
this.currentRelease = currentRelease;
|
this.currentRelease = currentRelease;
|
||||||
this.latestRelease = latestRelease;
|
this.latestRelease = latestRelease;
|
||||||
}).catch((err) => console.error(err));
|
}).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',
|
downloadConfig: 'Download Configuration',
|
||||||
madeBy: 'Made by',
|
madeBy: 'Made by',
|
||||||
donate: 'Donate',
|
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: {
|
ua: {
|
||||||
name: 'Ім`я',
|
name: 'Ім`я',
|
||||||
@@ -51,10 +57,17 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
disableClient: 'Вимкнути клієнта',
|
disableClient: 'Вимкнути клієнта',
|
||||||
enableClient: 'Увімкнути клієнта',
|
enableClient: 'Увімкнути клієнта',
|
||||||
noClients: 'Ще немає клієнтів.',
|
noClients: 'Ще немає клієнтів.',
|
||||||
|
noPrivKey: 'У цього клієнта немає відомого приватного ключа. Неможливо створити конфігурацію.',
|
||||||
showQR: 'Показати QR-код',
|
showQR: 'Показати QR-код',
|
||||||
downloadConfig: 'Завантажити конфігурацію',
|
downloadConfig: 'Завантажити конфігурацію',
|
||||||
madeBy: 'Зроблено',
|
madeBy: 'Зроблено',
|
||||||
donate: 'Пожертвувати',
|
donate: 'Пожертвувати',
|
||||||
|
toggleCharts: 'Показати/сховати діаграми',
|
||||||
|
theme: { dark: 'Темна тема', light: 'Світла тема', auto: 'Автоматична тема' },
|
||||||
|
restore: 'Відновити',
|
||||||
|
backup: 'Резервна копія',
|
||||||
|
titleRestoreConfig: 'Відновити конфігурацію',
|
||||||
|
titleBackupConfig: 'Створити резервну копію конфігурації',
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
name: 'Имя',
|
name: 'Имя',
|
||||||
@@ -78,10 +91,17 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
disableClient: 'Выключить клиента',
|
disableClient: 'Выключить клиента',
|
||||||
enableClient: 'Включить клиента',
|
enableClient: 'Включить клиента',
|
||||||
noClients: 'Пока нет клиентов.',
|
noClients: 'Пока нет клиентов.',
|
||||||
|
noPrivKey: 'Невозможно создать конфигурацию: у клиента нет известного приватного ключа.',
|
||||||
showQR: 'Показать QR-код',
|
showQR: 'Показать QR-код',
|
||||||
downloadConfig: 'Скачать конфигурацию',
|
downloadConfig: 'Скачать конфигурацию',
|
||||||
madeBy: 'Автор',
|
madeBy: 'Автор',
|
||||||
donate: 'Поблагодарить',
|
donate: 'Поблагодарить',
|
||||||
|
toggleCharts: 'Показать/скрыть графики',
|
||||||
|
theme: { dark: 'Темная тема', light: 'Светлая тема', auto: 'Как в системе' },
|
||||||
|
restore: 'Восстановить',
|
||||||
|
backup: 'Резервная копия',
|
||||||
|
titleRestoreConfig: 'Восстановить конфигурацию',
|
||||||
|
titleBackupConfig: 'Создать резервную копию конфигурации',
|
||||||
},
|
},
|
||||||
tr: { // Müslüm Barış Korkmazer @babico
|
tr: { // Müslüm Barış Korkmazer @babico
|
||||||
name: 'İsim',
|
name: 'İsim',
|
||||||
@@ -97,19 +117,25 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
deleteDialog2: 'Bu işlem geri alınamaz.',
|
deleteDialog2: 'Bu işlem geri alınamaz.',
|
||||||
cancel: 'İptal',
|
cancel: 'İptal',
|
||||||
create: 'Oluştur',
|
create: 'Oluştur',
|
||||||
createdAt: 'Şu saatte oluşturuldu: ',
|
createdOn: 'Şu saatte oluşturuldu: ',
|
||||||
lastSeen: 'Son görülme tarihi: ',
|
lastSeen: 'Son görülme tarihi: ',
|
||||||
totalDownload: 'Toplam İndirme: ',
|
totalDownload: 'Toplam İndirme: ',
|
||||||
totalUpload: 'Toplam Yükleme: ',
|
totalUpload: 'Toplam Yükleme: ',
|
||||||
newClient: 'Yeni Kullanıcı',
|
newClient: 'Yeni Kullanıcı',
|
||||||
disableClient: 'İstemciyi Devre Dışı Bırak',
|
disableClient: 'Kullanıcıyı Devre Dışı Bırak',
|
||||||
enableClient: 'İstemciyi Etkinleştir',
|
enableClient: 'Kullanıcıyı Etkinleştir',
|
||||||
noClients: 'Henüz kullanıcı yok.',
|
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',
|
showQR: 'QR Kodunu Göster',
|
||||||
downloadConfig: 'Yapılandırmayı İndir',
|
downloadConfig: 'Yapılandırmayı İndir',
|
||||||
madeBy: 'Yapan Kişi: ',
|
madeBy: 'Yapan Kişi: ',
|
||||||
donate: 'Bağış Yap',
|
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
|
no: { // github.com/digvalley
|
||||||
name: 'Navn',
|
name: 'Navn',
|
||||||
@@ -191,6 +217,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
downloadConfig: 'Télécharger la configuration',
|
downloadConfig: 'Télécharger la configuration',
|
||||||
madeBy: 'Développé par',
|
madeBy: 'Développé par',
|
||||||
donate: 'Soutenir',
|
donate: 'Soutenir',
|
||||||
|
restore: 'Restaurer',
|
||||||
|
backup: 'Sauvegarder',
|
||||||
|
titleRestoreConfig: 'Restaurer votre configuration',
|
||||||
|
titleBackupConfig: 'Sauvegarder votre configuration',
|
||||||
},
|
},
|
||||||
de: { // github.com/florian-asche
|
de: { // github.com/florian-asche
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -219,6 +249,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
downloadConfig: 'Konfiguration herunterladen',
|
downloadConfig: 'Konfiguration herunterladen',
|
||||||
madeBy: 'Erstellt von',
|
madeBy: 'Erstellt von',
|
||||||
donate: 'Spenden',
|
donate: 'Spenden',
|
||||||
|
restore: 'Wiederherstellen',
|
||||||
|
backup: 'Sichern',
|
||||||
|
titleRestoreConfig: 'Stelle deine Konfiguration wieder her',
|
||||||
|
titleBackupConfig: 'Sichere deine Konfiguration',
|
||||||
},
|
},
|
||||||
ca: { // github.com/guillembonet
|
ca: { // github.com/guillembonet
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
@@ -273,6 +307,12 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
downloadConfig: 'Descargar configuración',
|
downloadConfig: 'Descargar configuración',
|
||||||
madeBy: 'Hecho por',
|
madeBy: 'Hecho por',
|
||||||
donate: 'Donar',
|
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: {
|
ko: {
|
||||||
name: '이름',
|
name: '이름',
|
||||||
@@ -441,27 +481,27 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
password: '密碼',
|
password: '密碼',
|
||||||
signIn: '登入',
|
signIn: '登入',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
updateAvailable: '有新版本可用!',
|
updateAvailable: '有新版本可以使用!',
|
||||||
update: '更新',
|
update: '更新',
|
||||||
clients: '客戶',
|
clients: '使用者',
|
||||||
new: '新建',
|
new: '建立',
|
||||||
deleteClient: '刪除客戶',
|
deleteClient: '刪除使用者',
|
||||||
deleteDialog1: '您確定要刪除',
|
deleteDialog1: '您確定要刪除',
|
||||||
deleteDialog2: '此操作無法撤銷。',
|
deleteDialog2: '此作業無法復原。',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
create: '建立',
|
create: '建立',
|
||||||
createdOn: '建立於 ',
|
createdOn: '建立於 ',
|
||||||
lastSeen: '最後訪問於 ',
|
lastSeen: '最後存取於 ',
|
||||||
totalDownload: '總下載: ',
|
totalDownload: '總下載: ',
|
||||||
totalUpload: '總上傳: ',
|
totalUpload: '總上傳: ',
|
||||||
newClient: '新客戶',
|
newClient: '新用戶',
|
||||||
disableClient: '禁用客戶',
|
disableClient: '停用使用者',
|
||||||
enableClient: '啟用客戶',
|
enableClient: '啟用使用者',
|
||||||
noClients: '目前沒有客戶。',
|
noClients: '目前沒有使用者。',
|
||||||
showQR: '顯示二維碼',
|
showQR: '顯示 QR Code',
|
||||||
downloadConfig: '下載配置',
|
downloadConfig: '下載 Config 檔',
|
||||||
madeBy: '由',
|
madeBy: '由',
|
||||||
donate: '捐贈',
|
donate: '抖內',
|
||||||
},
|
},
|
||||||
it: {
|
it: {
|
||||||
name: 'Nome',
|
name: 'Nome',
|
||||||
@@ -489,6 +529,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
downloadConfig: 'Scarica configurazione',
|
downloadConfig: 'Scarica configurazione',
|
||||||
madeBy: 'Realizzato da',
|
madeBy: 'Realizzato da',
|
||||||
donate: 'Donazione',
|
donate: 'Donazione',
|
||||||
|
restore: 'Ripristina',
|
||||||
|
backup: 'Backup',
|
||||||
|
titleRestoreConfig: 'Ripristina la tua configurazione',
|
||||||
|
titleBackupConfig: 'Esegui il backup della tua configurazione',
|
||||||
},
|
},
|
||||||
th: {
|
th: {
|
||||||
name: 'ชื่อ',
|
name: 'ชื่อ',
|
||||||
@@ -517,4 +561,32 @@ const messages = { // eslint-disable-line no-unused-vars
|
|||||||
madeBy: 'สร้างโดย',
|
madeBy: 'สร้างโดย',
|
||||||
donate: 'บริจาค',
|
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