Browse Source

Merge branch 'wg-easy:master' into master

pull/1319/head
Vadim Babadzhanyan 2 years ago
committed by GitHub
parent
commit
e9548d23d3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .github/CODEOWNERS
  2. 2
      .github/ISSUE_TEMPLATE/01-bug-report.yml
  3. 2
      .github/ISSUE_TEMPLATE/02-feature-request.yml
  4. 17
      README.md
  5. 4
      docker-compose.yml
  6. 4
      src/config.js
  7. 95
      src/lib/Server.js
  8. 113
      src/lib/WireGuard.js
  9. 59
      src/package-lock.json
  10. 1
      src/package.json
  11. 20
      src/www/index.html
  12. 11
      src/www/js/api.js
  13. 13
      src/www/js/app.js
  14. 2
      src/www/js/i18n.js

4
.github/CODEOWNERS

@ -1,4 +1,6 @@
# Copyright (c) Emile Nijssen (WeeJeWel)
# Founder and Codeowner of WireGuard Easy (wg-easy)
# Maintained by Philip Heiduck (pheiduck)
# Maintained by Philip Heiduck (pheiduck) and Bernd Storath (kaaax0815)
* @WeeJeWel
* @pheiduck
* @kaaax0815

2
.github/ISSUE_TEMPLATE/01-bug-report.yml

@ -3,7 +3,7 @@ name: 🐛 Bug Report
description: Create a report to help us improve
title: "[Bug]: "
labels:
- "bug"
- "type: bug"
body:
- type: markdown

2
.github/ISSUE_TEMPLATE/02-feature-request.yml

@ -3,7 +3,7 @@ name: 🛠️ Feature Request
description: Suggest an idea to help us improve
title: "[Feat]: "
labels:
- "enhancement"
- "type: feature request"
body:
- type: markdown

17
README.md

@ -13,6 +13,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
</p>
## Features
* All-in-one: WireGuard + Web UI.
* Easy installation, simple to use.
* List, create, edit, delete, enable & disable clients.
@ -23,9 +24,10 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
* Gravatar support.
* Automatic Light / Dark Mode
* Multilanguage Support
* UI_TRAFFIC_STATS (default off)
* UI_SHOW_LINKS (default off)
* WG_ENABLE_EXPIRES_TIME (default off)
* Traffic Stats (default off)
* One Time Links (default off)
* Client Expiry (default off)
* Prometheus metrics support
## Requirements
@ -87,6 +89,8 @@ To automatically install & run wg-easy, simply run:
The Web UI will now be available on `http://0.0.0.0:51821`.
The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/)
> 💡 Your configuration files will be saved in `~/.wg-easy`
WireGuard Easy can be launched with Docker Compose as well - just download
@ -109,7 +113,7 @@ These options can be configured by setting environment variables using `-e KEY="
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
@ -123,10 +127,11 @@ These options can be configured by setting environment variables using `-e KEY="
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
| `UI_SHOW_LINKS` | `false` | `true` | Enable display of a short download link in Web UI |
| `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) |
| `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. |
| `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name |
| `ENABLE_PROMETHEUS_METRICS` | `false` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`|
| `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
> If you change `WG_PORT`, make sure to also change the exposed port.

4
docker-compose.yml

@ -27,9 +27,11 @@ services:
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
# - UI_TRAFFIC_STATS=true
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
# - UI_SHOW_LINKS=true
# - WG_ENABLE_ONE_TIME_LINKS=true
# - UI_ENABLE_SORT_CLIENTS=true
# - WG_ENABLE_EXPIRES_TIME=true
# - ENABLE_PROMETHEUS_METRICS=false
# - PROMETHEUS_METRICS_PASSWORD=$$2a$$12$$vkvKpeEAHD78gasyawIod.1leBMKg8sBwKW.pQyNsq78bXV3INf2G # (needs double $$, hash of 'prometheus_password'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy

4
src/config.js

@ -38,6 +38,8 @@ iptables -D FORWARD -o wg0 -j ACCEPT;
module.exports.LANG = process.env.LANG || 'en';
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false';
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
module.exports.UI_SHOW_LINKS = process.env.UI_SHOW_LINKS || 'false';
module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS || 'false';
module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false';
module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false';
module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'false';
module.exports.PROMETHEUS_METRICS_PASSWORD = process.env.PROMETHEUS_METRICS_PASSWORD;

95
src/lib/Server.js

@ -2,6 +2,7 @@
const bcrypt = require('bcryptjs');
const crypto = require('node:crypto');
const basicAuth = require('basic-auth');
const { createServer } = require('node:http');
const { stat, readFile } = require('node:fs/promises');
const { resolve, sep } = require('node:path');
@ -33,12 +34,15 @@ const {
LANG,
UI_TRAFFIC_STATS,
UI_CHART_TYPE,
UI_SHOW_LINKS,
WG_ENABLE_ONE_TIME_LINKS,
UI_ENABLE_SORT_CLIENTS,
WG_ENABLE_EXPIRES_TIME,
ENABLE_PROMETHEUS_METRICS,
PROMETHEUS_METRICS_PASSWORD,
} = require('../config');
const requiresPassword = !!PASSWORD_HASH;
const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD;
/**
* Checks if `password` matches the PASSWORD_HASH.
@ -48,13 +52,12 @@ const requiresPassword = !!PASSWORD_HASH;
* @param {string} password String to test
* @returns {boolean} true if matching environment, otherwise false
*/
const isPasswordValid = (password) => {
const isPasswordValid = (password, hash) => {
if (typeof password !== 'string') {
return false;
}
if (PASSWORD_HASH) {
return bcrypt.compareSync(password, PASSWORD_HASH);
if (hash) {
return bcrypt.compareSync(password, hash);
}
return false;
@ -106,9 +109,9 @@ module.exports = class Server {
return `"${UI_CHART_TYPE}"`;
}))
.get('/api/ui-show-links', defineEventHandler((event) => {
.get('/api/wg-enable-one-time-links', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return `${UI_SHOW_LINKS}`;
return `${WG_ENABLE_ONE_TIME_LINKS}`;
}))
.get('/api/ui-sort-clients', defineEventHandler((event) => {
@ -132,14 +135,21 @@ module.exports = class Server {
authenticated,
};
}))
.get('/:clientHash', defineEventHandler(async (event) => {
const clientHash = getRouterParam(event, 'clientHash');
.get('/cnf/:clientOneTimeLink', defineEventHandler(async (event) => {
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
throw createError({
status: 404,
message: 'Invalid state',
});
}
const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink');
const clients = await WireGuard.getClients();
const client = clients.find((client) => client.hash === clientHash);
const client = clients.find((client) => client.oneTimeLink === clientOneTimeLink);
if (!client) return;
const clientId = client.id;
const config = await WireGuard.getClientConfiguration({ clientId });
setHeader(event, 'Content-Disposition', `attachment; filename="${clientHash}.conf"`);
await WireGuard.eraseOneTimeLink({ clientId });
setHeader(event, 'Content-Disposition', `attachment; filename="${clientOneTimeLink}.conf"`);
setHeader(event, 'Content-Type', 'text/plain');
return config;
}))
@ -155,7 +165,7 @@ module.exports = class Server {
});
}
if (!isPasswordValid(password)) {
if (!isPasswordValid(password, PASSWORD_HASH)) {
throw createError({
status: 401,
message: 'Incorrect Password',
@ -185,7 +195,7 @@ module.exports = class Server {
}
if (req.url.startsWith('/api/') && req.headers['authorization']) {
if (isPasswordValid(req.headers['authorization'])) {
if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) {
return next();
}
return res.status(401).json({
@ -252,6 +262,20 @@ module.exports = class Server {
await WireGuard.enableClient({ clientId });
return { success: true };
}))
.post('/api/wireguard/client/:clientId/generateOneTimeLink', defineEventHandler(async (event) => {
if (WG_ENABLE_ONE_TIME_LINKS === 'false') {
throw createError({
status: 404,
message: 'Invalid state',
});
}
const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
throw createError({ status: 403 });
}
await WireGuard.generateOneTimeLink({ clientId });
return { success: true };
}))
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => {
const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
@ -311,6 +335,51 @@ module.exports = class Server {
});
};
// Prometheus Metrics API
const routerPrometheusMetrics = createRouter();
app.use(routerPrometheusMetrics);
// Check Prometheus credentials
app.use(
fromNodeMiddleware((req, res, next) => {
if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) {
return next();
}
const user = basicAuth(req);
if (requiresPrometheusPassword && !user) {
res.statusCode = 401;
return { error: 'Not Logged In' };
}
if (user.pass) {
if (isPasswordValid(user.pass, PROMETHEUS_METRICS_PASSWORD)) {
return next();
}
res.statusCode = 401;
return { error: 'Incorrect Password' };
}
res.statusCode = 401;
return { error: 'Not Logged In' };
}),
);
// Prometheus Routes
routerPrometheusMetrics
.get('/metrics', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'text/plain');
if (ENABLE_PROMETHEUS_METRICS === 'true') {
return WireGuard.getMetrics();
}
return '';
}))
.get('/metrics/json', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json');
if (ENABLE_PROMETHEUS_METRICS === 'true') {
return WireGuard.getMetricsJSON();
}
return '';
}));
// backup_restore
const router3 = createRouter();
app.use(router3);

113
src/lib/WireGuard.js

@ -25,6 +25,7 @@ const {
WG_PRE_DOWN,
WG_POST_DOWN,
WG_ENABLE_EXPIRES_TIME,
WG_ENABLE_ONE_TIME_LINKS,
} = require('../config');
module.exports = class WireGuard {
@ -152,12 +153,14 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
? new Date(client.expiredAt)
: null,
allowedIPs: client.allowedIPs,
hash: Math.abs(CRC32.str(clientId)).toString(16),
oneTimeLink: client.oneTimeLink ?? null,
oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null,
downloadableConfig: 'privateKey' in client,
persistentKeepalive: null,
latestHandshakeAt: null,
transferRx: null,
transferTx: null,
endpoint: null,
}));
// Loop WireGuard status
@ -186,6 +189,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
client.latestHandshakeAt = latestHandshakeAt === '0'
? null
: new Date(Number(`${latestHandshakeAt}000`));
client.endpoint = endpoint === '(none)' ? null : endpoint;
client.transferRx = Number(transferRx);
client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive;
@ -306,6 +310,23 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await this.saveConfig();
}
async generateOneTimeLink({ clientId }) {
const client = await this.getClient({ clientId });
const key = `${clientId}-${Math.floor(Math.random() * 1000)}`;
client.oneTimeLink = Math.abs(CRC32.str(key)).toString(16);
client.oneTimeLinkExpiresAt = new Date(Date.now() + 5 * 60 * 1000);
client.updatedAt = new Date();
await this.saveConfig();
}
async eraseOneTimeLink({ clientId }) {
const client = await this.getClient({ clientId });
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date();
await this.saveConfig();
}
async disableClient({ clientId }) {
const client = await this.getClient({ clientId });
@ -381,8 +402,9 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
async cronJobEveryMinute() {
const config = await this.getConfig();
let needSaveConfig = false;
// Expires Feature
if (WG_ENABLE_EXPIRES_TIME === 'true') {
let needSaveConfig = false;
for (const client of Object.values(config.clients)) {
if (client.enabled !== true) continue;
if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) {
@ -392,10 +414,93 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
client.updatedAt = new Date();
}
}
if (needSaveConfig) {
await this.saveConfig();
}
// One Time Link Feature
if (WG_ENABLE_ONE_TIME_LINKS === 'true') {
for (const client of Object.values(config.clients)) {
if (client.oneTimeLink !== null && new Date() > new Date(client.oneTimeLinkExpiresAt)) {
debug(`Client ${client.id} One Time Link expired.`);
needSaveConfig = true;
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date();
}
}
}
if (needSaveConfig) {
await this.saveConfig();
}
}
async getMetrics() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
let wireguardSentBytes = '';
let wireguardReceivedBytes = '';
let wireguardLatestHandshakeSeconds = '';
for (const client of Object.values(clients)) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferTx)}\n`;
wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferRx)}\n`;
wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`;
}
let returnText = '# HELP wg-easy and wireguard metrics\n';
returnText += '\n# HELP wireguard_configured_peers\n';
returnText += '# TYPE wireguard_configured_peers gauge\n';
returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`;
returnText += '\n# HELP wireguard_enabled_peers\n';
returnText += '# TYPE wireguard_enabled_peers gauge\n';
returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`;
returnText += '\n# HELP wireguard_connected_peers\n';
returnText += '# TYPE wireguard_connected_peers gauge\n';
returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`;
returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n';
returnText += '# TYPE wireguard_sent_bytes counter\n';
returnText += `${wireguardSentBytes}`;
returnText += '\n# HELP wireguard_received_bytes Bytes received from the peer\n';
returnText += '# TYPE wireguard_received_bytes counter\n';
returnText += `${wireguardReceivedBytes}`;
returnText += '\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n';
returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n';
returnText += `${wireguardLatestHandshakeSeconds}`;
return returnText;
}
async getMetricsJSON() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
for (const client of Object.values(clients)) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
}
return {
wireguard_configured_peers: Number(wireguardPeerCount),
wireguard_enabled_peers: Number(wireguardEnabledPeersCount),
wireguard_connected_peers: Number(wireguardConnectedPeersCount),
};
}
};

59
src/package-lock.json

@ -9,6 +9,7 @@
"version": "1.0.1",
"license": "CC BY-NC-SA 4.0",
"dependencies": {
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"crc-32": "^1.2.2",
"debug": "^4.3.6",
@ -992,6 +993,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@ -2186,6 +2199,26 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/express-session/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -2849,9 +2882,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4245,23 +4278,9 @@
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-regex-test": {

1
src/package.json

@ -15,6 +15,7 @@
"author": "Emile Nijssen",
"license": "CC BY-NC-SA 4.0",
"dependencies": {
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"crc-32": "^1.2.2",
"debug": "^4.3.6",

20
src/www/index.html

@ -256,8 +256,8 @@
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
</span>
</div>
<div v-if="uiShowLinks" :ref="'client-' + client.id + '-hash'" class="text-gray-400 text-xs">
<a :href="'./' + client.hash + ''">{{document.location.protocol}}//{{document.location.host}}/{{client.hash}}</a>
<div v-if="enableOneTimeLinks && client.oneTimeLink !== null && client.oneTimeLink !== ''" :ref="'client-' + client.id + '-link'" class="text-gray-400 text-xs">
<a :href="'./cnf/' + client.oneTimeLink + ''">{{document.location.protocol}}//{{document.location.host}}/cnf/{{client.oneTimeLink}}</a>
</div>
<!-- Expire Date -->
<div v-show="enableExpireTime" class=" block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs">
@ -384,6 +384,22 @@
</svg>
</a>
<!-- Short OneTime Link -->
<button v-if="enableOneTimeLinks" :disabled="!client.downloadableConfig"
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition"
:class="{
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig,
'is-disabled': !client.downloadableConfig
}"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('OneTimeLink')"
@click="if(client.downloadableConfig) { showOneTimeLink(client); }">
<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="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961"/>
</svg>
</button>
<!-- Delete -->
<button

11
src/www/js/api.js

@ -64,10 +64,10 @@ class API {
});
}
async getUIShowLinks() {
async getWGEnableOneTimeLinks() {
return this.call({
method: 'get',
path: '/ui-show-links',
path: '/wg-enable-one-time-links',
});
}
@ -132,6 +132,13 @@ class API {
});
}
async showOneTimeLink({ clientId }) {
return this.call({
method: 'post',
path: `/wireguard/client/${clientId}/generateOneTimeLink`,
});
}
async enableClient({ clientId }) {
return this.call({
method: 'post',

13
src/www/js/app.js

@ -92,7 +92,7 @@ new Vue({
uiTrafficStats: false,
uiChartType: 0,
uiShowLinks: false,
enableOneTimeLinks: false,
enableSortClient: false,
sortClient: true, // Sort clients by name, true = asc, false = desc
enableExpireTime: false,
@ -312,6 +312,11 @@ new Vue({
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
showOneTimeLink(client) {
this.api.showOneTimeLink({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
enableClient(client) {
this.api.enableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
@ -436,12 +441,12 @@ new Vue({
this.uiChartType = 0;
});
this.api.getUIShowLinks()
this.api.getWGEnableOneTimeLinks()
.then((res) => {
this.uiShowLinks = res;
this.enableOneTimeLinks = res;
})
.catch(() => {
this.uiShowLinks = false;
this.enableOneTimeLinks = false;
});
this.api.getUiSortClients()

2
src/www/js/i18n.js

@ -39,6 +39,7 @@ const messages = { // eslint-disable-line no-unused-vars
sort: 'Sort',
ExpireDate: 'Expire Date',
Permanent: 'Permanent',
OneTimeLink: 'Generate short one time link',
},
ua: {
name: 'Ім`я',
@ -112,6 +113,7 @@ const messages = { // eslint-disable-line no-unused-vars
sort: 'Сортировка',
ExpireDate: 'Дата истечения срока',
Permanent: 'Бессрочно',
OneTimeLink: 'Создать короткую одноразовую ссылку',
},
tr: { // Müslüm Barış Korkmazer @babico
name: 'İsim',

Loading…
Cancel
Save