mirror of
https://github.com/WeeJeWel/wg-easy.git
synced 2024-12-23 01:29:24 +08:00
Merge branch 'master' into add-WG_MTU
This commit is contained in:
commit
8e93ae76a5
11
.github/workflows/deploy.yml
vendored
11
.github/workflows/deploy.yml
vendored
@ -19,8 +19,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
- uses: benjlevesque/short-sha@v1.2
|
ref: production
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
@ -35,10 +35,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
- run: echo GH_BRANCH=${GITHUB_REF#refs/heads/} >> $GITHUB_ENV
|
- run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV
|
||||||
- run: echo VERSION=$(cat package.json | jq -r .version) >> $GITHUB_ENV
|
|
||||||
- run: echo DOCKER_TAGS=$(cat package.json | jq -r .docker[\"$GH_BRANCH\"].tags) >> $GITHUB_ENV
|
|
||||||
- run: echo Branch ${{ env.GH_BRANCH }}, Tags ${{ env.DOCKER_TAGS }}
|
|
||||||
|
|
||||||
# Build & Publish
|
# Build & Publish
|
||||||
- name: Build & Publish Docker Image
|
- name: Build & Publish Docker Image
|
||||||
@ -46,4 +43,4 @@ jobs:
|
|||||||
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: weejewel/wg-easy:v${{ env.VERSION }}, weejewel/wg-easy:${{ env.DOCKER_TAGS }}
|
tags: weejewel/wg-easy:latest, weejewel/wg-easy:${{ env.RELEASE }}
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
/config
|
/config
|
||||||
|
/wg0.conf
|
||||||
|
/wg0.json
|
@ -1,12 +1,14 @@
|
|||||||
FROM node:14-alpine
|
FROM docker.io/library/node:14-alpine@sha256:dc92f36e7cd917816fa2df041d4e9081453366381a00f40398d99e9392e78664
|
||||||
|
|
||||||
# Install Linux packages
|
# Install Linux packages
|
||||||
RUN apk add -U wireguard-tools
|
RUN apk add -U --no-cache wireguard-tools dumb-init
|
||||||
|
|
||||||
# Copy Web UI
|
# Copy Web UI
|
||||||
COPY src/ /app/
|
COPY src/ /app/
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN npm ci --production
|
RUN npm ci --production
|
||||||
|
RUN npm i -g nodemon
|
||||||
|
RUN mv /app/node_modules/ /node_modules/
|
||||||
|
|
||||||
# Expose Ports
|
# Expose Ports
|
||||||
EXPOSE 51820/udp
|
EXPOSE 51820/udp
|
||||||
@ -16,4 +18,4 @@ EXPOSE 51821/tcp
|
|||||||
ENV DEBUG=Server,WireGuard
|
ENV DEBUG=Server,WireGuard
|
||||||
|
|
||||||
# Run Web UI
|
# Run Web UI
|
||||||
CMD ["node", "server.js"]
|
CMD ["/usr/bin/dumb-init", "node", "server.js"]
|
||||||
|
11
LICENSE.md
Normal file
11
LICENSE.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
**You may:**
|
||||||
|
|
||||||
|
* Use this software for yourself;
|
||||||
|
* Use this software for a company;
|
||||||
|
* Modify this software, as long as you:
|
||||||
|
* Publish the changes on GitHub as an open-source & linked fork;
|
||||||
|
* Don't remove any links to the original project or donation pages;
|
||||||
|
|
||||||
|
**You may not:**
|
||||||
|
|
||||||
|
* Use this software in a commercial product without a license from the original author;
|
82
README.md
82
README.md
@ -9,19 +9,25 @@
|
|||||||
You have found the easiest way to install & manage WireGuard on any Linux host!
|
You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./assets/screenshot.png" width="702" />
|
<img src="./assets/screenshot.png" width="802" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* All-in-one: WireGuard + Web UI.
|
* All-in-one: WireGuard + Web UI.
|
||||||
* Easy installation, simple to use.
|
* Easy installation, simple to use.
|
||||||
* List, create, delete, enable & disable clients.
|
* List, create, edit, delete, enable & disable clients.
|
||||||
* Show a client's QR code.
|
* Show a client's QR code.
|
||||||
* Download a client's configuration file.
|
* Download a client's configuration file.
|
||||||
* Statistics for which clients are connected.
|
* Statistics for which clients are connected.
|
||||||
|
* Tx/Rx charts for each connected client.
|
||||||
* Gravatar support.
|
* Gravatar support.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* A host with a kernel that supports WireGuard (all modern kernels).
|
||||||
|
* A host with Docker installed.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### 1. Install Docker
|
### 1. Install Docker
|
||||||
@ -30,49 +36,69 @@ 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 sh get-docker.sh
|
|
||||||
$ sudo usermod -aG docker $(whoami)
|
$ sudo usermod -aG docker $(whoami)
|
||||||
$ bash
|
$ exit
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Configure WireGuard
|
And log in again.
|
||||||
|
|
||||||
Run these commands to prepare and configure WireGuard.
|
### 2. Run WireGuard Easy
|
||||||
|
|
||||||
```bash
|
To automatically install & run wg-easy, simply run:
|
||||||
$ mkdir ~/.wg-easy
|
|
||||||
$ cd ~/.wg-easy
|
|
||||||
$ wget https://raw.githubusercontent.com/WeeJeWel/wg-easy/master/docker-compose.yml
|
|
||||||
$ vim docker-compose.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Change `WG_HOST=raspberrypi.local` to your server's public address, e.g. `WG_HOST=vpn.mydomain.com`.
|
<pre>
|
||||||
|
$ docker run -d \
|
||||||
|
--name=wg-easy \
|
||||||
|
-e WG_HOST=<b>🚨YOUR_SERVER_IP</b> \
|
||||||
|
-e PASSWORD=<b>🚨YOUR_ADMIN_PASSWORD</b> \
|
||||||
|
-v ~/.wg-easy:/etc/wireguard \
|
||||||
|
-p 51820:51820/udp \
|
||||||
|
-p 51821:51821/tcp \
|
||||||
|
--cap-add=NET_ADMIN \
|
||||||
|
--cap-add=SYS_MODULE \
|
||||||
|
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
|
||||||
|
--sysctl="net.ipv4.ip_forward=1" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
weejewel/wg-easy
|
||||||
|
</pre>
|
||||||
|
|
||||||
Optionally, set a Web UI password by uncommenting `PASSWORD=foobar123` and change the password.
|
> 💡 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.
|
||||||
|
|
||||||
> By default, any WireGuard client will have access to the Web UI, unless you set a password.
|
The Web UI will now be available on `http://0.0.0.0:51821`.
|
||||||
|
|
||||||
### 3. Run WireGuard
|
> 💡 Your configuration files will be saved in `~/.wg-easy`
|
||||||
|
|
||||||
Finally, run WireGuard. It will automatically start after a reboot.
|
### 3. Sponsor
|
||||||
|
|
||||||
```bash
|
Are you enjoying this project? [Buy me a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
||||||
$ docker-compose up --detach
|
|
||||||
```
|
|
||||||
|
|
||||||
The Web UI will be available on `http://0.0.0.0:51821`. You can create new clients there.
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
These options can be configured in `docker-compose.yml` under `environment`.
|
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 |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `PASSWORD` | - | `foobar123` | When set, requires a password when logging in to the Web UI. |
|
| `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_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
|
||||||
| `WG_PORT` | `51820` | `51820` | The public UDP port of your VPN server |
|
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will always listen on `51820` inside the Docker container. |
|
||||||
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range |
|
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
|
||||||
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use |
|
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. |
|
||||||
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. (Server uses default WG MTU) |
|
| `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. |
|
||||||
|
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
|
||||||
|
|
||||||
> 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.
|
||||||
|
|
||||||
|
# Updating
|
||||||
|
|
||||||
|
To update to the latest version, simply run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop wg-easy
|
||||||
|
docker rm wg-easy
|
||||||
|
docker pull weejewel/wg-easy
|
||||||
|
```
|
||||||
|
|
||||||
|
And then run the `docker run -d \ ...` command above again.
|
Binary file not shown.
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 105 KiB |
9
docker-compose.dev.yml
Normal file
9
docker-compose.dev.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
wg-easy:
|
||||||
|
image: wg-easy
|
||||||
|
command: npm run serve
|
||||||
|
volumes:
|
||||||
|
- ./src/:/app/
|
||||||
|
# environment:
|
||||||
|
# - PASSWORD=p
|
@ -12,6 +12,7 @@ services:
|
|||||||
# - 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
|
||||||
|
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
|
||||||
|
|
||||||
image: weejewel/wg-easy
|
image: weejewel/wg-easy
|
||||||
container_name: wg-easy
|
container_name: wg-easy
|
||||||
|
6
docs/changelog.json
Normal file
6
docs/changelog.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"1": "Initial version. Enjoy!",
|
||||||
|
"2": "You can now rename a client, and update the address. Enjoy!",
|
||||||
|
"3": "Many improvements and small changes. Enjoy!",
|
||||||
|
"4": "Now with pretty charts for client's network speed. Enjoy!"
|
||||||
|
}
|
4
package-lock.json
generated
Normal file
4
package-lock.json
generated
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
@ -2,14 +2,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "docker build --tag wg-easy .",
|
"build": "docker build --tag wg-easy .",
|
||||||
|
"serve": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||||
"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"
|
||||||
},
|
|
||||||
"docker": {
|
|
||||||
"production": {
|
|
||||||
"tags": "latest"
|
|
||||||
},
|
|
||||||
"staging": {
|
|
||||||
"tags": "beta"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@
|
|||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"consistent-return": "off",
|
"consistent-return": "off",
|
||||||
"no-shadow": "off"
|
"no-shadow": "off",
|
||||||
|
"max-len": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { release } = require('./package.json');
|
||||||
|
|
||||||
|
module.exports.RELEASE = release;
|
||||||
module.exports.PORT = process.env.PORT || 51821;
|
module.exports.PORT = process.env.PORT || 51821;
|
||||||
module.exports.PASSWORD = process.env.PASSWORD;
|
module.exports.PASSWORD = process.env.PASSWORD;
|
||||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
||||||
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_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_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';
|
||||||
module.exports.WG_DEFAULT_DNS = process.env.WG_DEFAULT_DNS || '1.1.1.1';
|
module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string'
|
||||||
|
? process.env.WG_DEFAULT_DNS
|
||||||
|
: '1.1.1.1';
|
||||||
|
module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0';
|
||||||
|
@ -12,6 +12,7 @@ const WireGuard = require('../services/WireGuard');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
PORT,
|
PORT,
|
||||||
|
RELEASE,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
|
|
||||||
@ -29,6 +30,10 @@ module.exports = class Server {
|
|||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
.get('/api/release', (Util.promisify(async () => {
|
||||||
|
return RELEASE;
|
||||||
|
})))
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
.get('/api/session', Util.promisify(async req => {
|
.get('/api/session', Util.promisify(async req => {
|
||||||
const requiresPassword = !!process.env.PASSWORD;
|
const requiresPassword = !!process.env.PASSWORD;
|
||||||
@ -94,7 +99,8 @@ module.exports = class Server {
|
|||||||
const { clientId } = req.params;
|
const { clientId } = req.params;
|
||||||
const client = await WireGuard.getClient({ clientId });
|
const client = await WireGuard.getClient({ clientId });
|
||||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||||
res.header('Content-Disposition', `attachment; filename="${client.name}.conf"`);
|
const configName = client.name.replace(/[^a-zA-Z0-9_=+.-]/g, '-').replace(/(-{2,}|-$)/g, '-').replace(/-$/, '').substring(0, 32);
|
||||||
|
res.header('Content-Disposition', `attachment; filename="${configName}.conf"`);
|
||||||
res.header('Content-Type', 'text/plain');
|
res.header('Content-Type', 'text/plain');
|
||||||
res.send(config);
|
res.send(config);
|
||||||
}))
|
}))
|
||||||
@ -114,6 +120,16 @@ module.exports = class Server {
|
|||||||
const { clientId } = req.params;
|
const { clientId } = req.params;
|
||||||
return WireGuard.disableClient({ clientId });
|
return WireGuard.disableClient({ clientId });
|
||||||
}))
|
}))
|
||||||
|
.put('/api/wireguard/client/:clientId/name', Util.promisify(async req => {
|
||||||
|
const { clientId } = req.params;
|
||||||
|
const { name } = req.body;
|
||||||
|
return WireGuard.updateClientName({ clientId, name });
|
||||||
|
}))
|
||||||
|
.put('/api/wireguard/client/:clientId/address', Util.promisify(async req => {
|
||||||
|
const { clientId } = req.params;
|
||||||
|
const { address } = req.body;
|
||||||
|
return WireGuard.updateClientAddress({ clientId, address });
|
||||||
|
}))
|
||||||
|
|
||||||
.listen(PORT, () => {
|
.listen(PORT, () => {
|
||||||
debug(`Listening on http://0.0.0.0:${PORT}`);
|
debug(`Listening on http://0.0.0.0:${PORT}`);
|
||||||
|
@ -4,6 +4,19 @@ const childProcess = require('child_process');
|
|||||||
|
|
||||||
module.exports = class Util {
|
module.exports = class Util {
|
||||||
|
|
||||||
|
static isValidIPv4(str) {
|
||||||
|
const blocks = str.split('.');
|
||||||
|
if (blocks.length !== 4) return false;
|
||||||
|
|
||||||
|
for (let value of blocks) {
|
||||||
|
value = parseInt(value, 10);
|
||||||
|
if (Number.isNaN(value)) return false;
|
||||||
|
if (value < 0 || value > 255) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static promisify(fn) {
|
static promisify(fn) {
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
return function(req, res) {
|
return function(req, res) {
|
||||||
@ -39,9 +52,16 @@ module.exports = class Util {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async exec(cmd) {
|
static async exec(cmd, {
|
||||||
|
log = true,
|
||||||
|
} = {}) {
|
||||||
|
if (typeof log === 'string') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`$ ${log}`);
|
||||||
|
} else if (log === true) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`$ ${cmd}`);
|
console.log(`$ ${cmd}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform !== 'linux') {
|
if (process.platform !== 'linux') {
|
||||||
return '';
|
return '';
|
||||||
|
@ -17,6 +17,8 @@ const {
|
|||||||
WG_MTU,
|
WG_MTU,
|
||||||
WG_DEFAULT_DNS,
|
WG_DEFAULT_DNS,
|
||||||
WG_DEFAULT_ADDRESS,
|
WG_DEFAULT_ADDRESS,
|
||||||
|
WG_PERSISTENT_KEEPALIVE,
|
||||||
|
WG_ALLOWED_IPS,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
|
|
||||||
module.exports = class WireGuard {
|
module.exports = class WireGuard {
|
||||||
@ -36,7 +38,9 @@ module.exports = class WireGuard {
|
|||||||
debug('Configuration loaded.');
|
debug('Configuration loaded.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
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 address = WG_DEFAULT_ADDRESS.replace('x', '1');
|
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@ -51,6 +55,7 @@ module.exports = class WireGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.__saveConfig(config);
|
await this.__saveConfig(config);
|
||||||
|
await Util.exec('wg-quick down wg0').catch(() => { });
|
||||||
await Util.exec('wg-quick up wg0');
|
await Util.exec('wg-quick up wg0');
|
||||||
await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o eth0 -j MASQUERADE`);
|
await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o eth0 -j MASQUERADE`);
|
||||||
await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
||||||
@ -125,7 +130,9 @@ AllowedIPs = ${client.address}/32`;
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Loop WireGuard status
|
// Loop WireGuard status
|
||||||
const dump = await Util.exec('wg show wg0 dump');
|
const dump = await Util.exec('wg show wg0 dump', {
|
||||||
|
log: false,
|
||||||
|
});
|
||||||
dump
|
dump
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@ -174,7 +181,7 @@ AllowedIPs = ${client.address}/32`;
|
|||||||
[Interface]
|
[Interface]
|
||||||
PrivateKey = ${client.privateKey}
|
PrivateKey = ${client.privateKey}
|
||||||
Address = ${client.address}/24
|
Address = ${client.address}/24
|
||||||
DNS = ${WG_DEFAULT_DNS}
|
${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}` : ''}
|
||||||
if (typeof ${WG_MTU} !== 'undefined' || ${WG_MTU} !== null) {
|
if (typeof ${WG_MTU} !== 'undefined' || ${WG_MTU} !== null) {
|
||||||
MTU = ${WG_MTU}
|
MTU = ${WG_MTU}
|
||||||
}
|
}
|
||||||
@ -182,7 +189,8 @@ MTU = ${WG_MTU}
|
|||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = ${config.server.publicKey}
|
PublicKey = ${config.server.publicKey}
|
||||||
PresharedKey = ${client.preSharedKey}
|
PresharedKey = ${client.preSharedKey}
|
||||||
AllowedIPs = 0.0.0.0/0, ::/0
|
AllowedIPs = ${WG_ALLOWED_IPS}
|
||||||
|
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
|
||||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,4 +277,26 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
|||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateClientName({ clientId, name }) {
|
||||||
|
const client = await this.getClient({ clientId });
|
||||||
|
|
||||||
|
client.name = name;
|
||||||
|
client.updatedAt = new Date();
|
||||||
|
|
||||||
|
await this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateClientAddress({ clientId, address }) {
|
||||||
|
const client = await this.getClient({ clientId });
|
||||||
|
|
||||||
|
if (!Util.isValidIPv4(address)) {
|
||||||
|
throw new ServerError(`Invalid Address: ${address}`, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.address = address;
|
||||||
|
client.updatedAt = new Date();
|
||||||
|
|
||||||
|
await this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
882
src/package-lock.json
generated
882
src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"release": 4,
|
||||||
"name": "wg-easy",
|
"name": "wg-easy",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "DEBUG=Server,WireGuard WG_HOST=0.0.0.0 WG_PATH=../config/ nodemon server.js",
|
"serve": "DEBUG=Server,WireGuard nodemon server.js",
|
||||||
"serve-with-password": "PASSWORD=wg npm run serve",
|
"serve-with-password": "PASSWORD=wg npm run serve",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
@ -19,8 +20,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.27.0",
|
"eslint": "^7.27.0",
|
||||||
"eslint-config-athom": "^2.1.0",
|
"eslint-config-athom": "^2.1.0"
|
||||||
"nodemon": "^2.0.7"
|
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>WireGuard</title>
|
<title>WireGuard</title>
|
||||||
<link href="css/vendor/tailwind.min.css" rel="stylesheet">
|
<link href="/css/vendor/tailwind.min.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">
|
||||||
@ -12,7 +12,11 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-50">
|
<body class="bg-gray-50">
|
||||||
<div id="app" class="container mx-auto">
|
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<div class="container mx-auto max-w-3xl">
|
||||||
|
|
||||||
<div v-if="authenticated === true">
|
<div v-if="authenticated === true">
|
||||||
<span v-if="requiresPassword"
|
<span v-if="requiresPassword"
|
||||||
class="text-sm text-gray-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right" @click="logout">
|
class="text-sm text-gray-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right" @click="logout">
|
||||||
@ -29,6 +33,21 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<h2 class="text-sm text-gray-400 mb-10"></h2>
|
<h2 class="text-sm text-gray-400 mb-10"></h2>
|
||||||
|
|
||||||
|
<div v-if="latestRelease" class="bg-red-800 p-4 text-white text-sm font-small mb-10 rounded-md shadow-lg"
|
||||||
|
:title="`v${currentRelease} → v${latestRelease.version}`">
|
||||||
|
<div class="container mx-auto flex flex-row flex-auto items-center">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-bold">There is an update available!</p>
|
||||||
|
<p>{{latestRelease.changelog}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="https://github.com/WeeJeWel/wg-easy#updating" target="_blank"
|
||||||
|
class="p-3 rounded-md bg-white float-right font-sm font-semibold text-red-800 flex-shrink-0 border-2 border-red-800 hover:border-white hover:text-white hover:bg-red-800 transition-all">
|
||||||
|
Update →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="shadow-md rounded-lg bg-white overflow-hidden">
|
<div class="shadow-md rounded-lg bg-white overflow-hidden">
|
||||||
<div class="flex flex-row flex-auto items-center p-3 px-5 border border-b-2 border-gray-100">
|
<div class="flex flex-row flex-auto items-center p-3 px-5 border border-b-2 border-gray-100">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -39,7 +58,8 @@
|
|||||||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 border-2 border-gray-100 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 border-2 border-gray-100 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"
|
<svg class="w-4 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" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-sm">New</span>
|
<span class="text-sm">New</span>
|
||||||
</button>
|
</button>
|
||||||
@ -47,12 +67,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<!-- Client -->
|
||||||
<div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id"
|
<div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id"
|
||||||
class="p-5 flex flex-row">
|
class="relative overflow-hidden border-b border-gray-100 border-solid">
|
||||||
|
|
||||||
|
<!-- Chart -->
|
||||||
|
<div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%;">
|
||||||
|
<apexchart width="100%" height="100%" :options="client.chartOptions" :series="client.transferTxSeries">
|
||||||
|
</apexchart>
|
||||||
|
</div>
|
||||||
|
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%;">
|
||||||
|
<apexchart width="100%" height="100%" :options="client.chartOptions" :series="client.transferRxSeries"
|
||||||
|
style="transform: scaleY(-1);">
|
||||||
|
</apexchart>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative p-5 z-10 flex flex-row">
|
||||||
<div class="h-10 w-10 mr-5 rounded-full bg-gray-50 relative">
|
<div class="h-10 w-10 mr-5 rounded-full bg-gray-50 relative">
|
||||||
<svg class="w-6 m-2 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
<svg class="w-6 m-2 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||||
fill="currentColor">
|
fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
<img v-if="client.avatar" :src="client.avatar" class="w-10 rounded-full absolute top-0 left-0" />
|
<img v-if="client.avatar" :src="client.avatar" class="w-10 rounded-full absolute top-0 left-0" />
|
||||||
|
|
||||||
@ -64,10 +99,62 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<div class="text-gray-700" :title="'Created at ' + dateTime(new Date(client.createdAt))">{{client.name}}
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div class="text-gray-700 group" :title="'Created at ' + dateTime(new Date(client.createdAt))">
|
||||||
|
|
||||||
|
<!-- Show -->
|
||||||
|
<input v-show="clientEditNameId === client.id" v-model="clientEditName"
|
||||||
|
v-on:keyup.enter="updateClientName(client, clientEditName); clientEditName = null; clientEditNameId = null;"
|
||||||
|
v-on:keyup.escape="clientEditName = null; clientEditNameId = null;"
|
||||||
|
:ref="'client-' + client.id + '-name'"
|
||||||
|
class="rounded px-1 border-2 border-gray-100 focus:border-gray-200 outline-none w-30" />
|
||||||
|
<span v-show="clientEditNameId !== client.id"
|
||||||
|
class="inline-block border-t-2 border-b-2 border-transparent">{{client.name}}</span>
|
||||||
|
|
||||||
|
<!-- Edit -->
|
||||||
|
<span v-show="clientEditNameId !== client.id"
|
||||||
|
@click="clientEditName = client.name; clientEditNameId = client.id; setTimeout(() => $refs['client-' + client.id + '-name'][0].select(), 1);"
|
||||||
|
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-300 text-xs">{{client.address}}
|
|
||||||
<span v-if="client.transferTx" title="Download">
|
<!-- Info -->
|
||||||
|
<div class="text-gray-400 text-xs">
|
||||||
|
|
||||||
|
<!-- Address -->
|
||||||
|
<span class="group">
|
||||||
|
|
||||||
|
<!-- Show -->
|
||||||
|
<input v-show="clientEditAddressId === client.id" v-model="clientEditAddress"
|
||||||
|
v-on:keyup.enter="updateClientAddress(client, clientEditAddress); clientEditAddress = null; clientEditAddressId = null;"
|
||||||
|
v-on:keyup.escape="clientEditAddress = null; clientEditAddressId = null;"
|
||||||
|
:ref="'client-' + client.id + '-address'"
|
||||||
|
class="rounded border-2 border-gray-100 focus:border-gray-200 outline-none w-20 text-black" />
|
||||||
|
<span v-show="clientEditAddressId !== client.id"
|
||||||
|
class="inline-block border-t-2 border-b-2 border-transparent">{{client.address}}</span>
|
||||||
|
|
||||||
|
<!-- Edit -->
|
||||||
|
<span v-show="clientEditAddressId !== client.id"
|
||||||
|
@click="clientEditAddress = client.address; clientEditAddressId = client.id; setTimeout(() => $refs['client-' + client.id + '-address'][0].select(), 1);"
|
||||||
|
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Transfer TX -->
|
||||||
|
<span v-if="client.transferTx":title="'Total Download: ' + bytes(client.transferTx)">
|
||||||
·
|
·
|
||||||
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||||
fill="currentColor">
|
fill="currentColor">
|
||||||
@ -75,9 +162,11 @@
|
|||||||
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
|
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
{{client.transferTx | bytes}}
|
{{client.transferTxCurrent | bytes}}/s
|
||||||
</span>
|
</span>
|
||||||
<span v-if="client.transferRx" title="Upload">
|
|
||||||
|
<!-- Transfer RX -->
|
||||||
|
<span v-if="client.transferRx" :title="'Total Upload: ' + bytes(client.transferRx)">
|
||||||
·
|
·
|
||||||
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||||
fill="currentColor">
|
fill="currentColor">
|
||||||
@ -85,8 +174,10 @@
|
|||||||
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
|
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
{{client.transferRx | bytes}}
|
{{client.transferRxCurrent | bytes}}/s
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Last seen -->
|
||||||
<span v-if="client.latestHandshakeAt"
|
<span v-if="client.latestHandshakeAt"
|
||||||
:title="'Last seen at ' + dateTime(new Date(client.latestHandshakeAt))">
|
:title="'Last seen at ' + dateTime(new Date(client.latestHandshakeAt))">
|
||||||
· {{new Date(client.latestHandshakeAt) | timeago}}
|
· {{new Date(client.latestHandshakeAt) | timeago}}
|
||||||
@ -98,11 +189,11 @@
|
|||||||
<div class="text-gray-400">
|
<div class="text-gray-400">
|
||||||
|
|
||||||
<!-- Enable/Disable -->
|
<!-- Enable/Disable -->
|
||||||
<div @click="disableClient(client)" v-if="client.enabled === true"
|
<div @click="disableClient(client)" v-if="client.enabled === true" title="Disable Client"
|
||||||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all">
|
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all">
|
||||||
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
|
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
|
||||||
</div>
|
</div>
|
||||||
<div @click="enableClient(client)" v-if="client.enabled === false"
|
<div @click="enableClient(client)" v-if="client.enabled === false" title="Enable Client"
|
||||||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 cursor-pointer hover:bg-gray-300 transition-all">
|
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 cursor-pointer hover:bg-gray-300 transition-all">
|
||||||
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
|
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,6 +232,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
<div v-if="clients && clients.length === 0">
|
<div v-if="clients && clients.length === 0">
|
||||||
<p class="text-center m-10 text-gray-400 text-sm">There are no clients yet.<br /><br />
|
<p class="text-center m-10 text-gray-400 text-sm">There are no clients yet.<br /><br />
|
||||||
<button @click="clientCreate = true; clientCreateName = '';"
|
<button @click="clientCreate = true; clientCreateName = '';"
|
||||||
@ -168,10 +261,11 @@
|
|||||||
|
|
||||||
<!-- QR Code-->
|
<!-- QR Code-->
|
||||||
<div v-if="qrcode">
|
<div v-if="qrcode">
|
||||||
<div class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
<div class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center z-20">
|
||||||
<div class="bg-white rounded-md shadow-lg relative p-8">
|
<div class="bg-white rounded-md shadow-lg relative p-8">
|
||||||
<button @click="qrcode = null" class="absolute right-4 top-4 text-gray-600 hover:text-gray-800">
|
<button @click="qrcode = null" class="absolute right-4 top-4 text-gray-600 hover:text-gray-800">
|
||||||
<svg class="w-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-8" 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="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@ -359,7 +453,8 @@
|
|||||||
|
|
||||||
<div v-if="authenticated === null" class="text-gray-300 pt-24 pb-12">
|
<div v-if="authenticated === null" class="text-gray-300 pt-24 pb-12">
|
||||||
|
|
||||||
<svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
<svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
fill="currentColor">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<path class="opacity-75" fill="currentColor"
|
<path class="opacity-75" fill="currentColor"
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||||
@ -368,14 +463,19 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-center m-10 text-gray-300 text-xs">Made by <a target="_blank" class="hover:underline"
|
<p class="text-center m-10 text-gray-300 text-xs">Made by <a target="_blank" class="hover:underline"
|
||||||
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> · <a class="hover:underline"
|
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> · <a class="hover:underline"
|
||||||
|
href="https://github.com/sponsors/WeeJeWel" target="_blank">Donate</a> · <a class="hover:underline"
|
||||||
href="https://github.com/weejewel/wg-easy" target="_blank">GitHub</a></p>
|
href="https://github.com/weejewel/wg-easy" target="_blank">GitHub</a></p>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/vendor/vue.min.js"></script>
|
<script src="/js/vendor/vue.min.js"></script>
|
||||||
|
<script src="/js/vendor/apexcharts.min.js"></script>
|
||||||
|
<script src="/js/vendor/vue-apexcharts.min.js"></script>
|
||||||
<script src="/js/vendor/md5.min.js"></script>
|
<script src="/js/vendor/md5.min.js"></script>
|
||||||
<script src="/js/vendor/timeago.min.js"></script>
|
<script src="/js/vendor/timeago.min.js"></script>
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
|
@ -29,6 +29,13 @@ class API {
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRelease() {
|
||||||
|
return this.call({
|
||||||
|
method: 'get',
|
||||||
|
path: '/release',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getSession() {
|
async getSession() {
|
||||||
return this.call({
|
return this.call({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -94,4 +101,20 @@ class API {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateClientName({ clientId, name }) {
|
||||||
|
return this.call({
|
||||||
|
method: 'put',
|
||||||
|
path: `/wireguard/client/${clientId}/name/`,
|
||||||
|
body: { name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateClientAddress({ clientId, address }) {
|
||||||
|
return this.call({
|
||||||
|
method: 'put',
|
||||||
|
path: `/wireguard/client/${clientId}/address/`,
|
||||||
|
body: { address },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
authenticated: null,
|
authenticated: null,
|
||||||
authenticating: false,
|
authenticating: false,
|
||||||
@ -14,10 +17,79 @@ new Vue({
|
|||||||
requiresPassword: null,
|
requiresPassword: null,
|
||||||
|
|
||||||
clients: null,
|
clients: null,
|
||||||
|
clientsPersist: {},
|
||||||
clientDelete: null,
|
clientDelete: null,
|
||||||
clientCreate: null,
|
clientCreate: null,
|
||||||
clientCreateName: '',
|
clientCreateName: '',
|
||||||
|
clientEditName: null,
|
||||||
|
clientEditNameId: null,
|
||||||
|
clientEditAddress: null,
|
||||||
|
clientEditAddressId: null,
|
||||||
qrcode: null,
|
qrcode: null,
|
||||||
|
|
||||||
|
currentRelease: null,
|
||||||
|
latestRelease: null,
|
||||||
|
|
||||||
|
chartOptions: {
|
||||||
|
chart: {
|
||||||
|
background: 'transparent',
|
||||||
|
type: 'area',
|
||||||
|
toolbar: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
},
|
||||||
|
colors: ['#CCCCCC'],
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
labels: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTicks: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisBorder: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: false,
|
||||||
|
padding: {
|
||||||
|
left: -10,
|
||||||
|
right: 0,
|
||||||
|
bottom: -15,
|
||||||
|
top: -15,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
lines: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dateTime: value => {
|
dateTime: value => {
|
||||||
@ -38,10 +110,51 @@ new Vue({
|
|||||||
client.avatar = `https://www.gravatar.com/avatar/${md5(client.name)}?d=blank`;
|
client.avatar = `https://www.gravatar.com/avatar/${md5(client.name)}?d=blank`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.clientsPersist[client.id]) {
|
||||||
|
this.clientsPersist[client.id] = {};
|
||||||
|
this.clientsPersist[client.id].transferRxHistory = Array(20).fill(0);
|
||||||
|
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
||||||
|
this.clientsPersist[client.id].transferTxHistory = Array(20).fill(0);
|
||||||
|
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
||||||
|
|
||||||
|
this.clientsPersist[client.id].chartOptions = {
|
||||||
|
...this.chartOptions,
|
||||||
|
yaxis: {
|
||||||
|
...this.chartOptions.yaxis,
|
||||||
|
max: () => this.clientsPersist[client.id].chartMax,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
||||||
|
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
||||||
|
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
||||||
|
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
||||||
|
|
||||||
|
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
|
||||||
|
this.clientsPersist[client.id].transferRxHistory.shift();
|
||||||
|
|
||||||
|
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
|
||||||
|
this.clientsPersist[client.id].transferTxHistory.shift();
|
||||||
|
|
||||||
|
client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent;
|
||||||
|
client.transferTxSeries = [{
|
||||||
|
name: 'tx',
|
||||||
|
data: this.clientsPersist[client.id].transferTxHistory,
|
||||||
|
}];
|
||||||
|
|
||||||
|
client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent;
|
||||||
|
client.transferRxSeries = [{
|
||||||
|
name: 'rx',
|
||||||
|
data: this.clientsPersist[client.id].transferRxHistory,
|
||||||
|
}];
|
||||||
|
|
||||||
|
this.clientsPersist[client.id].chartMax = Math.max(...this.clientsPersist[client.id].transferTxHistory, ...this.clientsPersist[client.id].transferRxHistory);
|
||||||
|
|
||||||
|
client.chartOptions = this.clientsPersist[client.id].chartOptions;
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(clients);
|
|
||||||
},
|
},
|
||||||
login(e) {
|
login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -64,6 +177,7 @@ new Vue({
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.authenticating = false;
|
this.authenticating = false;
|
||||||
|
this.password = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout(e) {
|
logout(e) {
|
||||||
@ -101,28 +215,22 @@ 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));
|
||||||
},
|
},
|
||||||
|
updateClientName(client, name) {
|
||||||
|
this.api.updateClientName({ clientId: client.id, name })
|
||||||
|
.catch(err => alert(err.message || err.toString()))
|
||||||
|
.finally(() => this.refresh().catch(console.error));
|
||||||
|
},
|
||||||
|
updateClientAddress(client, address) {
|
||||||
|
this.api.updateClientAddress({ clientId: client.id, address })
|
||||||
|
.catch(err => alert(err.message || err.toString()))
|
||||||
|
.finally(() => this.refresh().catch(console.error));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
bytes,
|
||||||
timeago: value => {
|
timeago: value => {
|
||||||
return timeago().format(value);
|
return timeago().format(value);
|
||||||
},
|
},
|
||||||
bytes: (bytes, decimals, kib, maxunit) => {
|
|
||||||
kib = kib || false;
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'Not an number';
|
|
||||||
const k = kib ? 1024 : 1000;
|
|
||||||
const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2;
|
|
||||||
const sizes = kib
|
|
||||||
? ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB']
|
|
||||||
: ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
|
|
||||||
let i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
if (maxunit !== undefined) {
|
|
||||||
const index = sizes.indexOf(maxunit);
|
|
||||||
if (index !== -1) i = index;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.api = new API();
|
this.api = new API();
|
||||||
@ -141,5 +249,48 @@ new Vue({
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.refresh().catch(console.error);
|
this.refresh().catch(console.error);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
Promise.resolve().then(async () => {
|
||||||
|
const currentRelease = await this.api.getRelease();
|
||||||
|
const latestRelease = await fetch('https://weejewel.github.io/wg-easy/changelog.json')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(releases => {
|
||||||
|
const releasesArray = Object.entries(releases).map(([version, changelog]) => ({
|
||||||
|
version: parseInt(version, 10),
|
||||||
|
changelog,
|
||||||
|
}));
|
||||||
|
releasesArray.sort((a, b) => {
|
||||||
|
return b.version - a.version;
|
||||||
|
});
|
||||||
|
|
||||||
|
return releasesArray[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Current Release: ${currentRelease}`);
|
||||||
|
console.log(`Latest Release: ${latestRelease.version}`);
|
||||||
|
|
||||||
|
if (currentRelease >= latestRelease.version) return;
|
||||||
|
|
||||||
|
this.currentRelease = currentRelease;
|
||||||
|
this.latestRelease = latestRelease;
|
||||||
|
}).catch(console.error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function bytes(bytes, decimals, kib, maxunit) {
|
||||||
|
kib = kib || false;
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'NaN';
|
||||||
|
const k = kib ? 1024 : 1000;
|
||||||
|
const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2;
|
||||||
|
const sizes = kib
|
||||||
|
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB']
|
||||||
|
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
|
||||||
|
let i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
if (maxunit !== undefined) {
|
||||||
|
const index = sizes.indexOf(maxunit);
|
||||||
|
if (index !== -1) i = index;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
14
src/www/js/vendor/apexcharts.min.js
vendored
Normal file
14
src/www/js/vendor/apexcharts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/www/js/vendor/vue-apexcharts.min.js
vendored
Normal file
7
src/www/js/vendor/vue-apexcharts.min.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Minified by jsDelivr using Terser v5.7.1.
|
||||||
|
* Original file: /npm/vue-apexcharts@1.6.2/dist/vue-apexcharts.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
!function (t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e(require("apexcharts/dist/apexcharts.min")) : "function" == typeof define && define.amd ? define(["apexcharts/dist/apexcharts.min"], e) : t.VueApexCharts = e(t.ApexCharts) }(this, (function (t) { "use strict"; function e(t) { return (e = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (t) { return typeof t } : function (t) { return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t })(t) } function n(t, e, n) { return e in t ? Object.defineProperty(t, e, { value: n, enumerable: !0, configurable: !0, writable: !0 }) : t[e] = n, t } t = t && t.hasOwnProperty("default") ? t.default : t; var i = { props: { options: { type: Object }, type: { type: String }, series: { type: Array, required: !0, default: function () { return [] } }, width: { default: "100%" }, height: { default: "auto" } }, data: function () { return { chart: null } }, beforeMount: function () { window.ApexCharts = t }, mounted: function () { this.init() }, created: function () { var t = this; this.$watch("options", (function (e) { !t.chart && e ? t.init() : t.chart.updateOptions(t.options) })), this.$watch("series", (function (e) { !t.chart && e ? t.init() : t.chart.updateSeries(t.series) }));["type", "width", "height"].forEach((function (e) { t.$watch(e, (function () { t.refresh() })) })) }, beforeDestroy: function () { this.chart && this.destroy() }, render: function (t) { return t("div") }, methods: { init: function () { var e = this, n = { chart: { type: this.type || this.options.chart.type || "line", height: this.height, width: this.width, events: {} }, series: this.series }; Object.keys(this.$listeners).forEach((function (t) { n.chart.events[t] = e.$listeners[t] })); var i = this.extend(this.options, n); return this.chart = new t(this.$el, i), this.chart.render() }, isObject: function (t) { return t && "object" === e(t) && !Array.isArray(t) && null != t }, extend: function (t, e) { var i = this; "function" != typeof Object.assign && (Object.assign = function (t) { if (null == t) throw new TypeError("Cannot convert undefined or null to object"); for (var e = Object(t), n = 1; n < arguments.length; n++) { var i = arguments[n]; if (null != i) for (var r in i) i.hasOwnProperty(r) && (e[r] = i[r]) } return e }); var r = Object.assign({}, t); return this.isObject(t) && this.isObject(e) && Object.keys(e).forEach((function (o) { i.isObject(e[o]) && o in t ? r[o] = i.extend(t[o], e[o]) : Object.assign(r, n({}, o, e[o])) })), r }, refresh: function () { return this.destroy(), this.init() }, destroy: function () { this.chart.destroy() }, updateSeries: function (t, e) { return this.chart.updateSeries(t, e) }, updateOptions: function (t, e, n, i) { return this.chart.updateOptions(t, e, n, i) }, toggleSeries: function (t) { return this.chart.toggleSeries(t) }, showSeries: function (t) { this.chart.showSeries(t) }, hideSeries: function (t) { this.chart.hideSeries(t) }, appendSeries: function (t, e) { return this.chart.appendSeries(t, e) }, resetSeries: function () { this.chart.resetSeries() }, zoomX: function (t, e) { this.chart.zoomX(t, e) }, toggleDataPointSelection: function (t, e) { this.chart.toggleDataPointSelection(t, e) }, appendData: function (t) { return this.chart.appendData(t) }, addText: function (t) { this.chart.addText(t) }, addImage: function (t) { this.chart.addImage(t) }, addShape: function (t) { this.chart.addShape(t) }, dataURI: function () { return this.chart.dataURI() }, setLocale: function (t) { return this.chart.setLocale(t) }, addXaxisAnnotation: function (t, e) { this.chart.addXaxisAnnotation(t, e) }, addYaxisAnnotation: function (t, e) { this.chart.addYaxisAnnotation(t, e) }, addPointAnnotation: function (t, e) { this.chart.addPointAnnotation(t, e) }, removeAnnotation: function (t, e) { this.chart.removeAnnotation(t, e) }, clearAnnotations: function () { this.chart.clearAnnotations() } } }; return window.ApexCharts = t, i.install = function (e) { e.ApexCharts = t, window.ApexCharts = t, Object.defineProperty(e.prototype, "$apexcharts", { get: function () { return t } }) }, i }));
|
Loading…
Reference in New Issue
Block a user