Browse Source

Feat expiration date (#1296)

Closes #1287
Co-authored-by: Vadim Babadzhanyan <[email protected]>
pull/1304/head
Vadim Babadzhanyan 8 months ago
committed by GitHub
parent
commit
8145809e22
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 23
      .editorconfig
  2. 19
      README.md
  3. 3
      docker-compose.yml
  4. 1
      src/config.js
  5. 25
      src/lib/Server.js
  6. 51
      src/lib/WireGuard.js
  7. 24
      src/package-lock.json
  8. 5
      src/package.json
  9. 12
      src/www/css/app.css
  10. 40
      src/www/index.html
  11. 22
      src/www/js/api.js
  12. 29
      src/www/js/app.js
  13. 4
      src/www/js/i18n.js
  14. 4
      src/www/src/css/app.css

23
.editorconfig

@ -0,0 +1,23 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# The JSON files contain newlines inconsistently
[*.json]
insert_final_newline = ignore
# Minified JavaScript files shouldn't be changed
[**.min.js]
indent_style = ignore
insert_final_newline = ignore
[*.md]
trim_trailing_whitespace = false

19
README.md

@ -25,6 +25,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
* Multilanguage Support
* UI_TRAFFIC_STATS (default off)
* UI_SHOW_LINKS (default off)
* WG_ENABLE_EXPIRES_TIME (default off)
## Requirements
@ -111,19 +112,21 @@ These options can be configured by setting environment variables using `-e KEY="
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy)
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
| `WG_ENABLE_EXPIRES_TIME` | `false` | `true` | Enable expire time for clients |
| `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 |
| `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 |
| `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 |
| `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 |
> If you change `WG_PORT`, make sure to also change the exposed port.

3
docker-compose.yml

@ -6,7 +6,7 @@ services:
environment:
# Change Language:
# (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi)
- LANG=de
- LANG=en
# ⚠️ Required:
# Change this to your host's public address
- WG_HOST=raspberrypi.local
@ -29,6 +29,7 @@ services:
# - UI_CHART_TYPE=0 # (0 Charts disabled, 1 # Line chart, 2 # Area chart, 3 # Bar chart)
# - UI_SHOW_LINKS=true
# - UI_ENABLE_SORT_CLIENTS=true
# - WG_ENABLE_EXPIRES_TIME=true
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy

1
src/config.js

@ -40,3 +40,4 @@ 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.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false';
module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false';

25
src/lib/Server.js

@ -35,6 +35,7 @@ const {
UI_CHART_TYPE,
UI_SHOW_LINKS,
UI_ENABLE_SORT_CLIENTS,
WG_ENABLE_EXPIRES_TIME,
} = require('../config');
const requiresPassword = !!PASSWORD_HASH;
@ -59,6 +60,11 @@ const isPasswordValid = (password) => {
return false;
};
const cronJobEveryMinute = async () => {
await WireGuard.cronJobEveryMinute();
setTimeout(cronJobEveryMinute, 60 * 1000);
};
module.exports = class Server {
constructor() {
@ -110,6 +116,11 @@ module.exports = class Server {
return `${UI_ENABLE_SORT_CLIENTS}`;
}))
.get('/api/wg-enable-expire-time', defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/json');
return `${WG_ENABLE_EXPIRES_TIME}`;
}))
// Authentication
.get('/api/session', defineEventHandler((event) => {
const authenticated = requiresPassword
@ -224,7 +235,8 @@ module.exports = class Server {
}))
.post('/api/wireguard/client', defineEventHandler(async (event) => {
const { name } = await readBody(event);
await WireGuard.createClient({ name });
const { expiredDate } = await readBody(event);
await WireGuard.createClient({ name, expiredDate });
return { success: true };
}))
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => {
@ -265,6 +277,15 @@ module.exports = class Server {
const { address } = await readBody(event);
await WireGuard.updateClientAddress({ clientId, address });
return { success: true };
}))
.put('/api/wireguard/client/:clientId/expireDate', defineEventHandler(async (event) => {
const clientId = getRouterParam(event, 'clientId');
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') {
throw createError({ status: 403 });
}
const { expireDate } = await readBody(event);
await WireGuard.updateClientExpireDate({ clientId, expireDate });
return { success: true };
}));
const safePathJoin = (base, target) => {
@ -340,6 +361,8 @@ module.exports = class Server {
createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST);
debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
cronJobEveryMinute();
}
};

51
src/lib/WireGuard.js

@ -24,6 +24,7 @@ const {
WG_POST_UP,
WG_PRE_DOWN,
WG_POST_DOWN,
WG_ENABLE_EXPIRES_TIME,
} = require('../config');
module.exports = class WireGuard {
@ -147,6 +148,9 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
publicKey: client.publicKey,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
expiredAt: client.expiredAt !== null
? new Date(client.expiredAt)
: null,
allowedIPs: client.allowedIPs,
hash: Math.abs(CRC32.str(clientId)).toString(16),
downloadableConfig: 'privateKey' in client,
@ -227,7 +231,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
}
async createClient({ name }) {
async createClient({ name, expiredDate }) {
if (!name) {
throw new Error('Missing: Name');
}
@ -256,7 +260,6 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
if (!address) {
throw new Error('Maximum number of clients reached.');
}
// Create Client
const id = crypto.randomUUID();
const client = {
@ -269,10 +272,15 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
createdAt: new Date(),
updatedAt: new Date(),
expiredAt: null,
enabled: true,
};
if (expiredDate) {
client.expiredAt = new Date(expiredDate);
client.expiredAt.setHours(23);
client.expiredAt.setMinutes(59);
client.expiredAt.setSeconds(59);
}
config.clients[id] = client;
await this.saveConfig();
@ -329,6 +337,22 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await this.saveConfig();
}
async updateClientExpireDate({ clientId, expireDate }) {
const client = await this.getClient({ clientId });
if (expireDate) {
client.expiredAt = new Date(expireDate);
client.expiredAt.setHours(23);
client.expiredAt.setMinutes(59);
client.expiredAt.setSeconds(59);
} else {
client.expiredAt = null;
}
client.updatedAt = new Date();
await this.saveConfig();
}
async __reloadConfig() {
await this.__buildConfig();
await this.__syncConfig();
@ -355,4 +379,23 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await Util.exec('wg-quick down wg0').catch(() => {});
}
async cronJobEveryMinute() {
const config = await this.getConfig();
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)) {
debug(`Client ${client.id} expired.`);
needSaveConfig = true;
client.enabled = false;
client.updatedAt = new Date();
}
}
if (needSaveConfig) {
await this.saveConfig();
}
}
}
};

24
src/package-lock.json

@ -17,6 +17,7 @@
"qrcode": "^1.5.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"eslint-config-athom": "^3.1.3",
"nodemon": "^3.1.4",
"tailwindcss": "^3.4.10"
@ -452,6 +453,19 @@
"node": ">=14"
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -3261,6 +3275,16 @@
"node": ">=10.0.0"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"dev": true,
"license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",

5
src/package.json

@ -16,13 +16,14 @@
"license": "CC BY-NC-SA 4.0",
"dependencies": {
"bcryptjs": "^2.4.3",
"crc-32": "^1.2.2",
"debug": "^4.3.6",
"express-session": "^1.18.0",
"h3": "^1.12.0",
"qrcode": "^1.5.4",
"crc-32": "^1.2.2"
"qrcode": "^1.5.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"eslint-config-athom": "^3.1.3",
"nodemon": "^3.1.4",
"tailwindcss": "^3.4.10"

12
src/www/css/app.css

@ -714,6 +714,10 @@ video {
margin-bottom: 2.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
@ -1160,6 +1164,10 @@ video {
fill: #4b5563;
}
.p-0 {
padding: 0px;
}
.p-1 {
padding: 0.25rem;
}
@ -1465,6 +1473,10 @@ video {
cursor: default;
}
.p-0 {
padding: 0;
}
.last\:border-b-0:last-child {
border-bottom-width: 0px;
}

40
src/www/index.html

@ -127,7 +127,7 @@
</button>
<!-- New client -->
<button @click="clientCreate = true; clientCreateName = '';"
<button @click="clientCreate = true; clientCreateName = ''; clientExpiredDate = '';"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-r-full md:rounded inline-flex items-center transition">
<svg class="w-4 md:mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
@ -259,6 +259,32 @@
<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>
<!-- 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">
<span class="group">
<!-- Show -->
<input v-show="clientEditExpireDateId === client.id" v-model="clientEditExpireDate"
v-on:keyup.enter="updateClientExpireDate(client, clientEditExpireDate); clientEditExpireDate = null; clientEditExpireDateId = null;"
v-on:keyup.escape="clientEditExpireDate = null; clientEditExpireDateId = null;"
:ref="'client-' + client.id + '-expire'"
type="text"
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-70 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500 text-xs p-0" />
<span v-show="clientEditExpireDateId !== client.id"
class="inline-block ">{{client.expiredAt | expiredDateFormat}}</span>
<!-- Edit -->
<span v-show="clientEditExpireDateId !== client.id"
@click="clientEditExpireDate = client.expiredAt ? client.expiredAt.toISOString().slice(0, 10) : 'yyyy-mm-dd'; clientEditExpireDateId = client.id; setTimeout(() => $refs['client-' + client.id + '-expire'][0].select(), 1);"
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</span>
</span>
</div>
</div>
<!-- Info -->
@ -378,7 +404,7 @@
<div v-if="clients && clients.length === 0">
<p class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm">
{{$t("noClients")}}<br /><br />
<button @click="clientCreate = true; clientCreateName = '';"
<button @click="clientCreate = true; clientCreateName = ''; clientExpiredDate = '';"
class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition">
<svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
@ -470,6 +496,16 @@
type="text" v-model.trim="clientCreateName" :placeholder="$t('name')" />
</p>
</div>
<div class="mt-2" v-show="enableExpireTime">
<p class="text-sm text-gray-500">
<label class="block text-gray-900 dark:text-neutral-200 text-sm font-bold mb-2" for="expireDate">
{{$t("ExpireDate")}}
</label>
<input
class="rounded p-2 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none w-full"
type="date" v-model.trim="clientExpiredDate" :placeholder="$t('expireDate')" name="expireDate"/>
</p>
</div>
</div>
</div>
</div>

22
src/www/js/api.js

@ -71,6 +71,13 @@ class API {
});
}
async getWGEnableExpireTime() {
return this.call({
method: 'get',
path: '/wg-enable-expire-time',
});
}
async getSession() {
return this.call({
method: 'get',
@ -101,17 +108,20 @@ class API {
...client,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
expiredAt: client.expiredAt !== null
? new Date(client.expiredAt)
: null,
latestHandshakeAt: client.latestHandshakeAt !== null
? new Date(client.latestHandshakeAt)
: null,
})));
}
async createClient({ name }) {
async createClient({ name, expiredDate }) {
return this.call({
method: 'post',
path: '/wireguard/client',
body: { name },
body: { name, expiredDate },
});
}
@ -152,6 +162,14 @@ class API {
});
}
async updateClientExpireDate({ clientId, expireDate }) {
return this.call({
method: 'put',
path: `/wireguard/client/${clientId}/expireDate/`,
body: { expireDate },
});
}
async restoreConfiguration(file) {
return this.call({
method: 'put',

29
src/www/js/app.js

@ -77,10 +77,13 @@ new Vue({
clientDelete: null,
clientCreate: null,
clientCreateName: '',
clientExpiredDate: '',
clientEditName: null,
clientEditNameId: null,
clientEditAddress: null,
clientEditAddressId: null,
clientEditExpireDate: null,
clientEditExpireDateId: null,
qrcode: null,
currentRelease: null,
@ -92,6 +95,7 @@ new Vue({
uiShowLinks: false,
enableSortClient: false,
sortClient: true, // Sort clients by name, true = asc, false = desc
enableExpireTime: false,
uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
uiTheme: localStorage.theme || 'auto',
@ -296,9 +300,10 @@ new Vue({
},
createClient() {
const name = this.clientCreateName;
const expiredDate = this.clientExpiredDate;
if (!name) return;
this.api.createClient({ name })
this.api.createClient({ name, expiredDate })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
@ -327,6 +332,11 @@ new Vue({
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
updateClientExpireDate(client, expireDate) {
this.api.updateClientExpireDate({ clientId: client.id, expireDate })
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
restoreConfig(e) {
e.preventDefault();
const file = e.currentTarget.files.item(0);
@ -370,6 +380,15 @@ new Vue({
timeago: (value) => {
return timeago.format(value, i18n.locale);
},
expiredDateFormat: (value) => {
if (value === null) return i18n.t('Permanent');
const dateTime = new Date(value);
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return dateTime.toLocaleDateString(i18n.locale, options);
},
expiredDateEditFormat: (value) => {
if (value === null) return 'yyyy-MM-dd';
},
},
mounted() {
this.prefersDarkScheme.addListener(this.handlePrefersChange);
@ -433,6 +452,14 @@ new Vue({
this.enableSortClient = false;
});
this.api.getWGEnableExpireTime()
.then((res) => {
this.enableExpireTime = res;
})
.catch(() => {
this.enableExpireTime = false;
});
Promise.resolve().then(async () => {
const lang = await this.api.getLang();
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {

4
src/www/js/i18n.js

@ -37,6 +37,8 @@ const messages = { // eslint-disable-line no-unused-vars
rememberMe: 'Remember me',
titleRememberMe: 'Stay logged after closing the browser',
sort: 'Sort',
ExpireDate: 'Expire Date',
Permanent: 'Permanent',
},
ua: {
name: 'Ім`я',
@ -108,6 +110,8 @@ const messages = { // eslint-disable-line no-unused-vars
rememberMe: 'Запомнить меня',
titleRememberMe: 'Оставаться в системе после закрытия браузера',
sort: 'Сортировка',
ExpireDate: 'Дата истечения срока',
Permanent: 'Бессрочно',
},
tr: { // Müslüm Barış Korkmazer @babico
name: 'İsim',

4
src/www/src/css/app.css

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.p-0 {
padding: 0;
}

Loading…
Cancel
Save