mirror of
https://github.com/WeeJeWel/wg-easy.git
synced 2024-12-22 09:09:23 +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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: benjlevesque/short-sha@v1.2
|
||||
with:
|
||||
ref: production
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
@ -35,10 +35,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# Set environment variables
|
||||
- run: echo GH_BRANCH=${GITHUB_REF#refs/heads/} >> $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 }}
|
||||
- run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV
|
||||
|
||||
# Build & Publish
|
||||
- name: Build & Publish Docker Image
|
||||
@ -46,4 +43,4 @@ jobs:
|
||||
with:
|
||||
push: true
|
||||
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 }}
|
||||
|
4
.gitignore
vendored
4
.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
|
||||
RUN apk add -U wireguard-tools
|
||||
RUN apk add -U --no-cache wireguard-tools dumb-init
|
||||
|
||||
# Copy Web UI
|
||||
COPY src/ /app/
|
||||
WORKDIR /app
|
||||
RUN npm ci --production
|
||||
RUN npm i -g nodemon
|
||||
RUN mv /app/node_modules/ /node_modules/
|
||||
|
||||
# Expose Ports
|
||||
EXPOSE 51820/udp
|
||||
@ -16,4 +18,4 @@ EXPOSE 51821/tcp
|
||||
ENV DEBUG=Server,WireGuard
|
||||
|
||||
# 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!
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/screenshot.png" width="702" />
|
||||
<img src="./assets/screenshot.png" width="802" />
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
* All-in-one: WireGuard + Web UI.
|
||||
* Easy installation, simple to use.
|
||||
* List, create, delete, enable & disable clients.
|
||||
* List, create, edit, delete, enable & disable clients.
|
||||
* Show a client's QR code.
|
||||
* Download a client's configuration file.
|
||||
* Statistics for which clients are connected.
|
||||
* Tx/Rx charts for each connected client.
|
||||
* Gravatar support.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A host with a kernel that supports WireGuard (all modern kernels).
|
||||
* A host with Docker installed.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Docker
|
||||
@ -30,49 +36,69 @@ If you haven't installed Docker yet, install it by running:
|
||||
|
||||
```bash
|
||||
$ curl -sSL https://get.docker.com | sh
|
||||
$ sudo sh get-docker.sh
|
||||
$ 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
|
||||
$ mkdir ~/.wg-easy
|
||||
$ cd ~/.wg-easy
|
||||
$ wget https://raw.githubusercontent.com/WeeJeWel/wg-easy/master/docker-compose.yml
|
||||
$ vim docker-compose.yml
|
||||
```
|
||||
To automatically install & run wg-easy, simply run:
|
||||
|
||||
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
|
||||
$ docker-compose up --detach
|
||||
```
|
||||
|
||||
The Web UI will be available on `http://0.0.0.0:51821`. You can create new clients there.
|
||||
Are you enjoying this project? [Buy me a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
||||
|
||||
## 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 |
|
||||
| - | - | - | - |
|
||||
| `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_PORT` | `51820` | `51820` | The public UDP port of your VPN server |
|
||||
| `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_MTU` | `null` | `1420` | The MTU the clients will use. (Server uses default WG MTU) |
|
||||
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname 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_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. |
|
||||
| `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.
|
||||
|
||||
# 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_DNS=1.1.1.1
|
||||
# - WG_MTU=1420
|
||||
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
|
||||
|
||||
image: weejewel/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
|
||||
}
|
11
package.json
11
package.json
@ -2,14 +2,7 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"docker": {
|
||||
"production": {
|
||||
"tags": "latest"
|
||||
},
|
||||
"staging": {
|
||||
"tags": "beta"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
],
|
||||
"rules": {
|
||||
"consistent-return": "off",
|
||||
"no-shadow": "off"
|
||||
"no-shadow": "off",
|
||||
"max-len": "off"
|
||||
}
|
||||
}
|
@ -1,10 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const { release } = require('./package.json');
|
||||
|
||||
module.exports.RELEASE = release;
|
||||
module.exports.PORT = process.env.PORT || 51821;
|
||||
module.exports.PASSWORD = process.env.PASSWORD;
|
||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
||||
module.exports.WG_HOST = process.env.WG_HOST;
|
||||
module.exports.WG_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';
|
||||
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 {
|
||||
PORT,
|
||||
RELEASE,
|
||||
PASSWORD,
|
||||
} = require('../config');
|
||||
|
||||
@ -29,6 +30,10 @@ module.exports = class Server {
|
||||
saveUninitialized: true,
|
||||
}))
|
||||
|
||||
.get('/api/release', (Util.promisify(async () => {
|
||||
return RELEASE;
|
||||
})))
|
||||
|
||||
// Authentication
|
||||
.get('/api/session', Util.promisify(async req => {
|
||||
const requiresPassword = !!process.env.PASSWORD;
|
||||
@ -94,7 +99,8 @@ module.exports = class Server {
|
||||
const { clientId } = req.params;
|
||||
const client = await WireGuard.getClient({ 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.send(config);
|
||||
}))
|
||||
@ -114,6 +120,16 @@ module.exports = class Server {
|
||||
const { clientId } = req.params;
|
||||
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, () => {
|
||||
debug(`Listening on http://0.0.0.0:${PORT}`);
|
||||
|
@ -4,6 +4,19 @@ const childProcess = require('child_process');
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line func-names
|
||||
return function(req, res) {
|
||||
@ -39,9 +52,16 @@ module.exports = class Util {
|
||||
};
|
||||
}
|
||||
|
||||
static async exec(cmd) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`$ ${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
|
||||
console.log(`$ ${cmd}`);
|
||||
}
|
||||
|
||||
if (process.platform !== 'linux') {
|
||||
return '';
|
||||
|
@ -17,6 +17,8 @@ const {
|
||||
WG_MTU,
|
||||
WG_DEFAULT_DNS,
|
||||
WG_DEFAULT_ADDRESS,
|
||||
WG_PERSISTENT_KEEPALIVE,
|
||||
WG_ALLOWED_IPS,
|
||||
} = require('../config');
|
||||
|
||||
module.exports = class WireGuard {
|
||||
@ -36,7 +38,9 @@ module.exports = class WireGuard {
|
||||
debug('Configuration loaded.');
|
||||
} catch (err) {
|
||||
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');
|
||||
|
||||
config = {
|
||||
@ -51,6 +55,7 @@ module.exports = class WireGuard {
|
||||
}
|
||||
|
||||
await this.__saveConfig(config);
|
||||
await Util.exec('wg-quick down wg0').catch(() => { });
|
||||
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 -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
|
||||
@ -125,7 +130,9 @@ AllowedIPs = ${client.address}/32`;
|
||||
}));
|
||||
|
||||
// Loop WireGuard status
|
||||
const dump = await Util.exec('wg show wg0 dump');
|
||||
const dump = await Util.exec('wg show wg0 dump', {
|
||||
log: false,
|
||||
});
|
||||
dump
|
||||
.trim()
|
||||
.split('\n')
|
||||
@ -174,7 +181,7 @@ AllowedIPs = ${client.address}/32`;
|
||||
[Interface]
|
||||
PrivateKey = ${client.privateKey}
|
||||
Address = ${client.address}/24
|
||||
DNS = ${WG_DEFAULT_DNS}
|
||||
${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}` : ''}
|
||||
if (typeof ${WG_MTU} !== 'undefined' || ${WG_MTU} !== null) {
|
||||
MTU = ${WG_MTU}
|
||||
}
|
||||
@ -182,7 +189,8 @@ MTU = ${WG_MTU}
|
||||
[Peer]
|
||||
PublicKey = ${config.server.publicKey}
|
||||
PresharedKey = ${client.preSharedKey}
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
AllowedIPs = ${WG_ALLOWED_IPS}
|
||||
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
|
||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
}
|
||||
|
||||
@ -269,4 +277,26 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
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",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"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",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
@ -19,8 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-athom": "^2.1.0",
|
||||
"nodemon": "^2.0.7"
|
||||
"eslint-config-athom": "^2.1.0"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<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="icon" type="image/png" href="img/favicon.png">
|
||||
<link rel="apple-touch-icon" href="img/apple-touch-icon.png">
|
||||
@ -12,149 +12,429 @@
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50">
|
||||
<div id="app" class="container mx-auto">
|
||||
<div v-if="authenticated === true">
|
||||
<span v-if="requiresPassword"
|
||||
class="text-sm text-gray-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right" @click="logout">
|
||||
Logout
|
||||
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</span>
|
||||
<h1 class="text-4xl font-medium mt-10 mb-2">
|
||||
<img src="./img/logo.png" width="32" class="inline align-middle" />
|
||||
<span class="align-middle">WireGuard</span>
|
||||
</h1>
|
||||
<h2 class="text-sm text-gray-400 mb-10"></h2>
|
||||
|
||||
<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-grow">
|
||||
<p class="text-2xl font-medium">Clients</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
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"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">New</span>
|
||||
</button>
|
||||
<div id="app">
|
||||
|
||||
<div class="container mx-auto max-w-3xl">
|
||||
|
||||
<div v-if="authenticated === true">
|
||||
<span v-if="requiresPassword"
|
||||
class="text-sm text-gray-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right" @click="logout">
|
||||
Logout
|
||||
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</span>
|
||||
<h1 class="text-4xl font-medium mt-10 mb-2">
|
||||
<img src="./img/logo.png" width="32" class="inline align-middle" />
|
||||
<span class="align-middle">WireGuard</span>
|
||||
</h1>
|
||||
<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>
|
||||
<div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id"
|
||||
class="p-5 flex flex-row">
|
||||
<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"
|
||||
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" />
|
||||
</svg>
|
||||
<img v-if="client.avatar" :src="client.avatar" class="w-10 rounded-full absolute top-0 left-0" />
|
||||
|
||||
<div
|
||||
v-if="client.latestHandshakeAt && ((new Date() - new Date(client.latestHandshakeAt) < 1000 * 60 * 10))">
|
||||
<div class="animate-ping w-4 h-4 p-1 bg-red-100 rounded-full absolute -bottom-1 -right-1"></div>
|
||||
<div class="w-2 h-2 bg-red-800 rounded-full absolute bottom-0 right-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-grow">
|
||||
<div class="text-gray-700" :title="'Created at ' + dateTime(new Date(client.createdAt))">{{client.name}}
|
||||
</div>
|
||||
<div class="text-gray-300 text-xs">{{client.address}}
|
||||
<span v-if="client.transferTx" title="Download">
|
||||
·
|
||||
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
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" />
|
||||
</svg>
|
||||
{{client.transferTx | bytes}}
|
||||
</span>
|
||||
<span v-if="client.transferRx" title="Upload">
|
||||
·
|
||||
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
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" />
|
||||
</svg>
|
||||
{{client.transferRx | bytes}}
|
||||
</span>
|
||||
<span v-if="client.latestHandshakeAt"
|
||||
:title="'Last seen at ' + dateTime(new Date(client.latestHandshakeAt))">
|
||||
· {{new Date(client.latestHandshakeAt) | timeago}}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-2xl font-medium">Clients</p>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<div class="text-gray-400">
|
||||
|
||||
<!-- Enable/Disable -->
|
||||
<div @click="disableClient(client)" v-if="client.enabled === true"
|
||||
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>
|
||||
<div @click="enableClient(client)" v-if="client.enabled === false"
|
||||
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>
|
||||
|
||||
<!-- Show QR-->
|
||||
<button class="align-middle bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
|
||||
title="Show QR Code" @click="qrcode = `/api/wireguard/client/${client.id}/qrcode.svg`">
|
||||
<svg class="w-5" 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 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Download Config -->
|
||||
<a :href="'/api/wireguard/client/' + client.id + '/configuration'" download
|
||||
class="align-middle inline-block bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
|
||||
title="Download Configuration">
|
||||
<svg class="w-5" 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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Delete -->
|
||||
<button class="align-middle bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
|
||||
title="Delete Client" @click="clientDelete = client">
|
||||
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<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 />
|
||||
<div class="flex-shrink-0">
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
class="bg-red-800 text-white hover:bg-red-700 border-2 border-none 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"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">New Client</span>
|
||||
<span class="text-sm">New</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="clients === null" class="text-gray-200 p-5">
|
||||
|
||||
<div>
|
||||
<!-- Client -->
|
||||
<div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id"
|
||||
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">
|
||||
<svg class="w-6 m-2 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
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" />
|
||||
</svg>
|
||||
<img v-if="client.avatar" :src="client.avatar" class="w-10 rounded-full absolute top-0 left-0" />
|
||||
|
||||
<div
|
||||
v-if="client.latestHandshakeAt && ((new Date() - new Date(client.latestHandshakeAt) < 1000 * 60 * 10))">
|
||||
<div class="animate-ping w-4 h-4 p-1 bg-red-100 rounded-full absolute -bottom-1 -right-1"></div>
|
||||
<div class="w-2 h-2 bg-red-800 rounded-full absolute bottom-0 right-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
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" />
|
||||
</svg>
|
||||
{{client.transferTxCurrent | bytes}}/s
|
||||
</span>
|
||||
|
||||
<!-- 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"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
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" />
|
||||
</svg>
|
||||
{{client.transferRxCurrent | bytes}}/s
|
||||
</span>
|
||||
|
||||
<!-- Last seen -->
|
||||
<span v-if="client.latestHandshakeAt"
|
||||
:title="'Last seen at ' + dateTime(new Date(client.latestHandshakeAt))">
|
||||
· {{new Date(client.latestHandshakeAt) | timeago}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<div class="text-gray-400">
|
||||
|
||||
<!-- Enable/Disable -->
|
||||
<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">
|
||||
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
|
||||
</div>
|
||||
<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">
|
||||
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
|
||||
</div>
|
||||
|
||||
<!-- Show QR-->
|
||||
<button class="align-middle bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
|
||||
title="Show QR Code" @click="qrcode = `/api/wireguard/client/${client.id}/qrcode.svg`">
|
||||
<svg class="w-5" 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 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Download Config -->
|
||||
<a :href="'/api/wireguard/client/' + client.id + '/configuration'" download
|
||||
class="align-middle inline-block bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
|
||||
title="Download Configuration">
|
||||
<svg class="w-5" 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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Delete -->
|
||||
<button class="align-middle bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
|
||||
title="Delete Client" @click="clientDelete = client">
|
||||
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<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 />
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
class="bg-red-800 text-white hover:bg-red-700 border-2 border-none 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"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">New Client</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="clients === null" class="text-gray-200 p-5">
|
||||
<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>
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code-->
|
||||
<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 z-20">
|
||||
<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">
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
<img :src="qrcode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<div v-if="clientCreate" class="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div
|
||||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg class="h-6 w-6 text-white" 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>
|
||||
</div>
|
||||
<div class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
New Client
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
<input class="rounded p-2 border-2 border-gray-100 focus:border-gray-200 outline-none w-full"
|
||||
type="text" v-model.trim="clientCreateName" placeholder="Name" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button v-if="clientCreateName.length" type="button" @click="createClient(); clientCreate = null"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-800 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Create
|
||||
</button>
|
||||
<button v-else type="button"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-200 text-base font-medium text-white sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed">
|
||||
Create
|
||||
</button>
|
||||
<button type="button" @click="clientCreate = null"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Dialog -->
|
||||
<div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div
|
||||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
Delete Client
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Are you sure you want to delete <strong>{{clientDelete.name}}</strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" @click="deleteClient(clientDelete); clientDelete = null"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Delete
|
||||
</button>
|
||||
<button type="button" @click="clientDelete = null"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="authenticated === false">
|
||||
<h1 class="text-4xl font-medium my-16 text-gray-700 text-center">WireGuard</h1>
|
||||
|
||||
<form @submit="login" class="shadow rounded-md bg-white mx-auto w-64 p-5 overflow-hidden mt-10">
|
||||
<!-- Avatar -->
|
||||
<div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 relative overflow-hidden">
|
||||
<svg class="w-10 h-10 m-5 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password" placeholder="Password" v-model="password"
|
||||
class="px-3 py-2 text-sm text-gray-500 mb-5 border-2 border-gray-100 rounded-lg w-full focus:border-red-800 outline-none" />
|
||||
|
||||
<button v-if="authenticating"
|
||||
class="bg-red-800 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed">
|
||||
<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>
|
||||
@ -162,220 +442,40 @@
|
||||
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">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<input v-if="!authenticating && password" type="submit"
|
||||
class="bg-red-800 w-full rounded shadow py-2 text-sm text-white hover:bg-red-700 transition cursor-pointer"
|
||||
value="Sign In">
|
||||
<input v-if="!authenticating && !password" type="submit"
|
||||
class="bg-gray-200 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed" value="Sign In">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- QR Code-->
|
||||
<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-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">
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
<img :src="qrcode" />
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<div v-if="clientCreate" class="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div
|
||||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg class="h-6 w-6 text-white" 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>
|
||||
</div>
|
||||
<div class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
New Client
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
<input class="rounded p-2 border-2 border-gray-100 focus:border-gray-200 outline-none w-full"
|
||||
type="text" v-model.trim="clientCreateName" placeholder="Name" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button v-if="clientCreateName.length" type="button" @click="createClient(); clientCreate = null"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-800 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Create
|
||||
</button>
|
||||
<button v-else type="button"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-200 text-base font-medium text-white sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed">
|
||||
Create
|
||||
</button>
|
||||
<button type="button" @click="clientCreate = null"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Dialog -->
|
||||
<div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div
|
||||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
Delete Client
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Are you sure you want to delete <strong>{{clientDelete.name}}</strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" @click="deleteClient(clientDelete); clientDelete = null"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Delete
|
||||
</button>
|
||||
<button type="button" @click="clientDelete = null"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="authenticated === false">
|
||||
<h1 class="text-4xl font-medium my-16 text-gray-700 text-center">WireGuard</h1>
|
||||
|
||||
<form @submit="login" class="shadow rounded-md bg-white mx-auto w-64 p-5 overflow-hidden mt-10">
|
||||
<!-- Avatar -->
|
||||
<div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 relative overflow-hidden">
|
||||
<svg class="w-10 h-10 m-5 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password" placeholder="Password" v-model="password"
|
||||
class="px-3 py-2 text-sm text-gray-500 mb-5 border-2 border-gray-100 rounded-lg w-full focus:border-red-800 outline-none" />
|
||||
|
||||
<button v-if="authenticating"
|
||||
class="bg-red-800 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed">
|
||||
<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>
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<input v-if="!authenticating && password" type="submit"
|
||||
class="bg-red-800 w-full rounded shadow py-2 text-sm text-white hover:bg-red-700 transition cursor-pointer"
|
||||
value="Sign In">
|
||||
<input v-if="!authenticating && !password" type="submit"
|
||||
class="bg-gray-200 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed" value="Sign In">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
|
||||
<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://github.com/sponsors/WeeJeWel" target="_blank">Donate</a> · <a class="hover:underline"
|
||||
href="https://github.com/weejewel/wg-easy" target="_blank">GitHub</a></p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<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/timeago.min.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
|
@ -29,6 +29,13 @@ class API {
|
||||
return json;
|
||||
}
|
||||
|
||||
async getRelease() {
|
||||
return this.call({
|
||||
method: 'get',
|
||||
path: '/release',
|
||||
});
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
return this.call({
|
||||
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({
|
||||
el: '#app',
|
||||
components: {
|
||||
apexchart: VueApexCharts,
|
||||
},
|
||||
data: {
|
||||
authenticated: null,
|
||||
authenticating: false,
|
||||
@ -14,10 +17,79 @@ new Vue({
|
||||
requiresPassword: null,
|
||||
|
||||
clients: null,
|
||||
clientsPersist: {},
|
||||
clientDelete: null,
|
||||
clientCreate: null,
|
||||
clientCreateName: '',
|
||||
clientEditName: null,
|
||||
clientEditNameId: null,
|
||||
clientEditAddress: null,
|
||||
clientEditAddressId: 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: {
|
||||
dateTime: value => {
|
||||
@ -38,10 +110,51 @@ new Vue({
|
||||
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;
|
||||
});
|
||||
|
||||
console.log(clients);
|
||||
},
|
||||
login(e) {
|
||||
e.preventDefault();
|
||||
@ -64,6 +177,7 @@ new Vue({
|
||||
})
|
||||
.finally(() => {
|
||||
this.authenticating = false;
|
||||
this.password = null;
|
||||
});
|
||||
},
|
||||
logout(e) {
|
||||
@ -101,28 +215,22 @@ new Vue({
|
||||
.catch(err => alert(err.message || err.toString()))
|
||||
.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: {
|
||||
bytes,
|
||||
timeago: 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() {
|
||||
this.api = new API();
|
||||
@ -141,5 +249,48 @@ new Vue({
|
||||
setInterval(() => {
|
||||
this.refresh().catch(console.error);
|
||||
}, 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