Version 13: new framework, UI_CHART_TYPE, some bugfixes and more (#967)
Thanks to all contributors to get us able to release v13!
This commit is contained in:
commit
519f4efa20
|
@ -0,0 +1 @@
|
||||||
|
/src/node_modules
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Copyright (c) Emile Nijssen
|
||||||
|
# Founder and Codeowner of WireGuard Easy (wg-easy)
|
|
@ -0,0 +1,38 @@
|
||||||
|
name: Build Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Build & Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository_owner == 'wg-easy'
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: production
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||||
|
tags: ghcr.io/wg-easy/wg-easy:pr
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '20'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '20'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/config
|
/config
|
||||||
/wg0.conf
|
/wg0.conf
|
||||||
/wg0.json
|
/wg0.json
|
||||||
|
/src/node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
|
27
Dockerfile
27
Dockerfile
|
@ -1,17 +1,20 @@
|
||||||
# There's an issue with node:20-alpine.
|
# As a workaround we have to build on nodejs 18
|
||||||
# Docker deployment is canceled after 25< minutes.
|
# nodejs 20 hangs on build with armv6/armv7
|
||||||
|
|
||||||
FROM docker.io/library/node:18-alpine AS build_node_modules
|
FROM docker.io/library/node:18-alpine AS build_node_modules
|
||||||
|
|
||||||
|
# Update npm to latest
|
||||||
|
RUN npm install -g npm@latest
|
||||||
|
|
||||||
# Copy Web UI
|
# Copy Web UI
|
||||||
COPY src/ /app/
|
COPY src /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN npm ci --omit=dev &&\
|
RUN npm ci --omit=dev &&\
|
||||||
mv node_modules /node_modules
|
mv node_modules /node_modules
|
||||||
|
|
||||||
# Copy build result to a new image.
|
# Copy build result to a new image.
|
||||||
# This saves a lot of disk space.
|
# This saves a lot of disk space.
|
||||||
FROM docker.io/library/node:18-alpine
|
FROM docker.io/library/node:20-alpine
|
||||||
|
HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3
|
||||||
COPY --from=build_node_modules /app /app
|
COPY --from=build_node_modules /app /app
|
||||||
|
|
||||||
# Move node_modules one directory up, so during development
|
# Move node_modules one directory up, so during development
|
||||||
|
@ -23,14 +26,6 @@ COPY --from=build_node_modules /app /app
|
||||||
# than what runs inside of docker.
|
# than what runs inside of docker.
|
||||||
COPY --from=build_node_modules /node_modules /node_modules
|
COPY --from=build_node_modules /node_modules /node_modules
|
||||||
|
|
||||||
RUN \
|
|
||||||
# Enable this to run `npm run serve`
|
|
||||||
npm i -g nodemon &&\
|
|
||||||
# Workaround CVE-2023-42282
|
|
||||||
npm uninstall -g ip &&\
|
|
||||||
# Delete unnecessary files
|
|
||||||
npm cache clean --force && rm -rf ~/.npm
|
|
||||||
|
|
||||||
# Install Linux packages
|
# Install Linux packages
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
dpkg \
|
dpkg \
|
||||||
|
@ -42,9 +37,9 @@ RUN apk add --no-cache \
|
||||||
# Use iptables-legacy
|
# Use iptables-legacy
|
||||||
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save
|
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save
|
||||||
|
|
||||||
# Expose Ports
|
# Expose Ports (If needed on buildtime)
|
||||||
EXPOSE 51820/udp
|
#EXPOSE 51820/udp
|
||||||
EXPOSE 51821/tcp
|
#EXPOSE 51821/tcp
|
||||||
|
|
||||||
# Set Environment
|
# Set Environment
|
||||||
ENV DEBUG=Server,WireGuard
|
ENV DEBUG=Server,WireGuard
|
||||||
|
|
32
README.md
32
README.md
|
@ -37,9 +37,9 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
|
||||||
If you haven't installed Docker yet, install it by running:
|
If you haven't installed Docker yet, install it by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ curl -sSL https://get.docker.com | sh
|
curl -sSL https://get.docker.com | sh
|
||||||
$ sudo usermod -aG docker $(whoami)
|
sudo usermod -aG docker $(whoami)
|
||||||
$ exit
|
exit
|
||||||
```
|
```
|
||||||
|
|
||||||
And log in again.
|
And log in again.
|
||||||
|
@ -48,12 +48,14 @@ And log in again.
|
||||||
|
|
||||||
To automatically install & run wg-easy, simply run:
|
To automatically install & run wg-easy, simply run:
|
||||||
|
|
||||||
<pre>
|
```
|
||||||
$ docker run -d \
|
docker run -d \
|
||||||
--name=wg-easy \
|
--name=wg-easy \
|
||||||
-e LANG=de \
|
-e LANG=de \
|
||||||
-e WG_HOST=<b>🚨YOUR_SERVER_IP</b> \
|
-e WG_HOST=<🚨YOUR_SERVER_IP> \
|
||||||
-e PASSWORD=<b>🚨YOUR_ADMIN_PASSWORD</b> \
|
-e PASSWORD=<🚨YOUR_ADMIN_PASSWORD> \
|
||||||
|
-e PORT=51821 \
|
||||||
|
-e WG_PORT=51820 \
|
||||||
-v ~/.wg-easy:/etc/wireguard \
|
-v ~/.wg-easy:/etc/wireguard \
|
||||||
-p 51820:51820/udp \
|
-p 51820:51820/udp \
|
||||||
-p 51821:51821/tcp \
|
-p 51821:51821/tcp \
|
||||||
|
@ -63,7 +65,7 @@ $ docker run -d \
|
||||||
--sysctl="net.ipv4.ip_forward=1" \
|
--sysctl="net.ipv4.ip_forward=1" \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
ghcr.io/wg-easy/wg-easy
|
ghcr.io/wg-easy/wg-easy
|
||||||
</pre>
|
```
|
||||||
|
|
||||||
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
|
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
|
||||||
>
|
>
|
||||||
|
@ -73,6 +75,10 @@ The Web UI will now be available on `http://0.0.0.0:51821`.
|
||||||
|
|
||||||
> 💡 Your configuration files will be saved in `~/.wg-easy`
|
> 💡 Your configuration files will be saved in `~/.wg-easy`
|
||||||
|
|
||||||
|
WireGuard Easy can be launched with Docker Compose as well - just download
|
||||||
|
[`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
|
||||||
|
execute `docker compose up --detach`.
|
||||||
|
|
||||||
### 3. Sponsor
|
### 3. Sponsor
|
||||||
|
|
||||||
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
|
||||||
|
@ -98,7 +104,7 @@ These options can be configured by setting environment variables using `-e KEY="
|
||||||
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
|
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
|
||||||
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
|
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
|
||||||
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
|
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
|
||||||
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th). |
|
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
|
||||||
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
|
||||||
|
|
||||||
> 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.
|
||||||
|
@ -115,6 +121,14 @@ docker pull ghcr.io/wg-easy/wg-easy
|
||||||
|
|
||||||
And then run the `docker run -d \ ...` command above again.
|
And then run the `docker run -d \ ...` command above again.
|
||||||
|
|
||||||
|
With Docker Compose WireGuard Easy can be updated with a single command:
|
||||||
|
`docker compose up --detach --pull always` (if an image tag is specified in the
|
||||||
|
Compose file and it is not `latest`, make sure that it is changed to the desired
|
||||||
|
one; by default it is omitted and
|
||||||
|
[defaults to `latest`](https://docs.docker.com/engine/reference/run/#image-references)). \
|
||||||
|
The WireGuared Easy container will be automatically recreated if a newer image
|
||||||
|
was pulled.
|
||||||
|
|
||||||
## Common Use Cases
|
## Common Use Cases
|
||||||
|
|
||||||
* [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole)
|
* [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
version: "3.8"
|
|
||||||
services:
|
services:
|
||||||
wg-easy:
|
wg-easy:
|
||||||
image: wg-easy
|
image: wg-easy
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
version: "3.8"
|
|
||||||
volumes:
|
volumes:
|
||||||
etc_wireguard:
|
etc_wireguard:
|
||||||
|
|
||||||
|
@ -6,7 +5,7 @@ services:
|
||||||
wg-easy:
|
wg-easy:
|
||||||
environment:
|
environment:
|
||||||
# Change Language:
|
# Change Language:
|
||||||
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th)
|
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi)
|
||||||
- LANG=de
|
- LANG=de
|
||||||
# ⚠️ Required:
|
# ⚠️ Required:
|
||||||
# Change this to your host's public address
|
# Change this to your host's public address
|
||||||
|
@ -14,6 +13,7 @@ services:
|
||||||
|
|
||||||
# Optional:
|
# Optional:
|
||||||
# - PASSWORD=foobar123
|
# - PASSWORD=foobar123
|
||||||
|
# - PORT=51821
|
||||||
# - WG_PORT=51820
|
# - WG_PORT=51820
|
||||||
# - 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
|
||||||
|
@ -25,6 +25,7 @@ services:
|
||||||
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
|
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
|
||||||
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
|
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
|
||||||
# - UI_TRAFFIC_STATS=true
|
# - UI_TRAFFIC_STATS=true
|
||||||
|
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
|
||||||
|
|
||||||
image: ghcr.io/wg-easy/wg-easy
|
image: ghcr.io/wg-easy/wg-easy
|
||||||
container_name: wg-easy
|
container_name: wg-easy
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"8": "Updated to Node.js v18.",
|
"8": "Updated to Node.js v18.",
|
||||||
"9": "Fixed issue running on devices with older kernels.",
|
"9": "Fixed issue running on devices with older kernels.",
|
||||||
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
"10": "Added sessionless HTTP API auth & automatic dark mode.",
|
||||||
"11": "Multilanguage Support & various bugfixes",
|
"11": "Multilanguage Support & various bugfixes.",
|
||||||
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more."
|
"12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more.",
|
||||||
|
"13": "New framework (h3), UI_CHART_TYPE, some bugfixes and more."
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
|
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
|
||||||
"serve": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||||
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
|
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
/node_modules
|
|
|
@ -35,3 +35,4 @@ iptables -D FORWARD -o wg0 -j ACCEPT;
|
||||||
`.split('\n').join(' ');
|
`.split('\n').join(' ');
|
||||||
module.exports.LANG = process.env.LANG || 'en';
|
module.exports.LANG = process.env.LANG || 'en';
|
||||||
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
|
||||||
|
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
|
const { createServer } = require('node:http');
|
||||||
|
const { stat, readFile } = require('node:fs/promises');
|
||||||
|
const { join } = require('node:path');
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const expressSession = require('express-session');
|
const expressSession = require('express-session');
|
||||||
const debug = require('debug')('Server');
|
const debug = require('debug')('Server');
|
||||||
|
|
||||||
const Util = require('./Util');
|
const {
|
||||||
const ServerError = require('./ServerError');
|
createApp,
|
||||||
|
createError,
|
||||||
|
createRouter,
|
||||||
|
defineEventHandler,
|
||||||
|
fromNodeMiddleware,
|
||||||
|
getRouterParam,
|
||||||
|
toNodeListener,
|
||||||
|
readBody,
|
||||||
|
setHeader,
|
||||||
|
serveStatic,
|
||||||
|
} = require('h3');
|
||||||
|
|
||||||
const WireGuard = require('../services/WireGuard');
|
const WireGuard = require('../services/WireGuard');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -19,41 +31,50 @@ const {
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
LANG,
|
LANG,
|
||||||
UI_TRAFFIC_STATS,
|
UI_TRAFFIC_STATS,
|
||||||
|
UI_CHART_TYPE,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
|
|
||||||
module.exports = class Server {
|
module.exports = class Server {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Express
|
const app = createApp();
|
||||||
this.app = express()
|
this.app = app;
|
||||||
.disable('etag')
|
|
||||||
.use('/', express.static(path.join(__dirname, '..', 'www')))
|
app.use(fromNodeMiddleware(expressSession({
|
||||||
.use(express.json())
|
|
||||||
.use(expressSession({
|
|
||||||
secret: crypto.randomBytes(256).toString('hex'),
|
secret: crypto.randomBytes(256).toString('hex'),
|
||||||
resave: true,
|
resave: true,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
cookie: {
|
})));
|
||||||
httpOnly: true,
|
|
||||||
},
|
const router = createRouter();
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
router
|
||||||
|
.get('/api/release', defineEventHandler((event) => {
|
||||||
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
return RELEASE;
|
||||||
}))
|
}))
|
||||||
|
|
||||||
.get('/api/release', (Util.promisify(async () => {
|
.get('/api/lang', defineEventHandler((event) => {
|
||||||
return RELEASE;
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
})))
|
return `"${LANG}"`;
|
||||||
|
}))
|
||||||
|
|
||||||
.get('/api/lang', (Util.promisify(async () => {
|
.get('/api/ui-traffic-stats', defineEventHandler((event) => {
|
||||||
return LANG;
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
})))
|
return `"${UI_TRAFFIC_STATS}"`;
|
||||||
.get('/api/ui-traffic-stats', (Util.promisify(async () => {
|
}))
|
||||||
return UI_TRAFFIC_STATS === 'true';
|
|
||||||
})))
|
.get('/api/ui-chart-type', defineEventHandler((event) => {
|
||||||
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
return `"${UI_CHART_TYPE}"`;
|
||||||
|
}))
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
.get('/api/session', Util.promisify(async (req) => {
|
.get('/api/session', defineEventHandler((event) => {
|
||||||
const requiresPassword = !!process.env.PASSWORD;
|
const requiresPassword = !!process.env.PASSWORD;
|
||||||
const authenticated = requiresPassword
|
const authenticated = requiresPassword
|
||||||
? !!(req.session && req.session.authenticated)
|
? !!(event.node.req.session && event.node.req.session.authenticated)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -61,28 +82,35 @@ module.exports = class Server {
|
||||||
authenticated,
|
authenticated,
|
||||||
};
|
};
|
||||||
}))
|
}))
|
||||||
.post('/api/session', Util.promisify(async (req) => {
|
.post('/api/session', defineEventHandler(async (event) => {
|
||||||
const {
|
const { password } = await readBody(event);
|
||||||
password,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (typeof password !== 'string') {
|
if (typeof password !== 'string') {
|
||||||
throw new ServerError('Missing: Password', 401);
|
throw createError({
|
||||||
|
status: 401,
|
||||||
|
message: 'Missing: Password',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password !== PASSWORD) {
|
if (password !== PASSWORD) {
|
||||||
throw new ServerError('Incorrect Password', 401);
|
throw createError({
|
||||||
|
status: 401,
|
||||||
|
message: 'Incorrect Password',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.authenticated = true;
|
event.node.req.session.authenticated = true;
|
||||||
req.session.save();
|
event.node.req.session.save();
|
||||||
|
|
||||||
debug(`New Session: ${req.session.id}`);
|
debug(`New Session: ${event.node.req.session.id}`);
|
||||||
}))
|
|
||||||
|
return { succcess: true };
|
||||||
|
}));
|
||||||
|
|
||||||
// WireGuard
|
// WireGuard
|
||||||
.use((req, res, next) => {
|
app.use(
|
||||||
if (!PASSWORD) {
|
fromNodeMiddleware((req, res, next) => {
|
||||||
|
if (!PASSWORD || !req.url.startsWith('/api/')) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +118,7 @@ module.exports = class Server {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.path.startsWith('/api/') && req.headers['authorization']) {
|
if (req.url.startsWith('/api/') && req.headers['authorization']) {
|
||||||
if (bcrypt.compareSync(req.headers['authorization'], bcrypt.hashSync(PASSWORD, 10))) {
|
if (bcrypt.compareSync(req.headers['authorization'], bcrypt.hashSync(PASSWORD, 10))) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -102,25 +130,32 @@ module.exports = class Server {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
error: 'Not Logged In',
|
error: 'Not Logged In',
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
.delete('/api/session', Util.promisify(async (req) => {
|
);
|
||||||
const sessionId = req.session.id;
|
|
||||||
|
|
||||||
req.session.destroy();
|
const router2 = createRouter();
|
||||||
|
app.use(router2);
|
||||||
|
|
||||||
|
router2
|
||||||
|
.delete('/api/session', defineEventHandler((event) => {
|
||||||
|
const sessionId = event.node.req.session.id;
|
||||||
|
|
||||||
|
event.node.req.session.destroy();
|
||||||
|
|
||||||
debug(`Deleted Session: ${sessionId}`);
|
debug(`Deleted Session: ${sessionId}`);
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.get('/api/wireguard/client', Util.promisify(async (req) => {
|
.get('/api/wireguard/client', defineEventHandler(() => {
|
||||||
return WireGuard.getClients();
|
return WireGuard.getClients();
|
||||||
}))
|
}))
|
||||||
.get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => {
|
.get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
||||||
res.header('Content-Type', 'image/svg+xml');
|
setHeader(event, 'Content-Type', 'image/svg+xml');
|
||||||
res.send(svg);
|
return svg;
|
||||||
}))
|
}))
|
||||||
.get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => {
|
.get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
const client = await WireGuard.getClient({ clientId });
|
const client = await WireGuard.getClient({ clientId });
|
||||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||||
const configName = client.name
|
const configName = client.name
|
||||||
|
@ -128,52 +163,85 @@ module.exports = class Server {
|
||||||
.replace(/(-{2,}|-$)/g, '-')
|
.replace(/(-{2,}|-$)/g, '-')
|
||||||
.replace(/-$/, '')
|
.replace(/-$/, '')
|
||||||
.substring(0, 32);
|
.substring(0, 32);
|
||||||
res.header('Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
|
setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
|
||||||
res.header('Content-Type', 'text/plain');
|
setHeader(event, 'Content-Type', 'text/plain');
|
||||||
res.send(config);
|
return config;
|
||||||
}))
|
}))
|
||||||
.post('/api/wireguard/client', Util.promisify(async (req) => {
|
.post('/api/wireguard/client', defineEventHandler(async (event) => {
|
||||||
const { name } = req.body;
|
const { name } = await readBody(event);
|
||||||
return WireGuard.createClient({ name });
|
await WireGuard.createClient({ name });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.delete('/api/wireguard/client/:clientId', Util.promisify(async (req) => {
|
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
return WireGuard.deleteClient({ clientId });
|
await WireGuard.deleteClient({ clientId });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.post('/api/wireguard/client/:clientId/enable', Util.promisify(async (req, res) => {
|
.post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
res.end(403);
|
throw createError({ status: 403 });
|
||||||
}
|
}
|
||||||
return WireGuard.enableClient({ clientId });
|
await WireGuard.enableClient({ clientId });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.post('/api/wireguard/client/:clientId/disable', Util.promisify(async (req, res) => {
|
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
res.end(403);
|
throw createError({ status: 403 });
|
||||||
}
|
}
|
||||||
return WireGuard.disableClient({ clientId });
|
await WireGuard.disableClient({ clientId });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.put('/api/wireguard/client/:clientId/name', Util.promisify(async (req, res) => {
|
.put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
res.end(403);
|
throw createError({ status: 403 });
|
||||||
}
|
}
|
||||||
const { name } = req.body;
|
const { name } = await readBody(event);
|
||||||
return WireGuard.updateClientName({ clientId, name });
|
await WireGuard.updateClientName({ clientId, name });
|
||||||
|
return { success: true };
|
||||||
}))
|
}))
|
||||||
.put('/api/wireguard/client/:clientId/address', Util.promisify(async (req, res) => {
|
.put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => {
|
||||||
const { clientId } = req.params;
|
const clientId = getRouterParam(event, 'clientId');
|
||||||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
|
||||||
res.end(403);
|
throw createError({ status: 403 });
|
||||||
}
|
}
|
||||||
const { address } = req.body;
|
const { address } = await readBody(event);
|
||||||
return WireGuard.updateClientAddress({ clientId, address });
|
await WireGuard.updateClientAddress({ clientId, address });
|
||||||
}))
|
return { success: true };
|
||||||
|
}));
|
||||||
|
|
||||||
.listen(PORT, WEBUI_HOST, () => {
|
// Static assets
|
||||||
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
|
const publicDir = 'www';
|
||||||
|
app.use(
|
||||||
|
defineEventHandler((event) => {
|
||||||
|
return serveStatic(event, {
|
||||||
|
getContents: (id) => readFile(join(publicDir, id)),
|
||||||
|
getMeta: async (id) => {
|
||||||
|
const stats = await stat(join(publicDir, id)).catch(() => {});
|
||||||
|
|
||||||
|
if (!stats || !stats.isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.endsWith('.html')) setHeader(event, 'Content-Type', 'text/html');
|
||||||
|
if (id.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript');
|
||||||
|
if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css');
|
||||||
|
if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png');
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: stats.size,
|
||||||
|
mtime: stats.mtimeMs,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST);
|
||||||
|
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -95,7 +95,7 @@ module.exports = class WireGuard {
|
||||||
[Interface]
|
[Interface]
|
||||||
PrivateKey = ${config.server.privateKey}
|
PrivateKey = ${config.server.privateKey}
|
||||||
Address = ${config.server.address}/24
|
Address = ${config.server.address}/24
|
||||||
ListenPort = 51820
|
ListenPort = ${WG_PORT}
|
||||||
PreUp = ${WG_PRE_UP}
|
PreUp = ${WG_PRE_UP}
|
||||||
PostUp = ${WG_POST_UP}
|
PostUp = ${WG_POST_UP}
|
||||||
PreDown = ${WG_PRE_DOWN}
|
PreDown = ${WG_PRE_DOWN}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"release": "12",
|
"release": "13",
|
||||||
"name": "wg-easy",
|
"name": "wg-easy",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "DEBUG=Server,WireGuard nodemon server.js",
|
"serve": "DEBUG=Server,WireGuard npx nodemon server.js",
|
||||||
"serve-with-password": "PASSWORD=wg npm run serve",
|
"serve-with-password": "PASSWORD=wg npm run serve",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
|
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
|
||||||
|
@ -15,14 +15,15 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"express": "^4.18.3",
|
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
|
"h3": "^1.11.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-athom": "^3.1.3",
|
"eslint-config-athom": "^3.1.3",
|
||||||
"tailwindcss": "^3.4.1"
|
"nodemon": "^3.1.0",
|
||||||
|
"tailwindcss": "^3.4.3"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
@ -30,6 +31,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ process.on('SIGTERM', async () => {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle interupt signal
|
// Handle interrupt signal
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('SIGINT signal received.');
|
console.log('SIGINT signal received.');
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: 'media',
|
darkMode: 'selector',
|
||||||
content: ['./www/**/*.{html,js}'],
|
content: ['./www/**/*.{html,js}'],
|
||||||
theme: {
|
theme: {
|
||||||
screens: {
|
screens: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
|
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -211,6 +211,8 @@ textarea {
|
||||||
/* 1 */
|
/* 1 */
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
|
letter-spacing: inherit;
|
||||||
|
/* 1 */
|
||||||
color: inherit;
|
color: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -234,9 +236,9 @@ select {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
[type='button'],
|
input:where([type='button']),
|
||||||
[type='reset'],
|
input:where([type='reset']),
|
||||||
[type='submit'] {
|
input:where([type='submit']) {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -492,6 +494,10 @@ video {
|
||||||
--tw-backdrop-opacity: ;
|
--tw-backdrop-opacity: ;
|
||||||
--tw-backdrop-saturate: ;
|
--tw-backdrop-saturate: ;
|
||||||
--tw-backdrop-sepia: ;
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
}
|
}
|
||||||
|
|
||||||
::backdrop {
|
::backdrop {
|
||||||
|
@ -542,6 +548,10 @@ video {
|
||||||
--tw-backdrop-opacity: ;
|
--tw-backdrop-opacity: ;
|
||||||
--tw-backdrop-saturate: ;
|
--tw-backdrop-saturate: ;
|
||||||
--tw-backdrop-sepia: ;
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
@ -590,6 +600,18 @@ video {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
@ -692,8 +714,8 @@ video {
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-2 {
|
.mb-4 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-5 {
|
.mb-5 {
|
||||||
|
@ -732,6 +754,10 @@ video {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-5 {
|
.mt-5 {
|
||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
@ -804,10 +830,18 @@ video {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-5 {
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-6 {
|
.h-6 {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-8 {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-h-screen {
|
.min-h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
@ -876,6 +910,10 @@ video {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grow-0 {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.transform {
|
.transform {
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
}
|
}
|
||||||
|
@ -921,10 +959,18 @@ video {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-col-reverse {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-wrap {
|
.flex-wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.items-end {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -957,6 +1003,10 @@ video {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.self-end {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -1087,6 +1137,14 @@ video {
|
||||||
--tw-bg-opacity: 0.5;
|
--tw-bg-opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-gray-400 {
|
||||||
|
fill: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-gray-600 {
|
||||||
|
fill: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
.p-1 {
|
.p-1 {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -1141,11 +1199,6 @@ video {
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.py-5 {
|
|
||||||
padding-top: 1.25rem;
|
|
||||||
padding-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pb-1 {
|
.pb-1 {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -1276,6 +1329,11 @@ video {
|
||||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-neutral-400 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.text-red-600 {
|
.text-red-600 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||||
|
@ -1463,10 +1521,24 @@ video {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.peer:checked ~ .peer-checked\:fill-gray-600 {
|
||||||
|
fill: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 450px) {
|
@media (min-width: 450px) {
|
||||||
.xxs\:flex-row {
|
.xxs\:flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xxs\:self-center {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.xs\:mt-6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
|
@ -1597,6 +1669,11 @@ video {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md\:py-5 {
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:pb-0 {
|
.md\:pb-0 {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
@ -1607,208 +1684,222 @@ video {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.dark\:border-neutral-500:where(.dark, .dark *) {
|
||||||
.dark\:border-neutral-500 {
|
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:border-neutral-600 {
|
.dark\:border-neutral-600:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:border-neutral-800 {
|
.dark\:border-neutral-800:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
border-color: rgb(38 38 38 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:border-red-600 {
|
.dark\:border-red-600:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-black {
|
.dark\:bg-black:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-400 {
|
.dark\:bg-neutral-400:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-500 {
|
.dark\:bg-neutral-500:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-600 {
|
.dark\:bg-neutral-600:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-700 {
|
.dark\:bg-neutral-700:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
|
background-color: rgb(64 64 64 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-neutral-800 {
|
.dark\:bg-neutral-800:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
|
background-color: rgb(38 38 38 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-red-100 {
|
.dark\:bg-red-100:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-red-600 {
|
.dark\:bg-red-600:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:bg-red-800 {
|
.dark\:bg-red-800:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-gray-500 {
|
.dark\:fill-neutral-400:where(.dark, .dark *) {
|
||||||
|
fill: #a3a3a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:fill-neutral-600:where(.dark, .dark *) {
|
||||||
|
fill: #525252;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:text-gray-500:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-200 {
|
.dark\:text-neutral-200:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(229 229 229 / var(--tw-text-opacity));
|
color: rgb(229 229 229 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-300 {
|
.dark\:text-neutral-300:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(212 212 212 / var(--tw-text-opacity));
|
color: rgb(212 212 212 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-400 {
|
.dark\:text-neutral-400:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-50 {
|
.dark\:text-neutral-50:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(250 250 250 / var(--tw-text-opacity));
|
color: rgb(250 250 250 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-500 {
|
.dark\:text-neutral-500:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-neutral-600 {
|
.dark\:text-neutral-600:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(82 82 82 / var(--tw-text-opacity));
|
color: rgb(82 82 82 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-red-300 {
|
.dark\:text-red-300:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-red-600 {
|
.dark\:text-red-600:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:text-white {
|
.dark\:text-white:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:opacity-50 {
|
.dark\:opacity-50:where(.dark, .dark *) {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-400::-moz-placeholder {
|
.dark\:placeholder\:text-neutral-400:where(.dark, .dark *)::-moz-placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-400::placeholder {
|
.dark\:placeholder\:text-neutral-400:where(.dark, .dark *)::placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(163 163 163 / var(--tw-text-opacity));
|
color: rgb(163 163 163 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-500::-moz-placeholder {
|
.dark\:placeholder\:text-neutral-500:where(.dark, .dark *)::-moz-placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:placeholder\:text-neutral-500::placeholder {
|
.dark\:placeholder\:text-neutral-500:where(.dark, .dark *)::placeholder {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(115 115 115 / var(--tw-text-opacity));
|
color: rgb(115 115 115 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:border-neutral-600:hover {
|
.dark\:hover\:border-neutral-600:hover:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
border-color: rgb(82 82 82 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:border-red-600:hover {
|
.dark\:hover\:border-red-600:hover:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
border-color: rgb(220 38 38 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-neutral-500:hover {
|
.dark\:hover\:bg-neutral-500:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
background-color: rgb(115 115 115 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-neutral-600:hover {
|
.dark\:hover\:bg-neutral-600:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
background-color: rgb(82 82 82 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-600:hover {
|
.dark\:hover\:bg-red-600:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-700:hover {
|
.dark\:hover\:bg-red-700:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-800:hover {
|
.dark\:hover\:bg-red-800:hover:where(.dark, .dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-neutral-700:hover {
|
.dark\:hover\:text-neutral-700:hover:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(64 64 64 / var(--tw-text-opacity));
|
color: rgb(64 64 64 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-red-100:hover {
|
.dark\:hover\:text-red-100:hover:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-white:hover {
|
.dark\:hover\:text-white:hover:where(.dark, .dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:focus\:border-neutral-500:focus {
|
.dark\:focus\:border-neutral-500:focus:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:focus\:border-red-800:focus {
|
.dark\:focus\:border-red-800:focus:where(.dark, .dark *) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
border-color: rgb(153 27 27 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus\:dark\:border-neutral-500:focus {
|
.focus\:dark\:border-neutral-500:where(.dark, .dark *):focus {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
border-color: rgb(115 115 115 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group:hover .group-hover\:dark\:fill-neutral-500:where(.dark, .dark *) {
|
||||||
|
fill: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer:checked ~ .peer-checked\:dark\:fill-neutral-400:where(.dark, .dark *) {
|
||||||
|
fill: #a3a3a3;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>WireGuard</title>
|
<title>WireGuard</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<link rel="manifest" href="./manifest.json">
|
<link rel="manifest" href="./manifest.json">
|
||||||
<link rel="icon" type="image/png" href="./img/favicon.png">
|
<link rel="icon" type="image/png" href="./img/favicon.png">
|
||||||
|
@ -18,25 +19,56 @@
|
||||||
|
|
||||||
<body class="bg-gray-50 dark:bg-neutral-800">
|
<body class="bg-gray-50 dark:bg-neutral-800">
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0">
|
<div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6">
|
||||||
<div v-if="authenticated === true">
|
<div v-if="authenticated === true">
|
||||||
|
<div class="flex flex-col-reverse xxs:flex-row flex-auto items-center items-end gap-3">
|
||||||
|
<h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4">
|
||||||
|
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg mr-2" /><span class="align-middle">WireGuard</span>
|
||||||
|
</h1>
|
||||||
|
<div class="flex items-center grow-0 gap-3 items-end self-end xxs:self-center">
|
||||||
|
<!-- Dark / light theme -->
|
||||||
|
<button @click="toggleTheme"
|
||||||
|
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition" :title="$t(`theme.${uiTheme}`)">
|
||||||
|
<svg v-if="uiTheme === 'light'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||||
|
class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="uiTheme === 'dark'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||||
|
class="w-5 h-5 text-neutral-400">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
|
||||||
|
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400">
|
||||||
|
<path
|
||||||
|
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M3.8,12c0-4.5,3.7-8.2,8.2-8.2v16.5C7.5,20.2,3.8,16.5,3.8,12z" />
|
||||||
|
</svg>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- Show / hide charts -->
|
||||||
|
<label v-if="uiChartType > 0" class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group" :title="$t('toggleCharts')">
|
||||||
|
<input type="checkbox" value="" class="sr-only peer" v-model="uiShowCharts" @change="toggleCharts">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" fill="currentColor"
|
||||||
|
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition">
|
||||||
|
<path
|
||||||
|
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
<span v-if="requiresPassword"
|
<span v-if="requiresPassword"
|
||||||
class="text-sm text-gray-400 dark:text-neutral-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right"
|
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline"
|
||||||
@click="logout">
|
@click="logout">
|
||||||
{{$t("logout")}}
|
{{$t("logout")}}
|
||||||
|
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<h1 class="text-4xl dark:text-neutral-200 font-medium mt-2 mb-2">
|
</div>
|
||||||
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg" />
|
</div>
|
||||||
<span class="align-middle">WireGuard</span>
|
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5"></div>
|
||||||
</h1>
|
|
||||||
<h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2>
|
|
||||||
|
|
||||||
<div v-if="latestRelease"
|
<div v-if="latestRelease"
|
||||||
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg"
|
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg"
|
||||||
:title="`v${currentRelease} → v${latestRelease.version}`">
|
:title="`v${currentRelease} → v${latestRelease.version}`">
|
||||||
|
@ -77,17 +109,17 @@
|
||||||
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid">
|
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid">
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%;">
|
<div v-if="uiChartType" class="absolute z-0 bottom-0 left-0 right-0 h-6" >
|
||||||
<apexchart width="100%" height="100%" :options="client.chartOptions" :series="client.transferTxSeries">
|
<apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries">
|
||||||
</apexchart>
|
</apexchart>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%;">
|
<div v-if="uiChartType" class="absolute z-0 top-0 left-0 right-0 h-6" >
|
||||||
<apexchart width="100%" height="100%" :options="client.chartOptions" :series="client.transferRxSeries"
|
<apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries"
|
||||||
style="transform: scaleY(-1);">
|
style="transform: scaleY(-1);">
|
||||||
</apexchart>
|
</apexchart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative py-5 px-3 z-10 flex flex-col sm:flex-row justify-between gap-3">
|
<div class="relative py-3 md:py-5 px-3 z-10 flex flex-col sm:flex-row justify-between gap-3">
|
||||||
<div class="flex gap-3 md:gap-4 w-full items-center ">
|
<div class="flex gap-3 md:gap-4 w-full items-center ">
|
||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
|
|
|
@ -50,6 +50,13 @@ class API {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChartType() {
|
||||||
|
return this.call({
|
||||||
|
method: 'get',
|
||||||
|
path: '/ui-chart-type',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getSession() {
|
async getSession() {
|
||||||
return this.call({
|
return this.call({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|
|
@ -29,8 +29,24 @@ const i18n = new VueI18n({
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const UI_CHART_TYPES = [
|
||||||
|
{ type: false, strokeWidth: 0 },
|
||||||
|
{ type: 'line', strokeWidth: 3 },
|
||||||
|
{ type: 'area', strokeWidth: 0 },
|
||||||
|
{ type: 'bar', strokeWidth: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CHART_COLORS = {
|
||||||
|
rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' },
|
||||||
|
tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' },
|
||||||
|
gradient: { light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'] },
|
||||||
|
};
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
i18n,
|
i18n,
|
||||||
data: {
|
data: {
|
||||||
authenticated: null,
|
authenticated: null,
|
||||||
|
@ -52,13 +68,16 @@ new Vue({
|
||||||
currentRelease: null,
|
currentRelease: null,
|
||||||
latestRelease: null,
|
latestRelease: null,
|
||||||
|
|
||||||
isDark: null,
|
|
||||||
uiTrafficStats: false,
|
uiTrafficStats: false,
|
||||||
|
|
||||||
|
uiChartType: 0,
|
||||||
|
uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
|
||||||
|
uiTheme: localStorage.theme || 'auto',
|
||||||
|
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
|
||||||
|
|
||||||
chartOptions: {
|
chartOptions: {
|
||||||
chart: {
|
chart: {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
type: 'bar',
|
|
||||||
stacked: false,
|
stacked: false,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: false,
|
show: false,
|
||||||
|
@ -66,11 +85,27 @@ new Vue({
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
parentHeightOffset: 0,
|
||||||
|
sparkline: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: [],
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: {
|
||||||
|
shade: 'dark',
|
||||||
|
type: 'vertical',
|
||||||
|
shadeIntensity: 0,
|
||||||
|
gradientToColors: CHART_COLORS.gradient[this.theme],
|
||||||
|
inverseColors: false,
|
||||||
|
opacityTo: 0,
|
||||||
|
stops: [0, 100],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
colors: [
|
|
||||||
'#DDDDDD', // rx
|
|
||||||
'#EEEEEE', // tx
|
|
||||||
],
|
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
@ -84,10 +119,10 @@ new Vue({
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
axisTicks: {
|
axisTicks: {
|
||||||
show: true,
|
show: false,
|
||||||
},
|
},
|
||||||
axisBorder: {
|
axisBorder: {
|
||||||
show: true,
|
show: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
|
@ -153,27 +188,42 @@ new Vue({
|
||||||
// Debug
|
// Debug
|
||||||
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
||||||
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
||||||
|
// client.latestHandshakeAt = new Date();
|
||||||
|
// this.requiresPassword = true;
|
||||||
|
|
||||||
if (updateCharts) {
|
|
||||||
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious;
|
||||||
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
this.clientsPersist[client.id].transferRxPrevious = client.transferRx;
|
||||||
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious;
|
||||||
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
this.clientsPersist[client.id].transferTxPrevious = client.transferTx;
|
||||||
|
|
||||||
|
if (updateCharts) {
|
||||||
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
|
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent);
|
||||||
this.clientsPersist[client.id].transferRxHistory.shift();
|
this.clientsPersist[client.id].transferRxHistory.shift();
|
||||||
|
|
||||||
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
|
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent);
|
||||||
this.clientsPersist[client.id].transferTxHistory.shift();
|
this.clientsPersist[client.id].transferTxHistory.shift();
|
||||||
}
|
|
||||||
|
|
||||||
client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent;
|
this.clientsPersist[client.id].transferTxSeries = [{
|
||||||
client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent;
|
name: 'Tx',
|
||||||
|
data: this.clientsPersist[client.id].transferTxHistory,
|
||||||
|
}];
|
||||||
|
|
||||||
|
this.clientsPersist[client.id].transferRxSeries = [{
|
||||||
|
name: 'Rx',
|
||||||
|
data: this.clientsPersist[client.id].transferRxHistory,
|
||||||
|
}];
|
||||||
|
|
||||||
client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory;
|
client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory;
|
||||||
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
|
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory;
|
||||||
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory);
|
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory);
|
||||||
|
|
||||||
|
client.transferTxSeries = this.clientsPersist[client.id].transferTxSeries;
|
||||||
|
client.transferRxSeries = this.clientsPersist[client.id].transferRxSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent;
|
||||||
|
client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent;
|
||||||
|
|
||||||
client.hoverTx = this.clientsPersist[client.id].hoverTx;
|
client.hoverTx = this.clientsPersist[client.id].hoverTx;
|
||||||
client.hoverRx = this.clientsPersist[client.id].hoverRx;
|
client.hoverRx = this.clientsPersist[client.id].hoverRx;
|
||||||
|
|
||||||
|
@ -250,14 +300,25 @@ new Vue({
|
||||||
.finally(() => this.refresh().catch(console.error));
|
.finally(() => this.refresh().catch(console.error));
|
||||||
},
|
},
|
||||||
toggleTheme() {
|
toggleTheme() {
|
||||||
if (this.isDark) {
|
const themes = ['light', 'dark', 'auto'];
|
||||||
localStorage.theme = 'light';
|
const currentIndex = themes.indexOf(this.uiTheme);
|
||||||
document.documentElement.classList.remove('dark');
|
const newIndex = (currentIndex + 1) % themes.length;
|
||||||
} else {
|
this.uiTheme = themes[newIndex];
|
||||||
localStorage.theme = 'dark';
|
localStorage.theme = this.uiTheme;
|
||||||
document.documentElement.classList.add('dark');
|
this.setTheme(this.uiTheme);
|
||||||
|
},
|
||||||
|
setTheme(theme) {
|
||||||
|
const { classList } = document.documentElement;
|
||||||
|
const shouldAddDarkClass = theme === 'dark' || (theme === 'auto' && this.prefersDarkScheme.matches);
|
||||||
|
classList.toggle('dark', shouldAddDarkClass);
|
||||||
|
},
|
||||||
|
handlePrefersChange(e) {
|
||||||
|
if (localStorage.theme === 'auto') {
|
||||||
|
this.setTheme(e.matches ? 'dark' : 'light');
|
||||||
}
|
}
|
||||||
this.isDark = !this.isDark;
|
},
|
||||||
|
toggleCharts() {
|
||||||
|
localStorage.setItem('uiShowCharts', this.uiShowCharts ? 1 : 0);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
@ -267,10 +328,8 @@ new Vue({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.isDark = false;
|
this.prefersDarkScheme.addListener(this.handlePrefersChange);
|
||||||
if (localStorage.theme === 'dark') {
|
this.setTheme(this.uiTheme);
|
||||||
this.isDark = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api = new API();
|
this.api = new API();
|
||||||
this.api.getSession()
|
this.api.getSession()
|
||||||
|
@ -278,7 +337,7 @@ new Vue({
|
||||||
this.authenticated = session.authenticated;
|
this.authenticated = session.authenticated;
|
||||||
this.requiresPassword = session.requiresPassword;
|
this.requiresPassword = session.requiresPassword;
|
||||||
this.refresh({
|
this.refresh({
|
||||||
updateCharts: true,
|
updateCharts: this.updateCharts,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
alert(err.message || err.toString());
|
alert(err.message || err.toString());
|
||||||
});
|
});
|
||||||
|
@ -289,7 +348,7 @@ new Vue({
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.refresh({
|
this.refresh({
|
||||||
updateCharts: true,
|
updateCharts: this.updateCharts,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
@ -298,10 +357,17 @@ new Vue({
|
||||||
this.uiTrafficStats = res;
|
this.uiTrafficStats = res;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log('Failed to get ui-traffic-stats');
|
|
||||||
this.uiTrafficStats = false;
|
this.uiTrafficStats = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.api.getChartType()
|
||||||
|
.then((res) => {
|
||||||
|
this.uiChartType = parseInt(res, 10);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.uiChartType = 0;
|
||||||
|
});
|
||||||
|
|
||||||
Promise.resolve().then(async () => {
|
Promise.resolve().then(async () => {
|
||||||
const lang = await this.api.getLang();
|
const lang = await this.api.getLang();
|
||||||
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
|
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
|
||||||
|
@ -333,4 +399,33 @@ new Vue({
|
||||||
this.latestRelease = latestRelease;
|
this.latestRelease = latestRelease;
|
||||||
}).catch((err) => console.error(err));
|
}).catch((err) => console.error(err));
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
chartOptionsTX() {
|
||||||
|
const opts = {
|
||||||
|
...this.chartOptions,
|
||||||
|
colors: [CHART_COLORS.tx[this.theme]],
|
||||||
|
};
|
||||||
|
opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false;
|
||||||
|
opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth;
|
||||||
|
return opts;
|
||||||
|
},
|
||||||
|
chartOptionsRX() {
|
||||||
|
const opts = {
|
||||||
|
...this.chartOptions,
|
||||||
|
colors: [CHART_COLORS.rx[this.theme]],
|
||||||
|
};
|
||||||
|
opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false;
|
||||||
|
opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth;
|
||||||
|
return opts;
|
||||||
|
},
|
||||||
|
updateCharts() {
|
||||||
|
return this.uiChartType > 0 && this.uiShowCharts;
|
||||||
|
},
|
||||||
|
theme() {
|
||||||
|
if (this.uiTheme === 'auto') {
|
||||||
|
return this.prefersDarkScheme.matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
return this.uiTheme;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,8 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||||
downloadConfig: 'Download Configuration',
|
downloadConfig: 'Download Configuration',
|
||||||
madeBy: 'Made by',
|
madeBy: 'Made by',
|
||||||
donate: 'Donate',
|
donate: 'Donate',
|
||||||
|
toggleCharts: 'Show/hide Charts',
|
||||||
|
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
|
||||||
},
|
},
|
||||||
ua: {
|
ua: {
|
||||||
name: 'Ім`я',
|
name: 'Ім`я',
|
||||||
|
@ -273,6 +275,8 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||||
downloadConfig: 'Descargar configuración',
|
downloadConfig: 'Descargar configuración',
|
||||||
madeBy: 'Hecho por',
|
madeBy: 'Hecho por',
|
||||||
donate: 'Donar',
|
donate: 'Donar',
|
||||||
|
toggleCharts: 'Mostrar/Ocultar gráficos',
|
||||||
|
theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' },
|
||||||
},
|
},
|
||||||
ko: {
|
ko: {
|
||||||
name: '이름',
|
name: '이름',
|
||||||
|
@ -517,4 +521,32 @@ const messages = { // eslint-disable-line no-unused-vars
|
||||||
madeBy: 'สร้างโดย',
|
madeBy: 'สร้างโดย',
|
||||||
donate: 'บริจาค',
|
donate: 'บริจาค',
|
||||||
},
|
},
|
||||||
|
hi: { // github.com/rahilarious
|
||||||
|
name: 'नाम',
|
||||||
|
password: 'पासवर्ड',
|
||||||
|
signIn: 'लॉगिन',
|
||||||
|
logout: 'लॉगआउट',
|
||||||
|
updateAvailable: 'अपडेट उपलब्ध है!',
|
||||||
|
update: 'अपडेट',
|
||||||
|
clients: 'उपयोगकर्ताये',
|
||||||
|
new: 'नया',
|
||||||
|
deleteClient: 'उपयोगकर्ता हटाएँ',
|
||||||
|
deleteDialog1: 'क्या आपको पक्का हटाना है',
|
||||||
|
deleteDialog2: 'यह निर्णय पलट नहीं सकता।',
|
||||||
|
cancel: 'कुछ ना करें',
|
||||||
|
create: 'बनाएं',
|
||||||
|
createdOn: 'सर्जन तारीख ',
|
||||||
|
lastSeen: 'पिछली बार देखे गए थे ',
|
||||||
|
totalDownload: 'कुल डाउनलोड: ',
|
||||||
|
totalUpload: 'कुल अपलोड: ',
|
||||||
|
newClient: 'नया उपयोगकर्ता',
|
||||||
|
disableClient: 'उपयोगकर्ता स्थगित कीजिये',
|
||||||
|
enableClient: 'उपयोगकर्ता शुरू कीजिये',
|
||||||
|
noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।',
|
||||||
|
noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।',
|
||||||
|
showQR: 'क्यू आर कोड देखिये',
|
||||||
|
downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन',
|
||||||
|
madeBy: 'सर्जक',
|
||||||
|
donate: 'दान करें',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue