Version 14: Home Assistent support, PASSWORD_HASH (inc. Helper), translation updates, bugfixes and more (#1199)
This commit is contained in:
+3
-1
@@ -1,2 +1,4 @@
|
||||
# Copyright (c) Emile Nijssen
|
||||
# Copyright (c) Emile Nijssen (WeeJeWel)
|
||||
# Founder and Codeowner of WireGuard Easy (wg-easy)
|
||||
# Maintained by Philip Heiduck (pheiduck)
|
||||
* @pheiduck
|
||||
|
||||
@@ -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.
|
||||
@@ -2,7 +2,6 @@ name: Build & Publish Development
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -31,7 +30,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set environment variables
|
||||
run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV
|
||||
run: echo RELEASE=$(cat ./src/package.json | jq -r .release | jq -r .version) >> $GITHUB_ENV
|
||||
|
||||
- name: Build & Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
@@ -21,9 +21,6 @@ jobs:
|
||||
node-version: '20'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
src/package-lock.json
|
||||
|
||||
- name: npm run lint
|
||||
run: |
|
||||
|
||||
@@ -23,9 +23,6 @@ jobs:
|
||||
node-version: '20'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
src/package-lock.json
|
||||
|
||||
- name: Bot 🤖 "Updating NPM Packages..."
|
||||
run: |
|
||||
|
||||
@@ -26,6 +26,10 @@ COPY --from=build_node_modules /app /app
|
||||
# than what runs inside of docker.
|
||||
COPY --from=build_node_modules /node_modules /node_modules
|
||||
|
||||
# Copy the needed wg-password scripts
|
||||
COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw
|
||||
RUN chmod +x /bin/wgpw
|
||||
|
||||
# Install Linux packages
|
||||
RUN apk add --no-cache \
|
||||
dpkg \
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# wg-password
|
||||
|
||||
`wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `wg-easy`, enhancing security by requiring passwords.
|
||||
|
||||
## Features
|
||||
|
||||
- Generate bcrypt password hashes.
|
||||
- Easily integrate with `wg-easy` to enforce password requirements.
|
||||
|
||||
## Usage with Docker
|
||||
|
||||
To generate a bcrypt password hash using docker, run the following command :
|
||||
|
||||
```sh
|
||||
docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
|
||||
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
|
||||
```
|
||||
|
||||
*Important* : make sure to enclose your password in single quotes when you run `docker run` command :
|
||||
|
||||
```bash
|
||||
$ echo $2b$12$coPqCsPtcF
|
||||
b2
|
||||
$ echo "$2b$12$coPqCsPtcF"
|
||||
b2
|
||||
$ echo '$2b$12$coPqCsPtcF'
|
||||
$2b$12$coPqCsPtcF
|
||||
```
|
||||
@@ -32,7 +32,8 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||
|
||||
## Versions
|
||||
|
||||
We provide more then 1 docker image to get, this will help you decide which one is best for you.
|
||||
We provide more then 1 docker image to get, this will help you decide which one is best for you. <br>
|
||||
For **stable** versions instead of nightly or development please read **README** from the **production** branch!
|
||||
|
||||
| tag | Branch | Example | Description |
|
||||
| - | - | - | - |
|
||||
@@ -64,7 +65,7 @@ To automatically install & run wg-easy, simply run:
|
||||
--name=wg-easy \
|
||||
-e LANG=de \
|
||||
-e WG_HOST=<🚨YOUR_SERVER_IP> \
|
||||
-e PASSWORD=<🚨YOUR_ADMIN_PASSWORD> \
|
||||
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
|
||||
-e PORT=51821 \
|
||||
-e WG_PORT=51820 \
|
||||
-v ~/.wg-easy:/etc/wireguard \
|
||||
@@ -80,7 +81,7 @@ To automatically install & run wg-easy, simply run:
|
||||
|
||||
> 💡 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`.
|
||||
|
||||
@@ -98,26 +99,27 @@ Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/W
|
||||
|
||||
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
|
||||
|
||||
| Env | Default | Example | Description |
|
||||
| - | - | - | - |
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `PASSWORD` | - | `foobar123` | When set, requires a password when logging in to the Web UI. |
|
||||
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
|
||||
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
|
||||
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
|
||||
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
|
||||
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
|
||||
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
|
||||
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
|
||||
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
|
||||
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
|
||||
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
|
||||
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
|
||||
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
|
||||
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
|
||||
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
||||
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
|
||||
| Env | Default | Example | Description |
|
||||
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
|
||||
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
|
||||
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
|
||||
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
|
||||
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
|
||||
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
|
||||
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
|
||||
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
|
||||
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
|
||||
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
|
||||
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
|
||||
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
|
||||
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
|
||||
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
|
||||
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
|
||||
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
||||
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
|
||||
|
||||
> If you change `WG_PORT`, make sure to also change the exposed port.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
@@ -1,9 +1,17 @@
|
||||
services:
|
||||
wg-easy:
|
||||
image: wg-easy
|
||||
build:
|
||||
dockerfile: ./Dockerfile
|
||||
command: npm run serve
|
||||
volumes:
|
||||
- ./src/:/app/
|
||||
# - ./data/:/etc/wireguard
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
- "51821:51821/tcp"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
environment:
|
||||
# - PASSWORD=p
|
||||
- WG_HOST=192.168.1.233
|
||||
|
||||
+3
-1
@@ -12,9 +12,10 @@ services:
|
||||
- WG_HOST=raspberrypi.local
|
||||
|
||||
# Optional:
|
||||
# - PASSWORD=foobar123
|
||||
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
|
||||
# - PORT=51821
|
||||
# - WG_PORT=51820
|
||||
# - WG_CONFIG_PORT=92820
|
||||
# - WG_DEFAULT_ADDRESS=10.8.0.x
|
||||
# - WG_DEFAULT_DNS=1.1.1.1
|
||||
# - WG_MTU=1420
|
||||
@@ -38,6 +39,7 @@ services:
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
# - NET_RAW # ⚠️ Uncomment if using Podman
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"1": "Initial version. Enjoy!",
|
||||
"2": "You can now rename a client, and update the address. Enjoy!",
|
||||
"2": "You can now rename a client & update the address. Enjoy!",
|
||||
"3": "Many improvements and small changes. Enjoy!",
|
||||
"4": "Now with pretty charts for client's network speed. Enjoy!",
|
||||
"5": "Many small improvements & feature requests. Enjoy!",
|
||||
@@ -11,5 +11,6 @@
|
||||
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
||||
"11": "Multilanguage Support & various bugfixes.",
|
||||
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more.",
|
||||
"13": "New framework (h3), UI_CHART_TYPE, some bugfixes and more."
|
||||
"13": "New framework (h3), UI_CHART_TYPE, some bugfixes & more.",
|
||||
"14": "Home Assistent support, PASSWORD_HASH (inc. Helper), translation updates bugfixes & more."
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .",
|
||||
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
|
||||
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -1,15 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const { release } = require('./package.json');
|
||||
const { release: { version } } = require('./package.json');
|
||||
|
||||
module.exports.RELEASE = release;
|
||||
module.exports.RELEASE = version;
|
||||
module.exports.PORT = process.env.PORT || '51821';
|
||||
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
|
||||
module.exports.PASSWORD = process.env.PASSWORD;
|
||||
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
|
||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
||||
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
|
||||
module.exports.WG_HOST = process.env.WG_HOST;
|
||||
module.exports.WG_PORT = process.env.WG_PORT || '51820';
|
||||
module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820';
|
||||
module.exports.WG_MTU = process.env.WG_MTU || null;
|
||||
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0';
|
||||
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x';
|
||||
|
||||
+57
-7
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('node:crypto');
|
||||
const { createServer } = require('node:http');
|
||||
const { stat, readFile } = require('node:fs/promises');
|
||||
@@ -27,12 +28,34 @@ const {
|
||||
PORT,
|
||||
WEBUI_HOST,
|
||||
RELEASE,
|
||||
PASSWORD,
|
||||
PASSWORD_HASH,
|
||||
LANG,
|
||||
UI_TRAFFIC_STATS,
|
||||
UI_CHART_TYPE,
|
||||
} = require('../config');
|
||||
|
||||
const requiresPassword = !!PASSWORD_HASH;
|
||||
|
||||
/**
|
||||
* Checks if `password` matches the PASSWORD_HASH.
|
||||
*
|
||||
* If environment variable is not set, the password is always invalid.
|
||||
*
|
||||
* @param {string} password String to test
|
||||
* @returns {boolean} true if matching environment, otherwise false
|
||||
*/
|
||||
const isPasswordValid = (password) => {
|
||||
if (typeof password !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PASSWORD_HASH) {
|
||||
return bcrypt.compareSync(password, PASSWORD_HASH);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = class Server {
|
||||
|
||||
constructor() {
|
||||
@@ -71,7 +94,6 @@ module.exports = class Server {
|
||||
|
||||
// Authentication
|
||||
.get('/api/session', defineEventHandler((event) => {
|
||||
const requiresPassword = !!process.env.PASSWORD;
|
||||
const authenticated = requiresPassword
|
||||
? !!(event.node.req.session && event.node.req.session.authenticated)
|
||||
: true;
|
||||
@@ -84,14 +106,16 @@ module.exports = class Server {
|
||||
.post('/api/session', defineEventHandler(async (event) => {
|
||||
const { password } = await readBody(event);
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
if (!requiresPassword) {
|
||||
// if no password is required, the API should never be called.
|
||||
// Do not automatically authenticate the user.
|
||||
throw createError({
|
||||
status: 401,
|
||||
message: 'Missing: Password',
|
||||
message: 'Invalid state',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== PASSWORD) {
|
||||
if (!isPasswordValid(password)) {
|
||||
throw createError({
|
||||
status: 401,
|
||||
message: 'Incorrect Password',
|
||||
@@ -103,13 +127,13 @@ module.exports = class Server {
|
||||
|
||||
debug(`New Session: ${event.node.req.session.id}`);
|
||||
|
||||
return { succcess: true };
|
||||
return { success: true };
|
||||
}));
|
||||
|
||||
// WireGuard
|
||||
app.use(
|
||||
fromNodeMiddleware((req, res, next) => {
|
||||
if (!PASSWORD || !req.url.startsWith('/api/')) {
|
||||
if (!requiresPassword || !req.url.startsWith('/api/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -117,6 +141,15 @@ module.exports = class Server {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.url.startsWith('/api/') && req.headers['authorization']) {
|
||||
if (isPasswordValid(req.headers['authorization'])) {
|
||||
return next();
|
||||
}
|
||||
return res.status(401).json({
|
||||
error: 'Incorrect Password',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
error: 'Not Logged In',
|
||||
});
|
||||
@@ -225,6 +258,23 @@ module.exports = class Server {
|
||||
});
|
||||
};
|
||||
|
||||
// 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(
|
||||
|
||||
+76
-46
@@ -13,6 +13,7 @@ const {
|
||||
WG_PATH,
|
||||
WG_HOST,
|
||||
WG_PORT,
|
||||
WG_CONFIG_PORT,
|
||||
WG_MTU,
|
||||
WG_DEFAULT_DNS,
|
||||
WG_DEFAULT_ADDRESS,
|
||||
@@ -26,54 +27,60 @@ const {
|
||||
|
||||
module.exports = class WireGuard {
|
||||
|
||||
async __buildConfig() {
|
||||
this.__configPromise = Promise.resolve().then(async () => {
|
||||
if (!WG_HOST) {
|
||||
throw new Error('WG_HOST Environment Variable Not Set!');
|
||||
}
|
||||
|
||||
debug('Loading configuration...');
|
||||
let config;
|
||||
try {
|
||||
config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
|
||||
config = JSON.parse(config);
|
||||
debug('Configuration loaded.');
|
||||
} catch (err) {
|
||||
const privateKey = await Util.exec('wg genkey');
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
|
||||
|
||||
config = {
|
||||
server: {
|
||||
privateKey,
|
||||
publicKey,
|
||||
address,
|
||||
},
|
||||
clients: {},
|
||||
};
|
||||
debug('Configuration generated.');
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
return this.__configPromise;
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
if (!this.__configPromise) {
|
||||
this.__configPromise = Promise.resolve().then(async () => {
|
||||
if (!WG_HOST) {
|
||||
throw new Error('WG_HOST Environment Variable Not Set!');
|
||||
const config = await this.__buildConfig();
|
||||
|
||||
await this.__saveConfig(config);
|
||||
await Util.exec('wg-quick down wg0').catch(() => {});
|
||||
await Util.exec('wg-quick up wg0').catch((err) => {
|
||||
if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
|
||||
throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
|
||||
}
|
||||
|
||||
debug('Loading configuration...');
|
||||
let config;
|
||||
try {
|
||||
config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
|
||||
config = JSON.parse(config);
|
||||
debug('Configuration loaded.');
|
||||
} catch (err) {
|
||||
const privateKey = await Util.exec('wg genkey');
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
|
||||
|
||||
config = {
|
||||
server: {
|
||||
privateKey,
|
||||
publicKey,
|
||||
address,
|
||||
},
|
||||
clients: {},
|
||||
};
|
||||
debug('Configuration generated.');
|
||||
}
|
||||
|
||||
await this.__saveConfig(config);
|
||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
||||
await Util.exec('wg-quick up wg0').catch((err) => {
|
||||
if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
|
||||
throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
// await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`);
|
||||
// await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
|
||||
await this.__syncConfig();
|
||||
|
||||
return config;
|
||||
throw err;
|
||||
});
|
||||
// await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`);
|
||||
// await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
|
||||
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
|
||||
await this.__syncConfig();
|
||||
}
|
||||
|
||||
return this.__configPromise;
|
||||
@@ -207,7 +214,7 @@ PublicKey = ${config.server.publicKey}
|
||||
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
|
||||
}AllowedIPs = ${WG_ALLOWED_IPS}
|
||||
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
|
||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
|
||||
}
|
||||
|
||||
async getClientQRCodeSVG({ clientId }) {
|
||||
@@ -226,7 +233,9 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
const config = await this.getConfig();
|
||||
|
||||
const privateKey = await Util.exec('wg genkey');
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
|
||||
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
|
||||
log: 'echo ***hidden*** | wg pubkey',
|
||||
});
|
||||
const preSharedKey = await Util.exec('wg genpsk');
|
||||
|
||||
// Calculate next IP
|
||||
@@ -318,9 +327,30 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
async __reloadConfig() {
|
||||
await this.__buildConfig();
|
||||
await this.__syncConfig();
|
||||
}
|
||||
|
||||
async restoreConfiguration(config) {
|
||||
debug('Starting configuration restore process.');
|
||||
const _config = JSON.parse(config);
|
||||
await this.__saveConfig(_config);
|
||||
await this.__reloadConfig();
|
||||
debug('Configuration restore process completed.');
|
||||
}
|
||||
|
||||
async backupConfiguration() {
|
||||
debug('Starting configuration backup.');
|
||||
const config = await this.getConfig();
|
||||
const backup = JSON.stringify(config, null, 2);
|
||||
debug('Configuration backup completed.');
|
||||
return backup;
|
||||
}
|
||||
|
||||
// Shutdown wireguard
|
||||
async Shutdown() {
|
||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
||||
await Util.exec('wg-quick down wg0').catch(() => {});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Generated
+571
-169
File diff suppressed because it is too large
Load Diff
+10
-7
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"release": "13",
|
||||
"release": {
|
||||
"version": "14"
|
||||
},
|
||||
"name": "wg-easy",
|
||||
"version": "1.0.1",
|
||||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
||||
@@ -11,17 +13,18 @@
|
||||
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
|
||||
},
|
||||
"author": "Emile Nijssen",
|
||||
"license": "GPL",
|
||||
"license": "CC BY-NC-SA 4.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"debug": "^4.3.6",
|
||||
"express-session": "^1.18.0",
|
||||
"h3": "^1.11.1",
|
||||
"qrcode": "^1.5.3"
|
||||
"h3": "^1.12.0",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-athom": "^3.1.3",
|
||||
"nodemon": "^3.1.1",
|
||||
"tailwindcss": "^3.4.3"
|
||||
"nodemon": "^3.1.4",
|
||||
"tailwindcss": "^3.4.9"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
||||
@@ -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 "$@"
|
||||
+59
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.4.9 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -734,10 +734,6 @@ video {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.mt-0\.5 {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
@@ -802,6 +798,11 @@ video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.size-6 {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-1 {
|
||||
height: 0.25rem;
|
||||
}
|
||||
@@ -846,6 +847,10 @@ video {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
@@ -1041,6 +1046,16 @@ video {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-l-full {
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
}
|
||||
|
||||
.rounded-r-full {
|
||||
border-top-right-radius: 9999px;
|
||||
border-bottom-right-radius: 9999px;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -1454,6 +1469,10 @@ video {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.hover\:cursor-pointer:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover\:border-red-800:hover {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||
@@ -1525,6 +1544,25 @@ video {
|
||||
fill: #4b5563;
|
||||
}
|
||||
|
||||
@media not all and (min-width: 768px) {
|
||||
.max-md\:hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.max-md\:border-x-0 {
|
||||
border-left-width: 0px;
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
.max-md\:border-l-0 {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.max-md\:border-r-0 {
|
||||
border-right-width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.xxs\:flex-row {
|
||||
flex-direction: row;
|
||||
@@ -1652,6 +1690,14 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.md\:block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.md\:inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -1660,10 +1706,18 @@ video {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.md\:flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.md\:gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.md\:rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.md\:px-0 {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
|
||||
+29
-7
@@ -10,11 +10,15 @@
|
||||
<link rel="apple-touch-icon" href="./img/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
</head>
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
.line-chart .apexcharts-svg{
|
||||
transform: translateY(3px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-neutral-800">
|
||||
@@ -90,15 +94,33 @@
|
||||
<div class="flex-grow">
|
||||
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex md:block md:flex-shrink-0">
|
||||
<!-- Restore configuration -->
|
||||
<label for="inputRC" :title="$t('titleRestoreConfig')"
|
||||
class="hover:cursor-pointer hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-r-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-l-full md:rounded inline-flex items-center transition">
|
||||
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path>
|
||||
</svg>
|
||||
<span class="max-md:hidden text-sm">{{$t("restore")}}</span>
|
||||
<input id="inputRC" type="file" name="configurationfile" accept="text/*,.json" @change="restoreConfig" class="hidden"/>
|
||||
</label>
|
||||
<!-- Backup configuration -->
|
||||
<a href="./api/wireguard/backup" :title="$t('titleBackupConfig')"
|
||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition">
|
||||
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"></path>
|
||||
</svg>
|
||||
<span class="max-md:hidden text-sm">{{$t("backup")}}</span>
|
||||
</a>
|
||||
<!-- New client -->
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded inline-flex items-center transition">
|
||||
<svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-r-full md:rounded inline-flex items-center transition">
|
||||
<svg class="w-4 md:mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">{{$t("new")}}</span>
|
||||
<span class="max-md:hidden text-sm">{{$t("new")}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,11 +131,11 @@
|
||||
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid">
|
||||
|
||||
<!-- Chart -->
|
||||
<div v-if="uiChartType" class="absolute z-0 bottom-0 left-0 right-0 h-6" >
|
||||
<div v-if="uiChartType" :class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" >
|
||||
<apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries">
|
||||
</apexchart>
|
||||
</div>
|
||||
<div v-if="uiChartType" class="absolute z-0 top-0 left-0 right-0 h-6" >
|
||||
<div v-if="uiChartType" :class="`absolute z-0 top-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" >
|
||||
<apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries"
|
||||
style="transform: scaleY(-1);">
|
||||
</apexchart>
|
||||
@@ -538,7 +560,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password" :placeholder="$t('password')" v-model="password"
|
||||
<input type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
|
||||
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
|
||||
|
||||
<button v-if="authenticating"
|
||||
|
||||
@@ -138,4 +138,12 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
async restoreConfiguration(file) {
|
||||
return this.call({
|
||||
method: 'put',
|
||||
path: '/wireguard/restore',
|
||||
body: { file },
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -299,6 +299,22 @@ new Vue({
|
||||
.catch((err) => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
},
|
||||
restoreConfig(e) {
|
||||
e.preventDefault();
|
||||
const file = e.currentTarget.files.item(0);
|
||||
if (file) {
|
||||
file.text()
|
||||
.then((content) => {
|
||||
this.api.restoreConfiguration(content)
|
||||
.then((_result) => alert('The configuration was updated.'))
|
||||
.catch((err) => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
})
|
||||
.catch((err) => alert(err.message || err.toString()));
|
||||
} else {
|
||||
alert('Failed to load your file!');
|
||||
}
|
||||
},
|
||||
toggleTheme() {
|
||||
const themes = ['light', 'dark', 'auto'];
|
||||
const currentIndex = themes.indexOf(this.uiTheme);
|
||||
|
||||
+57
-17
@@ -30,6 +30,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
donate: 'Donate',
|
||||
toggleCharts: 'Show/hide Charts',
|
||||
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
|
||||
restore: 'Restore',
|
||||
backup: 'Backup',
|
||||
titleRestoreConfig: 'Restore your configuration',
|
||||
titleBackupConfig: 'Backup your configuration',
|
||||
},
|
||||
ua: {
|
||||
name: 'Ім`я',
|
||||
@@ -53,10 +57,17 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
disableClient: 'Вимкнути клієнта',
|
||||
enableClient: 'Увімкнути клієнта',
|
||||
noClients: 'Ще немає клієнтів.',
|
||||
noPrivKey: 'У цього клієнта немає відомого приватного ключа. Неможливо створити конфігурацію.',
|
||||
showQR: 'Показати QR-код',
|
||||
downloadConfig: 'Завантажити конфігурацію',
|
||||
madeBy: 'Зроблено',
|
||||
donate: 'Пожертвувати',
|
||||
toggleCharts: 'Показати/сховати діаграми',
|
||||
theme: { dark: 'Темна тема', light: 'Світла тема', auto: 'Автоматична тема' },
|
||||
restore: 'Відновити',
|
||||
backup: 'Резервна копія',
|
||||
titleRestoreConfig: 'Відновити конфігурацію',
|
||||
titleBackupConfig: 'Створити резервну копію конфігурації',
|
||||
},
|
||||
ru: {
|
||||
name: 'Имя',
|
||||
@@ -80,10 +91,17 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
disableClient: 'Выключить клиента',
|
||||
enableClient: 'Включить клиента',
|
||||
noClients: 'Пока нет клиентов.',
|
||||
noPrivKey: 'Невозможно создать конфигурацию: у клиента нет известного приватного ключа.',
|
||||
showQR: 'Показать QR-код',
|
||||
downloadConfig: 'Скачать конфигурацию',
|
||||
madeBy: 'Автор',
|
||||
donate: 'Поблагодарить',
|
||||
toggleCharts: 'Показать/скрыть графики',
|
||||
theme: { dark: 'Темная тема', light: 'Светлая тема', auto: 'Как в системе' },
|
||||
restore: 'Восстановить',
|
||||
backup: 'Резервная копия',
|
||||
titleRestoreConfig: 'Восстановить конфигурацию',
|
||||
titleBackupConfig: 'Создать резервную копию конфигурации',
|
||||
},
|
||||
tr: { // Müslüm Barış Korkmazer @babico
|
||||
name: 'İsim',
|
||||
@@ -99,19 +117,25 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
deleteDialog2: 'Bu işlem geri alınamaz.',
|
||||
cancel: 'İptal',
|
||||
create: 'Oluştur',
|
||||
createdAt: 'Şu saatte oluşturuldu: ',
|
||||
createdOn: 'Şu saatte oluşturuldu: ',
|
||||
lastSeen: 'Son görülme tarihi: ',
|
||||
totalDownload: 'Toplam İndirme: ',
|
||||
totalUpload: 'Toplam Yükleme: ',
|
||||
newClient: 'Yeni Kullanıcı',
|
||||
disableClient: 'İstemciyi Devre Dışı Bırak',
|
||||
enableClient: 'İstemciyi Etkinleştir',
|
||||
disableClient: 'Kullanıcıyı Devre Dışı Bırak',
|
||||
enableClient: 'Kullanıcıyı Etkinleştir',
|
||||
noClients: 'Henüz kullanıcı yok.',
|
||||
noPrivKey: 'Bu istemcinin bilinen bir özel anahtarı yok. Yapılandırma oluşturulamıyor.',
|
||||
showQR: 'QR Kodunu Göster',
|
||||
downloadConfig: 'Yapılandırmayı İndir',
|
||||
madeBy: 'Yapan Kişi: ',
|
||||
donate: 'Bağış Yap',
|
||||
changeLang: 'Dil Değiştir',
|
||||
toggleCharts: 'Grafiği göster/gizle',
|
||||
theme: { dark: 'Karanlık tema', light: 'Açık tema', auto: 'Otomatik tema' },
|
||||
restore: 'Geri yükle',
|
||||
backup: 'Yedekle',
|
||||
titleRestoreConfig: 'Yapılandırmanızı geri yükleyin',
|
||||
titleBackupConfig: 'Yapılandırmanızı yedekleyin',
|
||||
},
|
||||
no: { // github.com/digvalley
|
||||
name: 'Navn',
|
||||
@@ -193,6 +217,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Télécharger la configuration',
|
||||
madeBy: 'Développé par',
|
||||
donate: 'Soutenir',
|
||||
restore: 'Restaurer',
|
||||
backup: 'Sauvegarder',
|
||||
titleRestoreConfig: 'Restaurer votre configuration',
|
||||
titleBackupConfig: 'Sauvegarder votre configuration',
|
||||
},
|
||||
de: { // github.com/florian-asche
|
||||
name: 'Name',
|
||||
@@ -221,6 +249,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Konfiguration herunterladen',
|
||||
madeBy: 'Erstellt von',
|
||||
donate: 'Spenden',
|
||||
restore: 'Wiederherstellen',
|
||||
backup: 'Sichern',
|
||||
titleRestoreConfig: 'Stelle deine Konfiguration wieder her',
|
||||
titleBackupConfig: 'Sichere deine Konfiguration',
|
||||
},
|
||||
ca: { // github.com/guillembonet
|
||||
name: 'Nom',
|
||||
@@ -277,6 +309,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
donate: 'Donar',
|
||||
toggleCharts: 'Mostrar/Ocultar gráficos',
|
||||
theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' },
|
||||
restore: 'Restaurar',
|
||||
backup: 'Realizar copia de seguridad',
|
||||
titleRestoreConfig: 'Restaurar su configuración',
|
||||
titleBackupConfig: 'Realizar copia de seguridad de su configuración',
|
||||
},
|
||||
ko: {
|
||||
name: '이름',
|
||||
@@ -445,27 +481,27 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
password: '密碼',
|
||||
signIn: '登入',
|
||||
logout: '登出',
|
||||
updateAvailable: '有新版本可用!',
|
||||
updateAvailable: '有新版本可以使用!',
|
||||
update: '更新',
|
||||
clients: '客戶',
|
||||
new: '新建',
|
||||
deleteClient: '刪除客戶',
|
||||
clients: '使用者',
|
||||
new: '建立',
|
||||
deleteClient: '刪除使用者',
|
||||
deleteDialog1: '您確定要刪除',
|
||||
deleteDialog2: '此操作無法撤銷。',
|
||||
deleteDialog2: '此作業無法復原。',
|
||||
cancel: '取消',
|
||||
create: '建立',
|
||||
createdOn: '建立於 ',
|
||||
lastSeen: '最後訪問於 ',
|
||||
lastSeen: '最後存取於 ',
|
||||
totalDownload: '總下載: ',
|
||||
totalUpload: '總上傳: ',
|
||||
newClient: '新客戶',
|
||||
disableClient: '禁用客戶',
|
||||
enableClient: '啟用客戶',
|
||||
noClients: '目前沒有客戶。',
|
||||
showQR: '顯示二維碼',
|
||||
downloadConfig: '下載配置',
|
||||
newClient: '新用戶',
|
||||
disableClient: '停用使用者',
|
||||
enableClient: '啟用使用者',
|
||||
noClients: '目前沒有使用者。',
|
||||
showQR: '顯示 QR Code',
|
||||
downloadConfig: '下載 Config 檔',
|
||||
madeBy: '由',
|
||||
donate: '捐贈',
|
||||
donate: '抖內',
|
||||
},
|
||||
it: {
|
||||
name: 'Nome',
|
||||
@@ -493,6 +529,10 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||
downloadConfig: 'Scarica configurazione',
|
||||
madeBy: 'Realizzato da',
|
||||
donate: 'Donazione',
|
||||
restore: 'Ripristina',
|
||||
backup: 'Backup',
|
||||
titleRestoreConfig: 'Ripristina la tua configurazione',
|
||||
titleBackupConfig: 'Esegui il backup della tua configurazione',
|
||||
},
|
||||
th: {
|
||||
name: 'ชื่อ',
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user