From a268422e9d5409e2d98af1769b495836a1ea5983 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A8=8B=E8=B6=8A?=
 <44216455+cyicz123@users.noreply.github.com>
Date: Mon, 12 Aug 2024 11:12:21 +0800
Subject: [PATCH 01/39] fix(Doc): fix escaping issue for PASSWORD_HASH in
 docker-compose.yml

For users using docker-compose.yml, please note that you should not wrap the generated hash password in single quotes. Instead, replace each `$` symbol with two `$$` symbols.

For example, for the password 'foobar123', use the following command to generate the hash:
`docker run ghcr.io/wg-easy/wg-easy wgpw foobar123`

The resulting hash should be used in docker-compose.yml like this:
``` yaml
- PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG
```

Signed-off-by: cyicz123 <cyicz123@outlook.com>
---
 How_to_generate_an_bcrypt_hash.md | 10 +++++++++-
 docker-compose.yml                |  2 +-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/How_to_generate_an_bcrypt_hash.md b/How_to_generate_an_bcrypt_hash.md
index 1a6a63c..e77e371 100644
--- a/How_to_generate_an_bcrypt_hash.md
+++ b/How_to_generate_an_bcrypt_hash.md
@@ -16,7 +16,7 @@ docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
 PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
 ```
 
-*Important* : make sure to enclose your password in **single quotes** when you run `docker run` command :
+**Important** : make sure to enclose your password in **single quotes** when you run `docker run` command :
 
 ```bash
 $ echo $2b$12$coPqCsPtcF <-- not correct
@@ -26,3 +26,11 @@ b2
 $ echo '$2b$12$coPqCsPtcF' <-- correct
 $2b$12$coPqCsPtcF
 ```
+
+**Important** : Please note: don't wrap the generated hash password in single quotes when you use `docker-compose.yml`. Instead, replace each `$` symbol with two `$$` symbols. For example:
+
+``` yaml
+- PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG
+```
+
+This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw foobar123` and then inserted an additional `$` before each existing `$` symbal.
diff --git a/docker-compose.yml b/docker-compose.yml
index f4c93c4..dd450ed 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,7 +12,7 @@ services:
       - WG_HOST=raspberrypi.local
 
       # Optional:
-      # - PASSWORD_HASH='$2y$10$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG' (hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
+      # - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
       # - PORT=51821
       # - WG_PORT=51820
       # - WG_CONFIG_PORT=92820

From 0bf266d5cb77d754b450d91545c792a6314a87d5 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Mon, 12 Aug 2024 08:56:59 +0000
Subject: [PATCH 02/39] npm: package updates

---
 src/package-lock.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index a07c2bb..52d1e5f 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -2658,9 +2658,9 @@
       }
     },
     "node_modules/ignore": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
-      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
       "dev": true,
       "license": "MIT",
       "engines": {

From 0386a0da6b350efc7791166e9553256e28c50780 Mon Sep 17 00:00:00 2001
From: Bernd Storath <999999bst@gmail.com>
Date: Mon, 12 Aug 2024 16:30:58 +0200
Subject: [PATCH 03/39] add better issue template

Co-authored-by: Bernd Storath <bernd.storath@offizium.de>
---
 .github/ISSUE_TEMPLATE/01-bug-report.yml      | 38 +++++++++++++++
 .github/ISSUE_TEMPLATE/02-feature-request.yml | 48 +++++++++++++++++++
 .github/ISSUE_TEMPLATE/bug_report.md          | 38 ---------------
 .github/ISSUE_TEMPLATE/config.yml             |  5 ++
 4 files changed, 91 insertions(+), 38 deletions(-)
 create mode 100644 .github/ISSUE_TEMPLATE/01-bug-report.yml
 create mode 100644 .github/ISSUE_TEMPLATE/02-feature-request.yml
 delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
 create mode 100644 .github/ISSUE_TEMPLATE/config.yml

diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml
new file mode 100644
index 0000000..2a92e7b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml
@@ -0,0 +1,38 @@
+---
+name: 🐛 Bug Report
+description: Create a report to help us improve
+title: "[Bug]: "
+labels:
+  - "bug"
+
+body:
+  - type: markdown
+    attributes:
+      value: |
+        **Thanks :heart: for taking the time to fill out this bug report!**
+        We kindly ask that you search to see if an issue [already exists](https://github.com/wg-easy/wg-easy/issues?q=is%3Aissue+sort%3Acreated-desc+) for the bug you encountered.
+  
+  - type: textarea
+    id: what-happened
+    attributes:
+      label: Describe the bug
+      placeholder: Tell us what you see!
+      value: "A bug happened!"
+    validations:
+      required: true
+  
+  - type: textarea
+    id: what-should-happen
+    attributes:
+      label: Expected behavior
+      placeholder: Tell us what you expected!
+      value: "Work just fine!"
+    validations:
+      required: true
+  
+  - type: textarea
+    id: logs
+    attributes:
+      label: Relevant log output
+      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
+      render: shell
diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml
new file mode 100644
index 0000000..d23b5f6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml
@@ -0,0 +1,48 @@
+---
+name: 🛠️ Feature Request
+description: Suggest an idea to help us improve
+title: "[Feat]: "
+labels:
+  - "enhancement"
+
+body:
+  - type: markdown
+    attributes:
+      value: |
+        **Thanks :heart: for taking the time to fill out this feature request report!**
+        We kindly ask that you search to see if an issue [already exists](https://github.com/wg-easy/wg-easy/issues?q=is%3Aissue+sort%3Acreated-desc+) for your feature.
+
+        We are also happy to accept contributions from our users. For more details see [here](https://github.com/wg-easy/wg-easy/blob/master/contributing.md).
+
+  - type: textarea
+    attributes:
+      label: Description
+      description: |
+        A clear and concise description of the feature you're interested in.
+    validations:
+      required: true
+
+  - type: textarea
+    attributes:
+      label: Suggested Solution
+      description: |
+        Describe the solution you'd like. A clear and concise description of what you want to happen.
+    validations:
+      required: true
+
+  - type: textarea
+    attributes:
+      label: Alternatives
+      description: |
+        Describe alternatives you've considered.
+        A clear and concise description of any alternative solutions or features you've considered.
+    validations:
+      required: false
+
+  - type: textarea
+    attributes:
+      label: Additional Context
+      description: |
+        Add any other context about the problem here.
+    validations:
+      required: false
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 89daa66..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Desktop (please complete the following information):**
- - OS: [e.g. macOS 12.1]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
-
-**Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS 8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..421000e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+contact_links:
+  - name: Get Help
+    url: https://github.com/wg-easy/wg-easy/discussions/new?category=q-a
+    about: If you can't get something to work the way you expect, open a question in the discussions.
+blank_issues_enabled: false
\ No newline at end of file

From 4e79d0ee03211dc6148db66ee7e4dcd24c8098ac Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Mon, 12 Aug 2024 14:56:03 +0000
Subject: [PATCH 04/39] npm: package updates

---
 src/package-lock.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 52d1e5f..c1781a1 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -3905,9 +3905,9 @@
       }
     },
     "node_modules/postcss-selector-parser": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
-      "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {

From 3e6ded18a566e2afc3304237a07be58714c1f92c Mon Sep 17 00:00:00 2001
From: Viktor Yudov <me@spcfox.com>
Date: Tue, 13 Aug 2024 02:51:05 +0300
Subject: [PATCH 05/39] Add Remember me

---
 README.md          |  1 +
 src/config.js      |  1 +
 src/lib/Server.js  | 11 ++++++++++-
 src/www/index.html | 18 ++++++++++++++++++
 src/www/js/api.js  | 11 +++++++++--
 src/www/js/app.js  |  8 ++++++++
 src/www/js/i18n.js |  2 ++
 7 files changed, 49 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index be8a32c..f39e7e6 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,7 @@ 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                           |
+| `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.                                  |
 
 > If you change `WG_PORT`, make sure to also change the exposed port.
 
diff --git a/src/config.js b/src/config.js
index 9785053..e016b38 100644
--- a/src/config.js
+++ b/src/config.js
@@ -6,6 +6,7 @@ module.exports.RELEASE = version;
 module.exports.PORT = process.env.PORT || '51821';
 module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
 module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
+module.exports.MAX_AGE = parseInt(process.env.MAX_AGE, 10) * 1000 * 60 || 0;
 module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
 module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
 module.exports.WG_HOST = process.env.WG_HOST;
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 7f06da5..aa5b1d4 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -29,6 +29,7 @@ const {
   WEBUI_HOST,
   RELEASE,
   PASSWORD_HASH,
+  MAX_AGE,
   LANG,
   UI_TRAFFIC_STATS,
   UI_CHART_TYPE,
@@ -82,6 +83,11 @@ module.exports = class Server {
         return `"${LANG}"`;
       }))
 
+      .get('/api/remember-me', defineEventHandler((event) => {
+        setHeader(event, 'Content-Type', 'application/json');
+        return MAX_AGE > 0;
+      }))
+
       .get('/api/ui-traffic-stats', defineEventHandler((event) => {
         setHeader(event, 'Content-Type', 'application/json');
         return `"${UI_TRAFFIC_STATS}"`;
@@ -104,7 +110,7 @@ module.exports = class Server {
         };
       }))
       .post('/api/session', defineEventHandler(async (event) => {
-        const { password } = await readBody(event);
+        const { password, remember } = await readBody(event);
 
         if (!requiresPassword) {
           // if no password is required, the API should never be called.
@@ -122,6 +128,9 @@ module.exports = class Server {
           });
         }
 
+        if (MAX_AGE && remember) {
+          event.node.req.session.cookie.maxAge = MAX_AGE;
+        }
         event.node.req.session.authenticated = true;
         event.node.req.session.save();
 
diff --git a/src/www/index.html b/src/www/index.html
index 236a8f3..618acee 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -562,7 +562,25 @@
 
           <input type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
             class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
+          
+          <!-- Remember me -->
+          <label v-if="rememberMeEnabled"
+            class="inline-block mb-5 cursor-pointer whitespace-nowrap" :title="$t('titleRememberMe')">
+            <input type="checkbox" class="sr-only" v-model="remember">
+            
+            <div v-if="remember"
+              class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all">
+              <div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
+            </div>
 
+            <div v-if="!remember"
+              class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 dark:bg-neutral-400 cursor-pointer hover:bg-gray-300 dark:hover:bg-neutral-500 transition-all">
+              <div class="rounded-full w-4 h-4 m-1 bg-white"></div>
+            </div>
+
+            <span class="text-sm">{{$t("rememberMe")}}</span>
+          </label>
+  
           <button v-if="authenticating"
             class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed">
             <svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
diff --git a/src/www/js/api.js b/src/www/js/api.js
index 9006f5a..cb3f71e 100644
--- a/src/www/js/api.js
+++ b/src/www/js/api.js
@@ -43,6 +43,13 @@ class API {
     });
   }
 
+  async getRememberMeEnabled() {
+    return this.call({
+      method: 'get',
+      path: '/remember-me',
+    });
+  }
+
   async getuiTrafficStats() {
     return this.call({
       method: 'get',
@@ -64,11 +71,11 @@ class API {
     });
   }
 
-  async createSession({ password }) {
+  async createSession({ password, remember }) {
     return this.call({
       method: 'post',
       path: '/session',
-      body: { password },
+      body: { password, remember },
     });
   }
 
diff --git a/src/www/js/app.js b/src/www/js/app.js
index 61bb7c0..69fea67 100644
--- a/src/www/js/app.js
+++ b/src/www/js/app.js
@@ -53,6 +53,8 @@ new Vue({
     authenticating: false,
     password: null,
     requiresPassword: null,
+    remember: false,
+    rememberMeEnabled: false,
 
     clients: null,
     clientsPersist: {},
@@ -239,6 +241,7 @@ new Vue({
       this.authenticating = true;
       this.api.createSession({
         password: this.password,
+        remember: this.remember,
       })
         .then(async () => {
           const session = await this.api.getSession();
@@ -362,6 +365,11 @@ new Vue({
         alert(err.message || err.toString());
       });
 
+    this.api.getRememberMeEnabled()
+      .then((rememberMeEnabled) => {
+        this.rememberMeEnabled = rememberMeEnabled;
+      });
+
     setInterval(() => {
       this.refresh({
         updateCharts: this.updateCharts,
diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js
index 2666961..4a21403 100644
--- a/src/www/js/i18n.js
+++ b/src/www/js/i18n.js
@@ -34,6 +34,8 @@ const messages = { // eslint-disable-line no-unused-vars
     backup: 'Backup',
     titleRestoreConfig: 'Restore your configuration',
     titleBackupConfig: 'Backup your configuration',
+    rememberMe: 'Remember me',
+    titleRememberMe: 'Stay logged after closing the browser',
   },
   ua: {
     name: 'Ім`я',

From 4ead4c2cc9fd76578cd50332e9b11ced35e867f3 Mon Sep 17 00:00:00 2001
From: Bernd Storath <999999bst@gmail.com>
Date: Tue, 13 Aug 2024 08:30:51 +0200
Subject: [PATCH 06/39] fix pr template location

---
 .../pull_request_template.md => PULL_REQUEST_TEMPLATE.md}         | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename .github/{PULL_REQUEST_TEMPLATE/pull_request_template.md => PULL_REQUEST_TEMPLATE.md} (100%)

diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE.md
similarity index 100%
rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
rename to .github/PULL_REQUEST_TEMPLATE.md

From 8591b35d4e44142cd3ffabcaa14ca776583cd7c0 Mon Sep 17 00:00:00 2001
From: jkh0kr <admin@jkh.kr>
Date: Wed, 14 Aug 2024 10:00:58 +0900
Subject: [PATCH 07/39] Update i18n.js

Additional Korean language updates
---
 src/www/js/i18n.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js
index 2666961..226516b 100644
--- a/src/www/js/i18n.js
+++ b/src/www/js/i18n.js
@@ -340,6 +340,12 @@ const messages = { // eslint-disable-line no-unused-vars
     downloadConfig: '구성 다운로드',
     madeBy: '만든 사람',
     donate: '기부',
+    toggleCharts: '차트 표시/숨기기',
+    theme: { dark: '어두운 테마', light: '밝은 테마', auto: '자동 테마' },
+    restore: '복원',
+    backup: '백업',
+    titleRestoreConfig: '구성 파일 복원',
+    titleBackupConfig: '구성 파일 백업',
   },
   vi: {
     name: 'Tên',

From 2ea37dd7bac2271627db6ad82713109ccca8ec94 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Thu, 15 Aug 2024 15:04:36 +0000
Subject: [PATCH 08/39] npm: package updates

---
 src/package-lock.json | 8 ++++----
 src/package.json      | 2 +-
 src/www/css/app.css   | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index c1781a1..7d0e900 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -18,7 +18,7 @@
       "devDependencies": {
         "eslint-config-athom": "^3.1.3",
         "nodemon": "^3.1.4",
-        "tailwindcss": "^3.4.9"
+        "tailwindcss": "^3.4.10"
       },
       "engines": {
         "node": ">=18"
@@ -4687,9 +4687,9 @@
       "peer": true
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.9",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
-      "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
+      "version": "3.4.10",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
+      "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/src/package.json b/src/package.json
index 92582e9..655cb34 100644
--- a/src/package.json
+++ b/src/package.json
@@ -24,7 +24,7 @@
   "devDependencies": {
     "eslint-config-athom": "^3.1.3",
     "nodemon": "^3.1.4",
-    "tailwindcss": "^3.4.9"
+    "tailwindcss": "^3.4.10"
   },
   "nodemonConfig": {
     "ignore": [
diff --git a/src/www/css/app.css b/src/www/css/app.css
index 92bb704..ae3fc3c 100644
--- a/src/www/css/app.css
+++ b/src/www/css/app.css
@@ -1,5 +1,5 @@
 /*
-! tailwindcss v3.4.9 | MIT License | https://tailwindcss.com
+! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
 */
 
 /*

From 0a33b1f7df3576d99ef9644618d300ac652a5a8c Mon Sep 17 00:00:00 2001
From: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
Date: Fri, 16 Aug 2024 18:39:24 +0300
Subject: [PATCH 09/39] Supports displaying short links, for easy downloading
 on TVs and Android TVs

---
 README.md            |  2 ++
 docker-compose.yml   |  3 ++-
 src/config.js        |  1 +
 src/lib/Server.js    | 17 +++++++++++++++++
 src/lib/WireGuard.js |  2 ++
 src/www/index.html   |  7 +++++--
 src/www/js/api.js    |  7 +++++++
 src/www/js/app.js    |  9 +++++++++
 8 files changed, 45 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index be8a32c..67f5ecb 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
 * Automatic Light / Dark Mode
 * Multilanguage Support
 * UI_TRAFFIC_STATS (default off)
+* UI_SHOW_LINKS (default off)
 
 ## Requirements
 
@@ -120,6 +121,7 @@ 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                                                                                                       |
 
 > If you change `WG_PORT`, make sure to also change the exposed port.
 
diff --git a/docker-compose.yml b/docker-compose.yml
index dd450ed..dd135fd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,6 +27,7 @@ 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
 
     image: ghcr.io/wg-easy/wg-easy
     container_name: wg-easy
@@ -39,7 +40,7 @@ services:
     cap_add:
       - NET_ADMIN
       - SYS_MODULE
-      # - NET_RAW # ⚠️ Uncomment if using Podman 
+      # - NET_RAW # ⚠️ Uncomment if using Podman
     sysctls:
       - net.ipv4.ip_forward=1
       - net.ipv4.conf.all.src_valid_mark=1
diff --git a/src/config.js b/src/config.js
index 9785053..9335bfb 100644
--- a/src/config.js
+++ b/src/config.js
@@ -37,3 +37,4 @@ 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';
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 7f06da5..d46d29e 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -32,6 +32,7 @@ const {
   LANG,
   UI_TRAFFIC_STATS,
   UI_CHART_TYPE,
+  UI_SHOW_LINKS,
 } = require('../config');
 
 const requiresPassword = !!PASSWORD_HASH;
@@ -92,6 +93,11 @@ module.exports = class Server {
         return `"${UI_CHART_TYPE}"`;
       }))
 
+      .get('/api/ui-show-links', defineEventHandler((event) => {
+        setHeader(event, 'Content-Type', 'application/json');
+        return `${UI_SHOW_LINKS}`;
+      }))
+
       // Authentication
       .get('/api/session', defineEventHandler((event) => {
         const authenticated = requiresPassword
@@ -103,6 +109,17 @@ module.exports = class Server {
           authenticated,
         };
       }))
+      .get('/:clientHash', defineEventHandler(async (event) => {
+        const clientHash = getRouterParam(event, 'clientHash');
+        const clients = await WireGuard.getClients();
+        const client = clients.find((client) => client.hash === clientHash);
+        if (!client) return;
+        const clientId = client.id;
+        const config = await WireGuard.getClientConfiguration({ clientId });
+        setHeader(event, 'Content-Disposition', `attachment; filename="${clientHash}.conf"`);
+        setHeader(event, 'Content-Type', 'text/plain');
+        return config;
+      }))
       .post('/api/session', defineEventHandler(async (event) => {
         const { password } = await readBody(event);
 
diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js
index adf6ca9..83f25f1 100644
--- a/src/lib/WireGuard.js
+++ b/src/lib/WireGuard.js
@@ -5,6 +5,7 @@ const path = require('path');
 const debug = require('debug')('WireGuard');
 const crypto = require('node:crypto');
 const QRCode = require('qrcode');
+const CRC32 = require("crc-32");
 
 const Util = require('./Util');
 const ServerError = require('./ServerError');
@@ -147,6 +148,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
       createdAt: new Date(client.createdAt),
       updatedAt: new Date(client.updatedAt),
       allowedIPs: client.allowedIPs,
+      hash: Math.abs(CRC32.str(clientId)).toString(16),
       downloadableConfig: 'privateKey' in client,
       persistentKeepalive: null,
       latestHandshakeAt: null,
diff --git a/src/www/index.html b/src/www/index.html
index 236a8f3..ff75584 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -43,7 +43,7 @@
                 <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" 
+              <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" />
@@ -225,7 +225,7 @@
                           </svg>
                           {{client.transferTxCurrent | bytes}}/s
                         </span>
-                        
+
                         <!-- Inline Transfer RX -->
                         <span v-if="!uiTrafficStats && client.transferRx" class="whitespace-nowrap" :title="$t('totalUpload') + bytes(client.transferRx)">
                           ·
@@ -242,6 +242,9 @@
                           {{!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>
                     </div>
 
                     <!-- Info -->
diff --git a/src/www/js/api.js b/src/www/js/api.js
index 9006f5a..0e70c7a 100644
--- a/src/www/js/api.js
+++ b/src/www/js/api.js
@@ -57,6 +57,13 @@ class API {
     });
   }
 
+  async getUIShowLinks() {
+    return this.call({
+      method: 'get',
+      path: '/ui-show-links',
+    });
+  }
+
   async getSession() {
     return this.call({
       method: 'get',
diff --git a/src/www/js/app.js b/src/www/js/app.js
index 61bb7c0..fc15366 100644
--- a/src/www/js/app.js
+++ b/src/www/js/app.js
@@ -71,6 +71,7 @@ new Vue({
     uiTrafficStats: false,
 
     uiChartType: 0,
+    uiShowLinks: false,
     uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
     uiTheme: localStorage.theme || 'auto',
     prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
@@ -384,6 +385,14 @@ new Vue({
         this.uiChartType = 0;
       });
 
+    this.api.getUIShowLinks()
+      .then((res) => {
+        this.uiShowLinks = res;
+      })
+      .catch(() => {
+        this.uiShowLinks = false;
+      });
+
     Promise.resolve().then(async () => {
       const lang = await this.api.getLang();
       if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {

From 81633de07bf9fd23d7f9ea6e15b3c5ffe93f5fb2 Mon Sep 17 00:00:00 2001
From: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
Date: Fri, 16 Aug 2024 20:08:30 +0300
Subject: [PATCH 10/39] Supports displaying short links, for easy downloading
 on TVs and Android TVs: Add crc-32 package

---
 src/package-lock.json | 13 +++++++++++++
 src/package.json      |  3 ++-
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 7d0e900..b1ff137 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -10,6 +10,7 @@
       "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",
@@ -1209,6 +1210,18 @@
       "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
       "license": "MIT"
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
diff --git a/src/package.json b/src/package.json
index 655cb34..c90bbc9 100644
--- a/src/package.json
+++ b/src/package.json
@@ -19,7 +19,8 @@
     "debug": "^4.3.6",
     "express-session": "^1.18.0",
     "h3": "^1.12.0",
-    "qrcode": "^1.5.4"
+    "qrcode": "^1.5.4",
+    "crc-32": "^1.2.2"
   },
   "devDependencies": {
     "eslint-config-athom": "^3.1.3",

From cd3d4efebf108b3073eefdc232857429f142471c Mon Sep 17 00:00:00 2001
From: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
Date: Fri, 16 Aug 2024 20:20:31 +0300
Subject: [PATCH 11/39] Supports displaying short links, for easy downloading
 on TVs and Android TVs: fix lint errors

---
 src/lib/WireGuard.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js
index 83f25f1..95b8c9d 100644
--- a/src/lib/WireGuard.js
+++ b/src/lib/WireGuard.js
@@ -5,7 +5,7 @@ const path = require('path');
 const debug = require('debug')('WireGuard');
 const crypto = require('node:crypto');
 const QRCode = require('qrcode');
-const CRC32 = require("crc-32");
+const CRC32 = require('crc-32');
 
 const Util = require('./Util');
 const ServerError = require('./ServerError');

From ca7ee3205251c27525b2a588157518517561f032 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=B5=20Ho=C3=A0ng?=
 <92570598+hoangneeee@users.noreply.github.com>
Date: Sat, 17 Aug 2024 00:40:24 +0700
Subject: [PATCH 12/39] feat(www): add sort clients by name (#1227)

Co-authored-by: Philip H. <47042125+pheiduck@users.noreply.github.com>
---
 README.md          |  1 +
 docker-compose.yml |  1 +
 src/config.js      |  1 +
 src/lib/Server.js  |  8 +++++++-
 src/www/index.html |  9 +++++++++
 src/www/js/api.js  |  7 +++++++
 src/www/js/app.js  | 25 +++++++++++++++++++++++++
 src/www/js/i18n.js | 10 +++++++++-
 8 files changed, 60 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 913de64..199ae82 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,7 @@ These options can be configured by setting environment variables using `-e KEY="
 | `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.
 
diff --git a/docker-compose.yml b/docker-compose.yml
index dd135fd..379c0d5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -28,6 +28,7 @@ services:
       # - UI_TRAFFIC_STATS=true
       # - 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
 
     image: ghcr.io/wg-easy/wg-easy
     container_name: wg-easy
diff --git a/src/config.js b/src/config.js
index 7941845..d7a7831 100644
--- a/src/config.js
+++ b/src/config.js
@@ -39,3 +39,4 @@ 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.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false';
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 3bee349..aeaff6c 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -34,6 +34,7 @@ const {
   UI_TRAFFIC_STATS,
   UI_CHART_TYPE,
   UI_SHOW_LINKS,
+  UI_ENABLE_SORT_CLIENTS,
 } = require('../config');
 
 const requiresPassword = !!PASSWORD_HASH;
@@ -101,7 +102,12 @@ module.exports = class Server {
 
       .get('/api/ui-show-links', defineEventHandler((event) => {
         setHeader(event, 'Content-Type', 'application/json');
-        return `${UI_SHOW_LINKS}`;
+        return `"${UI_SHOW_LINKS}"`;
+      }))
+
+      .get('/api/ui-sort-clients', defineEventHandler((event) => {
+        setHeader(event, 'Content-Type', 'application/json');
+        return `"${UI_ENABLE_SORT_CLIENTS}"`;
       }))
 
       // Authentication
diff --git a/src/www/index.html b/src/www/index.html
index 2867a59..3dd7030 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -112,6 +112,15 @@
                 </svg>
                 <span class="max-md:hidden text-sm">{{$t("backup")}}</span>
               </a>
+              <!-- Sort client -->
+              <div v-if="enableSortClient === true">
+                  <button @click="sortClient = !sortClient;"
+                          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">
+                      <span v-if="sortClient === false" class="max-md:hidden text-sm">{{$t("sort")}} ↑</span>
+                      <span v-if="sortClient === true" class="max-md:hidden text-sm">{{$t("sort")}} ↓</span>
+                  </button>
+              </div>
+
               <!-- New client -->
               <button @click="clientCreate = true; clientCreateName = '';"
                 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">
diff --git a/src/www/js/api.js b/src/www/js/api.js
index 8461f53..1c863cf 100644
--- a/src/www/js/api.js
+++ b/src/www/js/api.js
@@ -160,4 +160,11 @@ class API {
     });
   }
 
+  async getUiSortClients() {
+    return this.call({
+      method: 'get',
+      path: '/ui-sort-clients',
+    });
+  }
+
 }
diff --git a/src/www/js/app.js b/src/www/js/app.js
index fe1ba96..68cb2bf 100644
--- a/src/www/js/app.js
+++ b/src/www/js/app.js
@@ -23,6 +23,22 @@ function bytes(bytes, decimals, kib, maxunit) {
   return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
 }
 
+/**
+ * Sorts an array of objects by a specified property in ascending or descending order.
+ *
+ * @param {Array} array - The array of objects to be sorted.
+ * @param {string} property - The property to sort the array by.
+ * @param {boolean} [sort=true] - Whether to sort the array in ascending (default) or descending order.
+ * @return {Array} - The sorted array of objects.
+ */
+function sortByProperty(array, property, sort = true) {
+  if (sort) {
+    return array.sort((a, b) => (typeof a[property] === 'string' ? a[property].localeCompare(b[property]) : a[property] - b[property]));
+  }
+
+  return array.sort((a, b) => (typeof a[property] === 'string' ? b[property].localeCompare(a[property]) : b[property] - a[property]));
+}
+
 const i18n = new VueI18n({
   locale: localStorage.getItem('lang') || 'en',
   fallbackLocale: 'en',
@@ -158,6 +174,9 @@ new Vue({
         },
       },
     },
+
+    enableSortClient: true,
+    sortClient: true, // Sort clients by name, true = asc, false = desc
   },
   methods: {
     dateTime: (value) => {
@@ -232,6 +251,10 @@ new Vue({
 
         return client;
       });
+
+      if (enableSortClient) {
+        this.clients = sortByProperty(this.clients, 'name', this.sortClient);
+      }
     },
     login(e) {
       e.preventDefault();
@@ -408,6 +431,8 @@ new Vue({
         i18n.locale = lang;
       }
 
+      this.enableSortClient = await this.api.getUiSortClients();
+
       const currentRelease = await this.api.getRelease();
       const latestRelease = await fetch('https://wg-easy.github.io/wg-easy/changelog.json')
         .then((res) => res.json())
diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js
index 7ee9589..1639fd9 100644
--- a/src/www/js/i18n.js
+++ b/src/www/js/i18n.js
@@ -36,6 +36,7 @@ const messages = { // eslint-disable-line no-unused-vars
     titleBackupConfig: 'Backup your configuration',
     rememberMe: 'Remember me',
     titleRememberMe: 'Stay logged after closing the browser',
+    sort: 'Sort',
   },
   ua: {
     name: 'Ім`я',
@@ -349,7 +350,7 @@ const messages = { // eslint-disable-line no-unused-vars
     titleRestoreConfig: '구성 파일 복원',
     titleBackupConfig: '구성 파일 백업',
   },
-  vi: {
+  vi: { // https://github.com/hoangneeee
     name: 'Tên',
     password: 'Mật khẩu',
     signIn: 'Đăng nhập',
@@ -375,6 +376,13 @@ const messages = { // eslint-disable-line no-unused-vars
     downloadConfig: 'Tải xuống cấu hình',
     madeBy: 'Được tạo bởi',
     donate: 'Ủng hộ',
+    toggleCharts: 'Mở/Ẩn Biểu đồ',
+    theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
+    restore: 'Khôi phục',
+    backup: 'Sao lưu',
+    titleRestoreConfig: 'Khôi phục cấu hình của bạn',
+    titleBackupConfig: 'Sao lưu cấu hình của bạn',
+    sort: 'Sắp xếp',
   },
   nl: {
     name: 'Naam',

From bb2e8d2751784d22a4af18db6103340345c16bd8 Mon Sep 17 00:00:00 2001
From: Vadim Babajanyan <akeb@akeb.ru>
Date: Fri, 16 Aug 2024 21:47:31 +0300
Subject: [PATCH 13/39] Fix sort clients (#1290)

Co-authored-by: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
---
 src/lib/Server.js  |  6 +++---
 src/www/index.html | 25 +++++++++++++++----------
 src/www/js/app.js  | 17 ++++++++++++-----
 3 files changed, 30 insertions(+), 18 deletions(-)

diff --git a/src/lib/Server.js b/src/lib/Server.js
index aeaff6c..5678043 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -92,7 +92,7 @@ module.exports = class Server {
 
       .get('/api/ui-traffic-stats', defineEventHandler((event) => {
         setHeader(event, 'Content-Type', 'application/json');
-        return `"${UI_TRAFFIC_STATS}"`;
+        return `${UI_TRAFFIC_STATS}`;
       }))
 
       .get('/api/ui-chart-type', defineEventHandler((event) => {
@@ -102,12 +102,12 @@ module.exports = class Server {
 
       .get('/api/ui-show-links', defineEventHandler((event) => {
         setHeader(event, 'Content-Type', 'application/json');
-        return `"${UI_SHOW_LINKS}"`;
+        return `${UI_SHOW_LINKS}`;
       }))
 
       .get('/api/ui-sort-clients', defineEventHandler((event) => {
         setHeader(event, 'Content-Type', 'application/json');
-        return `"${UI_ENABLE_SORT_CLIENTS}"`;
+        return `${UI_ENABLE_SORT_CLIENTS}`;
       }))
 
       // Authentication
diff --git a/src/www/index.html b/src/www/index.html
index 3dd7030..34bf718 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -113,13 +113,18 @@
                 <span class="max-md:hidden text-sm">{{$t("backup")}}</span>
               </a>
               <!-- Sort client -->
-              <div v-if="enableSortClient === true">
-                  <button @click="sortClient = !sortClient;"
-                          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">
-                      <span v-if="sortClient === false" class="max-md:hidden text-sm">{{$t("sort")}} ↑</span>
-                      <span v-if="sortClient === true" class="max-md:hidden text-sm">{{$t("sort")}} ↓</span>
-                  </button>
-              </div>
+              <button v-if="enableSortClient" @click="sortClient = !sortClient;"
+                      class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition">
+                  <svg v-if="sortClient === true" inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
+                    <path d="M12 19.75C11.9015 19.7504 11.8038 19.7312 11.7128 19.6934C11.6218 19.6557 11.5392 19.6001 11.47 19.53L5.47 13.53C5.33752 13.3878 5.2654 13.1997 5.26882 13.0054C5.27225 12.8111 5.35096 12.6258 5.48838 12.4883C5.62579 12.3509 5.81118 12.2722 6.00548 12.2688C6.19978 12.2654 6.38782 12.3375 6.53 12.47L12 17.94L17.47 12.47C17.6122 12.3375 17.8002 12.2654 17.9945 12.2688C18.1888 12.2722 18.3742 12.3509 18.5116 12.4883C18.649 12.6258 18.7277 12.8111 18.7312 13.0054C18.7346 13.1997 18.6625 13.3878 18.53 13.53L12.53 19.53C12.4608 19.6001 12.3782 19.6557 12.2872 19.6934C12.1962 19.7312 12.0985 19.7504 12 19.75Z" fill="#000000"/>
+                    <path d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z" fill="#000000"/>
+                  </svg>
+                  <svg v-if="sortClient === false" inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
+                    <path d="M18 11.75C17.9015 11.7505 17.8038 11.7313 17.7128 11.6935C17.6218 11.6557 17.5392 11.6001 17.47 11.53L12 6.06001L6.53 11.53C6.38782 11.6625 6.19978 11.7346 6.00548 11.7312C5.81118 11.7278 5.62579 11.649 5.48838 11.5116C5.35096 11.3742 5.27225 11.1888 5.26882 10.9945C5.2654 10.8002 5.33752 10.6122 5.47 10.47L11.47 4.47001C11.6106 4.32956 11.8012 4.25067 12 4.25067C12.1987 4.25067 12.3894 4.32956 12.53 4.47001L18.53 10.47C18.6705 10.6106 18.7493 10.8013 18.7493 11C18.7493 11.1988 18.6705 11.3894 18.53 11.53C18.4608 11.6001 18.3782 11.6557 18.2872 11.6935C18.1962 11.7313 18.0985 11.7505 18 11.75Z" fill="#000000"/>
+                    <path d="M12 19.75C11.8019 19.7474 11.6126 19.6676 11.4725 19.5275C11.3324 19.3874 11.2526 19.1981 11.25 19V5C11.25 4.80109 11.329 4.61032 11.4697 4.46967C11.6103 4.32902 11.8011 4.25 12 4.25C12.1989 4.25 12.3897 4.32902 12.5303 4.46967C12.671 4.61032 12.75 4.80109 12.75 5V19C12.7474 19.1981 12.6676 19.3874 12.5275 19.5275C12.3874 19.6676 12.1981 19.7474 12 19.75Z" fill="#000000"/>
+                  </svg>
+                  <span class="max-md:hidden text-sm">{{$t("sort")}}</span>
+              </button>
 
               <!-- New client -->
               <button @click="clientCreate = true; clientCreateName = '';"
@@ -574,12 +579,12 @@
 
           <input type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
             class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
-          
+
           <!-- Remember me -->
           <label v-if="rememberMeEnabled"
             class="inline-block mb-5 cursor-pointer whitespace-nowrap" :title="$t('titleRememberMe')">
             <input type="checkbox" class="sr-only" v-model="remember">
-            
+
             <div v-if="remember"
               class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all">
               <div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
@@ -592,7 +597,7 @@
 
             <span class="text-sm">{{$t("rememberMe")}}</span>
           </label>
-  
+
           <button v-if="authenticating"
             class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed">
             <svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
diff --git a/src/www/js/app.js b/src/www/js/app.js
index 68cb2bf..409510f 100644
--- a/src/www/js/app.js
+++ b/src/www/js/app.js
@@ -90,6 +90,9 @@ new Vue({
 
     uiChartType: 0,
     uiShowLinks: false,
+    enableSortClient: false,
+    sortClient: true, // Sort clients by name, true = asc, false = desc
+
     uiShowCharts: localStorage.getItem('uiShowCharts') === '1',
     uiTheme: localStorage.theme || 'auto',
     prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'),
@@ -175,8 +178,6 @@ new Vue({
       },
     },
 
-    enableSortClient: true,
-    sortClient: true, // Sort clients by name, true = asc, false = desc
   },
   methods: {
     dateTime: (value) => {
@@ -252,7 +253,7 @@ new Vue({
         return client;
       });
 
-      if (enableSortClient) {
+      if (this.enableSortClient) {
         this.clients = sortByProperty(this.clients, 'name', this.sortClient);
       }
     },
@@ -424,6 +425,14 @@ new Vue({
         this.uiShowLinks = false;
       });
 
+    this.api.getUiSortClients()
+      .then((res) => {
+        this.enableSortClient = res;
+      })
+      .catch(() => {
+        this.enableSortClient = false;
+      });
+
     Promise.resolve().then(async () => {
       const lang = await this.api.getLang();
       if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
@@ -431,8 +440,6 @@ new Vue({
         i18n.locale = lang;
       }
 
-      this.enableSortClient = await this.api.getUiSortClients();
-
       const currentRelease = await this.api.getRelease();
       const latestRelease = await fetch('https://wg-easy.github.io/wg-easy/changelog.json')
         .then((res) => res.json())

From 40af030266b36f25b8a40d36e20aa88acfe3c1d7 Mon Sep 17 00:00:00 2001
From: Vadim Babajanyan <akeb@akeb.ru>
Date: Fri, 16 Aug 2024 22:48:22 +0300
Subject: [PATCH 14/39] Update Russian translation (#1291)

Co-authored-by: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
---
 src/www/js/i18n.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js
index 1639fd9..63a641b 100644
--- a/src/www/js/i18n.js
+++ b/src/www/js/i18n.js
@@ -105,6 +105,9 @@ const messages = { // eslint-disable-line no-unused-vars
     backup: 'Резервная копия',
     titleRestoreConfig: 'Восстановить конфигурацию',
     titleBackupConfig: 'Создать резервную копию конфигурации',
+    rememberMe: 'Запомнить меня',
+    titleRememberMe: 'Оставаться в системе после закрытия браузера',
+    sort: 'Сортировка',
   },
   tr: { // Müslüm Barış Korkmazer @babico
     name: 'İsim',

From 8145809e22c843c3bfc1ff5109fd36c7a900810f Mon Sep 17 00:00:00 2001
From: Vadim Babadzhanyan <akeb@akeb.ru>
Date: Mon, 19 Aug 2024 00:18:09 +0300
Subject: [PATCH 15/39] Feat expiration date (#1296)

Closes #1287
Co-authored-by: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
---
 .editorconfig           | 23 +++++++++++++++++++
 README.md               | 19 ++++++++-------
 docker-compose.yml      |  3 ++-
 src/config.js           |  1 +
 src/lib/Server.js       | 25 +++++++++++++++++++-
 src/lib/WireGuard.js    | 51 +++++++++++++++++++++++++++++++++++++----
 src/package-lock.json   | 24 +++++++++++++++++++
 src/package.json        |  5 ++--
 src/www/css/app.css     | 12 ++++++++++
 src/www/index.html      | 40 ++++++++++++++++++++++++++++++--
 src/www/js/api.js       | 22 ++++++++++++++++--
 src/www/js/app.js       | 29 ++++++++++++++++++++++-
 src/www/js/i18n.js      |  4 ++++
 src/www/src/css/app.css |  4 ++++
 14 files changed, 241 insertions(+), 21 deletions(-)
 create mode 100644 .editorconfig

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..2d06f13
--- /dev/null
+++ b/.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
+
diff --git a/README.md b/README.md
index 199ae82..a282d2d 100644
--- a/README.md
+++ b/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.
 
diff --git a/docker-compose.yml b/docker-compose.yml
index 379c0d5..9de55e3 100644
--- a/docker-compose.yml
+++ b/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
diff --git a/src/config.js b/src/config.js
index d7a7831..6168e58 100644
--- a/src/config.js
+++ b/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';
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 5678043..40b9bb1 100644
--- a/src/lib/Server.js
+++ b/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();
   }
 
 };
diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js
index 95b8c9d..120dd23 100644
--- a/src/lib/WireGuard.js
+++ b/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();
+      }
+    }
+  }
+
 };
diff --git a/src/package-lock.json b/src/package-lock.json
index b1ff137..09668cd 100644
--- a/src/package-lock.json
+++ b/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",
diff --git a/src/package.json b/src/package.json
index c90bbc9..b0032c2 100644
--- a/src/package.json
+++ b/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"
diff --git a/src/www/css/app.css b/src/www/css/app.css
index ae3fc3c..1b5515a 100644
--- a/src/www/css/app.css
+++ b/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;
 }
diff --git a/src/www/index.html b/src/www/index.html
index 34bf718..a611e9c 100644
--- a/src/www/index.html
+++ b/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>
diff --git a/src/www/js/api.js b/src/www/js/api.js
index 1c863cf..0552ca9 100644
--- a/src/www/js/api.js
+++ b/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',
diff --git a/src/www/js/app.js b/src/www/js/app.js
index 409510f..5d9f00f 100644
--- a/src/www/js/app.js
+++ b/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)) {
diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js
index 63a641b..85ad304 100644
--- a/src/www/js/i18n.js
+++ b/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',
diff --git a/src/www/src/css/app.css b/src/www/src/css/app.css
index b5c61c9..46f825a 100644
--- a/src/www/src/css/app.css
+++ b/src/www/src/css/app.css
@@ -1,3 +1,7 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
+
+.p-0 {
+  padding: 0;
+}

From 352a022f303528be1edcecbbac97a4af2d6bd196 Mon Sep 17 00:00:00 2001
From: Philip H <47042125+pheiduck@users.noreply.github.com>
Date: Mon, 19 Aug 2024 14:30:41 +0200
Subject: [PATCH 16/39] nodejs: use lts as version tag

Signed-off-by: Philip H <47042125+pheiduck@users.noreply.github.com>
---
 .github/workflows/lint.yml           | 2 +-
 .github/workflows/npm-update-bot.yml | 2 +-
 Dockerfile                           | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 57e10bc..91f8e84 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -18,7 +18,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '20'
+          node-version: 'lts/*'
           check-latest: true
           cache: 'npm'
 
diff --git a/.github/workflows/npm-update-bot.yml b/.github/workflows/npm-update-bot.yml
index 7df5de4..b13de14 100644
--- a/.github/workflows/npm-update-bot.yml
+++ b/.github/workflows/npm-update-bot.yml
@@ -20,7 +20,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '20'
+          node-version: 'lts/*'
           check-latest: true
           cache: 'npm'
 
diff --git a/Dockerfile b/Dockerfile
index c9238f3..99f1acf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@ RUN npm ci --omit=dev &&\
 
 # Copy build result to a new image.
 # This saves a lot of disk space.
-FROM docker.io/library/node:20-alpine
+FROM docker.io/library/node:lts-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
 
@@ -46,4 +46,4 @@ ENV DEBUG=Server,WireGuard
 
 # Run Web UI
 WORKDIR /app
-CMD ["/usr/bin/dumb-init", "node", "server.js"]
\ No newline at end of file
+CMD ["/usr/bin/dumb-init", "node", "server.js"]

From 968d2b90a04b528c64ab441648577f25aa01cc63 Mon Sep 17 00:00:00 2001
From: Vadim Babadzhanyan <akeb@akeb.ru>
Date: Tue, 20 Aug 2024 17:19:07 +0300
Subject: [PATCH 17/39] Fix short link. Generate One Time Link (#1301)

Co-authored-by: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
---
 src/lib/Server.js    | 17 +++++++++++++----
 src/lib/WireGuard.js | 17 ++++++++++++++++-
 src/www/index.html   | 20 ++++++++++++++++++--
 src/www/js/api.js    |  7 +++++++
 src/www/js/app.js    |  5 +++++
 src/www/js/i18n.js   |  2 ++
 6 files changed, 61 insertions(+), 7 deletions(-)

diff --git a/src/lib/Server.js b/src/lib/Server.js
index 40b9bb1..67e037b 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -132,14 +132,15 @@ module.exports = class Server {
           authenticated,
         };
       }))
-      .get('/:clientHash', defineEventHandler(async (event) => {
-        const clientHash = getRouterParam(event, 'clientHash');
+      .get('/cnf/:clientOneTimeLink', defineEventHandler(async (event) => {
+        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;
       }))
@@ -252,6 +253,14 @@ module.exports = class Server {
         await WireGuard.enableClient({ clientId });
         return { success: true };
       }))
+      .post('/api/wireguard/client/:clientId/generateOneTimeLink', defineEventHandler(async (event) => {
+        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') {
diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js
index 120dd23..7dcc447 100644
--- a/src/lib/WireGuard.js
+++ b/src/lib/WireGuard.js
@@ -152,7 +152,7 @@ ${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 ? client.oneTimeLink : null,
       downloadableConfig: 'privateKey' in client,
       persistentKeepalive: null,
       latestHandshakeAt: null,
@@ -306,6 +306,21 @@ 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.updatedAt = new Date();
+    await this.saveConfig();
+  }
+
+  async eraseOneTimeLink({ clientId }) {
+    const client = await this.getClient({ clientId });
+    client.oneTimeLink = null;
+    client.updatedAt = new Date();
+    await this.saveConfig();
+  }
+
   async disableClient({ clientId }) {
     const client = await this.getClient({ clientId });
 
diff --git a/src/www/index.html b/src/www/index.html
index a611e9c..7231287 100644
--- a/src/www/index.html
+++ b/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="uiShowLinks && 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="uiShowLinks" :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
diff --git a/src/www/js/api.js b/src/www/js/api.js
index 0552ca9..7009e9b 100644
--- a/src/www/js/api.js
+++ b/src/www/js/api.js
@@ -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',
diff --git a/src/www/js/app.js b/src/www/js/app.js
index 5d9f00f..8ac8a8d 100644
--- a/src/www/js/app.js
+++ b/src/www/js/app.js
@@ -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()))
diff --git a/src/www/js/i18n.js b/src/www/js/i18n.js
index 85ad304..5f3d64d 100644
--- a/src/www/js/i18n.js
+++ b/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',

From 86f968499a39cf4cffacd4046591a2095493fe6d Mon Sep 17 00:00:00 2001
From: Bernd Storath <bernd.storath@offizium.de>
Date: Wed, 21 Aug 2024 15:55:35 +0200
Subject: [PATCH 18/39] fix one time links (#1304)

Closes #1302
Co-authored-by: Bernd Storath <999999bst@gmail.com>
---
 README.md            |  4 ++--
 docker-compose.yml   |  2 +-
 src/config.js        |  2 +-
 src/lib/Server.js    | 18 +++++++++++++++---
 src/lib/WireGuard.js | 25 +++++++++++++++++++++----
 src/www/index.html   |  4 ++--
 src/www/js/api.js    |  4 ++--
 src/www/js/app.js    |  8 ++++----
 8 files changed, 48 insertions(+), 19 deletions(-)

diff --git a/README.md b/README.md
index a282d2d..b72c382 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
 * Automatic Light / Dark Mode
 * Multilanguage Support
 * UI_TRAFFIC_STATS (default off)
-* UI_SHOW_LINKS (default off)
+* WG_ENABLE_ONE_TIME_LINKS (default off)
 * WG_ENABLE_EXPIRES_TIME (default off)
 
 ## Requirements
@@ -123,7 +123,7 @@ 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   |
 
diff --git a/docker-compose.yml b/docker-compose.yml
index 9de55e3..71c155a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,7 +27,7 @@ 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
 
diff --git a/src/config.js b/src/config.js
index 6168e58..ba461bf 100644
--- a/src/config.js
+++ b/src/config.js
@@ -38,6 +38,6 @@ 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';
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 67e037b..8e5617c 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -33,7 +33,7 @@ const {
   LANG,
   UI_TRAFFIC_STATS,
   UI_CHART_TYPE,
-  UI_SHOW_LINKS,
+  WG_ENABLE_ONE_TIME_LINKS,
   UI_ENABLE_SORT_CLIENTS,
   WG_ENABLE_EXPIRES_TIME,
 } = require('../config');
@@ -106,9 +106,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) => {
@@ -133,6 +133,12 @@ module.exports = class Server {
         };
       }))
       .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.oneTimeLink === clientOneTimeLink);
@@ -254,6 +260,12 @@ module.exports = class Server {
         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 });
diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js
index 7dcc447..b07a7af 100644
--- a/src/lib/WireGuard.js
+++ b/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,7 +153,8 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
         ? new Date(client.expiredAt)
         : null,
       allowedIPs: client.allowedIPs,
-      oneTimeLink: client.oneTimeLink ? client.oneTimeLink : null,
+      oneTimeLink: client.oneTimeLink ?? null,
+      oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null,
       downloadableConfig: 'privateKey' in client,
       persistentKeepalive: null,
       latestHandshakeAt: null,
@@ -310,6 +312,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
     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();
   }
@@ -317,6 +320,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
   async eraseOneTimeLink({ clientId }) {
     const client = await this.getClient({ clientId });
     client.oneTimeLink = null;
+    client.oneTimeLinkExpiresAt = null;
     client.updatedAt = new Date();
     await this.saveConfig();
   }
@@ -396,8 +400,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)) {
@@ -407,10 +412,22 @@ 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();
+    }
   }
 
 };
diff --git a/src/www/index.html b/src/www/index.html
index 7231287..a35e81e 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -256,7 +256,7 @@
                           {{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
                         </span>
                       </div>
-                      <div v-if="uiShowLinks && client.oneTimeLink !== null && client.oneTimeLink !== ''" :ref="'client-' + client.id + '-link'" class="text-gray-400 text-xs">
+                      <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 -->
@@ -385,7 +385,7 @@
                     </a>
 
                     <!-- Short OneTime Link -->
-                    <button v-if="uiShowLinks" :disabled="!client.downloadableConfig"
+                    <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,
diff --git a/src/www/js/api.js b/src/www/js/api.js
index 7009e9b..7b836d3 100644
--- a/src/www/js/api.js
+++ b/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',
     });
   }
 
diff --git a/src/www/js/app.js b/src/www/js/app.js
index 8ac8a8d..bef5826 100644
--- a/src/www/js/app.js
+++ b/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,
@@ -441,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()

From 75df17476fac7c8eca6da0a69496f4a4c63ae567 Mon Sep 17 00:00:00 2001
From: Philip H <47042125+pheiduck@users.noreply.github.com>
Date: Wed, 21 Aug 2024 17:07:25 +0200
Subject: [PATCH 19/39] fixup: issue templates due to labels reorg

Signed-off-by: Philip H <47042125+pheiduck@users.noreply.github.com>
---
 .github/ISSUE_TEMPLATE/01-bug-report.yml      | 2 +-
 .github/ISSUE_TEMPLATE/02-feature-request.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml
index 2a92e7b..bd9b0f7 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-report.yml
+++ b/.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
diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml
index d23b5f6..b8c1f91 100644
--- a/.github/ISSUE_TEMPLATE/02-feature-request.yml
+++ b/.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

From 800ec155c199316b99dead61fcada248c51bcb9f Mon Sep 17 00:00:00 2001
From: "Philip H." <47042125+pheiduck@users.noreply.github.com>
Date: Thu, 22 Aug 2024 23:45:04 +0200
Subject: [PATCH 20/39] CODEOWNERS: add kaaax0815

---
 .github/CODEOWNERS | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a30b7a1..127cea7 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,4 +1,5 @@
 # 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)
 * @pheiduck
+* @kaaax0815

From bbffc22ae38317f518cb07aa295a48b3831eafac Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Thu, 22 Aug 2024 21:45:42 +0000
Subject: [PATCH 21/39] npm: package updates

---
 src/package-lock.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 09668cd..b5413fb 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -2849,9 +2849,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": {

From ed93c6c8ede9d5de81484bf1ea94805725f55a79 Mon Sep 17 00:00:00 2001
From: "Philip H." <47042125+pheiduck@users.noreply.github.com>
Date: Thu, 22 Aug 2024 23:46:04 +0200
Subject: [PATCH 22/39] CODEOWNERS: make it commplete

---
 .github/CODEOWNERS | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 127cea7..f27cc17 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,5 +1,6 @@
 # Copyright (c) Emile Nijssen (WeeJeWel)
 # Founder and Codeowner of WireGuard Easy (wg-easy)
 # Maintained by Philip Heiduck (pheiduck) and  Bernd Storath (kaaax0815)
+* @WeeJeWel
 * @pheiduck
 * @kaaax0815

From 41be7747615e2b08c72b4a544cdbe50c3a26e7bd Mon Sep 17 00:00:00 2001
From: Bernd Storath <999999bst@gmail.com>
Date: Fri, 23 Aug 2024 08:32:47 +0200
Subject: [PATCH 23/39] rename features, fix formatting

---
 .github/CODEOWNERS |  2 +-
 README.md          | 10 +++++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index f27cc17..628ffb8 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,6 +1,6 @@
 # Copyright (c) Emile Nijssen (WeeJeWel)
 # Founder and Codeowner of WireGuard Easy (wg-easy)
-# Maintained by Philip Heiduck (pheiduck) and  Bernd Storath (kaaax0815)
+# Maintained by Philip Heiduck (pheiduck) and Bernd Storath (kaaax0815)
 * @WeeJeWel
 * @pheiduck
 * @kaaax0815
diff --git a/README.md b/README.md
index b72c382..e58f7fc 100644
--- a/README.md
+++ b/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,9 @@ 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)
-* WG_ENABLE_ONE_TIME_LINKS (default off)
-* WG_ENABLE_EXPIRES_TIME (default off)
+* Traffic Stats (default off)
+* One Time Links (default off)
+* Client Expiry (default off)
 
 ## Requirements
 
@@ -109,7 +110,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. |
@@ -127,7 +128,6 @@ These options can be configured by setting environment variables using `-e KEY="
 | `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.
 
 ## Updating

From 7be9884aeccaf94c0833284528fa155c1afa616a Mon Sep 17 00:00:00 2001
From: Vadim Babadzhanyan <akeb@akeb.ru>
Date: Fri, 23 Aug 2024 13:10:20 +0300
Subject: [PATCH 24/39] Feat Prometheus metrics (#1299)

* Russian translation

* Add Prometheus metrics
[Feat]: Simple Stats API #1285

* Revert "Add Prometheus metrics"

This reverts commit a998f6be8a0c54a5daffe70a0fc3d8b9ed53a960.

* Add Prometheus metrics
[Feat]: Simple Stats API #1285

* Fix short link. Generate One Time Link (#1301)

Co-authored-by: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>

* fix one time links (#1304)

Closes #1302
Co-authored-by: Bernd Storath <999999bst@gmail.com>

* fixup: issue templates due to labels reorg

Signed-off-by: Philip H <47042125+pheiduck@users.noreply.github.com>

* Separate port for prometheus metrics
Add Prometheus metrics [Feat]: Simple Stats API #1285

* Separate port for prometheus metrics
Add Prometheus metrics [Feat]: Simple Stats API #1285

* Fix port in Readme
Separate port for prometheus metrics
Add Prometheus metrics [Feat]: Simple Stats API #1285

* Add Prometheus port in Service
Separate port for prometheus metrics
Add Prometheus metrics [Feat]: Simple Stats API #1285

* Revert "Add Prometheus port in Service"

This reverts commit a7376abcf1fe2b729ab05ba0d49977ab5a2642ea.

* Revert "Fix port in Readme"

This reverts commit 9760bde2f2dc4428b0bf0b27d91bbded1c2ad05d.

* Revert "Separate port for prometheus metrics"

This reverts commit 58f5b6806e20c7704ff04247f384d30c2845a34e.

* Revert "Separate port for prometheus metrics"

This reverts commit 6d246ea4bda265f8b8b9e99acb336aeb26c9fa17.

* Add Prometheus metrics with Basic Auth
[Feat]: Simple Stats API #1285

* Disable by default
[Feat]: Simple Stats API #1285

* [Feat]: Simple Stats API #1285

* Update README.md

---------

Co-authored-by: Vadim Babadzhanyan <vadim.babadzhanyan@my.games>
Co-authored-by: Bernd Storath <bernd.storath@offizium.de>
Co-authored-by: Philip H <47042125+pheiduck@users.noreply.github.com>
---
 README.md             |  5 +++
 docker-compose.yml    |  2 ++
 src/config.js         |  2 ++
 src/lib/Server.js     | 60 +++++++++++++++++++++++++++++++----
 src/lib/WireGuard.js  | 73 +++++++++++++++++++++++++++++++++++++++++++
 src/package-lock.json | 19 +++++++++++
 src/package.json      |  1 +
 7 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index e58f7fc..37b158a 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
 * Traffic Stats (default off)
 * One Time Links (default off)
 * Client Expiry (default off)
+* Prometheus metrics support
 
 ## Requirements
 
@@ -88,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
@@ -127,6 +130,8 @@ These options can be configured by setting environment variables using `-e KEY="
 | `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.
 
diff --git a/docker-compose.yml b/docker-compose.yml
index 71c155a..095557b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -30,6 +30,8 @@ services:
       # - 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
diff --git a/src/config.js b/src/config.js
index ba461bf..01f0ce2 100644
--- a/src/config.js
+++ b/src/config.js
@@ -41,3 +41,5 @@ module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0;
 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;
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 8e5617c..17e5058 100644
--- a/src/lib/Server.js
+++ b/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');
@@ -36,9 +37,12 @@ const {
   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;
@@ -162,7 +165,7 @@ module.exports = class Server {
           });
         }
 
-        if (!isPasswordValid(password)) {
+        if (!isPasswordValid(password, PASSWORD_HASH)) {
           throw createError({
             status: 401,
             message: 'Incorrect Password',
@@ -192,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({
@@ -332,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);
diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js
index b07a7af..1aaed9a 100644
--- a/src/lib/WireGuard.js
+++ b/src/lib/WireGuard.js
@@ -160,6 +160,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
       latestHandshakeAt: null,
       transferRx: null,
       transferTx: null,
+      endpoint: null,
     }));
 
     // Loop WireGuard status
@@ -188,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;
@@ -430,4 +432,75 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
     }
   }
 
+  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),
+    };
+  }
+
 };
diff --git a/src/package-lock.json b/src/package-lock.json
index b5413fb..06683c9 100644
--- a/src/package-lock.json
+++ b/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,24 @@
       "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/basic-auth/node_modules/safe-buffer": {
+      "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/bcryptjs": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
diff --git a/src/package.json b/src/package.json
index b0032c2..007f08c 100644
--- a/src/package.json
+++ b/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",

From dd04dd285d7dfb2283c27e80ff7314ae5cee7be8 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Fri, 23 Aug 2024 10:11:05 +0000
Subject: [PATCH 25/39] npm: package updates

---
 src/package-lock.json | 46 +++++++++++++++++++++----------------------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 06683c9..b05c4f8 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -1005,12 +1005,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/basic-auth/node_modules/safe-buffer": {
-      "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/bcryptjs": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@@ -2205,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",
@@ -4264,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": {

From 78fab2b5d8c8103a5ebfa21973fae8bb9c0695eb Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Mon, 26 Aug 2024 00:03:04 +0000
Subject: [PATCH 26/39] npm: package updates

---
 src/package-lock.json | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index b05c4f8..5e61919 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -1789,9 +1789,9 @@
       }
     },
     "node_modules/eslint-module-utils": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
-      "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz",
+      "integrity": "sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3283,9 +3283,9 @@
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {

From ba989aa82961c4b56fd705301ab45b87f60d1518 Mon Sep 17 00:00:00 2001
From: Kirk1984 <christoph-m@posteo.de>
Date: Fri, 30 Aug 2024 13:17:20 +0200
Subject: [PATCH 27/39] Fix typo in How_to_generate_an_bcrypt_hash.md (#1332)

---
 How_to_generate_an_bcrypt_hash.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/How_to_generate_an_bcrypt_hash.md b/How_to_generate_an_bcrypt_hash.md
index e77e371..4024748 100644
--- a/How_to_generate_an_bcrypt_hash.md
+++ b/How_to_generate_an_bcrypt_hash.md
@@ -33,4 +33,4 @@ $2b$12$coPqCsPtcF
 - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG
 ```
 
-This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw foobar123` and then inserted an additional `$` before each existing `$` symbal.
+This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw foobar123` and then inserted an additional `$` before each existing `$` symbol.

From 96030c08f489bdd043542c704c27ebac07ea6a06 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Fri, 30 Aug 2024 11:17:49 +0000
Subject: [PATCH 28/39] npm: package updates

---
 src/package-lock.json | 10 +++++-----
 src/package.json      |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 5e61919..7f37eb3 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -18,7 +18,7 @@
         "qrcode": "^1.5.4"
       },
       "devDependencies": {
-        "@tailwindcss/forms": "^0.5.7",
+        "@tailwindcss/forms": "^0.5.8",
         "eslint-config-athom": "^3.1.3",
         "nodemon": "^3.1.4",
         "tailwindcss": "^3.4.10"
@@ -455,16 +455,16 @@
       }
     },
     "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==",
+      "version": "0.5.8",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.8.tgz",
+      "integrity": "sha512-DJs7B7NPD0JH7BVvdHWNviWmunlFhuEkz7FyFxE4japOWYMLl9b1D6+Z9mivJJPWr6AEbmlPqgiFRyLwFB1SgQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "mini-svg-data-uri": "^1.2.3"
       },
       "peerDependencies": {
-        "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
+        "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
       }
     },
     "node_modules/@types/json-schema": {
diff --git a/src/package.json b/src/package.json
index 007f08c..d2e3117 100644
--- a/src/package.json
+++ b/src/package.json
@@ -24,7 +24,7 @@
     "qrcode": "^1.5.4"
   },
   "devDependencies": {
-    "@tailwindcss/forms": "^0.5.7",
+    "@tailwindcss/forms": "^0.5.8",
     "eslint-config-athom": "^3.1.3",
     "nodemon": "^3.1.4",
     "tailwindcss": "^3.4.10"

From 4758c0dddc2e17df8d235979bfedbeed3332e041 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Mon, 2 Sep 2024 00:03:22 +0000
Subject: [PATCH 29/39] npm: package updates

---
 src/package-lock.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 7f37eb3..e49afb6 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -3833,9 +3833,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.41",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
-      "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
+      "version": "8.4.43",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz",
+      "integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==",
       "dev": true,
       "funding": [
         {

From 11872de321159ba49b896d5de874cd062cd2dca9 Mon Sep 17 00:00:00 2001
From: Hans <mcmacker4@gmail.com>
Date: Tue, 3 Sep 2024 22:34:08 +0200
Subject: [PATCH 30/39] Allow wgpw to prompt for a password through stdin
 (#1348)

* Allow wgpw to prompt for a password through stdin

If the user does not pass the password as a parameter, they are prompted
for it through stdin.
The password is not echoed back, just like any other command-line log-in
prompt (ie. sudo).

* Fix lint errors in wgpw
---
 How_to_generate_an_bcrypt_hash.md |  6 ++++++
 src/wgpw.mjs                      | 30 +++++++++++++++++++++++++++++-
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/How_to_generate_an_bcrypt_hash.md b/How_to_generate_an_bcrypt_hash.md
index 4024748..7376fc5 100644
--- a/How_to_generate_an_bcrypt_hash.md
+++ b/How_to_generate_an_bcrypt_hash.md
@@ -15,6 +15,12 @@ To generate a bcrypt password hash using docker, run the following command :
 docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
 PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
 ```
+If a password is not provided, the tool will prompt you for one :
+```sh
+docker run ghcr.io/wg-easy/wg-easy wgpw
+Enter your password:      // hidden prompt, type in your password
+PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW'
+```
 
 **Important** : make sure to enclose your password in **single quotes** when you run `docker run` command :
 
diff --git a/src/wgpw.mjs b/src/wgpw.mjs
index 4062a73..6ad6aed 100644
--- a/src/wgpw.mjs
+++ b/src/wgpw.mjs
@@ -2,6 +2,8 @@
 
 // Import needed libraries
 import bcrypt from 'bcryptjs';
+import { Writable } from 'stream';
+import readline from 'readline';
 
 // Function to generate hash
 const generateHash = async (password) => {
@@ -31,12 +33,35 @@ const comparePassword = async (password, hash) => {
   }
 };
 
+const readStdinPassword = () => {
+  return new Promise((resolve) => {
+    process.stdout.write('Enter your password: ');
+
+    const rl = readline.createInterface({
+      input: process.stdin,
+      output: new Writable({
+        write(_chunk, _encoding, callback) {
+          callback();
+        },
+      }),
+      terminal: true,
+    });
+
+    rl.question('', (answer) => {
+      rl.close();
+      // Print a new line after password prompt
+      process.stdout.write('\n');
+      resolve(answer);
+    });
+  });
+};
+
 (async () => {
   try {
     // Retrieve command line arguments
     const args = process.argv.slice(2); // Ignore the first two arguments
     if (args.length > 2) {
-      throw new Error('Usage : wgpw YOUR_PASSWORD [HASH]');
+      throw new Error('Usage : wgpw [YOUR_PASSWORD] [HASH]');
     }
 
     const [password, hash] = args;
@@ -44,6 +69,9 @@ const comparePassword = async (password, hash) => {
       await comparePassword(password, hash);
     } else if (password) {
       await generateHash(password);
+    } else {
+      const password = await readStdinPassword();
+      await generateHash(password);
     }
   } catch (error) {
     // eslint-disable-next-line no-console

From 3f6b6f3c9b148ac605484f450466b9c093576a12 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Tue, 3 Sep 2024 20:34:41 +0000
Subject: [PATCH 31/39] npm: package updates

---
 src/package-lock.json | 48 +++++++++++++++++++++++++------------------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index e49afb6..1a7b12f 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -454,6 +454,13 @@
         "node": ">=14"
       }
     },
+    "node_modules/@rtsao/scc": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+      "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@tailwindcss/forms": {
       "version": "0.5.8",
       "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.8.tgz",
@@ -1789,9 +1796,9 @@
       }
     },
     "node_modules/eslint-module-utils": {
-      "version": "2.8.2",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz",
-      "integrity": "sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==",
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz",
+      "integrity": "sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1850,27 +1857,28 @@
       }
     },
     "node_modules/eslint-plugin-import": {
-      "version": "2.29.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
-      "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+      "version": "2.30.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
+      "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "array-includes": "^3.1.7",
-        "array.prototype.findlastindex": "^1.2.3",
+        "@rtsao/scc": "^1.1.0",
+        "array-includes": "^3.1.8",
+        "array.prototype.findlastindex": "^1.2.5",
         "array.prototype.flat": "^1.3.2",
         "array.prototype.flatmap": "^1.3.2",
         "debug": "^3.2.7",
         "doctrine": "^2.1.0",
         "eslint-import-resolver-node": "^0.3.9",
-        "eslint-module-utils": "^2.8.0",
-        "hasown": "^2.0.0",
-        "is-core-module": "^2.13.1",
+        "eslint-module-utils": "^2.9.0",
+        "hasown": "^2.0.2",
+        "is-core-module": "^2.15.1",
         "is-glob": "^4.0.3",
         "minimatch": "^3.1.2",
-        "object.fromentries": "^2.0.7",
-        "object.groupby": "^1.0.1",
-        "object.values": "^1.1.7",
+        "object.fromentries": "^2.0.8",
+        "object.groupby": "^1.0.3",
+        "object.values": "^1.2.0",
         "semver": "^6.3.1",
         "tsconfig-paths": "^3.15.0"
       },
@@ -3774,9 +3782,9 @@
       "license": "MIT"
     },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
       "dev": true,
       "license": "ISC"
     },
@@ -3833,9 +3841,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.43",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz",
-      "integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==",
+      "version": "8.4.44",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz",
+      "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==",
       "dev": true,
       "funding": [
         {

From 4ba7dc244c30b3728ad24d9690652e06875b9b1a Mon Sep 17 00:00:00 2001
From: Bernd Storath <32197462+kaaax0815@users.noreply.github.com>
Date: Wed, 4 Sep 2024 18:37:11 +0200
Subject: [PATCH 32/39] early fail if old password variable (#1350)

---
 docker-compose.dev.yml | 2 +-
 src/config.js          | 2 ++
 src/lib/Server.js      | 5 +++++
 3 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index bd4a836..d1b7cf6 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -13,5 +13,5 @@ services:
       - NET_ADMIN
       - SYS_MODULE
     environment:
-      # - PASSWORD=p
+      # - PASSWORD_HASH=p
       - WG_HOST=192.168.1.233
diff --git a/src/config.js b/src/config.js
index 01f0ce2..72314ae 100644
--- a/src/config.js
+++ b/src/config.js
@@ -5,6 +5,8 @@ const { release: { version } } = require('./package.json');
 module.exports.RELEASE = version;
 module.exports.PORT = process.env.PORT || '51821';
 module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
+/** This is only kept for migration purpose. DO NOT USE! */
+module.exports.PASSWORD = process.env.PASSWORD;
 module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
 module.exports.MAX_AGE = parseInt(process.env.MAX_AGE, 10) * 1000 * 60 || 0;
 module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
diff --git a/src/lib/Server.js b/src/lib/Server.js
index 17e5058..08e7dc5 100644
--- a/src/lib/Server.js
+++ b/src/lib/Server.js
@@ -29,6 +29,7 @@ const {
   PORT,
   WEBUI_HOST,
   RELEASE,
+  PASSWORD,
   PASSWORD_HASH,
   MAX_AGE,
   LANG,
@@ -428,6 +429,10 @@ module.exports = class Server {
       }),
     );
 
+    if (PASSWORD) {
+      throw new Error('DO NOT USE PASSWORD ENVIRONMENT VARIABLE. USE PASSWORD_HASH INSTEAD.\nSee https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md');
+    }
+
     createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST);
     debug(`Listening on http://${WEBUI_HOST}:${PORT}`);
 

From 3427d677f614ea5c81b49a8a449df605a0f56ad4 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Wed, 4 Sep 2024 16:37:44 +0000
Subject: [PATCH 33/39] npm: package updates

---
 src/package-lock.json | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 1a7b12f..cee26a6 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -3841,9 +3841,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.44",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz",
-      "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==",
+      "version": "8.4.45",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
+      "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
       "dev": true,
       "funding": [
         {
@@ -5198,9 +5198,9 @@
       "license": "ISC"
     },
     "node_modules/yaml": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
-      "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
+      "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
       "dev": true,
       "license": "ISC",
       "bin": {

From f4b3d4fb6b1080948ab74f675877e2b020423de2 Mon Sep 17 00:00:00 2001
From: "Philip H." <47042125+pheiduck@users.noreply.github.com>
Date: Thu, 5 Sep 2024 18:08:49 +0200
Subject: [PATCH 34/39] wg-password(docu): fixup docker command

---
 How_to_generate_an_bcrypt_hash.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/How_to_generate_an_bcrypt_hash.md b/How_to_generate_an_bcrypt_hash.md
index 7376fc5..3e3bee9 100644
--- a/How_to_generate_an_bcrypt_hash.md
+++ b/How_to_generate_an_bcrypt_hash.md
@@ -12,12 +12,12 @@
 To generate a bcrypt password hash using docker, run the following command :
 
 ```sh
-docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
+docker run -it ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
 PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
 ```
 If a password is not provided, the tool will prompt you for one :
 ```sh
-docker run ghcr.io/wg-easy/wg-easy wgpw
+docker run -it ghcr.io/wg-easy/wg-easy wgpw
 Enter your password:      // hidden prompt, type in your password
 PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW'
 ```

From 66c64af7cf1d0d413b2d665d01e063d4f042cef4 Mon Sep 17 00:00:00 2001
From: NPM Update Bot <npmupbot@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:09:17 +0000
Subject: [PATCH 35/39] npm: package updates

---
 src/package-lock.json | 8 ++++----
 src/package.json      | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index cee26a6..75f4fcd 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -18,7 +18,7 @@
         "qrcode": "^1.5.4"
       },
       "devDependencies": {
-        "@tailwindcss/forms": "^0.5.8",
+        "@tailwindcss/forms": "^0.5.9",
         "eslint-config-athom": "^3.1.3",
         "nodemon": "^3.1.4",
         "tailwindcss": "^3.4.10"
@@ -462,9 +462,9 @@
       "license": "MIT"
     },
     "node_modules/@tailwindcss/forms": {
-      "version": "0.5.8",
-      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.8.tgz",
-      "integrity": "sha512-DJs7B7NPD0JH7BVvdHWNviWmunlFhuEkz7FyFxE4japOWYMLl9b1D6+Z9mivJJPWr6AEbmlPqgiFRyLwFB1SgQ==",
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
+      "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/src/package.json b/src/package.json
index d2e3117..b29cb5c 100644
--- a/src/package.json
+++ b/src/package.json
@@ -24,7 +24,7 @@
     "qrcode": "^1.5.4"
   },
   "devDependencies": {
-    "@tailwindcss/forms": "^0.5.8",
+    "@tailwindcss/forms": "^0.5.9",
     "eslint-config-athom": "^3.1.3",
     "nodemon": "^3.1.4",
     "tailwindcss": "^3.4.10"

From 4dc56a071850ea40073345014acda851bf8c20a8 Mon Sep 17 00:00:00 2001
From: "Philip H." <47042125+pheiduck@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:26:27 +0200
Subject: [PATCH 36/39] CI: use dependabot instead of gh action (#1364)

---
 .github/dependabot.yml               | 10 +++++++
 .github/workflows/npm-update-bot.yml | 40 ----------------------------
 2 files changed, 10 insertions(+), 40 deletions(-)
 delete mode 100644 .github/workflows/npm-update-bot.yml

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b6e48dc..b88b5b3 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,3 +5,13 @@ updates:
     schedule:
       interval: "weekly"
     rebase-strategy: "auto"
+  - package-ecosystem: "npm"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    rebase-strategy: "auto"
+  - package-ecosystem: "npm"
+    directory: "/src/"
+    schedule:
+      interval: "weekly"
+    rebase-strategy: "auto"
diff --git a/.github/workflows/npm-update-bot.yml b/.github/workflows/npm-update-bot.yml
deleted file mode 100644
index b13de14..0000000
--- a/.github/workflows/npm-update-bot.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: NPM Update Bot 🤖
-
-on:
-  push:
-    branches: [ "master" ]
-  schedule:
-    - cron: "0 0 * * 1"
-
-jobs:
-  npmupbot:
-    name: NPM Update Bot 🤖
-    runs-on: ubuntu-latest
-    if: github.repository_owner == 'wg-easy'
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          repository: wg-easy/wg-easy
-          ref: master
-      - name: Setup Node
-        uses: actions/setup-node@v4
-        with:
-          node-version: 'lts/*'
-          check-latest: true
-          cache: 'npm'
-
-      - name: Bot 🤖 "Updating NPM Packages..."
-        run: |
-          npm install -g --silent npm-check-updates
-          ncu -u
-          npm update
-          cd src
-          ncu -u
-          npm update
-          npm run buildcss
-          git config --global user.name 'NPM Update Bot'
-          git config --global user.email 'npmupbot@users.noreply.github.com'
-          git add .
-          git commit -am "npm: package updates" || true
-          git push

From 52d83dbf35b162af974f521fb2acb0da4b640604 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:29:05 +0200
Subject: [PATCH 37/39] build(deps): bump debug from 4.3.6 to 4.3.7 in /src
 (#1365)

Bumps [debug](https://github.com/debug-js/debug) from 4.3.6 to 4.3.7.
- [Release notes](https://github.com/debug-js/debug/releases)
- [Commits](https://github.com/debug-js/debug/compare/4.3.6...4.3.7)

---
updated-dependencies:
- dependency-name: debug
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 src/package-lock.json | 18 ++++++++----------
 src/package.json      |  2 +-
 2 files changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/package-lock.json b/src/package-lock.json
index 75f4fcd..f50190a 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -12,7 +12,7 @@
         "basic-auth": "^2.0.1",
         "bcryptjs": "^2.4.3",
         "crc-32": "^1.2.2",
-        "debug": "^4.3.6",
+        "debug": "^4.3.7",
         "express-session": "^1.18.0",
         "h3": "^1.12.0",
         "qrcode": "^1.5.4"
@@ -1353,12 +1353,11 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.6",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
-      "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
-      "license": "MIT",
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -3360,10 +3359,9 @@
       }
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "license": "MIT"
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
     },
     "node_modules/mz": {
       "version": "2.7.0",
diff --git a/src/package.json b/src/package.json
index b29cb5c..618bdbb 100644
--- a/src/package.json
+++ b/src/package.json
@@ -18,7 +18,7 @@
     "basic-auth": "^2.0.1",
     "bcryptjs": "^2.4.3",
     "crc-32": "^1.2.2",
-    "debug": "^4.3.6",
+    "debug": "^4.3.7",
     "express-session": "^1.18.0",
     "h3": "^1.12.0",
     "qrcode": "^1.5.4"

From 942f35916cfbcc902a5371ba066892b0c26c51a3 Mon Sep 17 00:00:00 2001
From: "Philip H." <47042125+pheiduck@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:35:03 +0200
Subject: [PATCH 38/39] deploy-nightly.yml: reference to master branch

---
 .github/workflows/deploy-nightly.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/deploy-nightly.yml b/.github/workflows/deploy-nightly.yml
index eac5a29..906d407 100644
--- a/.github/workflows/deploy-nightly.yml
+++ b/.github/workflows/deploy-nightly.yml
@@ -16,7 +16,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
       with:
-        ref: production
+        ref: master
 
     - name: Set up QEMU
       uses: docker/setup-qemu-action@v3

From 067b7bcf8451022d2607a3ee8b90e3da1265732f Mon Sep 17 00:00:00 2001
From: Philip H <47042125+pheiduck@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:39:34 +0200
Subject: [PATCH 39/39] CI: fixup dev deploy too

Signed-off-by: Philip H <47042125+pheiduck@users.noreply.github.com>
---
 .github/workflows/deploy-development.yml | 2 +-
 .github/workflows/deploy-pr.yml          | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml
index da26f88..2f8f1e6 100644
--- a/.github/workflows/deploy-development.yml
+++ b/.github/workflows/deploy-development.yml
@@ -14,7 +14,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
       with:
-        ref: production
+        ref: master
 
     - name: Set up QEMU
       uses: docker/setup-qemu-action@v3
diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml
index 8acd5e6..d32d818 100644
--- a/.github/workflows/deploy-pr.yml
+++ b/.github/workflows/deploy-pr.yml
@@ -15,7 +15,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
       with:
-        ref: production
+        ref: master
 
     - name: Set up QEMU
       uses: docker/setup-qemu-action@v3