Compare commits

...

208 Commits

Author SHA1 Message Date
daanselen 56ac97b15c Merge remote-tracking branch 'origin/dependabot/docker/docker/development/alpine-3.24' into development 2026-07-08 23:34:46 +02:00
daanselen 21570cb27f Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vite-8.0.16' into development 2026-07-08 23:32:52 +02:00
daanselen 0cf3ec2965 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/dayjs-1.11.21' into development 2026-07-08 23:32:45 +02:00
daanselen 3c493be351 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/axios-1.18.0' into development 2026-07-08 23:32:41 +02:00
daanselen 1128c332ae Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vuepic/vue-datepicker-14.0.0' into development 2026-07-08 23:32:25 +02:00
daanselen 65645d6775 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vite-8.0.16' into development 2026-07-08 23:32:02 +02:00
daanselen 46d15959b1 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/cidr-tools-12.0.2' into development 2026-07-08 23:31:56 +02:00
daanselen 8e9ab60ce7 Merge remote-tracking branch 'origin/dependabot/pip/src/development/sqlalchemy-2.0.51' into development 2026-07-08 23:31:36 +02:00
daanselen ca8f47b73e Merge remote-tracking branch 'origin/dependabot/pip/src/development/requests-2.34.2' into development 2026-07-08 23:31:32 +02:00
daanselen c098aea151 Merge remote-tracking branch 'origin/dependabot/pip/src/development/pyotp-2.10.0' into development 2026-07-08 23:31:29 +02:00
daanselen 68be16bf81 Merge remote-tracking branch 'origin/dependabot/pip/src/development/pymysql-1.2.0' into development 2026-07-08 23:31:25 +02:00
daanselen 9d88a602cd Merge remote-tracking branch 'origin/dependabot/pip/src/development/flask-cors-6.0.5' into development 2026-07-08 23:31:18 +02:00
Donald Zou 9e8ca0e630 Updated OIDC Algorithm Fallback 2026-07-01 16:36:17 +08:00
Donald Zou 0343e8f915 Added OIDC for admin side 2026-06-26 20:22:50 +08:00
dependabot[bot] 2a5043424e build(deps): bump axios from 1.16.0 to 1.18.0 in /src/static/client
Bumps [axios](https://github.com/axios/axios) from 1.16.0 to 1.18.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.18.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 20:36:19 +00:00
dependabot[bot] 7df99a12d0 build(deps): bump sqlalchemy from 2.0.49 to 2.0.51 in /src
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.49 to 2.0.51.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 20:34:48 +00:00
dependabot[bot] 59c88853de build(deps): bump alpine from 3.23 to 3.24 in /docker
Bumps alpine from 3.23 to 3.24.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.24'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 20:34:40 +00:00
dependabot[bot] 6574b216fb build(deps): bump pyotp from 2.9.0 to 2.10.0 in /src
Bumps [pyotp](https://github.com/pyauth/pyotp) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/pyauth/pyotp/releases)
- [Changelog](https://github.com/pyauth/pyotp/blob/main/Changes.rst)
- [Commits](https://github.com/pyauth/pyotp/compare/v2.9.0...v2.10.0)

---
updated-dependencies:
- dependency-name: pyotp
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 20:34:34 +00:00
dependabot[bot] 6da668616b build(deps): bump dayjs from 1.11.20 to 1.11.21 in /src/static/client
Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.11.20 to 1.11.21.
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.20...v1.11.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:36:06 +00:00
dependabot[bot] 9a1ac86ee6 build(deps-dev): bump vite from 8.0.12 to 8.0.16 in /src/static/client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.12 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:35:58 +00:00
dependabot[bot] dae13faf25 build(deps): bump cidr-tools from 11.3.5 to 12.0.2 in /src/static/app
Bumps [cidr-tools](https://github.com/silverwind/cidr-tools) from 11.3.5 to 12.0.2.
- [Release notes](https://github.com/silverwind/cidr-tools/releases)
- [Commits](https://github.com/silverwind/cidr-tools/compare/11.3.5...12.0.2)

---
updated-dependencies:
- dependency-name: cidr-tools
  dependency-version: 12.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:35:17 +00:00
dependabot[bot] cdd7901b7d build(deps-dev): bump vite from 8.0.10 to 8.0.16 in /src/static/app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.10 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:35:01 +00:00
dependabot[bot] be2ac265bb build(deps): bump pymysql from 1.1.3 to 1.2.0 in /src
Bumps [pymysql](https://github.com/PyMySQL/PyMySQL) from 1.1.3 to 1.2.0.
- [Release notes](https://github.com/PyMySQL/PyMySQL/releases)
- [Changelog](https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyMySQL/PyMySQL/compare/v1.1.3...v1.2.0)

---
updated-dependencies:
- dependency-name: pymysql
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:34:46 +00:00
dependabot[bot] 3cdeb4c5b5 build(deps): bump flask-cors from 6.0.2 to 6.0.5 in /src
Bumps [flask-cors](https://github.com/corydolphin/flask-cors) from 6.0.2 to 6.0.5.
- [Release notes](https://github.com/corydolphin/flask-cors/releases)
- [Changelog](https://github.com/corydolphin/flask-cors/blob/main/CHANGELOG.md)
- [Commits](https://github.com/corydolphin/flask-cors/compare/6.0.2...6.0.5)

---
updated-dependencies:
- dependency-name: flask-cors
  dependency-version: 6.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:34:42 +00:00
dependabot[bot] 4d15555e7d build(deps): bump @vuepic/vue-datepicker in /src/static/app
Bumps [@vuepic/vue-datepicker](https://github.com/Vuepic/vue-datepicker) from 12.1.0 to 14.0.0.
- [Release notes](https://github.com/Vuepic/vue-datepicker/releases)
- [Changelog](https://github.com/Vuepic/vue-datepicker/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Vuepic/vue-datepicker/compare/v12.1.0...v14.0.0)

---
updated-dependencies:
- dependency-name: "@vuepic/vue-datepicker"
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:34:37 +00:00
dependabot[bot] 769f96fdc7 build(deps): bump requests from 2.34.0 to 2.34.2 in /src
Bumps [requests](https://github.com/psf/requests) from 2.34.0 to 2.34.2.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.34.0...v2.34.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 20:34:25 +00:00
dependabot[bot] 4f99096d80 build(deps): bump vue-router from 4.5.1 to 5.1.0 in /src/static/client
Bumps [vue-router](https://github.com/vuejs/router) from 4.5.1 to 5.1.0.
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v4.5.1...v5.1.0)

---
updated-dependencies:
- dependency-name: vue-router
  dependency-version: 5.0.6
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 17:18:22 +00:00
daanselen 3bc88a3006 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vue-3.5.34' into development 2026-06-05 19:01:43 +02:00
daanselen 9982841993 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vite-plugin-vue-devtools-8.1.2' into development 2026-06-05 19:01:25 +02:00
daanselen 4bff8e1d4d Merge remote-tracking branch 'origin/dependabot/pip/src/development/requests-2.34.0' into development 2026-06-05 19:01:04 +02:00
dependabot[bot] d837012d86 build(deps-dev): bump vite-plugin-vue-devtools in /src/static/client
Bumps [vite-plugin-vue-devtools](https://github.com/vuejs/devtools/tree/HEAD/packages/vite) from 8.1.1 to 8.1.2.
- [Release notes](https://github.com/vuejs/devtools/releases)
- [Commits](https://github.com/vuejs/devtools/commits/v8.1.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite-plugin-vue-devtools
  dependency-version: 8.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 17:00:49 +00:00
dependabot[bot] 96dd08e6c7 build(deps): bump vue from 3.5.32 to 3.5.35 in /src/static/client
Bumps [vue](https://github.com/vuejs/core) from 3.5.32 to 3.5.35.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.32...v3.5.35)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 17:00:41 +00:00
daanselen b524fcd88b Merge remote-tracking branch 'origin/dependabot/pip/src/development/pymysql-1.1.3' into development 2026-06-05 19:00:36 +02:00
daanselen 1658b73724 Merge remote-tracking branch 'origin/dependabot/pip/src/development/pydantic-2.13.4' into development 2026-06-05 19:00:11 +02:00
daanselen 1f28539bff Merge remote-tracking branch 'origin/dependabot/pip/src/development/psycopg-3.3.4' into development 2026-06-05 19:00:02 +02:00
daanselen e9fb32d9a8 Merge remote-tracking branch 'origin/dependabot/pip/src/development/gunicorn-26.0.0' into development 2026-06-05 18:59:56 +02:00
daanselen bd91b3c97a fix: port readme correct to development 2026-06-05 18:59:09 +02:00
daanselen dfcd80de5b Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vite-8.0.12' into development 2026-06-05 18:58:02 +02:00
daanselen 85cbf498c0 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/axios-1.16.0' into development 2026-06-05 18:57:51 +02:00
daanselen ee6586e50d Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vueuse/shared-14.3.0' into development 2026-06-05 18:57:39 +02:00
daanselen 4c59f81c85 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vue-router-5.0.6' into development 2026-06-05 18:57:33 +02:00
daanselen 1db476e1b9 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vite-8.0.10' into development 2026-06-05 18:57:06 +02:00
daanselen 7cb845a65d Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/npm-11.13.0' into development 2026-06-05 18:56:58 +02:00
daanselen 3da0039004 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/cidr-tools-11.3.5' into development 2026-06-05 18:56:54 +02:00
Truffle 3d966b928f Fix Python 3.11 SyntaxError in AmneziaPeer.py (#1290)
Python 3.11 raises 'f-string expression part cannot include a backslash'
on line 91, where strip('\n') sits inside an f-string {...}. PEP 701
lifted this restriction in 3.12.

Bind the decoded output to a local so the strip call happens outside
the f-string, which also removes the duplicate decode+strip already
performed on the preceding length check.

Closes #1289

Signed-off-by: Truffle <truffleagent@gmail.com>
2026-06-05 18:54:47 +02:00
Max 58fa826862 Update README.md for Docker (#1292) 2026-06-05 18:54:35 +02:00
dependabot[bot] 2be7e08b7c build(deps-dev): bump vite from 8.0.8 to 8.0.12 in /src/static/client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.8 to 8.0.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:09:38 +00:00
dependabot[bot] aa8bfe179a build(deps): bump pydantic from 2.13.3 to 2.13.4 in /src
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.3 to 2.13.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.13.4/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.13.3...v2.13.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:06:07 +00:00
dependabot[bot] 8207c1209c build(deps): bump gunicorn from 25.0.3 to 26.0.0 in /src
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 25.0.3 to 26.0.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/25.0.3...26.0.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 26.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:06:01 +00:00
dependabot[bot] 20a6cfdec3 build(deps): bump requests from 2.32.5 to 2.34.0 in /src
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.34.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.34.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 03:05:57 +00:00
Donald Zou 71077880ad Merge pull request #1273 from sgtdeagle/name-attachment-file-after-configname
change email attachmentname to peerName if not empty
2026-05-10 13:34:52 +08:00
Donald Zou d51889a222 Merge pull request #1276 from WGDashboard/#1257-fix
#1257 fix
2026-05-10 13:33:05 +08:00
Donald Zou ece7cd1896 Use network_address for single-address IPs
When parsing allowed_ip entries in API_ping_getAllPeersIpAddress(), detect networks with a single address (ip.num_addresses == 1) and append ip.network_address directly to the parsed list. This replaces the previous host-list construction (list(ip.hosts()) / host[0]) to simplify logic and avoid iterating hosts for /32 or single-address ranges while keeping existing error logging for invalid addresses.
2026-05-10 13:31:58 +08:00
Donald Zou 4d20d00631 Merge pull request #1258 from SnedS91/fix-totp-verification
Fix TOTP verification with valid window
2026-05-10 13:13:59 +08:00
sgtdeagle 6ec8d2f201 change email attachmentname to peerName if not empty 2026-05-07 14:27:31 +00:00
dependabot[bot] 195ef37928 build(deps): bump axios from 1.15.1 to 1.16.0 in /src/static/client
Bumps [axios](https://github.com/axios/axios) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.1...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:30:05 +00:00
dependabot[bot] b16f0aa3c2 build(deps): bump cidr-tools from 11.3.3 to 11.3.5 in /src/static/app
Bumps [cidr-tools](https://github.com/silverwind/cidr-tools) from 11.3.3 to 11.3.5.
- [Release notes](https://github.com/silverwind/cidr-tools/releases)
- [Commits](https://github.com/silverwind/cidr-tools/compare/11.3.3...11.3.5)

---
updated-dependencies:
- dependency-name: cidr-tools
  dependency-version: 11.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:28:16 +00:00
dependabot[bot] dc26b13ea6 build(deps-dev): bump vite from 8.0.9 to 8.0.10 in /src/static/app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.9 to 8.0.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:28:10 +00:00
dependabot[bot] b66a619e81 build(deps): bump vue-router from 5.0.4 to 5.0.6 in /src/static/app
Bumps [vue-router](https://github.com/vuejs/router) from 5.0.4 to 5.0.6.
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v5.0.4...v5.0.6)

---
updated-dependencies:
- dependency-name: vue-router
  dependency-version: 5.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:27:56 +00:00
dependabot[bot] dd93cbcb42 build(deps): bump @vueuse/shared in /src/static/app
Bumps [@vueuse/shared](https://github.com/vueuse/vueuse/tree/HEAD/packages/shared) from 14.2.1 to 14.3.0.
- [Release notes](https://github.com/vueuse/vueuse/releases)
- [Commits](https://github.com/vueuse/vueuse/commits/v14.3.0/packages/shared)

---
updated-dependencies:
- dependency-name: "@vueuse/shared"
  dependency-version: 14.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:27:48 +00:00
dependabot[bot] a5be721f39 build(deps): bump npm from 11.12.1 to 11.13.0 in /src/static/app
Bumps [npm](https://github.com/npm/cli) from 11.12.1 to 11.13.0.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/v11.13.0/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.12.1...v11.13.0)

---
updated-dependencies:
- dependency-name: npm
  dependency-version: 11.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:27:41 +00:00
dependabot[bot] 40c0a91afa build(deps): bump psycopg from 3.3.3 to 3.3.4 in /src
Bumps [psycopg](https://github.com/psycopg/psycopg) from 3.3.3 to 3.3.4.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.3...3.3.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:27:33 +00:00
dependabot[bot] 35e1a3af25 build(deps): bump pymysql from 1.1.2 to 1.1.3 in /src
Bumps [pymysql](https://github.com/PyMySQL/PyMySQL) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/PyMySQL/PyMySQL/releases)
- [Changelog](https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyMySQL/PyMySQL/compare/v1.1.2...v1.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 00:27:29 +00:00
sneds91 91de807557 Fix TOTP verification with valid window 2026-05-02 10:54:21 +00:00
dependabot[bot] 9d5478e3e3 build(deps): bump bootstrap from 5.3.6 to 5.3.8 in /src/static/client
Bumps [bootstrap](https://github.com/twbs/bootstrap) from 5.3.6 to 5.3.8.
- [Release notes](https://github.com/twbs/bootstrap/releases)
- [Commits](https://github.com/twbs/bootstrap/compare/v5.3.6...v5.3.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-30 14:28:48 +00:00
daanselen b07be518fb Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vue/language-server-3.2.7' into development 2026-04-30 16:27:26 +02:00
daanselen 35be7ca3db Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vitejs/plugin-vue-6.0.6' into development 2026-04-30 16:27:21 +02:00
dependabot[bot] 36521a7d70 build(deps-dev): bump @vitejs/plugin-vue in /src/static/app
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 6.0.4 to 6.0.6.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.6/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-version: 6.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-30 14:27:07 +00:00
daanselen 3520087dce Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vitejs/plugin-vue-6.0.6' into development 2026-04-30 16:26:54 +02:00
daanselen d354e8ad38 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/uuid-14.0.0' into development 2026-04-30 16:26:50 +02:00
daanselen 4be7cd4073 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/pinia-3.0.4' into development 2026-04-30 16:26:46 +02:00
daanselen b04be970e6 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/axios-1.15.1' into development 2026-04-30 16:26:33 +02:00
daanselen de81384f88 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vite-8.0.9' into development 2026-04-30 16:25:31 +02:00
daanselen 071a4765b9 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/uuid-14.0.0' into development 2026-04-30 16:25:23 +02:00
daanselen fbaa15cb8d Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/ol-10.9.0' into development 2026-04-30 16:25:18 +02:00
daanselen c9abfa5f61 Merge remote-tracking branch 'origin/dependabot/pip/src/development/pydantic-2.13.3' into development 2026-04-30 16:24:42 +02:00
freetushkan d3d7c7e678 Fix config info loss when renaming a tunnel. (#1254)
This migrates data to the new tunnel when renaming, such as: Description, OverridePeerSettings, PeerGroups, PeerHistoricalEndpointTracking, PeerTrafficTracking.
2026-04-30 16:23:47 +02:00
freetushkan 930c4169c5 A patch to support a wide range of AmneziaWG versions. (#1210)
Support for all keys ever used in AmneziaWG has been added. Setting an empty value to a key will prevent it from being used or rendered in the configuration. This ensures flexible support for all versions and specific configurations.
2026-04-30 16:23:26 +02:00
Giuseppe Marinelli 90614a6360 Refactor peer schedule job API to use proper REST verbs (#1239)
* Fixed quotation marks

* Update wgd.sh

* Refactor peer schedule job API to use proper REST verbs

Replace single POST endpoints with POST/PUT/DELETE for peer schedule jobs.
Add require_fields decorator for request validation. Add fetchPut and
fetchDelete helpers in the frontend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add local development with Docker section to docker/README.md

Explains how to mount src/ as a volume for live code editing
using a compose-local.yaml setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update README dev section with frontend build instructions

Replace Vite dev server tip with npm install/build workflow
and docker restart step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Donald Zou <donaldzou@live.hk>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 20:30:19 +02:00
dependabot[bot] ef8459415b build(deps): bump pinia from 3.0.2 to 3.0.4 in /src/static/client
Bumps [pinia](https://github.com/vuejs/pinia) from 3.0.2 to 3.0.4.
- [Release notes](https://github.com/vuejs/pinia/releases)
- [Commits](https://github.com/vuejs/pinia/compare/v3.0.2...v3.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:29:04 +00:00
dependabot[bot] 365f07da0b build(deps): bump uuid from 13.0.0 to 14.0.0 in /src/static/client
Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:28:56 +00:00
dependabot[bot] 8d00cca321 build(deps): bump axios from 1.14.0 to 1.15.1 in /src/static/client
Bumps [axios](https://github.com/axios/axios) from 1.14.0 to 1.15.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.14.0...v1.15.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:28:49 +00:00
dependabot[bot] 1fffa33dd5 build(deps-dev): bump @vitejs/plugin-vue in /src/static/client
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 5.2.4 to 6.0.6.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.6/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-version: 6.0.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:28:42 +00:00
dependabot[bot] ff34e4f8c8 build(deps): bump ol from 10.8.0 to 10.9.0 in /src/static/app
Bumps [ol](https://github.com/openlayers/openlayers) from 10.8.0 to 10.9.0.
- [Release notes](https://github.com/openlayers/openlayers/releases)
- [Commits](https://github.com/openlayers/openlayers/compare/v10.8.0...v10.9.0)

---
updated-dependencies:
- dependency-name: ol
  dependency-version: 10.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:27:04 +00:00
dependabot[bot] af385a4537 build(deps): bump @vue/language-server in /src/static/app
Bumps [@vue/language-server](https://github.com/vuejs/language-tools/tree/HEAD/packages/language-server) from 3.2.4 to 3.2.7.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.2.7/packages/language-server)

---
updated-dependencies:
- dependency-name: "@vue/language-server"
  dependency-version: 3.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:26:38 +00:00
dependabot[bot] e2ff34ce1d build(deps-dev): bump vite from 8.0.8 to 8.0.9 in /src/static/app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.8 to 8.0.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:26:28 +00:00
dependabot[bot] 2ac9fea881 build(deps): bump uuid from 13.0.0 to 14.0.0 in /src/static/app
Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:26:15 +00:00
dependabot[bot] 9e59ad0ebc build(deps): bump pydantic from 2.13.0 to 2.13.3 in /src
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.0 to 2.13.3.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.13.0...v2.13.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 22:26:14 +00:00
Tom Mills 18c2568c22 Add preshared key default toggle (#1207)
* Add previous PR changes

* Import lowercase dayjs

* Build dist

* Revert "Build dist"

This reverts commit 152da2ca10
Dist not to be built at @DaanSelen request
2026-04-20 10:00:35 +02:00
daanselen d58e082336 feat: commit PR 1237 to development 2026-04-16 20:57:23 +02:00
Mikhail Solovev fedf7db8a4 Quote table and column identifiers using SQLAlchemy dialect preparer when adding missing columns to avoid SQL injection and syntax errors. (#1237) 2026-04-16 20:56:16 +02:00
daanselen 523fee7b42 Merge remote-tracking branch 'origin/dependabot/pip/src/development/sqlalchemy-2.0.49' into development 2026-04-16 20:54:43 +02:00
daanselen 624206d5a7 Merge remote-tracking branch 'origin/dependabot/pip/src/development/pydantic-2.13.0' into development 2026-04-16 20:54:40 +02:00
daanselen fa9d9f24b5 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vue-3.5.32' into development 2026-04-16 20:54:15 +02:00
daanselen c42bc47761 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vite-plugin-vue-devtools-8.1.1' into development 2026-04-16 20:54:10 +02:00
dependabot[bot] 24e0f514d7 build(deps): bump sqlalchemy from 2.0.46 to 2.0.49 in /src
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.49.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 20:53:49 +02:00
dependabot[bot] 1744570086 build(deps): bump pydantic from 2.12.5 to 2.13.0 in /src
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.5 to 2.13.0.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.12.5...v2.13.0)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 20:53:49 +02:00
dependabot[bot] 9669782b0c build(deps): bump vue from 3.5.16 to 3.5.32 in /src/static/client
Bumps [vue](https://github.com/vuejs/core) from 3.5.16 to 3.5.32.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.16...v3.5.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 20:53:49 +02:00
dependabot[bot] 03eddd2846 build(deps-dev): bump vite-plugin-vue-devtools in /src/static/client
Bumps [vite-plugin-vue-devtools](https://github.com/vuejs/devtools/tree/HEAD/packages/vite) from 7.7.6 to 8.1.1.
- [Release notes](https://github.com/vuejs/devtools/releases)
- [Commits](https://github.com/vuejs/devtools/commits/v8.1.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite-plugin-vue-devtools
  dependency-version: 8.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 20:53:49 +02:00
dependabot[bot] 26b82745fc build(deps-dev): bump vite-plugin-vue-devtools in /src/static/client
Bumps [vite-plugin-vue-devtools](https://github.com/vuejs/devtools/tree/HEAD/packages/vite) from 7.7.6 to 8.1.1.
- [Release notes](https://github.com/vuejs/devtools/releases)
- [Commits](https://github.com/vuejs/devtools/commits/v8.1.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite-plugin-vue-devtools
  dependency-version: 8.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 18:53:31 +00:00
neolegshishov abc82369d9 fix: misspell (#1231) 2026-04-16 20:52:55 +02:00
dependabot[bot] f1764d3822 build(deps): bump vue from 3.5.16 to 3.5.32 in /src/static/client
Bumps [vue](https://github.com/vuejs/core) from 3.5.16 to 3.5.32.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.16...v3.5.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 18:52:46 +00:00
daanselen 8fd15b9ca6 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/vite-8.0.8' into development 2026-04-16 20:51:01 +02:00
daanselen 5f62e9ac69 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/uuid-13.0.0' into development 2026-04-16 20:50:54 +02:00
daanselen 8eb7e97d6e Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/client/development/oidc-client-ts-3.5.0' into development 2026-04-16 20:50:49 +02:00
daanselen 3b03099700 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vue-3.5.32' into development 2026-04-16 20:50:14 +02:00
daanselen 8d6eb4bb41 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vite-8.0.8' into development 2026-04-16 20:50:09 +02:00
daanselen 387adfee8c Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/ol-10.8.0' into development 2026-04-16 20:50:03 +02:00
daanselen a33d6bfc9a Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/fuse.js-7.3.0' into development 2026-04-16 20:49:57 +02:00
dependabot[bot] 5726627058 build(deps): bump pydantic from 2.12.5 to 2.13.0 in /src
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.5 to 2.13.0.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.12.5...v2.13.0)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 21:28:46 +00:00
Donald Zou cdd85b659c Merge pull request #1227 from WGDashboard/v4.3.3-quick-fix
Update wgd.sh
2026-04-10 16:27:04 +08:00
Donald Zou 42f9460369 Update wgd.sh 2026-04-10 16:18:12 +08:00
Donald Zou ba11a7a355 Merge pull request #1226 from WGDashboard/v4.3.3-quick-fix
Fixed quotation marks
2026-04-10 15:50:10 +08:00
Donald Zou 71f4449741 Fixed quotation marks 2026-04-10 15:45:26 +08:00
dependabot[bot] 14b5885814 build(deps): bump oidc-client-ts in /src/static/client
Bumps [oidc-client-ts](https://github.com/authts/oidc-client-ts) from 3.2.1 to 3.5.0.
- [Release notes](https://github.com/authts/oidc-client-ts/releases)
- [Commits](https://github.com/authts/oidc-client-ts/compare/v3.2.1...v3.5.0)

---
updated-dependencies:
- dependency-name: oidc-client-ts
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 06:51:57 +00:00
dependabot[bot] 3d8cdcaf84 build(deps): bump uuid from 11.1.0 to 13.0.0 in /src/static/client
Bumps [uuid](https://github.com/uuidjs/uuid) from 11.1.0 to 13.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.1.0...v13.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 06:51:38 +00:00
dependabot[bot] f1448b0e52 build(deps): bump cidr-tools from 11.3.2 to 11.3.3 in /src/static/app
Bumps [cidr-tools](https://github.com/silverwind/cidr-tools) from 11.3.2 to 11.3.3.
- [Release notes](https://github.com/silverwind/cidr-tools/releases)
- [Commits](https://github.com/silverwind/cidr-tools/compare/11.3.2...11.3.3)

---
updated-dependencies:
- dependency-name: cidr-tools
  dependency-version: 11.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 06:51:35 +00:00
dependabot[bot] 6e92b2b6a2 build(deps-dev): bump vite from 6.4.1 to 8.0.8 in /src/static/client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 8.0.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 06:51:24 +00:00
dependabot[bot] 18ee8822d9 build(deps-dev): bump vite from 7.3.1 to 8.0.8 in /src/static/app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 8.0.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 06:51:20 +00:00
Donald Zou 081c63cd43 Merge pull request #1197 from WGDashboard/development
v4.3.3 Merge
2026-04-10 14:50:10 +08:00
dependabot[bot] 326628d258 build(deps): bump fuse.js from 7.1.0 to 7.3.0 in /src/static/app
Bumps [fuse.js](https://github.com/krisk/Fuse) from 7.1.0 to 7.3.0.
- [Release notes](https://github.com/krisk/Fuse/releases)
- [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md)
- [Commits](https://github.com/krisk/Fuse/compare/v7.1.0...v7.3.0)

---
updated-dependencies:
- dependency-name: fuse.js
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:38:34 +00:00
dependabot[bot] 566dda93f3 build(deps): bump vue from 3.5.31 to 3.5.32 in /src/static/app
Bumps [vue](https://github.com/vuejs/core) from 3.5.31 to 3.5.32.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.31...v3.5.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:38:04 +00:00
dependabot[bot] 5c28e639ae build(deps): bump ol from 10.7.0 to 10.8.0 in /src/static/app
Bumps [ol](https://github.com/openlayers/openlayers) from 10.7.0 to 10.8.0.
- [Release notes](https://github.com/openlayers/openlayers/releases)
- [Commits](https://github.com/openlayers/openlayers/compare/v10.7.0...v10.8.0)

---
updated-dependencies:
- dependency-name: ol
  dependency-version: 10.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:37:57 +00:00
dependabot[bot] 3dfe740780 build(deps): bump sqlalchemy from 2.0.46 to 2.0.49 in /src
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.49.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:37:52 +00:00
Donald Zou ebd538e2d8 Merge branch 'main' into development 2026-04-03 17:39:46 +08:00
Donald Zou b36b0469e1 Build Admin and Clients 2026-04-03 17:37:21 +08:00
Donald Zou 2b9a39f124 Added /json to whitelist, and fixed Email.is_ready() 2026-04-03 17:36:30 +08:00
Donald Zou cd709c2807 Updated Peers Notes element 2026-04-03 17:00:52 +08:00
DaanSelen 6bc54e5d65 chore: also trigger on development branch (#1204)
* chore: also trigger on development branch

* chore: remove old debug workflow

---------

Co-authored-by: DaanSelen <dselen@systemec.nl>
2026-04-02 12:03:47 +02:00
DaanSelen 65c2e69ac6 chore: correct per review comment 2026-04-02 11:34:50 +02:00
Donald Zou 96acc10153 Merge branch 'development' of https://github.com/WGDashboard/WGDashboard into development 2026-04-02 17:34:18 +08:00
Donald Zou 1a667a60c1 Updated UpdatePeer logic 2026-04-02 17:34:02 +08:00
DaanSelen bbc85d6a2d Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vueuse/core-14.2.1' into development 2026-04-02 11:12:38 +02:00
DaanSelen 3995a5db47 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vue-router-5.0.4' into development 2026-04-02 11:12:31 +02:00
DaanSelen b8750860a8 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/development/vue-3.5.31' into development 2026-04-02 11:12:26 +02:00
dependabot[bot] 4711c750cc build(deps): bump cidr-tools from 11.0.8 to 11.3.2 in /src/static/app
Bumps [cidr-tools](https://github.com/silverwind/cidr-tools) from 11.0.8 to 11.3.2.
- [Release notes](https://github.com/silverwind/cidr-tools/releases)
- [Commits](https://github.com/silverwind/cidr-tools/compare/11.0.8...11.3.2)

---
updated-dependencies:
- dependency-name: cidr-tools
  dependency-version: 11.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 09:07:28 +00:00
dependabot[bot] 51bee91c32 build(deps): bump vue from 3.5.28 to 3.5.31 in /src/static/app
Bumps [vue](https://github.com/vuejs/core) from 3.5.28 to 3.5.31.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.28...v3.5.31)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 09:07:17 +00:00
dependabot[bot] 5fcfe97fab build(deps): bump @vueuse/core from 14.2.0 to 14.2.1 in /src/static/app
Bumps [@vueuse/core](https://github.com/vueuse/vueuse/tree/HEAD/packages/core) from 14.2.0 to 14.2.1.
- [Release notes](https://github.com/vueuse/vueuse/releases)
- [Commits](https://github.com/vueuse/vueuse/commits/v14.2.1/packages/core)

---
updated-dependencies:
- dependency-name: "@vueuse/core"
  dependency-version: 14.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 09:07:03 +00:00
dependabot[bot] 74bee75df3 build(deps): bump vue-router from 5.0.2 to 5.0.4 in /src/static/app
Bumps [vue-router](https://github.com/vuejs/router) from 5.0.2 to 5.0.4.
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v5.0.2...v5.0.4)

---
updated-dependencies:
- dependency-name: vue-router
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 09:06:54 +00:00
DaanSelen 1aa1b8a51b chore: make dependabot target the dev branch (#1198)
Co-authored-by: DaanSelen <dselen@systemec.nl>
2026-04-02 11:06:00 +02:00
Donald Zou 833ee88340 Build Admin and Client 2026-04-02 16:35:00 +08:00
daanselen 6cb4bac673 Merge remote-tracking branch 'origin/dependabot/pip/src/psycopg-binary--3.3.3' into v4.3.3-dev 2026-04-02 16:30:11 +08:00
daanselen e87c52b6f2 Merge remote-tracking branch 'origin/dependabot/docker/docker/v4.3.3-dev/golang-1.26-alpine3.23' into v4.3.3-dev 2026-04-02 16:29:51 +08:00
Ivan Prokudin f53da6eacb Updated resolvconf call in entrypoint.sh to fix dns inside container (#1081)
* removed resolvconf call from entrypoint.sh to fix dns inside container

* Revert "removed resolvconf call from entrypoint.sh to fix dns inside container"

This reverts commit 428908ff84.

* Added resolvconf -a to save initial DNS configuration
2026-04-02 16:27:57 +08:00
Donald Zou 8d35887950 Dependabot: set target branch to development
Configure Dependabot to open updates against the development branch. Added target-branch: "development" for pip (/src), npm (/src/static/app), github-actions (/.github), docker (/docker) and docker-compose (/docker). Also added a new npm updates entry for /src/static/client. Update schedules remain weekly.
2026-04-02 16:25:32 +08:00
daanselen ebf37c879d fix: white screen in development image 2026-04-02 16:15:52 +08:00
Donald Zou 21bea56426 Merging main branch into v4.3.2-dev 2026-03-29 23:05:29 +08:00
Donald Zou 0a33132d26 Update README.md 2026-03-29 14:53:26 +08:00
daanselen f58226e398 chore: add the option for docker to not be dominant 2026-03-16 19:02:51 +01:00
Dan Hollis eeedf705aa fix: peer key validation regex (#1158)
* fix: peer key validation regex

* refactor: cache cleaned AllowedIPs from validation pass to avoid duplication

---------

Co-authored-by: Dan Hollis <dh@redteam.sh>
2026-03-03 09:04:30 +01:00
DaanSelen b9c271ff4c fix: regex invalid escape sequence 2026-03-02 10:38:16 +01:00
mahemium ba4ac7c1ec v4.3.1 bug fix (#1149)
* Fix bugs

* Fixes and improvements

* Update comment

* Reduce comment
2026-02-26 12:15:52 +01:00
freetushkan 06097316ec fix: incorrect backup regex
* More accurate backup regex.

Fixed an issue with picking up other backups when the names partially match.

* More accurate backup regex 2.
2026-02-24 10:23:01 +01:00
DaanSelen e71179a2d4 fix: error on startup regarding oidc config file being weird 2026-02-23 16:35:27 +01:00
daanselen bb18c55003 fix: Configuration creation bug due to failed Nonetype checking 2026-02-16 18:54:10 +01:00
Donald Zou 8e08d20497 Merge pull request #1130 from WGDashboard/night-refac
refac: some WGDashboard code
2026-02-14 00:05:21 +08:00
DaanSelen bb5d45a66d Merge remote-tracking branch 'origin/main' into v4.3.2-dev 2026-02-12 09:45:15 +01:00
daanselen f742f9de49 refac: some WGDashboard code 2026-02-07 03:03:35 +01:00
daanselen 686fc139ec Merge branch 'main' into v4.3.2-dev 2026-02-06 20:44:11 +01:00
daanselen a56297bf87 minor readme adjust 2026-02-06 20:24:42 +01:00
daanselen 80b1b8875d test: test if the trigger on the dev branch actually works 2026-02-06 20:20:57 +01:00
daanselen 8ca0363fd0 feat: working AWG 2.0 parameters 2026-02-06 20:18:32 +01:00
daanselen 07534b6f66 fix: 1116 2026-02-06 19:00:02 +01:00
Mikhail Solovev 82f170cd57 Fixed Bug. Unpredicted grow of cumulative data counters when using MySQL DB with 7 digits float precision. (#1125)
* build(deps): bump @vue/language-server in /src/static/app (#1063)

Bumps [@vue/language-server](https://github.com/vuejs/language-tools/tree/HEAD/packages/language-server) from 3.1.8 to 3.2.2.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.2.2/packages/language-server)

---
updated-dependencies:
- dependency-name: "@vue/language-server"
  dependency-version: 3.2.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump pinia from 3.0.3 to 3.0.4 in /src/static/app (#1065)

Bumps [pinia](https://github.com/vuejs/pinia) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/vuejs/pinia/releases)
- [Commits](https://github.com/vuejs/pinia/compare/v3.0.3...v3.0.4)

---
updated-dependencies:
- dependency-name: pinia
  dependency-version: 3.0.4
  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>

* build(deps-dev): bump @vitejs/plugin-vue in /src/static/app (#1067)

Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 6.0.1 to 6.0.3.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.3/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-version: 6.0.3
  dependency-type: direct:development
  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>

* build(deps): bump electron-builder in /src/static/app (#1064)

Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 26.0.12 to 26.4.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.4.0/packages/electron-builder)

---
updated-dependencies:
- dependency-name: electron-builder
  dependency-version: 26.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump @volar/language-server in /src/static/app (#1066)

Bumps [@volar/language-server](https://github.com/volarjs/volar.js/tree/HEAD/packages/language-server) from 2.4.26 to 2.4.27.
- [Release notes](https://github.com/volarjs/volar.js/releases)
- [Changelog](https://github.com/volarjs/volar.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/volarjs/volar.js/commits/v2.4.27/packages/language-server)

---
updated-dependencies:
- dependency-name: "@volar/language-server"
  dependency-version: 2.4.27
  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>

* chore: change docs

* docs: correctly escape pipes

* build(deps): bump npm from 11.6.4 to 11.8.0 in /src/static/app (#1104)

Bumps [npm](https://github.com/npm/cli) from 11.6.4 to 11.8.0.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.6.4...v11.8.0)

---
updated-dependencies:
- dependency-name: npm
  dependency-version: 11.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump gunicorn from 23.0.0 to 24.1.1 in /src (#1103)

Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 23.0.0 to 24.1.1.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/23.0.0...24.1.1)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 24.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump is-cidr from 6.0.1 to 6.0.2 in /src/static/app (#1102)

Bumps [is-cidr](https://github.com/silverwind/is-cidr) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/silverwind/is-cidr/releases)
- [Commits](https://github.com/silverwind/is-cidr/compare/6.0.1...6.0.2)

---
updated-dependencies:
- dependency-name: is-cidr
  dependency-version: 6.0.2
  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>

* build(deps): bump sqlalchemy from 2.0.45 to 2.0.46 in /src (#1101)

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.45 to 2.0.46.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: sqlalchemy
  dependency-version: 2.0.46
  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>

* Fix npm audit tar vulnerabilities (#1093)

* Harden npm deps via tar override

* chore: retrigger review

---------

Co-authored-by: Super User <root@vpn.northcaptiva.com>

* build(deps): bump @vue/language-server in /src/static/app (#1100)

Bumps [@vue/language-server](https://github.com/vuejs/language-tools/tree/HEAD/packages/language-server) from 3.2.2 to 3.2.4.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.2.4/packages/language-server)

---
updated-dependencies:
- dependency-name: "@vue/language-server"
  dependency-version: 3.2.4
  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>

* build(deps): bump vue from 3.5.26 to 3.5.27 in /src/static/app (#1087)

Bumps [vue](https://github.com/vuejs/core) from 3.5.26 to 3.5.27.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.26...v3.5.27)

---
updated-dependencies:
- dependency-name: vue
  dependency-version: 3.5.27
  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>

* build(deps-dev): bump vite from 7.3.0 to 7.3.1 in /src/static/app (#1082)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.0 to 7.3.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  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>

* build(deps): bump psutil from 7.2.1 to 7.2.2 in /src

Bumps [psutil](https://github.com/giampaolo/psutil) from 7.2.1 to 7.2.2.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-7.2.1...release-7.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump gunicorn from 24.1.1 to 25.0.1 in /src

Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 24.1.1 to 25.0.1.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/24.1.1...25.0.1)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 25.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Refine total_sent and total_receive with precision factor

Adjusted total_sent and total_receive calculations for accuracy.
The database engine may have lower floating-point precision than the Python engine. This can lead to situations where the current bytes count compares lower than the database data. This causes an increase in cumulative traffic each time such a precision collision occurs.

* Update WireguardConfiguration.py

It better make some fix in comparation only, and don't touch saved values.

* Update WireguardConfiguration.py

wrong symbol

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: DaanSelen <dselen@systemec.nl>
Co-authored-by: zombu4 <dom@northcaptiva.com>
Co-authored-by: Super User <root@vpn.northcaptiva.com>
2026-02-05 23:22:31 +01:00
Donald Zou 515bd0f806 Fixed #913 2026-02-02 16:13:27 +08:00
Donald Zou 27742d65c6 Update gunicorn.conf.py 2026-02-02 15:23:50 +08:00
DaanSelen ddf378d617 chore: remove dependency on vulnerability for scanning 2026-01-29 14:29:55 +01:00
DaanSelen 35c45b159b docs: adjust to the database support 2026-01-29 13:32:03 +01:00
DaanSelen 654bf6d779 chore: replace old values 2026-01-29 13:25:09 +01:00
DaanSelen fb66fd2dda feat: add support for database configuration 2026-01-29 13:23:42 +01:00
DaanSelen 44d274a998 chore: remove unneeded reference 2026-01-15 09:00:20 +01:00
daanselen b6e2f084e6 chore: add implicit TLS support for email 2026-01-14 23:45:42 +01:00
DaanSelen cb0619036f chore: add oidc-providers json file to the data volume location 2026-01-09 16:09:23 +01:00
DaanSelen 3a7e16ca9e chore: rename the email settings panel 2026-01-09 16:00:21 +01:00
Donald Zou 64506c9088 Merge pull request #1060 from WGDashboard/fix_domainnx_error_logoout
fix: domainnx error logoout
2026-01-05 06:35:27 +08:00
daanselen d2b05b77c8 chore: make the error handling a bit more graceful 2026-01-03 13:32:55 +01:00
DaanSelen 17e23685ec refac: Email.py module with bugfix (#1058)
* refac: refactor Email.py module for clarity

* chore: rework ready method for dynamic config

* chore: implement resolvconf from v4.3.2-dev
2026-01-03 13:00:23 +01:00
daanselen 9a7b23748d Merge remote-tracking branch 'origin/main' into v4.3.2-dev 2026-01-03 12:41:42 +01:00
daanselen 6239b8237a Merge branch 'amnezia-params' into v4.3.2-dev 2026-01-03 12:36:46 +01:00
DaanSelen dd6ffa694f chore: fallback echo defined nameserver
Update DNS configuration handling in entrypoint script.
2026-01-03 01:20:02 +01:00
DaanSelen 5486b3aaf6 fix: Docker autostart issue due to invalid resolvectl (#1053)
* Update funding information in FUNDING.yml

* Update README.md

* refac: refactor the logic with which the Docker container links its files to the volume

* fix: 1032 docker autostart issue

---------

Co-authored-by: Donald Zou <donaldzou@live.hk>
Co-authored-by: DaanSelen <dselen@systemec.nl>
2025-12-30 19:19:20 +01:00
Donald Zou c8967b449b Added checks and output to log wether configuration started properly or not 2025-12-30 21:55:37 +08:00
Donald Zou 72effeecf0 Update WireguardConfiguration.py 2025-12-30 21:47:33 +08:00
Donald Zou dcaf5eff32 Update WireguardConfiguration.py 2025-12-30 21:38:06 +08:00
daanselen 540a085821 chore: new files 2025-12-29 18:34:20 +01:00
daanselen 06cdb0b59e feat: added new AWG parameters 2025-12-29 17:56:35 +01:00
DaanSelen faf9e663b3 chore: trying to inplement the AWG parameters 2025-12-29 17:03:21 +01:00
Donald Zou a3058d2a28 Added toggle for signing up local clients 2025-12-28 17:02:14 +08:00
Donald Zou 0d70d13d0f Updated APP_PREFIX for Admin Dashboard 2025-12-26 10:59:24 +08:00
Donald Zou a0bbace7ee Update prefix issue 2025-12-25 16:49:32 +08:00
Donald Zou 53f7a32202 Merge pull request #1048 from leviofanh/app-prefix
Full support for the app-prefix parameter has been added to the frontend.
2025-12-25 15:41:46 +08:00
Donald Zou c35f06b262 Merge branch 'v4.3.2-dev' into app-prefix 2025-12-25 15:39:26 +08:00
Donald Zou 6de90410d0 Build 2025-12-19 09:23:38 +08:00
Donald Zou 87ff8afefe Add peer tracking toggle to WireGuard dashboard
Introduces a 'peer_tracking' configuration option for WireGuard in DashboardConfig and updates the dashboard background thread to respect this setting. The Vue component now includes a UI toggle for peer tracking, updates the server configuration via API, and conditionally loads tracking data based on the toggle state.
2025-12-19 09:22:45 +08:00
Donald Zou 9ae5fea7b4 Build 2025-12-19 07:04:21 +08:00
Donald Zou 40e2dddc00 Optimize peer tracking table counts API and UI
Enhanced the /api/getPeerTrackingTableCounts endpoint to support returning counts for all configurations when no name is provided. Updated frontend components to fetch all tracking data in a single request, improving efficiency and user experience. Bumped dashboard version to 4.3.2.
2025-12-19 07:04:07 +08:00
leviofanh a584bacdba Recompiled frontend to support new changes in the app-prefix mechanism (WGDasboardClient fix and vite dev server support) 2025-12-18 07:18:39 +01:00
leviofanh e6dcef2b01 App-prefix template transferred to javascript to support the vite dev server 2025-12-18 07:14:24 +01:00
leviofanh f58e572558 Fix: Fixed support for WGDashboardClient, now its static files are processed separately 2025-12-18 06:40:52 +01:00
leviofanh 721ac89420 Recompiled frontend for app-prefix support 2025-12-18 02:51:07 +01:00
leviofanh 9e9f07408d Added full support for the app-prefix parameter 2025-12-18 02:38:05 +01:00
Donald Zou b1d7199e84 Improve peer details access and update signin footer UI
Peer details button is now hidden for restricted peers, showing a message instead. The signin page footer layout is updated for better alignment, and the client app link is moved to the right with improved styling.
2025-12-16 17:51:45 +08:00
Donald Zou 0a64b1b377 Merge pull request #1044 from WGDashboard/client_app_button
feat: add client app button
2025-12-16 17:09:25 +08:00
Donald Zou bc20057de6 Merge pull request #1043 from WGDashboard/main
Merging main into v4.3.2-dev
2025-12-16 07:12:19 +08:00
Donald Zou c7c9d62e80 Update WireguardConfiguration.py 2025-12-15 21:02:22 +08:00
Donald Zou f5ccbdf9d3 Fixed Restricted Peer might still appear after restricted 2025-12-15 17:13:46 +08:00
Donald Zou a09342ed2a Added fallback when instance is completely disconnected from internet 2025-12-15 16:25:13 +08:00
daanselen 26171ee091 feat: add client app button 2025-12-14 16:56:24 +01:00
158 changed files with 4617 additions and 4201 deletions
+4
View File
@@ -3,3 +3,7 @@
*.md
tests/
docs/
src/db
src/wg-dashboard.ini
src/static/app
src/static/client
+11
View File
@@ -7,25 +7,36 @@ version: 2
updates:
- package-ecosystem: "pip"
directory: "/src"
target-branch: "development"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/src/static/app"
target-branch: "development"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/src/static/client"
target-branch: "development"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/.github"
target-branch: "development"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/docker"
target-branch: "development"
schedule:
interval: "weekly"
- package-ecosystem: "docker-compose"
directory: "/docker"
target-branch: "development"
schedule:
interval: "weekly"
-106
View File
@@ -1,106 +0,0 @@
name: Clone of Docker Build and Push
on:
workflow_dispatch:
env:
DOCKERHUB_PREFIX: docker.io
GITHUB_CONTAINER_PREFIX: ghcr.io
DOCKER_IMAGE: WGDashboard
jobs:
docker_build_debug:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_PREFIX }}
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_CONTAINER_PREFIX }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract Docker metadata from environment
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_PREFIX }}/donaldzou/${{ env.DOCKER_IMAGE }}
${{ env.GITHUB_CONTAINER_PREFIX }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=short,prefix=
- name: Build and export Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
docker_scan_debug:
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
needs: docker_build_debug
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_PREFIX }}
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_CONTAINER_PREFIX }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Scout CVEs
uses: docker/scout-action@v1
with:
command: cves
image: ${{ env.GITHUB_CONTAINER_PREFIX }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE }}:main
only-severities: critical,high
only-fixed: true
write-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }}
exit-code: true
- name: Docker Scout Compare
uses: docker/scout-action@v1
with:
command: compare
# Set to Github for maximum compat
image: ${{ env.GITHUB_CONTAINER_PREFIX }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE }}:main
to: ${{ env.GITHUB_CONTAINER_PREFIX }}/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE }}:latest
only-severities: critical,high
ignore-unchanged: true
github-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -5,6 +5,7 @@ on:
push:
branches:
- 'main'
- 'development'
- '**dev'
tags:
- '*'
@@ -103,7 +104,6 @@ jobs:
only-fixed: true
write-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }}
exit-code: true
- name: Docker Scout Compare
uses: docker/scout-action@v1
+3 -1
View File
@@ -1,8 +1,10 @@
> [!WARNING]
> All users running WGDashboard v4.2.x or later and hosted on the public internet are strongly advised to update to the latest release immediately. For more information: [v4.3.2 Release](https://github.com/WGDashboard/WGDashboard/releases/tag/v4.3.2)
> [!TIP]
> 🎉 To help us better understand and improve WGDashboards performance, were launching the **WGDashboard Testing Program**. As part of this program, participants will receive free WireGuard VPN access to our server in Toronto, Canada, valid for **24 hours** or up to **1GB of total traffic**—whichever comes first. If youd like to join, visit [https://wg.wgdashboard.dev/](https://wg.wgdashboard.dev/) for more details!
![](https://wgdashboard-resources.tor1.cdn.digitaloceanspaces.com/Posters/Banner.png)
+9 -6
View File
@@ -4,7 +4,7 @@
#
# Pull the current golang-alpine image.
FROM golang:1.25-alpine AS awg-go
FROM golang:1.26-alpine3.23 AS awg-go
# Install build-dependencies.
RUN apk add --no-cache \
@@ -30,7 +30,7 @@ RUN go version && \
# AWG TOOLS BUILDING STAGE
# Base: Alpine
#
FROM alpine:latest AS awg-tools
FROM alpine:3.24 AS awg-tools
# Install needed dependencies.
RUN apk add --no-cache \
@@ -55,7 +55,7 @@ RUN make && chmod +x wg*
#
# Use the python-alpine image for building pip dependencies
FROM python:3.14-alpine AS pip-builder
FROM python:3.14-alpine3.23 AS pip-builder
ARG TARGETPLATFORM
@@ -91,7 +91,7 @@ RUN . /opt/wgdashboard/src/venv/bin/activate && \
#
# Running with the python-alpine image.
FROM python:3.14-alpine AS final
FROM python:3.14-alpine3.23 AS final
LABEL maintainer="dselen@nerthus.nl"
# Install only the runtime dependencies
@@ -114,15 +114,18 @@ ENV TZ="Europe/Amsterdam" \
global_dns="9.9.9.9" \
wgd_port="10086" \
public_ip="" \
WGDASH=/opt/wgdashboard
WGDASH=/opt/wgdashboard \
dynamic_config="true"
# Create directories needed for operation
RUN mkdir /data /configs -p ${WGDASH}/src /etc/amnezia/amneziawg
RUN mkdir /data /configs -p ${WGDASH}/src /etc/amnezia/amneziawg \
&& echo "name_servers=${global_dns}" >> /etc/resolvconf.conf
# Copy the venv and source files from local compiled locations or repos
COPY ./src ${WGDASH}/src
COPY --from=pip-builder /opt/wgdashboard/src/venv /opt/wgdashboard/src/venv
COPY ./docker/wg0.conf.template /tmp/wg0.conf.template
COPY ./docker/wg-dashboard-oidc-providers.json.template /tmp/wg-dashboard-oidc-providers.json.template
# Copy in the runtime script, essential.
COPY ./docker/entrypoint.sh /entrypoint.sh
+78 -3
View File
@@ -23,7 +23,7 @@ To get the container running you either pull the pre-made image from a remote re
- ghcr.io/wgdashboard/wgdashboard:<tag>
- docker.io/donaldzou/wgdashboard:<tag>
> tags should be either: latest, main, <version> or <commit-sha>.
> tags should be either: latest, main, <version>, <branch-name> (if built) or <commit-sha>.
From there either use the environment variables described below as parameters or use the Docker Compose file: `compose.yaml`.<br>
Be careful, the default generated WireGuard configuration file uses port 51820/udp. So make sure to use this port if you want to use it out of the box.<br>
@@ -96,7 +96,8 @@ Updating the WGDashboard container should be through 'The Docker Way' - by pulli
## ⚙️ Environment Variables
| Variable | Accepted Values | Default | Example | Description |
| ------------------ | ---------------------------------------- | ----------------------- | ------------------------ | ----------------------------------------------------------------------- |
| ------------------ | ---------------------------------------- | ----------------------- | --------------------- | ----------------------------------------------------------------------- |
| `dynamic_config` | true, yes, false, no | `true` | `true` or `no` | Turns on or off the dynamic configuration feature, on by default for Docker |
| `tz` | Timezone | `Europe/Amsterdam` | `America/New_York` | Sets the container's timezone. Useful for accurate logs and scheduling. |
| `global_dns` | IPv4 and IPv6 addresses | `9.9.9.9` | `8.8.8.8`, `1.1.1.1` | Default DNS for WireGuard clients. |
| `public_ip` | Public IP address | Retrieved automatically | `253.162.134.73` | Used to generate accurate client configs. Needed if container is NATd. |
@@ -104,7 +105,7 @@ Updating the WGDashboard container should be through 'The Docker Way' - by pulli
| `username` | Any nonempty string | `-` | `admin` | Username for the WGDashboard web interface account. |
| `password` | Any nonempty string | `-` | `s3cr3tP@ss` | Password for the WGDashboard web interface account (stored hashed). |
| `enable_totp` | `true`, `false` | `true` | `false` | Enable TOTPbased twofactor authentication for the account. |
| `wg_autostart` | Wireguard interface name | `-` | `wg0` or `wg0\|\|wg1\|\|wg2` | Autostart the WireGuard interface when the container launches. |
| `wg_autostart` | Wireguard interface name | `- ` | `wg0` | Automatically start the specified WireGuard interface when the container starts |
| `email_server` | SMTP server address | `-` | `smtp.gmail.com` | SMTP server for sending email notifications. |
| `email_port` | SMTP port number | `-` | `587` | Port for connecting to the SMTP server. |
| `email_encryption` | `TLS`, `SSL`, etc. | `-` | `TLS` | Encryption method for email communication. |
@@ -112,6 +113,11 @@ Updating the WGDashboard container should be through 'The Docker Way' - by pulli
| `email_password` | Any non-empty string | `-` | `app_password` | Password for SMTP authentication. |
| `email_from` | Valid email address | `-` | `noreply@example.com` | Email address used as the sender for notifications. |
| `email_template` | Path to template file | `-` | `your-template` | Custom template for email notifications. |
| `database_type` | `sqlite`, `postgresql`, `mariadb+mariadbconnector`, etc. | `-` | `postgresql` | Type of [sqlalchemy database engine](https://docs.sqlalchemy.org/en/21/core/engines.html). |
| `database_host` | Any non-empty string | `-` | `localhost` | IP-Address or hostname of the SQL-database server. |
| `database_port` | Any non-empty string (or int for port) | `-` | `5432` | Port for the database communication. |
| `database_username`| Valid database username | `-` | `database_user` | Database user username. |
| `database_password`| Valid database password | `-` | `database_password` | Database user password. |
---
@@ -207,6 +213,75 @@ ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
- Access the web interface via `http://your-ip:10086` (or whichever port you specified in the compose).
- The first time run will auto-generate WireGuard keys and configs (configs are generated from the template).
## 🧑‍💻 Local Development with Docker
You can develop against WGDashboard locally by mounting the `src/` directory into the container. This lets you edit Python and frontend code on your host and see changes reflected immediately (with a service restart for Python).
Create a `docker/compose-local.yaml` alongside the existing `compose.yaml`:
```yaml
services:
wgdashboard:
image: ghcr.io/wgdashboard/wgdashboard:latest
restart: unless-stopped
container_name: wgdashboard
ports:
- 10086:10086/tcp
- 51820:51820/udp
volumes:
- aconf:/etc/amnezia/amneziawg
- conf:/etc/wireguard
- data:/data
# Mount local src for live editing
- ../src:/opt/wgdashboard/src
# Keep venv in a named volume so it isn't overwritten by the mount
- venv:/opt/wgdashboard/src/venv
cap_add:
- NET_ADMIN
volumes:
aconf:
conf:
data:
venv:
```
The key additions compared to the production compose file:
- `../src:/opt/wgdashboard/src` — mounts your local `src/` directory into the container so code changes are reflected without rebuilding the image.
- `venv:/opt/wgdashboard/src/venv` — keeps the Python virtual environment in a named Docker volume. Without this, the host mount would overwrite the venv created during image build.
To start the development container:
```bash
cd docker
docker compose -f compose-local.yaml up -d
```
After editing Python files (e.g. `src/dashboard.py`), restart the container to pick up changes:
```bash
docker restart wgdashboard
```
For frontend changes, install dependencies and rebuild the Vue app on your host:
```bash
cd src/static/app
npm install
npm run build
```
Then restart the container so it serves the updated dist files:
```bash
docker restart wgdashboard
```
---
## Closing remarks:
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
+1
View File
@@ -13,6 +13,7 @@ services:
# By default its all disabled, but uncomment the following lines to apply these. (uncommenting is removing the # character)
# Refer to the documentation on https://wgdashboard.dev/ for more info on what everything means.
#environment:
#- wg_autostart=wg0
#- tz= # <--- Set container timezone, default: Europe/Amsterdam.
#- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me.
#- wgd_port= # <--- Set the port WGDashboard will use for its web-server.
+68 -8
View File
@@ -85,8 +85,6 @@ echo "------------------------- START ----------------------------"
echo "Starting the WGDashboard Docker container."
ensure_installation() {
echo "Quick-installing..."
# Make the wgd.sh script executable.
chmod +x "${WGDASH}"/src/wgd.sh
cd "${WGDASH}"/src || exit
@@ -102,23 +100,51 @@ ensure_installation() {
echo "Removing clear command from wgd.sh for better Docker logging."
sed -i '/clear/d' ./wgd.sh
# PERSISTENCE FOR databases directory
# Create required directories and links
if [ ! -d "/data/db" ]; then
echo "Creating database dir"
mkdir -p /data/db
fi
if [ ! -d "${WGDASH}/src/db" ]; then
ln -s /data/db "${WGDASH}/src/db"
if [[ ! -L "${WGDASH}/src/db" ]] && [[ -d "${WGDASH}/src/db" ]]; then
echo "Removing ${WGDASH}/src/db since its not a symbolic link."
rm -rfv "${WGDASH}/src/db"
fi
if [[ -L "${WGDASH}/src/db" ]]; then
echo "${WGDASH}/src/db is a symbolic link."
else
ln -sv /data/db "${WGDASH}/src/db"
fi
# PERSISTENCE FOR wg-dashboard-oidc-providers.json
if [ ! -f "/data/wg-dashboard-oidc-providers.json" ]; then
echo "Creating wg-dashboard-oidc-providers.json file"
cp -v /tmp/wg-dashboard-oidc-providers.json.template /data/wg-dashboard-oidc-providers.json
fi
if [[ ! -L "${WGDASH}/src/wg-dashboard-oidc-providers.json" ]] && [[ -f "${WGDASH}/src/wg-dashboard-oidc-providers.json" ]]; then
echo "Removing ${WGDASH}/src/wg-dashboard-oidc-providers.json since its not a symbolic link."
rm -fv "${WGDASH}/src/wg-dashboard-oidc-providers.json"
fi
if [[ -L "${WGDASH}/src/wg-dashboard-oidc-providers.json" ]]; then
echo "${WGDASH}/src/wg-dashboard-oidc-providers.json is a symbolic link."
else
ln -sv /data/wg-dashboard-oidc-providers.json "${WGDASH}/src/wg-dashboard-oidc-providers.json"
fi
# PERSISTENCE FOR wg-dashboard.ini
if [ ! -f "${config_file}" ]; then
echo "Creating wg-dashboard.ini file"
touch "${config_file}"
fi
if [ ! -f "${WGDASH}/src/wg-dashboard.ini" ]; then
ln -s "${config_file}" "${WGDASH}/src/wg-dashboard.ini"
if [[ ! -L "${WGDASH}/src/wg-dashboard.ini" ]] && [[ -f "${WGDASH}/src/wg-dashboard.ini" ]]; then
echo "Removing ${WGDASH}/src/wg-dashboard.ini since its not a symbolic link."
rm -fv "${WGDASH}/src/wg-dashboard.ini"
fi
if [[ -L "${WGDASH}/src/wg-dashboard.ini" ]]; then
echo "${WGDASH}/src/wg-dashboard.ini is a symbolic link."
else
ln -sv "${config_file}" "${WGDASH}/src/wg-dashboard.ini"
fi
# Setup WireGuard if needed
@@ -142,13 +168,24 @@ set_envvars() {
# Check if config file is empty
if [ ! -s "${config_file}" ]; then
echo "Config file is empty. Creating initial structure."
elif [[ ${dynamic_config,,} =~ ^(false|no)$ ]]; then
echo "Dynamic configuration feature turned off, not changing anything"
return
fi
echo "Checking basic configuration:"
set_ini Peers peer_global_dns "${global_dns}"
if [ -z "${public_ip}" ]; then
public_ip=$(curl -s ifconfig.me)
public_ip=$(curl -s https://ifconfig.me)
if [ -z "${public_ip}" ]; then
echo "Using fallback public IP resolution website"
public_ip=$(curl -s https://api.ipify.org)
fi
if [ -z "${public_ip}" ]; then
echo "Failed to resolve publicly. Using private address."
public_ip=$(hostname -i)
fi
echo "Automatically detected public IP: ${public_ip}"
fi
@@ -183,6 +220,24 @@ set_envvars() {
set_ini WireGuardConfiguration autostart "${wg_autostart}"
fi
# Database (check if any settings need to be configured)
database_vars=("database_type" "database_host" "database_port" "database_username" "database_password")
for var in "${database_vars[@]}"; do
if [ -n "${!var}" ]; then
echo "Configuring database settings:"
break
fi
done
# Database (iterate through all possible fields)
database_fields=("type:database_type" "host:database_host" "port:database_port"
"username:database_username" "password:database_password")
for field_pair in "${database_fields[@]}"; do
IFS=: read -r field var <<< "$field_pair"
[[ -n "${!var}" ]] && set_ini Database "$field" "${!var}"
done
# Email (check if any settings need to be configured)
email_vars=("email_server" "email_port" "email_encryption" "email_username" "email_password" "email_from" "email_template")
for var in "${email_vars[@]}"; do
@@ -207,6 +262,9 @@ set_envvars() {
start_and_monitor() {
printf "\n---------------------- STARTING CORE -----------------------\n"
# Due to resolvconf resetting the DNS we echo back the one we defined (or fallback to default).
resolvconf -u
# Due to some instances complaining about this, making sure its there every time.
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
@@ -220,7 +278,9 @@ start_and_monitor() {
${WGDASH}/src/venv/bin/gunicorn --config ${WGDASH}/src/gunicorn.conf.py
cp /etc/resolv.conf /etc/resolv.conf.docker
/usr/sbin/resolvconf -u
cat /etc/resolv.conf.docker | resolvconf -a docker.inet
if [ $? -ne 0 ]; then
echo "Loading WGDashboard failed... Look above for details."
@@ -0,0 +1,16 @@
{
"Admin": {
"Provider": {
"client_id": "",
"client_secret": "",
"issuer": ""
}
},
"Client": {
"Provider": {
"client_id": "",
"client_secret": "",
"issuer": ""
}
}
}
+19 -4
View File
@@ -4,8 +4,9 @@ from tzlocal import get_localzone
from functools import wraps
from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for
from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for, send_from_directory
import os
import mimetypes
from modules.WireguardConfiguration import WireguardConfiguration
from modules.DashboardConfig import DashboardConfig
@@ -53,6 +54,8 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
@client.post(f'{prefix}/api/signup')
def ClientAPI_SignUp():
if not dashboardConfig.GetConfig("Clients", "sign_up")[1]:
abort(404)
data = request.get_json()
status, msg = dashboardClients.SignUp(**data)
return ResponseObject(status, msg)
@@ -97,7 +100,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
date = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
emailSender = EmailSender(dashboardConfig)
if not emailSender.ready():
if not emailSender.is_ready():
return ResponseObject(False, "We can't send you an email due to your Administrator has not setup email service. Please contact your administrator.")
data = request.get_json()
@@ -192,14 +195,26 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
})
return ResponseObject(status, msg)
@client.get(f'{prefix}/assets/<path:filename>')
@client.get(f'{prefix}/img/<path:filename>')
def serve_client_static(filename):
client_dist_folder = os.path.abspath("./static/dist/WGDashboardClient")
mimetype = mimetypes.guess_type(filename)[0]
subfolder = 'assets' if 'assets' in request.path else 'img'
return send_from_directory(os.path.join(client_dist_folder, subfolder), os.path.basename(filename), mimetype=mimetype)
@client.get(prefix)
def ClientIndex():
return render_template('client.html')
app_prefix = dashboardConfig.GetConfig("Server", "app_prefix")[1]
return render_template('client.html', APP_PREFIX=app_prefix)
@client.get(f'{prefix}/api/serverInformation')
def ClientAPI_ServerInformation():
return ResponseObject(data={
"ServerTimezone": str(get_localzone())
"ServerTimezone": str(get_localzone()),
"SignUp": {
"enable": dashboardConfig.GetConfig("Clients", "sign_up")[1]
}
})
@client.get(f'{prefix}/api/validateAuthentication')
+205 -71
View File
@@ -2,13 +2,15 @@ import logging
import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess
import time, re, uuid, bcrypt, psutil, pyotp, threading
import traceback
from functools import wraps
from urllib.parse import unquote
from uuid import uuid4
from zipfile import ZipFile
from datetime import datetime, timedelta
import sqlalchemy
from jinja2 import Template
from flask import Flask, request, render_template, session, send_file
from flask import Flask, request, render_template, session, send_file, current_app, redirect, url_for
from flask_cors import CORS
from icmplib import ping, traceroute
from flask.json.provider import DefaultJSONProvider
@@ -17,8 +19,7 @@ from itertools import islice
from sqlalchemy import RowMapping
from modules.Utilities import (
RegexMatch, StringToBoolean,
ValidateIPAddressesWithRange, ValidateDNSAddress,
RegexMatch, StringToBoolean, ValidateDNSAddress,
GenerateWireguardPublicKey, GenerateWireguardPrivateKey
)
from packaging import version
@@ -30,7 +31,8 @@ from modules.PeerShareLinks import PeerShareLinks
from modules.PeerJobs import PeerJobs
from modules.DashboardConfig import DashboardConfig
from modules.WireguardConfiguration import WireguardConfiguration
from modules.AmneziaWireguardConfiguration import AmneziaWireguardConfiguration
from modules.AmneziaConfiguration import AmneziaConfiguration
from modules.DashboardOIDC import DashboardOIDC
from client import createClientBlueprint
@@ -69,10 +71,29 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
response.content_type = "application/json"
return response
def require_fields(*fields):
"""Decorator that validates required fields in request.json."""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
data = request.json
if data is None:
return ResponseObject(False, "Request body must be JSON", status_code=400)
missing = [field for field in fields if field not in data]
if missing:
return ResponseObject(False, f"Missing required fields: {', '.join(missing)}", status_code=400)
return f(*args, **kwargs)
return decorated
return decorator
'''
Flask App
'''
app = Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
_, APP_PREFIX_INIT = DashboardConfig().GetConfig("Server", "app_prefix")
app = Flask("WGDashboard",
template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"),
static_folder=os.path.abspath("./static/dist/WGDashboardAdmin"),
static_url_path=APP_PREFIX_INIT if APP_PREFIX_INIT else '')
def peerInformationBackgroundThread():
global WireguardConfigurations
@@ -92,6 +113,8 @@ def peerInformationBackgroundThread():
c.getPeersTransfer()
c.getPeersEndpoint()
c.getPeers()
if DashboardConfig.GetConfig('WireGuardConfiguration', 'peer_tracking')[1] is True:
print("[WGDashboard] Tracking Peers")
if delay == 6:
if c.configurationInfo.PeerTrafficTracking:
c.logPeersTraffic()
@@ -161,10 +184,10 @@ def InitWireguardConfigurationsList(startup: bool = False):
if i in WireguardConfigurations.keys():
if WireguardConfigurations[i].configurationFileChanged():
with app.app_context():
WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i)
WireguardConfigurations[i] = AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i)
else:
with app.app_context():
WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i, startup=startup)
WireguardConfigurations[i] = AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i, startup=startup)
except WireguardConfiguration.InvalidConfigurationFileException as e:
app.logger.error(f"{i} have an invalid configuration file.")
@@ -203,6 +226,7 @@ with app.app_context():
NewConfigurationTemplates: NewConfigurationTemplates = NewConfigurationTemplates()
InitWireguardConfigurationsList(startup=True)
DashboardClients: DashboardClients = DashboardClients(WireguardConfigurations)
AdminOIDC = DashboardOIDC("Admin")
app.register_blueprint(createClientBlueprint(WireguardConfigurations, DashboardConfig, DashboardClients))
_, APP_PREFIX = DashboardConfig.GetConfig("Server", "app_prefix")
@@ -257,6 +281,8 @@ def auth_req():
f'{appPrefix}/api/sharePeer/get',
f'{appPrefix}/api/isTotpEnabled',
f'{appPrefix}/api/locale',
f'{appPrefix}/api/oidc/providers',
f'{appPrefix}/api/oidc/authenticate',
]
@@ -264,7 +290,9 @@ def auth_req():
("username" not in session or session.get("role") != "admin")
and (f"{appPrefix}/" != request.path and f"{appPrefix}" != request.path)
and not request.path.startswith(f'{appPrefix}/client')
and not request.path.startswith(f'{appPrefix}/static')
and not request.path.startswith(f'{appPrefix}/img')
and not request.path.startswith(f'{appPrefix}/json')
and not request.path.startswith(f'{appPrefix}/assets')
and request.path not in whiteList
):
response = Flask.make_response(app, {
@@ -292,6 +320,38 @@ def API_ValidateAuthentication():
def API_RequireAuthentication():
return ResponseObject(data=DashboardConfig.GetConfig("Server", "auth_req")[1])
# OIDC for Admin
@app.get(f'{APP_PREFIX}/api/oidc/providers')
def API_OIDC_GetProviders():
_, oidc = DashboardConfig.GetConfig("OIDC", "admin_enable")
if not oidc:
return ResponseObject(status=False, message="OIDC is disabled")
return ResponseObject(data=AdminOIDC.GetProviders())
@app.post(f'{APP_PREFIX}/api/oidc/authenticate')
def API_OIDC_Authenticate():
_, oidc = DashboardConfig.GetConfig("OIDC", "admin_enable")
if not oidc:
return ResponseObject(False, "OIDC is disabled")
requestData = request.get_json()
status, data = AdminOIDC.VerifyToken(**requestData)
if not status:
return ResponseObject(False, "OIDC Authentication Failed. Reason: " + data)
session['role'] = 'admin'
authToken = hashlib.sha256(f'${data['sid']}{datetime.now()}{app.secret_key}'.encode()).hexdigest()
session['username'] = authToken
session['signInMethod'] = 'OIDC'
session['signInPayload'] = {
"Provider": requestData.get('provider'),
"Payload": data
}
resp = ResponseObject()
resp.set_cookie('authToken', authToken)
return resp
@app.post(f'{APP_PREFIX}/api/authenticate')
def API_AuthenticateLogin():
data = request.get_json()
@@ -311,7 +371,10 @@ def API_AuthenticateLogin():
totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
totpValid = False
if totpEnabled:
totpValid = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now() == data['totp']
totp_code = str(data.get("totp", "")).strip()
totpValid = pyotp.TOTP(
DashboardConfig.GetConfig("Account", "totp_key")[1]
).verify(totp_code, valid_window=1)
if (valid
and data['username'] == DashboardConfig.GetConfig("Account", "username")[1]
@@ -320,6 +383,7 @@ def API_AuthenticateLogin():
authToken = hashlib.sha256(f"{data['username']}{datetime.now()}".encode()).hexdigest()
session['role'] = 'admin'
session['username'] = authToken
session['signInMethod'] = 'local'
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
resp.set_cookie("authToken", authToken)
session.permanent = True
@@ -335,6 +399,14 @@ def API_AuthenticateLogin():
def API_SignOut():
resp = ResponseObject(True, "")
resp.delete_cookie("authToken")
if session.get('signInMethod') == "OIDC":
status, oidc_config = AdminOIDC.GetProviderConfiguration(session.get('signInPayload').get("Provider"))
signOut = requests.get(
oidc_config.get("end_session_endpoint"),
params={
'id_token_hint': session.get('signInPayload').get("Payload").get('sid')
}
)
session.clear()
return resp
@@ -422,11 +494,11 @@ def API_addWireguardConfiguration():
)
WireguardConfigurations[data['ConfigurationName']] = (
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else (
AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName']))
AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName']))
else:
WireguardConfigurations[data['ConfigurationName']] = (
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else (
AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data))
AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data))
return ResponseObject()
@app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration')
@@ -523,7 +595,7 @@ def API_renameWireguardConfiguration():
status, message = rc.renameConfiguration(data.get("NewConfigurationName"))
if status:
WireguardConfigurations[data.get("NewConfigurationName")] = (WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")) if rc.Protocol == 'wg' else AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")))
WireguardConfigurations[data.get("NewConfigurationName")] = (WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")) if rc.Protocol == 'wg' else AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")))
else:
WireguardConfigurations[data.get("ConfigurationName")] = rc
return ResponseObject(status, message)
@@ -562,8 +634,8 @@ def API_getAllWireguardConfigurationBackup():
files.sort(key=lambda x: x[1], reverse=True)
for f, ct in files:
if RegexMatch(r"^(.*)_(.*)\.(conf)$", f):
s = re.search(r"^(.*)_(.*)\.(conf)$", f)
if RegexMatch(r"^(.+)_(\d+)\.(conf)$", f):
s = re.search(r"^(.+)_(\d+)\.(conf)$", f)
name = s.group(1)
if name not in existingConfiguration:
if name not in data['NonExistingConfigurations'].keys():
@@ -706,15 +778,30 @@ def API_updatePeerSettings(configName):
preshared_key = data['preshared_key']
mtu = data['mtu']
keepalive = data['keepalive']
notes = data.get('notes', '')
wireguardConfig = WireguardConfigurations[configName]
foundPeer, peer = wireguardConfig.searchPeer(id)
if foundPeer:
if wireguardConfig.Protocol == 'wg':
status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses,
allowed_ip, endpoint_allowed_ip, mtu, keepalive)
status, msg = peer.updatePeer(name,
private_key,
preshared_key,
dns_addresses,
allowed_ip,
endpoint_allowed_ip,
mtu,
keepalive,
notes)
else:
status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses,
allowed_ip, endpoint_allowed_ip, mtu, keepalive, "off")
status, msg = peer.updatePeer(name,
private_key,
preshared_key,
dns_addresses,
allowed_ip,
endpoint_allowed_ip,
mtu,
keepalive,
notes)
wireguardConfig.getPeers()
DashboardWebHooks.RunWebHook('peer_updated', {
"configuration": wireguardConfig.Name,
@@ -864,6 +951,7 @@ def API_addPeers(configName):
mtu: int = data.get('mtu', None)
keep_alive: int = data.get('keepalive', None)
notes: str = data.get('notes', '')
preshared_key: str = data.get('preshared_key', "")
if type(mtu) is not int or mtu < 0 or mtu > 1460:
@@ -919,7 +1007,7 @@ def API_addPeers(configName):
"endpoint_allowed_ip": endpoint_allowed_ip,
"mtu": mtu,
"keepalive": keep_alive,
"advanced_security": "off"
"notes": ""
})
if addedCount == bulkAddAmount:
break
@@ -962,8 +1050,11 @@ def API_addPeers(configName):
for i in allowed_ips:
found = False
for subnet in availableIps.keys():
try:
network = ipaddress.ip_network(subnet, False)
ap = ipaddress.ip_network(i)
except ValueError as e:
return ResponseObject(False, str(e))
if network.version == ap.version and ap.subnet_of(network):
found = True
@@ -981,14 +1072,13 @@ def API_addPeers(configName):
"DNS": dns_addresses,
"mtu": mtu,
"keepalive": keep_alive,
"advanced_security": "off"
"notes": notes
}]
)
return ResponseObject(status=status, message=message, data=addedPeers)
except Exception as e:
app.logger.error("Add peers failed", e)
return ResponseObject(False,
f"Add peers failed. Reason: {message}")
return ResponseObject(False, f"Add peers failed.")
return ResponseObject(False, "Configuration does not exist")
@@ -1125,14 +1215,25 @@ def API_GetPeerTraffics():
@app.get(f'{APP_PREFIX}/api/getPeerTrackingTableCounts')
def API_GetPeerTrackingTableCounts():
configurationName = request.args.get("configurationName")
if configurationName not in WireguardConfigurations.keys():
if configurationName and configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist")
if configurationName:
c = WireguardConfigurations.get(configurationName)
return ResponseObject(data={
"TrafficTrackingTableSize": c.getTransferTableSize(),
"HistoricalTrackingTableSize": c.getHistoricalEndpointTableSize()
})
d = {}
for i in WireguardConfigurations.keys():
c = WireguardConfigurations.get(i)
d[i] = {
"TrafficTrackingTableSize": c.getTransferTableSize(),
"HistoricalTrackingTableSize": c.getHistoricalEndpointTableSize()
}
return ResponseObject(data=d)
@app.get(f'{APP_PREFIX}/api/downloadPeerTrackingTable')
def API_DownloadPeerTackingTable():
configurationName = request.args.get("configurationName")
@@ -1168,52 +1269,75 @@ def API_getDashboardTheme():
def API_getDashboardVersion():
return ResponseObject(data=DashboardConfig.GetConfig("Server", "version")[1])
@app.post(f'{APP_PREFIX}/api/savePeerScheduleJob')
@app.post(f'{APP_PREFIX}/api/PeerScheduleJob')
@require_fields('Configuration', 'Peer', 'Field', 'Operator', 'Value', 'Action')
def API_savePeerScheduleJob():
data = request.json
if "Job" not in data.keys():
return ResponseObject(False, "Please specify job")
job: dict = data['Job']
if "Peer" not in job.keys() or "Configuration" not in job.keys():
return ResponseObject(False, "Please specify peer and configuration")
configuration = WireguardConfigurations.get(job['Configuration'])
configuration = WireguardConfigurations.get(data['Configuration'])
if configuration is None:
return ResponseObject(False, "Configuration does not exist")
f, fp = configuration.searchPeer(job['Peer'])
if not f:
return ResponseObject(False, "Peer does not exist")
return ResponseObject(False, "Configuration does not exist", status_code=404)
peerKey = unquote(data['Peer'])
found, _ = configuration.searchPeer(peerKey)
if not found:
return ResponseObject(False, "Peer does not exist", status_code=404)
s, p = AllPeerJobs.saveJob(PeerJob(
job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
job['CreationDate'], job['ExpireDate'], job['Action']))
if s:
return ResponseObject(s, data=p)
return ResponseObject(s, message=p)
jobID = data.get('JobID', str(uuid4()))
if len(AllPeerJobs.searchJobById(jobID)) > 0:
return ResponseObject(False, "Job already exists", status_code=409)
@app.post(f'{APP_PREFIX}/api/deletePeerScheduleJob')
success, result = AllPeerJobs.saveJob(PeerJob(
jobID, data['Configuration'], peerKey, data['Field'], data['Operator'], data['Value'],
datetime.now(), data.get('ExpireDate'), data['Action']))
if success:
return ResponseObject(success, data=result)
return ResponseObject(success, message=result)
@app.put(f'{APP_PREFIX}/api/PeerScheduleJob')
@require_fields('JobID', 'Configuration', 'Peer', 'Field', 'Operator', 'Value', 'Action')
def API_updatePeerScheduleJob():
data = request.json
configuration = WireguardConfigurations.get(data['Configuration'])
if configuration is None:
return ResponseObject(False, "Configuration does not exist", status_code=404)
peerKey = unquote(data['Peer'])
found, _ = configuration.searchPeer(peerKey)
if not found:
return ResponseObject(False, "Peer does not exist", status_code=404)
existing = AllPeerJobs.searchJobById(data['JobID'])
if len(existing) == 0:
return ResponseObject(False, "Job does not exist", status_code=404)
success, result = AllPeerJobs.saveJob(PeerJob(
data['JobID'], data['Configuration'], peerKey, data['Field'], data['Operator'], data['Value'],
datetime.now(), data.get('ExpireDate'), data['Action']))
if success:
return ResponseObject(success, data=result)
return ResponseObject(success, message=result)
@app.delete(f'{APP_PREFIX}/api/PeerScheduleJob')
@require_fields('JobID', 'Configuration', 'Peer')
def API_deletePeerScheduleJob():
data = request.json
if "Job" not in data.keys():
return ResponseObject(False, "Please specify job")
job: dict = data['Job']
if "Peer" not in job.keys() or "Configuration" not in job.keys():
return ResponseObject(False, "Please specify peer and configuration")
configuration = WireguardConfigurations.get(job['Configuration'])
configuration = WireguardConfigurations.get(data['Configuration'])
if configuration is None:
return ResponseObject(False, "Configuration does not exist")
# f, fp = configuration.searchPeer(job['Peer'])
# if not f:
# return ResponseObject(False, "Peer does not exist")
return ResponseObject(False, "Configuration does not exist", status_code=404)
s, p = AllPeerJobs.deleteJob(PeerJob(
job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
job['CreationDate'], job['ExpireDate'], job['Action']))
if s:
return ResponseObject(s)
return ResponseObject(s, message=p)
peerKey = unquote(data['Peer'])
success, result = AllPeerJobs.deleteJob(PeerJob(
data['JobID'], data['Configuration'], peerKey, data.get('Field', ''),
data.get('Operator', ''), data.get('Value', ''),
datetime.now(), data.get('ExpireDate'), data.get('Action', '')))
if success:
return ResponseObject(success, message="Job deleted successfully")
return ResponseObject(success, message=result)
@app.get(f'{APP_PREFIX}/api/getPeerScheduleJobLogs/<configName>')
@app.get(f'{APP_PREFIX}/api/PeerScheduleJobLogs/<configName>')
def API_getPeerScheduleJobLogs(configName):
if configName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist")
@@ -1238,11 +1362,11 @@ def API_ping_getAllPeersIpAddress():
for x in allowed_ip:
try:
ip = ipaddress.ip_network(x, strict=False)
if ip.num_addresses == 1:
parsed.append(str(ip.network_address))
except ValueError as e:
app.logger.error(f"Failed to parse IP address of {p.id} - {c.Name}")
host = list(ip.hosts())
if len(host) == 1:
parsed.append(str(host[0]))
endpoint = p.endpoint.replace(" ", "").replace("(none)", "")
if len(p.name) > 0:
cips[f"{p.name} - {p.id}"] = {
@@ -1326,11 +1450,16 @@ def API_traceroute_execute():
d = r.json()
for i in range(len(result)):
result[i]['geo'] = d[i]
except Exception as e:
return ResponseObject(data=result, message="Failed to request IP address geolocation")
return ResponseObject(data=result)
except Exception as exp:
return ResponseObject(False, exp)
except Exception as e:
app.logger.error(f"Failed to gather the geolocation data: {e}")
return ResponseObject(data=result, message="Failed to request IP address geolocation")
except Exception as e:
app.logger.error(f"Failed to execute the traceroute: {e}")
return ResponseObject(data=[], message="Failed to traceroute the given parameter")
else:
return ResponseObject(False, "Please provide ipAddress")
@@ -1374,11 +1503,15 @@ def API_Welcome_GetTotpLink():
@app.post(f'{APP_PREFIX}/api/Welcome_VerifyTotpLink')
def API_Welcome_VerifyTotpLink():
data = request.get_json()
totp = pyotp.TOTP(DashboardConfig.GetConfig("Account", "totp_key")[1]).now()
if totp == data['totp']:
totp_code = str(data.get("totp", "")).strip()
totpValid = pyotp.TOTP(
DashboardConfig.GetConfig("Account", "totp_key")[1]
).verify(totp_code, valid_window=1)
if totpValid:
DashboardConfig.SetConfig("Account", "totp_verified", "true")
DashboardConfig.SetConfig("Account", "enable_totp", "true")
return ResponseObject(totp == data['totp'])
return ResponseObject(totpValid)
@app.post(f'{APP_PREFIX}/api/Welcome_Finish')
def API_Welcome_Finish():
@@ -1446,7 +1579,7 @@ def API_Locale_Update():
@app.get(f'{APP_PREFIX}/api/email/ready')
def API_Email_Ready():
return ResponseObject(EmailSender.ready())
return ResponseObject(EmailSender.is_ready())
@app.post(f'{APP_PREFIX}/api/email/send')
def API_Email_Send():
@@ -1470,7 +1603,8 @@ def API_Email_Send():
subject = Template(data.get('Subject', '')).render(peer=p.toJson(), configurationFile=download)
if data.get('IncludeAttachment', False):
u = str(uuid4())
attachmentName = f'{u}.conf'
peerName = p.toJson().get('name', '').strip()
attachmentName = f'{peerName if peerName else u}.conf'
with open(os.path.join('./attachments', attachmentName,), 'w+') as f:
f.write(download['file'])
@@ -1700,7 +1834,7 @@ Index Page
@app.get(f'{APP_PREFIX}/')
def index():
return render_template('index.html')
return render_template('index.html', APP_PREFIX=APP_PREFIX)
if __name__ == "__main__":
startThreads()
+2 -1
View File
@@ -1,4 +1,5 @@
import dashboard
import os
from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger, Dash
app_host, app_port = dashboard.gunicornConfig()
@@ -16,7 +17,7 @@ daemon = True
pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log"
loglevel = "info"
loglevel = os.environ['log_level'] if 'log_level' in os.environ else 'info'
capture_output = True
errorlog = f"./log/error_{date}.log"
pythonpath = "., ./modules"
@@ -4,28 +4,43 @@ AmneziaWG Configuration
import random, sqlalchemy, os, subprocess, re, uuid
from flask import current_app
from .PeerJobs import PeerJobs
from .AmneziaWGPeer import AmneziaWGPeer
from .AmneziaPeer import AmneziaPeer
from .PeerShareLinks import PeerShareLinks
from .Utilities import RegexMatch
from .Utilities import RegexMatch, CheckAddress, CheckPeerKey
from .WireguardConfiguration import WireguardConfiguration
from .DashboardWebHooks import DashboardWebHooks
class AmneziaWireguardConfiguration(WireguardConfiguration):
def __init__(self, DashboardConfig,
class AmneziaConfiguration(WireguardConfiguration):
def __init__(self,
DashboardConfig,
AllPeerJobs: PeerJobs,
AllPeerShareLinks: PeerShareLinks,
DashboardWebHooks: DashboardWebHooks,
name: str = None, data: dict = None, backup: dict = None, startup: bool = False):
name: str = None,
data: dict = None,
backup: dict = None,
startup: bool = False):
self.Jc = 0
self.Jmin = 0
self.Jmax = 0
self.S1 = 0
self.S2 = 0
self.S3 = 0
self.S4 = 0
self.H1 = 1
self.H2 = 2
self.H3 = 3
self.H4 = 4
self.I1 = ""
self.I2 = ""
self.I3 = ""
self.I4 = ""
self.I5 = ""
self.J1 = ""
self.J2 = ""
self.J3 = ""
self.Itime = ""
super().__init__(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, name, data, backup, startup, wg=False)
@@ -58,65 +73,68 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
"Jmax": self.Jmax,
"S1": self.S1,
"S2": self.S2,
"S3": self.S3,
"S4": self.S4,
"H1": self.H1,
"H2": self.H2,
"H3": self.H3,
"H4": self.H4
"H4": self.H4,
"I1": self.I1,
"I2": self.I2,
"I3": self.I3,
"I4": self.I4,
"I5": self.I5,
"J1": self.J1,
"J2": self.J2,
"J3": self.J3,
"Itime": self.Itime
}
def createDatabase(self, dbName = None):
def generate_column_obj():
return [
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('notes', sqlalchemy.Text),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255))
]
if dbName is None:
dbName = self.Name
self.peersTable = sqlalchemy.Table(
dbName, self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('advanced_security', sqlalchemy.String(255)),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
extend_existing=True
f'{dbName}', self.metadata, *generate_column_obj(), extend_existing=True
)
self.peersRestrictedTable = sqlalchemy.Table(
f'{dbName}_restrict_access', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('advanced_security', sqlalchemy.String(255)),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
extend_existing=True
f'{dbName}_restrict_access', self.metadata, *generate_column_obj(), extend_existing=True
)
self.peersDeletedTable = sqlalchemy.Table(
f'{dbName}_deleted', self.metadata, *generate_column_obj(), extend_existing=True
)
if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite':
time_col_type = sqlalchemy.DATETIME
else:
time_col_type = sqlalchemy.TIMESTAMP
self.peersTransferTable = sqlalchemy.Table(
f'{dbName}_transfer', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
@@ -126,38 +144,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP),
server_default=sqlalchemy.func.now()),
extend_existing=True
)
self.peersDeletedTable = sqlalchemy.Table(
f'{dbName}_deleted', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('advanced_security', sqlalchemy.String(255)),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
extend_existing=True
)
self.infoTable = sqlalchemy.Table(
'ConfigurationsInfo', self.metadata,
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
sqlalchemy.Column('Info', sqlalchemy.Text),
sqlalchemy.Column('time', time_col_type, server_default=sqlalchemy.func.now()),
extend_existing=True
)
@@ -165,8 +152,13 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
f'{dbName}_history_endpoint', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('time',
(sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
sqlalchemy.Column('time', time_col_type)
)
self.infoTable = sqlalchemy.Table(
'ConfigurationsInfo', self.metadata,
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
sqlalchemy.Column('Info', sqlalchemy.Text),
extend_existing=True
)
@@ -211,11 +203,9 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
if tempPeer is None:
tempPeer = {
"id": i['PublicKey'],
"advanced_security": i.get('AdvancedSecurity', 'off'),
"private_key": "",
"DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1],
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[
1],
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1],
"name": i.get("name"),
"total_receive": 0,
"total_sent": 0,
@@ -229,6 +219,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
"cumu_data": 0,
"mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1],
"keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1],
"notes": "",
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
}
@@ -243,14 +234,14 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
self.peersTable.columns.id == i['PublicKey']
)
)
self.Peers.append(AmneziaWGPeer(tempPeer, self))
self.Peers.append(AmneziaPeer(tempPeer, self))
except Exception as e:
current_app.logger.error(f"{self.Name} getPeers() Error", e)
else:
with self.engine.connect() as conn:
existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall()
for i in existingPeers:
self.Peers.append(AmneziaWGPeer(i, self))
self.Peers.append(AmneziaPeer(i, self))
def addPeers(self, peers: list) -> tuple[bool, list, str]:
result = {
@@ -258,6 +249,15 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
"peers": []
}
try:
cleanedAllowedIPs = {}
for p in peers:
newAllowedIPs = p['allowed_ip'].replace(" ", "")
if not CheckAddress(newAllowedIPs):
return False, [], "Allowed IPs entry format is incorrect"
if not CheckPeerKey(p["id"]):
return False, [], "Peer key format is incorrect"
cleanedAllowedIPs[p["id"]] = newAllowedIPs
with self.engine.begin() as conn:
for i in peers:
newPeer = {
@@ -278,9 +278,9 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
"cumu_data": 0,
"mtu": i['mtu'],
"keepalive": i['keepalive'],
"notes": i.get('notes', ''),
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["preshared_key"],
"advanced_security": i['advanced_security']
"preshared_key": i["preshared_key"]
}
conn.execute(
self.peersTable.insert().values(newPeer)
@@ -293,13 +293,15 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
with open(uid, "w+") as f:
f.write(p['preshared_key'])
subprocess.check_output(
f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
shell=True, stderr=subprocess.STDOUT)
command = [self.Protocol, "set", self.Name, "peer", p['id'], "allowed-ips", cleanedAllowedIPs[p["id"]], "preshared-key", uid if presharedKeyExist else "/dev/null"]
subprocess.check_output(command, stderr=subprocess.STDOUT)
if presharedKeyExist:
os.remove(uid)
subprocess.check_output(
f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
command = [f"{self.Protocol}-quick", "save", self.Name]
subprocess.check_output(command, stderr=subprocess.STDOUT)
self.getPeers()
for p in peers:
p = self.searchPeer(p['id'])
@@ -311,7 +313,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
})
except Exception as e:
current_app.logger.error("Add peers error", e)
return False, [], str(e)
return False, [], "Internal server error"
return True, result['peers'], ""
def getRestrictedPeers(self):
@@ -319,4 +321,4 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
with self.engine.connect() as conn:
restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall()
for i in restricted:
self.RestrictedPeers.append(AmneziaWGPeer(i, self))
self.RestrictedPeers.append(AmneziaPeer(i, self))
+121
View File
@@ -0,0 +1,121 @@
import os
from flask import current_app
import random
import re
import subprocess
import uuid
from flask import current_app
from .Peer import Peer
from .Utilities import CheckAddress, ValidateDNSAddress, GenerateWireguardPublicKey
class AmneziaPeer(Peer):
def __init__(self, tableData, configuration):
super().__init__(tableData, configuration)
def updatePeer(self, name: str, private_key: str,
preshared_key: str,
dns_addresses: str,
allowed_ip: str,
endpoint_allowed_ip: str,
mtu: int,
keepalive: int,
notes: str
) -> tuple[bool, str | None]:
if not self.configuration.getStatus():
self.configuration.toggleConfiguration()
# Before we do any compute, let us check if the given endpoint allowed ip is valid at all
if not CheckAddress(endpoint_allowed_ip):
return False, f"Endpoint Allowed IPs format is incorrect"
peers = []
for peer in self.configuration.getPeersList():
# Make sure to exclude your own data when updating since its not really relevant
if peer.id != self.id:
continue
peers.append(peer)
used_allowed_ips = []
for peer in peers:
ips = peer.allowed_ip.split(',')
ips = [ip.strip() for ip in ips]
used_allowed_ips.append(ips)
if allowed_ip in used_allowed_ips:
return False, "Allowed IP already taken by another peer"
if not ValidateDNSAddress(dns_addresses):
return False, f"DNS IP-Address or FQDN is incorrect"
if isinstance(mtu, str):
mtu = 0
if isinstance(keepalive, str):
keepalive = 0
if mtu not in range(0, 1461):
return False, "MTU format is not correct"
if keepalive < 0:
return False, "Persistent Keepalive format is not correct"
if len(private_key) > 0:
pubKey = GenerateWireguardPublicKey(private_key)
if not pubKey[0] or pubKey[1] != self.id:
return False, "Private key does not match with the public key"
try:
rand = random.Random()
uid = str(uuid.UUID(int=rand.getrandbits(128), version=4))
psk_exist = len(preshared_key) > 0
if psk_exist:
with open(uid, "w+") as f:
f.write(preshared_key)
newAllowedIPs = allowed_ip.replace(" ", "")
if not CheckAddress(newAllowedIPs):
return False, "Allowed IPs entry format is incorrect"
command = [self.configuration.Protocol, "set", self.configuration.Name, "peer", self.id, "allowed-ips", newAllowedIPs, "preshared-key", uid if psk_exist else "/dev/null"]
updateAllowedIp = subprocess.check_output(command, stderr=subprocess.STDOUT)
if psk_exist: os.remove(uid)
output = updateAllowedIp.decode().strip("\n")
if len(output) != 0:
current_app.logger.error(f"Update peer failed when updating Allowed IPs.\nInput: {newAllowedIPs}\nOutput: {output}")
return False, "Internal server error"
command = [f"{self.configuration.Protocol}-quick", "save", self.configuration.Name]
saveConfig = subprocess.check_output(command, stderr=subprocess.STDOUT)
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
current_app.logger.error("Update peer failed when saving the configuration")
return False, "Internal server error"
with self.configuration.engine.begin() as conn:
conn.execute(
self.configuration.peersTable.update().values({
"name": name,
"private_key": private_key,
"DNS": dns_addresses,
"endpoint_allowed_ip": endpoint_allowed_ip,
"mtu": mtu,
"keepalive": keepalive,
"notes": notes,
"preshared_key": preshared_key
}).where(
self.configuration.peersTable.c.id == self.id
)
)
self.configuration.getPeers()
return True, None
except subprocess.CalledProcessError as exc:
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode('UTF-8')}")
return False, "Internal server error"
-92
View File
@@ -1,92 +0,0 @@
import os
import random
import re
import subprocess
import uuid
from .Peer import Peer
from .Utilities import ValidateIPAddressesWithRange, ValidateDNSAddress, GenerateWireguardPublicKey
class AmneziaWGPeer(Peer):
def __init__(self, tableData, configuration):
self.advanced_security = tableData["advanced_security"]
super().__init__(tableData, configuration)
def updatePeer(self, name: str, private_key: str,
preshared_key: str,
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
keepalive: int, advanced_security: str) -> tuple[bool, str] or tuple[bool, None]:
if not self.configuration.getStatus():
self.configuration.toggleConfiguration()
existingAllowedIps = [item for row in list(
map(lambda x: [q.strip() for q in x.split(',')],
map(lambda y: y.allowed_ip,
list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row]
if allowed_ip in existingAllowedIps:
return False, "Allowed IP already taken by another peer"
if not ValidateIPAddressesWithRange(endpoint_allowed_ip):
return False, f"Endpoint Allowed IPs format is incorrect"
if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses):
return False, f"DNS format is incorrect"
if type(mtu) is str:
mtu = 0
if type(keepalive) is str:
keepalive = 0
if mtu < 0 or mtu > 1460:
return False, "MTU format is not correct"
if keepalive < 0:
return False, "Persistent Keepalive format is not correct"
if advanced_security != "on" and advanced_security != "off":
return False, "Advanced Security can only be on or off"
if len(private_key) > 0:
pubKey = GenerateWireguardPublicKey(private_key)
if not pubKey[0] or pubKey[1] != self.id:
return False, "Private key does not match with the public key"
try:
rd = random.Random()
uid = str(uuid.UUID(int=rd.getrandbits(128), version=4))
pskExist = len(preshared_key) > 0
if pskExist:
with open(uid, "w+") as f:
f.write(preshared_key)
newAllowedIPs = allowed_ip.replace(" ", "")
updateAllowedIp = subprocess.check_output(
f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}",
shell=True, stderr=subprocess.STDOUT)
if pskExist: os.remove(uid)
if len(updateAllowedIp.decode().strip("\n")) != 0:
return False, "Update peer failed when updating Allowed IPs"
saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}",
shell=True, stderr=subprocess.STDOUT)
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
return False, "Update peer failed when saving the configuration"
with self.configuration.engine.begin() as conn:
conn.execute(
self.configuration.peersTable.update().values({
"name": name,
"private_key": private_key,
"DNS": dns_addresses,
"endpoint_allowed_ip": endpoint_allowed_ip,
"mtu": mtu,
"keepalive": keepalive,
"preshared_key": preshared_key,
"advanced_security": advanced_security
}).where(
self.configuration.peersTable.c.id == self.id
)
)
self.configuration.getPeers()
return True, None
except subprocess.CalledProcessError as exc:
return False, exc.output.decode("UTF-8").strip()
+1 -1
View File
@@ -8,7 +8,7 @@ import pyotp
import sqlalchemy as db
import requests
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
from .DashboardClientsTOTP import DashboardClientsTOTP
from .DashboardOIDC import DashboardOIDC
@@ -1,7 +1,7 @@
import datetime
import uuid
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from .DashboardLogger import DashboardLogger
import sqlalchemy as db
from .WireguardConfiguration import WireguardConfiguration
+1 -1
View File
@@ -3,7 +3,7 @@ import hashlib
import uuid
import sqlalchemy as db
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
class DashboardClientsTOTP:
+59 -9
View File
@@ -7,16 +7,12 @@ import sqlalchemy as db
from datetime import datetime
from typing import Any
from flask import current_app
from .ConnectionString import ConnectionString
from .Utilities import (
GetRemoteEndpoint, ValidateDNSAddress
)
from .DatabaseConnection import ConnectionString
from .Utilities import (GetRemoteEndpoint, ValidateDNSAddress)
from .DashboardAPIKey import DashboardAPIKey
class DashboardConfig:
DashboardVersion = 'v4.3.2'
DashboardVersion = 'v4.3.4'
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini')
@@ -55,7 +51,8 @@ class DashboardConfig:
"peer_display_mode": "grid",
"remote_endpoint": GetRemoteEndpoint(),
"peer_MTU": "1420",
"peer_keep_alive": "21"
"peer_keep_alive": "21",
"peer_preshared_key_default": "false"
},
"Other": {
"welcome_session": "true"
@@ -83,9 +80,11 @@ class DashboardConfig:
},
"Clients": {
"enable": "true",
"sign_up": "true"
},
"WireGuardConfiguration": {
"autostart": ""
"autostart": "",
"peer_tracking": "false"
}
}
@@ -102,6 +101,57 @@ class DashboardConfig:
self.APIAccessed = False
self.SetConfig("Server", "version", DashboardConfig.DashboardVersion)
def EnsureDatabaseIntegrity(self, wireguardConfigurations):
expected_columns = {
'id': db.String(255),
'private_key': db.String(255),
'DNS': db.Text,
'endpoint_allowed_ip': db.Text,
'name': db.Text,
'total_receive': db.Float,
'total_sent': db.Float,
'total_data': db.Float,
'endpoint': db.String(255),
'status': db.String(255),
'latest_handshake': db.String(255),
'allowed_ip': db.String(255),
'cumu_receive': db.Float,
'cumu_sent': db.Float,
'cumu_data': db.Float,
'mtu': db.Integer,
'keepalive': db.Integer,
'notes': db.Text,
'remote_endpoint': db.String(255),
'preshared_key': db.String(255)
}
inspector = db.inspect(self.engine)
with self.engine.begin() as conn:
for cfg_name, cfg_obj in wireguardConfigurations.items():
tables_to_check = [
cfg_name,
f'{cfg_name}_restrict_access',
f'{cfg_name}_deleted'
]
for table_name in tables_to_check:
if not table_name:
continue
if not inspector.has_table(table_name):
continue
existing_columns = [c['name'] for c in inspector.get_columns(table_name)]
for col_name, col_type in expected_columns.items():
if col_name not in existing_columns:
type_str = col_type().compile(dialect=self.engine.dialect)
current_app.logger.info(f"Adding missing column '{col_name}' to table '{table_name}'")
preparer = self.engine.dialect.identifier_preparer
quoted_table = preparer.quote_identifier(table_name)
quoted_column = preparer.quote_identifier(col_name)
conn.execute(db.text(f"ALTER TABLE {quoted_table} ADD COLUMN {quoted_column} {type_str}"))
def getConnectionString(self, database) -> str or None:
sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db")
+1 -1
View File
@@ -4,7 +4,7 @@ Dashboard Logger Class
import uuid
import sqlalchemy as db
from flask import current_app
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
class DashboardLogger:
+7 -2
View File
@@ -85,10 +85,15 @@ class DashboardOIDC:
key = next(k for k in jwks["keys"] if k["kid"] == kid)
alg = key.get("alg") or headers.get("alg")
supported_algs = oidc_config.get("id_token_signing_alg_values_supported")
if supported_algs and alg not in supported_algs:
return False, f"Unsupported token signing algorithm: {alg}"
payload = jwt.decode(
id_token,
key,
algorithms=[key["alg"]],
algorithms=[alg],
audience=provider_info.get('client_id'),
issuer=issuer,
access_token=access_token
@@ -134,7 +139,7 @@ class DashboardOIDC:
'openid_configuration': oidc_config
}
self.provider_secret[k] = providers[k]['client_secret']
current_app.logger.info(f"Registered OIDC Provider: {k}")
current_app.logger.info(f"Registered OIDC Provider[{self.mode}]: {k}")
except Exception as e:
current_app.logger.error(f"Failed to register OIDC config for {k}", exc_info=e)
except Exception as e:
+1 -1
View File
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
import requests
from pydantic import BaseModel, field_serializer
import sqlalchemy as db
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from flask import current_app
WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated']
@@ -1,14 +1,15 @@
import configparser
import os
from sqlalchemy_utils import database_exists, create_database
from flask import current_app
def ConnectionString(database) -> str:
parser = configparser.ConfigParser(strict=False)
parser.read_file(open('wg-dashboard.ini', "r+"))
sqlitePath = os.path.join("db")
if not os.path.isdir(sqlitePath):
os.mkdir(sqlitePath)
if parser.get("Database", "type") == "postgresql":
cn = f'postgresql+psycopg://{parser.get("Database", "username")}:{parser.get("Database", "password")}@{parser.get("Database", "host")}/{database}'
elif parser.get("Database", "type") == "mysql":
@@ -19,7 +20,6 @@ def ConnectionString(database) -> str:
if not database_exists(cn):
create_database(cn)
except Exception as e:
current_app.logger.error("Database error. Terminating...", e)
exit(1)
return cn
+64 -39
View File
@@ -1,76 +1,101 @@
import os.path
import ssl
import smtplib
# Email libaries
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from email.utils import formatdate
class EmailSender:
def __init__(self, DashboardConfig):
self.smtp = None
self.DashboardConfig = DashboardConfig
if not os.path.exists('./attachments'):
os.mkdir('./attachments')
def Server(self):
return self.DashboardConfig.GetConfig("Email", "server")[1]
self.refresh_vals()
def Port(self):
return self.DashboardConfig.GetConfig("Email", "port")[1]
def refresh_vals(self) -> None:
self.Server = self.DashboardConfig.GetConfig("Email", "server")[1]
self.Port = self.DashboardConfig.GetConfig("Email", "port")[1]
def Encryption(self):
return self.DashboardConfig.GetConfig("Email", "encryption")[1]
self.Encryption = self.DashboardConfig.GetConfig("Email", "encryption")[1]
self.AuthRequired = self.DashboardConfig.GetConfig("Email", "authentication_required")[1]
self.Username = self.DashboardConfig.GetConfig("Email", "username")[1]
self.Password = self.DashboardConfig.GetConfig("Email", "email_password")[1]
def Username(self):
return self.DashboardConfig.GetConfig("Email", "username")[1]
self.SendFrom = self.DashboardConfig.GetConfig("Email", "send_from")[1]
def Password(self):
return self.DashboardConfig.GetConfig("Email", "email_password")[1]
def is_ready(self) -> bool:
self.refresh_vals()
def SendFrom(self):
return self.DashboardConfig.GetConfig("Email", "send_from")[1]
if self.AuthRequired:
ready = all([
self.Server, self.Port, self.Encryption,
self.Username, self.Password, self.SendFrom
])
else:
ready = all([
self.Server, self.Port, self.Encryption, self.SendFrom
])
return ready
# Thank you, @gdeeble from GitHub
def AuthenticationRequired(self):
return self.DashboardConfig.GetConfig("Email", "authentication_required")[1]
def send(self, receiver, subject, body, includeAttachment: bool = False, attachmentName: str = "") -> tuple[bool, str | None]:
if not self.is_ready():
return False, "SMTP not configured"
def ready(self):
if self.AuthenticationRequired():
return all([self.Server(), self.Port(), self.Encryption(), self.Username(), self.Password(), self.SendFrom()])
return all([self.Server(), self.Port(), self.Encryption(), self.SendFrom()])
def send(self, receiver, subject, body, includeAttachment = False, attachmentName = "") -> tuple[bool, str] | tuple[bool, None]:
if self.ready():
try:
self.smtp = smtplib.SMTP(self.Server(), port=int(self.Port()))
self.smtp.ehlo()
if self.Encryption() == "STARTTLS":
self.smtp.starttls()
if self.AuthenticationRequired():
self.smtp.login(self.Username(), self.Password())
message = MIMEMultipart()
message['Subject'] = subject
message['From'] = self.SendFrom()
message['From'] = self.SendFrom
message["To"] = receiver
message["Date"] = formatdate(localtime=True)
message.attach(MIMEText(body, "plain"))
if includeAttachment and len(attachmentName) > 0:
attachmentPath = os.path.join('./attachments', attachmentName)
if os.path.exists(attachmentPath):
if not os.path.exists(attachmentPath):
return False, "Attachment does not exist"
attachment = MIMEBase("application", "octet-stream")
with open(os.path.join('./attachments', attachmentName), 'rb') as f:
attachment.set_payload(f.read())
encoders.encode_base64(attachment)
attachment.add_header("Content-Disposition", f"attachment; filename= {attachmentName}",)
message.attach(attachment)
smtp = None
try:
context = ssl.create_default_context()
if self.Encryption == "IMPLICITTLS":
smtp = smtplib.SMTP_SSL(self.Server, port=int(self.Port), context=context)
else:
self.smtp.close()
return False, "Attachment does not exist"
self.smtp.sendmail(self.SendFrom(), receiver, message.as_string())
self.smtp.close()
smtp = smtplib.SMTP(self.Server, port=int(self.Port))
smtp.ehlo()
# Configure SMTP encryption type
if self.Encryption == "STARTTLS":
smtp.starttls(context=context)
smtp.ehlo()
# Log into the SMTP server if required
if self.AuthRequired:
smtp.login(self.Username, self.Password)
# Send the actual email from the SMTP object
smtp.sendmail(self.SendFrom, receiver, message.as_string())
return True, None
except Exception as e:
return False, f"Send failed | Reason: {e}"
return False, "SMTP not configured"
finally:
if smtp:
try:
smtp.quit()
except Exception:
pass
+1 -1
View File
@@ -2,7 +2,7 @@ import uuid
from pydantic import BaseModel, field_serializer
import sqlalchemy as db
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
class NewConfigurationTemplate(BaseModel):
+86 -42
View File
@@ -10,8 +10,9 @@ from datetime import timedelta
import jinja2
import sqlalchemy as db
from .PeerJob import PeerJob
from flask import current_app
from .PeerShareLink import PeerShareLink
from .Utilities import GenerateWireguardPublicKey, ValidateIPAddressesWithRange, ValidateDNSAddress
from .Utilities import GenerateWireguardPublicKey, CheckAddress, ValidateDNSAddress
class Peer:
@@ -34,6 +35,7 @@ class Peer:
self.cumu_data = tableData["cumu_data"]
self.mtu = tableData["mtu"]
self.keepalive = tableData["keepalive"]
self.notes = tableData.get("notes", "")
self.remote_endpoint = tableData["remote_endpoint"]
self.preshared_key = tableData["preshared_key"]
self.jobs: list[PeerJob] = []
@@ -49,62 +51,89 @@ class Peer:
def __repr__(self):
return str(self.toJson())
def updatePeer(self, name: str, private_key: str,
def updatePeer(self, name: str,
private_key: str,
preshared_key: str,
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
keepalive: int) -> tuple[bool, str] or tuple[bool, None]:
dns_addresses: str,
allowed_ip: str,
endpoint_allowed_ip: str,
mtu: int,
keepalive: int,
notes: str
) -> tuple[bool, str | None]:
if not self.configuration.getStatus():
self.configuration.toggleConfiguration()
existingAllowedIps = [item for row in list(
map(lambda x: [q.strip() for q in x.split(',')],
map(lambda y: y.allowed_ip,
list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row]
if allowed_ip in existingAllowedIps:
return False, "Allowed IP already taken by another peer"
if not ValidateIPAddressesWithRange(endpoint_allowed_ip):
# Before we do any compute, let us check if the given endpoint allowed ip is valid at all
if not CheckAddress(endpoint_allowed_ip):
return False, f"Endpoint Allowed IPs format is incorrect"
if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses):
return False, f"DNS format is incorrect"
peers = []
for peer in self.configuration.getPeersList():
# Make sure to exclude your own data when updating since its not really relevant
if peer.id == self.id:
continue
peers.append(peer)
if type(mtu) is str or mtu is None:
used_allowed_ips = []
for peer in peers:
ips = peer.allowed_ip.split(',')
ips = [ip.strip() for ip in ips]
used_allowed_ips.append(ips)
if allowed_ip in used_allowed_ips:
return False, "Allowed IP already taken by another peer"
if not ValidateDNSAddress(dns_addresses):
return False, f"DNS IP-Address or FQDN is incorrect"
if isinstance(mtu, str):
mtu = 0
if mtu < 0 or mtu > 1460:
return False, "MTU format is not correct"
if type(keepalive) is str or keepalive is None:
if isinstance(keepalive, str):
keepalive = 0
if mtu not in range(0, 1461):
return False, "MTU format is not correct"
if keepalive < 0:
return False, "Persistent Keepalive format is not correct"
if len(private_key) > 0:
pubKey = GenerateWireguardPublicKey(private_key)
if not pubKey[0] or pubKey[1] != self.id:
return False, "Private key does not match with the public key"
try:
rd = random.Random()
uid = str(uuid.UUID(int=rd.getrandbits(128), version=4))
pskExist = len(preshared_key) > 0
if pskExist:
try:
rand = random.Random()
uid = str(uuid.UUID(int=rand.getrandbits(128), version=4))
psk_exist = len(preshared_key) > 0
if psk_exist:
with open(uid, "w+") as f:
f.write(preshared_key)
newAllowedIPs = allowed_ip.replace(" ", "")
updateAllowedIp = subprocess.check_output(
f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}",
shell=True, stderr=subprocess.STDOUT)
if pskExist: os.remove(uid)
newAllowedIPs = allowed_ip.replace(" ", "")
if not CheckAddress(newAllowedIPs):
return False, "Allowed IPs entry format is incorrect"
command = [self.configuration.Protocol, "set", self.configuration.Name, "peer", self.id, "allowed-ips", newAllowedIPs, "preshared-key", uid if psk_exist else "/dev/null"]
updateAllowedIp = subprocess.check_output(command, stderr=subprocess.STDOUT)
if psk_exist: os.remove(uid)
if len(updateAllowedIp.decode().strip("\n")) != 0:
return False, "Update peer failed when updating Allowed IPs"
saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}",
shell=True, stderr=subprocess.STDOUT)
current_app.logger.error("Update peer failed when updating Allowed IPs")
return False, "Internal server error"
command = [f"{self.configuration.Protocol}-quick", "save", self.configuration.Name]
saveConfig = subprocess.check_output(command, stderr=subprocess.STDOUT)
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
return False, "Update peer failed when saving the configuration"
current_app.logger.error("Update peer failed when saving the configuration")
return False, "Internal server error"
with self.configuration.engine.begin() as conn:
conn.execute(
self.configuration.peersTable.update().values({
@@ -114,6 +143,7 @@ class Peer:
"endpoint_allowed_ip": endpoint_allowed_ip,
"mtu": mtu,
"keepalive": keepalive,
"notes": notes,
"preshared_key": preshared_key
}).where(
self.configuration.peersTable.c.id == self.id
@@ -121,7 +151,8 @@ class Peer:
)
return True, None
except subprocess.CalledProcessError as exc:
return False, exc.output.decode("UTF-8").strip()
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode('UTF-8')}")
return False, "Internal server error"
def downloadPeer(self) -> dict[str, str]:
final = {
@@ -132,12 +163,14 @@ class Peer:
if len(filename) == 0:
filename = "UntitledPeer"
filename = "".join(filename.split(' '))
filename = f"{filename}"
illegal_filename = [".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2", "com3",
"com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4",
"lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "con", "nul", "prn"]
for i in illegal_filename:
filename = filename.replace(i, "")
# use previous filtering code if code below is insufficient or faulty
filename = re.sub(r'[.,/?<>\\:*|"]', '', filename).rstrip(". ") # remove special characters
reserved_pattern = r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$" # match com1-9, lpt1-9, con, nul, prn, aux, nul
if re.match(reserved_pattern, filename, re.IGNORECASE):
filename = f"file_{filename}" # prepend "file_" if it matches
for i in filename:
if re.match("^[a-zA-Z0-9_=+.-]$", i):
@@ -163,10 +196,21 @@ class Peer:
"Jmax": self.configuration.Jmax,
"S1": self.configuration.S1,
"S2": self.configuration.S2,
"S3": self.configuration.S3,
"S4": self.configuration.S4,
"H1": self.configuration.H1,
"H2": self.configuration.H2,
"H3": self.configuration.H3,
"H4": self.configuration.H4
"H4": self.configuration.H4,
"I1": self.configuration.I1,
"I2": self.configuration.I2,
"I3": self.configuration.I3,
"I4": self.configuration.I4,
"I5": self.configuration.I5,
"J1": self.configuration.J1,
"J2": self.configuration.J2,
"J3": self.configuration.J3,
"Itime": self.configuration.Itime
})
peerSection = {
+1 -1
View File
@@ -8,7 +8,7 @@ import sqlalchemy as db
from flask import current_app
from sqlalchemy import RowMapping
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from .Log import Log
class PeerJobLogger:
+1 -1
View File
@@ -3,7 +3,7 @@ Peer Jobs
"""
import sqlalchemy
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from .PeerJob import PeerJob
from .PeerJobLogger import PeerJobLogger
import sqlalchemy as db
+1 -1
View File
@@ -1,4 +1,4 @@
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from .PeerShareLink import PeerShareLink
import sqlalchemy as db
from datetime import datetime
+33 -21
View File
@@ -1,6 +1,6 @@
import re, ipaddress
import subprocess
import sqlalchemy
def RegexMatch(regex, text) -> bool:
"""
@@ -18,10 +18,18 @@ def GetRemoteEndpoint() -> str:
@return:
"""
import socket
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("1.1.1.1", 80)) # Connecting to a public IP
wgd_remote_endpoint = s.getsockname()[0]
return str(wgd_remote_endpoint)
except (socket.error, OSError):
pass
try:
return socket.gethostbyname(socket.gethostname())
except (socket.error, OSError):
pass
return "127.0.0.1"
def StringToBoolean(value: str):
@@ -33,31 +41,35 @@ def StringToBoolean(value: str):
return (value.strip().replace(" ", "").lower() in
("yes", "true", "t", "1", 1))
def ValidateIPAddressesWithRange(ips: str) -> bool:
s = ips.replace(" ", "").split(",")
for ip in s:
def CheckAddress(ips_str: str) -> bool:
if len(ips_str) == 0:
return False
for ip in ips_str.split(','):
stripped_ip = ip.strip()
try:
ipaddress.ip_network(ip)
except ValueError as e:
# Verify the IP-address, with the strict flag as false also allows for /32 and /128
ipaddress.ip_network(stripped_ip, strict=False)
except ValueError:
return False
return True
def ValidateIPAddresses(ips) -> bool:
s = ips.replace(" ", "").split(",")
for ip in s:
try:
ipaddress.ip_address(ip)
except ValueError as e:
return False
return True
def CheckPeerKey(peer_key: str) -> bool:
return re.match(r"^[A-Za-z0-9+/]{43}=$", peer_key)
def ValidateDNSAddress(addresses_str: str) -> tuple[bool, str | None]:
if len(addresses_str) == 0:
return False, "Got an empty list/string to check for valid DNS-addresses"
addresses = addresses_str.split(',')
for address in addresses:
stripped_address = address.strip()
if not CheckAddress(stripped_address) and not RegexMatch(r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z]{0,61}[a-z]", stripped_address):
return False, f"{stripped_address} does not appear to be a valid IP-address or FQDN"
return True, None
def ValidateDNSAddress(addresses) -> tuple[bool, str]:
s = addresses.replace(" ", "").split(",")
for address in s:
if not ValidateIPAddresses(address) and not RegexMatch(
r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z]{0,61}[a-z]", address):
return False, f"{address} does not appear to be an valid DNS address"
return True, ""
def ValidateEndpointAllowedIPs(IPs) -> tuple[bool, str] | tuple[bool, None]:
ips = IPs.replace(" ", "").split(",")
+165 -133
View File
@@ -10,13 +10,18 @@ from datetime import datetime, timedelta
from itertools import islice
from flask import current_app
from .ConnectionString import ConnectionString
from .DatabaseConnection import ConnectionString
from .DashboardConfig import DashboardConfig
from .Peer import Peer
from .PeerJobs import PeerJobs
from .PeerShareLinks import PeerShareLinks
from .Utilities import StringToBoolean, GenerateWireguardPublicKey, RegexMatch, ValidateDNSAddress, \
ValidateEndpointAllowedIPs
from .Utilities import StringToBoolean, \
GenerateWireguardPublicKey, \
RegexMatch, \
ValidateDNSAddress, \
ValidateEndpointAllowedIPs, \
CheckAddress, \
CheckPeerKey
from .WireguardConfigurationInfo import WireguardConfigurationInfo, PeerGroupsClass
from .DashboardWebHooks import DashboardWebHooks
@@ -61,6 +66,7 @@ class WireguardConfiguration:
self.Protocol = "wg" if wg else "awg"
self.AllPeerJobs = AllPeerJobs
self.DashboardConfig = DashboardConfig
self.DashboardConfig.EnsureDatabaseIntegrity({self.Name: self})
self.AllPeerShareLinks = AllPeerShareLinks
self.DashboardWebHooks = DashboardWebHooks
self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf')
@@ -103,16 +109,31 @@ class WireguardConfiguration:
}
if self.Protocol == 'awg':
self.__parser["Interface"]["Jc"] = self.Jc
self.__parser["Interface"]["Jc"] = self.Jc
self.__parser["Interface"]["Jmin"] = self.Jmin
self.__parser["Interface"]["Jmax"] = self.Jmax
self.__parser["Interface"]["S1"] = self.S1
self.__parser["Interface"]["S2"] = self.S2
self.__parser["Interface"]["H1"] = self.H1
self.__parser["Interface"]["H2"] = self.H2
self.__parser["Interface"]["H3"] = self.H3
self.__parser["Interface"]["H4"] = self.H4
values = {
"Jc": self.Jc,
"Jmin": self.Jmin,
"Jmax": self.Jmax,
"S1": self.S1,
"S2": self.S2,
"S3": self.S3,
"S4": self.S4,
"H1": self.H1,
"H2": self.H2,
"H3": self.H3,
"H4": self.H4,
"I1": self.I1,
"I2": self.I2,
"I3": self.I3,
"I4": self.I4,
"I5": self.I5,
"J1": self.J1,
"J2": self.J2,
"J3": self.J3,
"Itime": self.Itime
}
for key, value in values.items():
if value != None and str(value).strip():
self.__parser["Interface"][key] = str(value)
if "Backup" not in data.keys():
self.createDatabase()
@@ -127,7 +148,10 @@ class WireguardConfiguration:
current_app.logger.info(f"Initialized Configuration: {name}")
self.__dumpDatabase()
if self.getAutostartStatus() and not self.getStatus() and startup:
self.toggleConfiguration()
status, ext = self.toggleConfiguration()
if not status:
current_app.logger.error(f"Failed to autostart configuration: {name}. Reason: {ext}")
else:
current_app.logger.info(f"Autostart Configuration: {name}")
self.configurationInfo: WireguardConfigurationInfo | None = None
@@ -141,7 +165,6 @@ class WireguardConfiguration:
if self.Status:
self.addAutostart()
def __getProtocolPath(self) -> str:
_, path = self.DashboardConfig.GetConfig("Server", "wg_conf_path") if self.Protocol == "wg" \
else self.DashboardConfig.GetConfig("Server", "awg_conf_path")
@@ -232,54 +255,50 @@ class WireguardConfiguration:
return True
def createDatabase(self, dbName = None):
def generate_column_obj():
return [
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('notes', sqlalchemy.Text),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255))
]
if dbName is None:
dbName = self.Name
self.peersTable = sqlalchemy.Table(
dbName, self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
extend_existing=True
f'{dbName}', self.metadata, *generate_column_obj(), extend_existing=True
)
self.peersRestrictedTable = sqlalchemy.Table(
f'{dbName}_restrict_access', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
extend_existing=True
f'{dbName}_restrict_access', self.metadata, *generate_column_obj(), extend_existing=True
)
self.peersDeletedTable = sqlalchemy.Table(
f'{dbName}_deleted', self.metadata, *generate_column_obj(), extend_existing=True
)
if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite':
time_col_type = sqlalchemy.DATETIME
else:
time_col_type = sqlalchemy.TIMESTAMP
self.peersTransferTable = sqlalchemy.Table(
f'{dbName}_transfer', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
@@ -289,8 +308,7 @@ class WireguardConfiguration:
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP),
server_default=sqlalchemy.func.now()),
sqlalchemy.Column('time', time_col_type, server_default=sqlalchemy.func.now()),
extend_existing=True
)
@@ -298,34 +316,9 @@ class WireguardConfiguration:
f'{dbName}_history_endpoint', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('time',
(sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
extend_existing=True
sqlalchemy.Column('time', time_col_type)
)
self.peersDeletedTable = sqlalchemy.Table(
f'{dbName}_deleted', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
sqlalchemy.Column('DNS', sqlalchemy.Text),
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
sqlalchemy.Column('name', sqlalchemy.Text),
sqlalchemy.Column('total_receive', sqlalchemy.Float),
sqlalchemy.Column('total_sent', sqlalchemy.Float),
sqlalchemy.Column('total_data', sqlalchemy.Float),
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('mtu', sqlalchemy.Integer),
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
extend_existing=True
)
self.infoTable = sqlalchemy.Table(
'ConfigurationsInfo', self.metadata,
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
@@ -404,6 +397,7 @@ class WireguardConfiguration:
try:
if "[Peer]" not in content:
current_app.logger.info(f"{self.Name} config has no [Peer] section")
self.Peers = []
return
peerStarts = content.index("[Peer]")
@@ -439,8 +433,7 @@ class WireguardConfiguration:
"id": i['PublicKey'],
"private_key": "",
"DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1],
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[
1],
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1],
"name": i.get("name"),
"total_receive": 0,
"total_sent": 0,
@@ -454,6 +447,7 @@ class WireguardConfiguration:
"cumu_data": 0,
"mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1] if len(self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1]) > 0 else None,
"keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1] if len(self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1]) > 0 else None,
"notes": "",
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
}
@@ -526,6 +520,15 @@ class WireguardConfiguration:
"peers": []
}
try:
cleanedAllowedIPs = {}
for p in peers:
newAllowedIPs = p['allowed_ip'].replace(" ", "")
if not CheckAddress(newAllowedIPs):
return False, [], "Allowed IPs entry format is incorrect"
if not CheckPeerKey(p["id"]):
return False, [], "Peer key format is incorrect"
cleanedAllowedIPs[p["id"]] = newAllowedIPs
with self.engine.begin() as conn:
for i in peers:
newPeer = {
@@ -546,6 +549,7 @@ class WireguardConfiguration:
"cumu_data": 0,
"mtu": i['mtu'],
"keepalive": i['keepalive'],
"notes": i.get("notes", ""),
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["preshared_key"]
}
@@ -560,12 +564,15 @@ class WireguardConfiguration:
with open(uid, "w+") as f:
f.write(p['preshared_key'])
subprocess.check_output(f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
shell=True, stderr=subprocess.STDOUT)
command = [self.Protocol, "set", self.Name, "peer", p['id'], "allowed-ips", cleanedAllowedIPs[p["id"]], "preshared-key", uid if presharedKeyExist else "/dev/null"]
subprocess.check_output(command, stderr=subprocess.STDOUT)
if presharedKeyExist:
os.remove(uid)
subprocess.check_output(
f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
command = [f"{self.Protocol}-quick", "save", self.Name]
subprocess.check_output(command, stderr=subprocess.STDOUT)
self.getPeers()
for p in peers:
p = self.searchPeer(p['id'])
@@ -577,7 +584,7 @@ class WireguardConfiguration:
})
except Exception as e:
current_app.logger.error("Add peers error", e)
return False, [], str(e)
return False, [], "Internal server error"
return True, result['peers'], ""
def searchPeer(self, publicKey):
@@ -615,8 +622,16 @@ class WireguardConfiguration:
with open(uid, "w+") as f:
f.write(restrictedPeer['preshared_key'])
subprocess.check_output(f"{self.Protocol} set {self.Name} peer {restrictedPeer['id']} allowed-ips {restrictedPeer['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
shell=True, stderr=subprocess.STDOUT)
newAllowedIPs = restrictedPeer['allowed_ip'].replace(" ", "")
if not CheckAddress(newAllowedIPs):
return False, "Allowed IPs entry format is incorrect"
if not CheckPeerKey(restrictedPeer["id"]):
return False, "Peer key format is incorrect"
command = [self.Protocol, "set", self.Name, "peer", restrictedPeer["id"], "allowed-ips", newAllowedIPs, "preshared-key", uid if presharedKeyExist else "/dev/null"]
subprocess.check_output(command, stderr=subprocess.STDOUT)
if presharedKeyExist: os.remove(uid)
else:
return False, "Failed to allow access of peer " + i
@@ -636,8 +651,9 @@ class WireguardConfiguration:
found, pf = self.searchPeer(p)
if found:
try:
subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove",
shell=True, stderr=subprocess.STDOUT)
command = [self.Protocol, "set", self.Name, "peer", pf.id, "remove"]
subprocess.check_output(command, stderr=subprocess.STDOUT)
conn.execute(
self.peersRestrictedTable.insert().from_select(
[c.name for c in self.peersTable.columns],
@@ -665,9 +681,8 @@ class WireguardConfiguration:
if not self.__wgSave():
return False, "Failed to save configuration through WireGuard"
self.getRestrictedPeers()
self.getPeers()
if numOfRestrictedPeers == len(listOfPublicKeys):
return True, f"Restricted {numOfRestrictedPeers} peer(s)"
return False, f"Restricted {numOfRestrictedPeers} peer(s) successfully. Failed to restrict {numOfFailedToRestrictPeers} peer(s)"
@@ -719,17 +734,20 @@ class WireguardConfiguration:
def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]:
try:
subprocess.check_output(f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
command = [f"{self.Protocol}-quick", "save", self.Name]
subprocess.check_output(command, stderr=subprocess.STDOUT)
return True, None
except subprocess.CalledProcessError as e:
return False, str(e)
current_app.logger.error(f"Failed to process command:\n{str(e)}")
return False, "Internal server error"
def getPeersLatestHandshake(self):
if not self.getStatus():
self.toggleConfiguration()
try:
latestHandshake = subprocess.check_output(f"{self.Protocol} show {self.Name} latest-handshakes",
shell=True, stderr=subprocess.STDOUT)
command = [self.Protocol, "show", self.Name, "latest-handshakes"]
latestHandshake = subprocess.check_output(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
latestHandshake = latestHandshake.decode("UTF-8").split()
@@ -768,8 +786,9 @@ class WireguardConfiguration:
if not self.getStatus():
self.toggleConfiguration()
# try:
data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} transfer",
shell=True, stderr=subprocess.STDOUT)
command = [self.Protocol, "show", self.Name, "transfer"]
data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT)
data_usage = data_usage.decode("UTF-8").split("\n")
data_usage = [p.split("\t") for p in data_usage]
@@ -783,15 +802,13 @@ class WireguardConfiguration:
)
).mappings().fetchone()
if cur_i is not None:
# print(cur_i is None)
total_sent = cur_i['total_sent']
# print(cur_i is None)
total_receive = cur_i['total_receive']
cur_total_sent = float(data_usage[i][2]) / (1024 ** 3)
cur_total_receive = float(data_usage[i][1]) / (1024 ** 3)
cumulative_receive = cur_i['cumu_receive'] + total_receive
cumulative_sent = cur_i['cumu_sent'] + total_sent
if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
if (total_sent * 0.999 ) <= cur_total_sent and (total_receive * 0.999) <= cur_total_receive: # An accuracy of 1K ppm is sufficient
total_sent = cur_total_sent
total_receive = cur_total_receive
else:
@@ -826,10 +843,11 @@ class WireguardConfiguration:
if not self.getStatus():
self.toggleConfiguration()
try:
data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} endpoints",
shell=True, stderr=subprocess.STDOUT)
command = [self.Protocol, "show", self.Name, "endpoints"]
data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
count = 0
with self.engine.begin() as conn:
@@ -847,14 +865,17 @@ class WireguardConfiguration:
self.getStatus()
if self.Status:
try:
check = subprocess.check_output(f"{self.Protocol}-quick down {self.Name}",
shell=True, stderr=subprocess.STDOUT)
command = [f"{self.Protocol}-quick", "down", self.Name]
check = subprocess.check_output(command, stderr=subprocess.STDOUT)
self.removeAutostart()
except subprocess.CalledProcessError as exc:
return False, str(exc.output.strip().decode("utf-8"))
else:
try:
check = subprocess.check_output(f"{self.Protocol}-quick up {self.Name}", shell=True, stderr=subprocess.STDOUT)
command = [f"{self.Protocol}-quick", "up", self.Name]
check = subprocess.check_output(command, stderr=subprocess.STDOUT)
self.addAutostart()
except subprocess.CalledProcessError as exc:
return False, str(exc.output.strip().decode("utf-8"))
@@ -921,8 +942,8 @@ class WireguardConfiguration:
files.sort(key=lambda x: x[1], reverse=True)
for f, ct in files:
if RegexMatch(f"^({self.Name})_(.*)\\.(conf)$", f):
s = re.search(f"^({self.Name})_(.*)\\.(conf)$", f)
if RegexMatch(rf"^({self.Name})_(\d+)\\.(conf)$", f):
s = re.search(rf"^({self.Name})_(\d+)\\.(conf)$", f)
date = s.group(2)
d = {
"filename": f,
@@ -994,8 +1015,10 @@ class WireguardConfiguration:
with open(self.configPath, 'r') as f:
original = [l.rstrip("\n") for l in f.readlines()]
allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"]
awgKeys = []
if self.Protocol == 'awg':
allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4"]
awgKeys = ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5", "J1", "J2", "J3", "Itime"]
allowEdit += awgKeys
start = original.index("[Interface]")
try:
end = original.index("[Peer]")
@@ -1009,7 +1032,10 @@ class WireguardConfiguration:
if split[0] not in allowEdit:
new.append(original[line])
for key in allowEdit:
new.insert(1, f"{key} = {str(newData[key]).strip()}")
val = str(newData.get(key, "")).strip()
if key in awgKeys and val == "":
continue
new.insert(1, f"{key} = {val}")
new.append("")
for line in range(end, len(original)):
new.append(original[line])
@@ -1033,31 +1059,38 @@ class WireguardConfiguration:
return True
def renameConfiguration(self, newConfigurationName) -> tuple[bool, str]:
newConfigurationName = os.path.basename(newConfigurationName)
if len(newConfigurationName) > 15 or not re.match(r'^[a-zA-Z0-9_=\+\.\-]{1,15}$', newConfigurationName):
return False, "Configuration name is either too long or contains an illegal character"
newConfigurationName = newConfigurationName.replace("`", "") # double check
try:
if self.getStatus():
self.toggleConfiguration()
self.createDatabase(newConfigurationName)
with self.engine.begin() as conn:
def doRenameStatement(suffix):
newConfig = f"{newConfigurationName}{suffix}"
oldConfig = f"{self.Name}{suffix}"
conn.execute(
sqlalchemy.text(
f'INSERT INTO "{newConfigurationName}" SELECT * FROM "{self.Name}"'
f'INSERT INTO `{newConfig}` SELECT * FROM `{oldConfig}`'
)
)
doRenameStatement("")
doRenameStatement("_restrict_access")
doRenameStatement("_deleted")
doRenameStatement("_transfer")
conn.execute(
sqlalchemy.text(
f'INSERT INTO "{newConfigurationName}_restrict_access" SELECT * FROM "{self.Name}_restrict_access"'
)
)
conn.execute(
sqlalchemy.text(
f'INSERT INTO "{newConfigurationName}_deleted" SELECT * FROM "{self.Name}_deleted"'
)
)
conn.execute(
sqlalchemy.text(
f'INSERT INTO "{newConfigurationName}_transfer" SELECT * FROM "{self.Name}_transfer"'
)
self.infoTable.update()
.where(self.infoTable.c.ID == self.Name)
.values(ID=newConfigurationName)
)
self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName)
shutil.copy(
self.configPath,
@@ -1065,8 +1098,8 @@ class WireguardConfiguration:
)
self.deleteConfiguration()
except Exception as e:
traceback.print_stack()
return False, str(e)
current_app.logger.error(f"Failed to rename configuration.\nNew Configuration Name: {newConfigurationName}\nError: {str(e)}")
return False, "Internal server error"
return True, None
def getNumberOfAvailableIP(self):
@@ -1226,7 +1259,6 @@ class WireguardConfiguration:
def __validateOverridePeerSettings(self, key: str, value: str | int) -> tuple[bool, None] | tuple[bool, str]:
status = True
msg = None
print(value)
if key == "DNS" and value:
status, msg = ValidateDNSAddress(value)
elif key == "EndpointAllowedIPs" and value:
+8 -8
View File
@@ -1,17 +1,17 @@
bcrypt==5.0.0
ifcfg==0.24
psutil==7.2.2
pyotp==2.9.0
pyotp==2.10.0
Flask==3.1.2
flask-cors==6.0.2
flask-cors==6.0.5
icmplib==3.0.4
gunicorn==25.0.3
requests==2.32.5
gunicorn==26.0.0
requests==2.34.2
tcconfig==0.30.1
sqlalchemy==2.0.46
sqlalchemy==2.0.51
sqlalchemy_utils==0.42.1
psycopg[binary]==3.3.2
PyMySQL==1.1.2
psycopg[binary]==3.3.4
PyMySQL==1.2.0
tzlocal==5.3.1
python-jose==3.5.0
pydantic==2.12.5
pydantic==2.13.4
+37 -4
View File
@@ -6,14 +6,47 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="WGDashboard">
<meta name="apple-mobile-web-app-title" content="WGDashboard">
<link rel="manifest" href="/json/manifest.json">
<link rel="icon" href="/img/Logo-2-512x512.png">
<link rel="manifest" href="./json/manifest.json">
<link rel="icon" href="./img/Logo-2-512x512.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WGDashboard</title>
<base href="./">
<style>
#preloader{
width: 100vw; height: 100vh; display: flex
}
#preloader_placeholder{
width: 50px; height: 50px; background-color: #000; margin: auto;
animation: 3s ease-in-out fadeInPreloader infinite;
}
@keyframes fadeInPreloader {
0%{
opacity: 0;
}
50%{
opacity: 1;
}
100%{
opacity: 0;
}
}
</style>
<script>
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
const base = document.querySelector('base');
if (base && isViteDevMode) {
base.href = '/';
}
</script>
</head>
<body>
<div id="app"></div>
<div id="app">
<div id="preloader">
<div id="preloader_placeholder">
<img style="width: 100%" src="./img/Logo-2-128x128.png" alt="WGDashboard Admin" />
</div>
</div>
</div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
+1045 -1391
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.3.2",
"version": "4.3.4",
"private": true,
"type": "module",
"module": "es2022",
@@ -13,35 +13,35 @@
},
"dependencies": {
"@volar/language-server": "2.4.28",
"@vue/language-server": "3.2.4",
"@vuepic/vue-datepicker": "^12.1.0",
"@vueuse/core": "^14.2.0",
"@vueuse/shared": "^14.1.0",
"@vue/language-server": "3.2.7",
"@vuepic/vue-datepicker": "^14.0.0",
"@vueuse/core": "^14.2.1",
"@vueuse/shared": "^14.3.0",
"animate.css": "^4.1.1",
"bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.3",
"cidr-tools": "^11.0.8",
"cidr-tools": "^12.0.2",
"css-color-converter": "^2.0.0",
"dayjs": "^1.11.19",
"electron-builder": "^26.7.0",
"fuse.js": "^7.0.0",
"fuse.js": "^7.3.0",
"i": "^0.3.7",
"is-cidr": "^6.0.3",
"npm": "^11.8.0",
"ol": "^10.7.0",
"npm": "^11.13.0",
"ol": "^10.9.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"qrcode": "^1.5.3",
"qrcodejs": "^1.0.0",
"simple-code-editor": "^2.0.9",
"uuid": "^13.0.0",
"vue": "^3.5.28",
"uuid": "^14.0.0",
"vue": "^3.5.32",
"vue-chartjs": "^5.3.3",
"vue-router": "^5.0.2"
"vue-router": "^5.0.6"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.4",
"vite": "^7.3.1"
"@vitejs/plugin-vue": "^6.0.6",
"vite": "^8.0.16"
},
"overrides": {
"tar": "^7.5.6"
@@ -2,7 +2,7 @@
import { ref, reactive } from "vue"
import LocaleText from "@/components/text/localeText.vue";
import OidcSettings from "@/components/clientComponents/clientSettingComponents/oidcSettings.vue";
import { fetchGet } from "@/utilities/fetch.js"
import { fetchGet, fetchPost } from "@/utilities/fetch.js"
const emits = defineEmits(['close'])
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore"
const dashboardConfigurationStore = DashboardConfigurationStore()
@@ -12,12 +12,16 @@ const values = reactive({
})
const toggling = ref(false)
const toggleClientSideApp = async () => {
const updateSettings = async (key: string) => {
toggling.value = true
await fetchGet("/api/clients/toggleStatus", {}, (res) => {
values.enableClients = res.data
})
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "Clients",
key: key,
value: dashboardConfigurationStore.Configuration.Clients[key]
}, async (res) => {
await dashboardConfigurationStore.getConfiguration()
toggling.value = false
})
}
</script>
@@ -37,16 +41,43 @@ const toggleClientSideApp = async () => {
</h6>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" for="oidc_switch">
<LocaleText :t="values.enableClients ? 'Enabled':'Disabled'"></LocaleText>
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.enable ? 'Enabled':'Disabled'"></LocaleText>
</label>
<input
:disabled="oidcStatusLoading"
v-model="values.enableClients"
@change="toggleClientSideApp()"
:disabled="toggling"
v-model="dashboardConfigurationStore.Configuration.Clients.enable"
@change="updateSettings('enable')"
class="form-check-input" type="checkbox" role="switch" id="oidc_switch">
</div>
</div>
<hr>
<div>
<div class="d-flex align-items-center">
<h6 class="mb-0">
<LocaleText t="Sign Up as Local Client"></LocaleText>
</h6>
<div class="form-check form-switch ms-auto">
<label class="form-check-label" for="sign_up_switch">
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.sign_up ? 'Enabled':'Disabled'"></LocaleText>
</label>
<input
:disabled="toggling"
v-model="dashboardConfigurationStore.Configuration.Clients.sign_up"
@change="updateSettings('sign_up')"
class="form-check-input" type="checkbox" role="switch" id="sign_up_switch">
</div>
</div>
<small class="text-muted mb-0">
<LocaleText t="Allow clients to sign up with Email and Password"></LocaleText>
</small>
</div>
<div>
<OidcSettings mode="Client"></OidcSettings>
<small class="text-muted mb-0">
<LocaleText t="Allow clients to access with OpenID"></LocaleText>
</small>
</div>
</div>
</div>
</div>
@@ -197,14 +197,14 @@ const deleteConfigurationModal = ref(false)
v-model="data[key]"
:id="'configuration_' + key">
</div>
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4']"
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']"
v-if="configurationInfo.Protocol === 'awg'">
<label :for="'configuration_' + key" class="form-label">
<small class="text-muted">
<LocaleText :t="key"></LocaleText>
</small>
</label>
<input type="number" class="form-control form-control-sm rounded-3"
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="saving"
v-model="data[key]"
:id="'configuration_' + key">
@@ -0,0 +1,31 @@
<script>
import LocaleText from "@/components/text/localeText.vue";
export default {
name: "notesInput",
components: {LocaleText},
props: {
bulk: Boolean,
data: Object,
saving: Boolean
}
}
</script>
<template>
<div :class="{inactiveField: this.bulk}">
<label for="peer_notes_textbox" class="form-label">
<small class="text-muted">
<LocaleText t="Notes"></LocaleText>
</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving || this.bulk"
v-model="this.data.notes"
id="peer_notes_textbox" placeholder="">
</div>
</template>
<style scoped>
</style>
@@ -6,17 +6,26 @@ export default {
components: {LocaleText},
props: {
data: Object,
saving: Boolean
saving: Boolean,
defaultEnabled: Boolean
},
data(){
return{
enable: false
}
},
mounted() {
const hasKey = !!(this.data && this.data.preshared_key && this.data.preshared_key.length > 0)
if (hasKey || this.defaultEnabled){
this.enable = true
}
},
watch:{
enable(){
if (this.enable) {
if (!this.data.preshared_key){
this.data.preshared_key = window.wireguard.generateKeypair().presharedKey
}
} else {
this.data.preshared_key = ""
}
@@ -134,12 +134,17 @@ export default {
</div>
</div>
</div>
<div class="card-footer" role="button" @click="$emit('details')">
<div class="card-footer" role="button" @click="$emit('details')" v-if="!this.Peer.restricted">
<small class="d-flex align-items-center">
<LocaleText t="Details"></LocaleText>
<i class="bi bi-chevron-right ms-auto"></i>
</small>
</div>
<div class="card-footer" v-else>
<small class="d-flex align-items-center text-muted">
<LocaleText t="Allow access to view details"></LocaleText>
</small>
</div>
</div>
</template>
@@ -15,6 +15,7 @@ import MtuInput from "@/components/configurationComponents/newPeersComponents/mt
import PersistentKeepAliveInput
from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import NotesInput from "./newPeersComponents/notesInput.vue";
const dashboardStore = DashboardConfigurationStore()
const wireguardStore = WireguardConfigurationsStore()
@@ -27,11 +28,11 @@ const peerData = ref({
public_key: "",
DNS: dashboardStore.Configuration.Peers.peer_global_dns,
endpoint_allowed_ip: dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip,
notes: "",
keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive),
mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu),
preshared_key: "",
preshared_key_bulkAdd: false,
advanced_security: "off",
preshared_key_bulkAdd: Boolean(dashboardStore.Configuration.Peers.peer_preshared_key_default),
allowed_ips_validation: true,
})
const availableIp = ref([])
@@ -105,6 +106,7 @@ watch(() => {
<template v-if="!peerData.bulkAdd">
<hr class="mb-0 mt-2">
<NameInput :saving="saving" :data="peerData"></NameInput>
<NotesInput :saving="saving" :data="peerData"></NotesInput>
<PrivatePublicKeyInput :saving="saving" :data="peerData"></PrivatePublicKeyInput>
<AllowedIPsInput :availableIp="availableIp" :saving="saving" :data="peerData"></AllowedIPsInput>
</template>
@@ -126,7 +128,7 @@ watch(() => {
<EndpointAllowedIps :saving="saving" :data="peerData"></EndpointAllowedIps>
<div class="row gy-3">
<div class="col-sm" v-if="!peerData.bulkAdd">
<PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd"></PresharedKeyInput>
<PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd" :defaultEnabled="Boolean(dashboardStore.Configuration.Peers.peer_preshared_key_default)"></PresharedKeyInput>
</div>
<div class="col-sm">
@@ -36,8 +36,7 @@ export default {
keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive),
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
preshared_key: "",
preshared_key_bulkAdd: false,
advanced_security: "off",
preshared_key_bulkAdd: Boolean(this.dashboardStore.Configuration.Peers.peer_preshared_key_default),
},
availableIp: undefined,
availableIpSearchString: "",
@@ -134,7 +133,7 @@ export default {
<hr class="mb-0 mt-2">
<div class="row gy-3">
<div class="col-sm" v-if="!this.data.bulkAdd">
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd"></PresharedKeyInput>
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd" :defaultEnabled="Boolean(this.dashboardStore.Configuration.Peers.peer_preshared_key_default)"></PresharedKeyInput>
</div>
<div class="col-sm">
@@ -32,6 +32,7 @@ Chart.register(
import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue";
import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue";
import PeerEndpoints from "@/components/peerDetailsModalComponents/peerEndpoints.vue";
import { GetLocale } from "@/utilities/locale"
const props = defineProps(['selectedPeer'])
const selectedDate = ref(undefined)
defineEmits(['close'])
@@ -49,14 +50,19 @@ defineEmits(['close'])
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
</div>
<div class="card-body px-4">
<div class="d-flex justify-content-between align-items-start mb-2 flex-column flex-md-row">
<div>
<p class="mb-0 text-muted"><small>
<LocaleText t="Peer"></LocaleText>
</small></p>
<h2>
{{ selectedPeer.name }}
<p class="mb-0 text-muted"><small><LocaleText t="Peer" /></small></p>
<h2 :class="{'text-muted': selectedPeer.name.length === 0 }">
{{ selectedPeer.name.length > 0 ? selectedPeer.name : GetLocale("Untitled Peer") }}
</h2>
</div>
<div v-if="selectedPeer.notes" class="text-start text-md-end">
<p class="mb-0 text-muted"><small><LocaleText t="Notes" /></small></p>
<p class="mb-0" style="white-space: pre-wrap">{{ selectedPeer.notes }}</p>
</div>
</div>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-12 col-lg-3">
<div class="card rounded-3 bg-transparent h-100">
@@ -26,7 +26,7 @@ export default {
methods: {
async fetchLog(){
this.dataLoading = true;
await fetchGet(`/api/getPeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
await fetchGet(`/api/PeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
this.data = res.data;
this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss")
this.dataLoading = false;
@@ -2,7 +2,7 @@
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
import {ref} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost} from "@/utilities/fetch.js";
import {fetchPost, fetchPut, fetchDelete} from "@/utilities/fetch.js";
import { VueDatePicker } from "@vuepic/vue-datepicker";
import dayjs from "dayjs";
import LocaleText from "@/components/text/localeText.vue";
@@ -46,9 +46,8 @@ export default {
methods: {
save(){
if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){
fetchPost(`/api/savePeerScheduleJob`, {
Job: this.job
}, (res) => {
const fn = this.newJob ? fetchPost : fetchPut;
fn(`/api/PeerScheduleJob`, this.job, (res) => {
if (res.status){
this.edit = false;
this.store.newMessage("Server", "Peer job saved", "success")
@@ -84,9 +83,7 @@ export default {
},
delete(){
if(this.job.CreationDate){
fetchPost(`/api/deletePeerScheduleJob`, {
Job: this.job
}, (res) => {
fetchDelete(`/api/PeerScheduleJob`, this.job, (res) => {
if (!res.status){
this.store.newMessage("Server", res.message, "danger")
this.$emit('delete')
@@ -99,6 +99,17 @@ export default {
v-model="this.data.name"
id="peer_name_textbox" placeholder="">
</div>
<div>
<label for="peer_notes_textbox" class="form-label">
<small class="text-muted">
<LocaleText t="Notes"></LocaleText>
</small>
</label>
<input type="text" class="form-control form-control-sm rounded-3"
:disabled="this.saving"
v-model="this.data.notes"
id="peer_notes_textbox" placeholder="">
</div>
<div>
<div class="d-flex position-relative">
<label for="peer_private_key_textbox" class="form-label">
@@ -61,7 +61,7 @@ const sendTestEmail = async () => {
<div class="card-header">
<h6 class="my-2 d-flex">
<i class="bi bi-envelope-fill me-2"></i>
<LocaleText t="Email Account"></LocaleText>
<LocaleText t="Email Server Settings"></LocaleText>
<span class="text-success ms-auto" v-if="emailIsReady">
<i class="bi bi-check-circle-fill me-2"></i>
<LocaleText t="Ready"></LocaleText>
@@ -115,6 +115,9 @@ const sendTestEmail = async () => {
<select class="form-select rounded-3"
v-model="store.Configuration.Email.encryption"
id="encryption">
<option value="IMPLICITTLS">
IMPLICIT TLS
</option>
<option value="STARTTLS">
STARTTLS
</option>
@@ -4,19 +4,69 @@ import LocaleText from "@/components/text/localeText.vue";
import ConfigurationTracking
from "@/components/settingsComponent/dashboardWireguardConfigurationTrackingComponents/configurationTracking.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {onMounted, ref, watch} from "vue";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const store = WireguardConfigurationsStore()
const dashboardStore = DashboardConfigurationStore()
const peerTrackingStatus = ref(dashboardStore.Configuration.WireGuardConfiguration.peer_tracking)
const loaded = ref(false)
const trackingData = ref({})
onMounted(async () => {
if (peerTrackingStatus.value){
await loadData()
}
})
const loadData = async () => {
await fetchGet("/api/getPeerTrackingTableCounts", {}, (ref) => {
if (ref.status){
trackingData.value = ref.data
}
loaded.value = true
})
}
watch(peerTrackingStatus, async (newVal) => {
await fetchPost("/api/updateDashboardConfigurationItem", {
section: "WireGuardConfiguration",
key: "peer_tracking",
value: newVal
}, async (res) => {
if (res.status){
dashboardStore.newMessage("Server", newVal ? "Peer tracking enabled" : "Peer tracking disabled", "success")
if (newVal) await loadData()
}
})
})
</script>
<template>
<div class="card">
<div class="card-header">
<div class="card-header d-flex align-items-center">
<h6 class="my-2">
<LocaleText t="Peer Tracking"></LocaleText>
</h6>
<div class="form-check form-switch ms-auto">
<input class="form-check-input"
v-model="peerTrackingStatus"
type="checkbox" role="switch" id="peerTrackingStatus">
<label class="form-check-label" for="peerTrackingStatus">
<LocaleText t="Enabled" v-if="peerTrackingStatus"></LocaleText>
<LocaleText t="Disabled" v-else></LocaleText>
</label>
</div>
<div class="card-body d-flex flex-column gap-3">
<ConfigurationTracking :configuration="configuration" v-for="configuration in store.Configurations"/>
</div>
<div class="card-body d-flex flex-column gap-3" v-if="peerTrackingStatus">
<template v-if="!loaded">
<div class="spinner-border text-body m-auto"></div>
</template>
<template v-else>
<ConfigurationTracking :configuration="configuration"
:trackingData="trackingData"
v-for="configuration in store.Configurations"/>
</template>
</div>
</div>
</template>
@@ -4,7 +4,7 @@ import {onMounted, ref, watch} from "vue";
import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const props = defineProps(['configuration'])
const props = defineProps(['configuration', 'trackingData'])
const sizes = ref({
HistoricalTrackingTableSize: 0,
TrafficTrackingTableSize: 0
@@ -13,16 +13,15 @@ const sizeDataLoaded = ref(false)
const toggling = ref(false)
await onMounted(async () => {
await loadSizeData();
sizes.value = props.trackingData[props.configuration.Name]
})
const loadSizeData = async () => {
sizeDataLoaded.value = false;
await fetchGet("/api/getPeerTrackingTableCounts", {
configurationName: props.configuration.Name
}, (res) => {
sizes.value = res.data;
sizeDataLoaded.value = true;
})
}
@@ -148,8 +147,7 @@ const deleteRecord = async (key) => {
<hr />
<div class="d-flex align-items-start align-items-md-center flex-column flex-md-row gap-2">
<div>
<h6 class="placeholder animate__animated animate__flash animate__infinite animate__slower w-100 mb-0" v-if="!sizeDataLoaded"></h6>
<h6 v-else class="mb-0">
<h6 class="mb-0">
{{ sizes.HistoricalTrackingTableSize }} <span class="text-muted fw-normal"><LocaleText t="Records"></LocaleText></span>
</h6>
</div>
@@ -22,6 +22,8 @@ import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefau
targetData="peer_mtu" title="MTU"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput
targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput
targetData="peer_preshared_key_default" title="Pre-Shared Key Default"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput
targetData="remote_endpoint" title="Peer Remote Endpoint"
:warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
@@ -31,6 +31,11 @@ export default {
mounted() {
this.value = this.store.Configuration.Peers[this.targetData];
},
computed: {
isBoolean(){
return typeof this.value === "boolean"
}
},
methods:{
async useValidation(){
if(this.changed){
@@ -67,7 +72,14 @@ export default {
<LocaleText :t="this.title"></LocaleText>
</small></strong>
</label>
<input type="text" class="form-control"
<div v-if="isBoolean" class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
v-model="this.value"
:id="this.uuid"
@change="this.changed = true; useValidation()"
:disabled="this.updating">
</div>
<input v-else type="text" class="form-control"
:class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
:id="this.uuid"
v-model="this.value"
@@ -0,0 +1,58 @@
<script setup async>
import {computed, onMounted, ref} from "vue";
import {fetchGet} from "@/utilities/fetch.js";
import LocaleText from "@/components/text/localeText.vue";
const providers = ref([])
const oidcEnabled = ref(false)
await fetchGet("/api/oidc/providers", {}, async (res) => {
if (res.status){
await Promise.all(Object.entries(res.data).map(([key, value]) => {
return fetch(`${value.issuer}/.well-known/openid-configuration`).then((res) => {
providers.value.push({
Provider: key,
ProviderInfo: value
})
}).catch(() => {
console.log("Failed to request " + key)
})
}))
oidcEnabled.value = providers.value.length > 0
}
})
console.log(providers.value)
const oidcUrl = (provider) => {
const params = new URLSearchParams({
client_id: provider.ProviderInfo.client_id,
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname,
response_type: 'code',
state: provider.Provider,
scope: 'openid email profile'
})
console.log(params.entries())
const url = new URL(provider.ProviderInfo.openid_configuration.authorization_endpoint)
url.search = params.toString()
return url
}
</script>
<template>
<div>
<div v-if="oidcEnabled">
<hr class="mb-4"/>
<div class="d-flex gap-2">
<a class="btn bg-body border w-100 rounded-3"
:href="oidcUrl(provider)"
v-for="provider in providers" style="flex: 1 1 0;">
<LocaleText :t="'Sign In With ' + provider.Provider"/>
</a>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,11 @@
<script setup>
</script>
<template>
</template>
<style scoped>
</style>
@@ -20,7 +20,6 @@ const squareHeight = computed(() => {
<template>
<div class="flex-grow-1 square rounded-3 border position-relative p-2"
@mouseenter="show = true"
@mouseleave="show = false"
:style="{'background-color': `rgb(13 110 253 / ${percentage*10}%)`}">
@@ -49,7 +49,7 @@ const data = computed(() => {
<div class="progress" role="progressbar" style="height: 6px">
<div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div>
</div>
<div class="d-flex mt-2 gap-1">
<div class="d-grid mt-2 gap-1" style="grid-template-columns: repeat(10, 1fr)">
<CpuCore
v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu"
:key="count"
@@ -74,7 +74,7 @@ const data = computed(() => {
<div class="progress" role="progressbar" style="height: 6px">
<div class="progress-bar bg-success" :style="{width: `${data?.Disks.find(x => x.mountPoint === '/') ? data?.Disks.find(x => x.mountPoint === '/').percent : data?.Disks[0].percent}%` }"></div>
</div>
<div class="d-flex mt-2 gap-1">
<div class="d-grid mt-2 gap-1" style="grid-template-columns: repeat(10, 1fr)">
<StorageMount v-for="(disk, count) in data?.Disks"
v-if="data"
:key="disk.mountPoint"
+48 -1
View File
@@ -7,9 +7,23 @@ import '@vuepic/vue-datepicker/dist/main.css'
import {createApp, markRaw} from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router/router.js'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const params = new URLSearchParams(window.location.search)
const state = params.get('state')
const code = params.get('code')
if (state && code) {
window.history.replaceState({}, '', window.location.pathname);
console.log('After replaceState:', window.location.href);
}
const initApp = async () => {
const { default: router } = await import('./router/router.js')
const app = createApp(App)
app.use(router)
@@ -22,3 +36,36 @@ pinia.use(({ store }) => {
app.use(pinia)
app.mount('#app')
console.log('After router import:', window.location.href)
}
export const getUrl = (url) => {
if (import.meta.env.MODE === 'development') {
return url;
}
return `./.${url}`;
}
if (state && code){
await fetch(getUrl('/api/oidc/authenticate'), {
method: "POST",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
provider: state,
code: code,
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
})
}).then(res => res.json()).then( async (res) => {
if (res.status){
window.location.replace(window.location.protocol + '//' + window.location.host + window.location.pathname)
}else{
await initApp()
const store = DashboardConfigurationStore()
store.newMessage('Server OIDC', res.message, 'danger')
}
})
}else{
await initApp()
}
+57 -2
View File
@@ -4,6 +4,7 @@ const getHeaders = () => {
let headers = {
"Content-Type": "application/json"
}
try{
const store = DashboardConfigurationStore();
const crossServer = store.getActiveCrossServer();
if (crossServer){
@@ -16,19 +17,27 @@ const getHeaders = () => {
}
}
}
}catch (e) {
}
return headers
}
export const getUrl = (url) => {
try{
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
return `${apiKey.host}${url}`
}
return import.meta.env.MODE === 'development' ? url
: `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
}catch (e){}
if (import.meta.env.MODE === 'development') {
return url;
}
// const appPrefix = window.APP_PREFIX || '';
return `./.${url}`;
}
export const fetchGet = async (url, params=undefined, callback=undefined) => {
@@ -77,3 +86,49 @@ export const fetchPost = async (url, body, callback) => {
router.push({path: '/signin'})
})
}
export const fetchPut = async (url, body, callback) => {
await fetch(`${getUrl(url)}`, {
headers: getHeaders(),
method: "PUT",
body: JSON.stringify(body)
}).then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log("Error:", x)
router.push({path: '/signin'})
})
}
export const fetchDelete = async (url, body, callback) => {
await fetch(`${getUrl(url)}`, {
headers: getHeaders(),
method: "DELETE",
body: JSON.stringify(body)
}).then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
store.newMessage("WGDashboard", "Sign in session ended, please sign in again", "warning")
}
throw new Error(x.statusText)
}
}else{
return x.json()
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log("Error:", x)
router.push({path: '/signin'})
})
}
+19 -10
View File
@@ -43,10 +43,17 @@ export default {
Jmax: 998,
S1: 17,
S2: 110,
S3: 1,
S4: 2,
H1: 0,
H2: 0,
H3: 0,
H4: 0
H4: 0,
I1: "0",
I2: "0",
I3: "0",
I4: "0",
I5: "0"
},
numberOfAvailableIPs: "0",
error: false,
@@ -59,14 +66,16 @@ export default {
},
created() {
this.wireguardGenerateKeypair();
let hValue = []
while ([...new Set(hValue)].length !== 4){
hValue = [this.rand(1, (2**31) - 1), this.rand(1, (2**31) - 1), this.rand(1, (2**31) - 1), this.rand(1, (2**31) - 1)]
}
this.newConfiguration.H1 = hValue[0]
this.newConfiguration.H2 = hValue[1]
this.newConfiguration.H3 = hValue[2]
this.newConfiguration.H4 = hValue[3]
// Generate 4 random numbers for H1, H2, H3, H4
['H1', 'H2', 'H3', 'H4'].forEach(key => {
this.newConfiguration[key] = this.rand(1, 2**31);
});
// Initialize I1 to I5 as "0"
['I1', 'I2', 'I3', 'I4', 'I5'].forEach(key => {
this.newConfiguration[key] = "0";
});
},
methods: {
rand(min, max){
@@ -379,7 +388,7 @@ export default {
<div class="card rounded-3"
v-if="this.newConfiguration.Protocol === 'awg'"
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4']">
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']">
<div class="card-header">{{ key }}</div>
<div class="card-body">
<input type="text"
+22 -6
View File
@@ -7,10 +7,11 @@ import {GetLocale} from "@/utilities/locale.js";
import LocaleText from "@/components/text/localeText.vue";
import SignInInput from "@/components/signIn/signInInput.vue";
import SignInTOTP from "@/components/signIn/signInTOTP.vue";
import SignInOIDC from "@/components/signIn/signInOIDC.vue";
export default {
name: "signin",
components: {SignInTOTP, SignInInput, LocaleText, RemoteServerList, Message},
components: {SignInOIDC, SignInTOTP, SignInInput, LocaleText, RemoteServerList, Message},
async setup(){
const store = DashboardConfigurationStore()
let theme = "dark"
@@ -171,23 +172,38 @@ export default {
</form>
<RemoteServerList v-else></RemoteServerList>
<div class="d-flex mt-3" v-if="!this.store.IsElectronApp">
<div class="d-flex mt-3 flex-column" v-if="!this.store.IsElectronApp">
<div class="form-check form-switch ms-auto">
<input
v-model="this.store.CrossServerConfiguration.Enable"
:disabled="loading"
class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked">
class="form-check-input"
type="checkbox"
role="switch"
id="flexSwitchCheckChecked">
<label class="form-check-label" for="flexSwitchCheckChecked">
<LocaleText t="Access Remote Server"></LocaleText>
</label>
</div>
<SignInOIDC />
</div>
</div>
</div>
<small class="text-muted pb-3 d-block w-100 text-center mt-3">
WGDashboard {{ this.version }} | Developed with by
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
<div class="d-flex container-fluid align-items-center my-1 w-100">
<small class="text-muted">
WGDashboard <strong>{{ this.version }}</strong> | Made with by
<a href="https://github.com/WGDashboard"
class="text-decoration-none text-body"
target="_blank"><strong>WGDashboard</strong></a>
</small>
<a href="./client" target="_blank"
class="text-decoration-none ms-auto text-body"
style="white-space: nowrap">
<small><i class="bi bi-box-arrow-up-right me-1"></i>
<LocaleText t="Client App" /></small>
</a>
</div>
<div class="messageCentre text-body position-absolute d-flex">
<TransitionGroup name="message" tag="div"
class="position-relative flex-sm-grow-0 flex-grow-1 d-flex align-items-end ms-sm-auto flex-column gap-2">
+2 -2
View File
@@ -176,7 +176,7 @@ const memoryHistoricalChartData = computed(() => {
<div class="card rounded-3 h-100 shadow">
<div class="card-body p-4">
<div class="d-flex flex-column gap-3">
<div class="d-flex flex-column gap-3" style="height: 130px">
<div class="d-flex flex-column gap-3" style="min-height: 130px">
<div class="d-flex align-items-center">
<h3 class="text-muted mb-0">
<i class="bi bi-cpu-fill me-2"></i>
@@ -192,7 +192,7 @@ const memoryHistoricalChartData = computed(() => {
<div class="progress" role="progressbar" style="height: 10px">
<div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div>
</div>
<div class="d-flex gap-1">
<div class="d-grid gap-1" style="grid-template-columns: repeat(10, 1fr)">
<CpuCore
v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu"
:square="true"
+1 -1
View File
@@ -32,7 +32,7 @@ export default defineConfig(({mode}) => {
}
return {
base: "/static/dist/WGDashboardAdmin",
base: "./",
plugins: [
vue(),
],
+11 -3
View File
@@ -2,9 +2,10 @@
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/Logo-2-128x128.png">
<link rel="icon" href="./img/Logo-2-128x128.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WGDashboard Client</title>
<base href="./client/">
<style>
*{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
@@ -28,15 +29,22 @@
}
}
</style>
<script>
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
const base = document.querySelector('base');
if (base && isViteDevMode) {
base.href = '/';
}
</script>
</head>
<body>
<div id="app">
<div id="preloader">
<div id="preloader_placeholder">
<img style="width: 100%" src="/img/Logo-2-128x128.png" alt="WGDashboard Client" />
<img style="width: 100%" src="./img/Logo-2-128x128.png" alt="WGDashboard Client" />
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="./src/main.js"></script>
</body>
</html>
+1675 -1726
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "client",
"version": "v1.0",
"version": "v1.0.1",
"private": true,
"type": "module",
"scripts": {
@@ -9,20 +9,20 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.9.0",
"bootstrap": "^5.3.6",
"axios": "^1.18.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"oidc-client-ts": "^3.2.1",
"pinia": "^3.0.2",
"dayjs": "^1.11.21",
"oidc-client-ts": "^3.5.0",
"pinia": "^3.0.4",
"qrcode": "^1.5.4",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
"uuid": "^14.0.0",
"vue": "^3.5.35",
"vue-router": "^5.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2"
"@vitejs/plugin-vue": "^6.0.6",
"vite": "^8.0.16",
"vite-plugin-vue-devtools": "^8.1.2"
}
}
+2 -10
View File
@@ -1,20 +1,12 @@
<script setup async>
<script setup>
import './assets/main.css'
import NotificationList from "@/components/Notification/notificationList.vue";
import {clientStore} from "@/stores/clientStore.js";
const store = clientStore()
fetch("/client/api/serverInformation")
.then(res => res.json())
.then(res => store.serverInformation = res.data)
</script>
<template>
<div data-bs-theme="dark" class="text-body bg-body vw-100 vh-100 bg-body">
<div class="d-flex vw-100 p-sm-4 overflow-y-scroll innerContainer d-flex flex-column">
<div class="mx-auto my-sm-auto position-relative"
id="listContainer"
>
<div class="mx-auto my-sm-auto position-relative" id="listContainer">
<Suspense>
<RouterView v-slot="{ Component }">
<Transition name="app" type="transition" mode="out-in">
@@ -2,7 +2,7 @@
import {computed, ref} from "vue";
import ConfigurationQRCode from "@/components/Configuration/configurationQRCode.vue";
import dayjs from "dayjs";
import Duration from 'dayjs/plugin/Duration'
import Duration from 'dayjs/plugin/duration'
dayjs.extend(Duration);
const props = defineProps([
'config'
@@ -96,7 +96,7 @@ if (route.query.Email){
</span>
</button>
</form>
<div>
<div v-if="store.serverInformation.SignUp.enable">
<hr class="my-4">
<div class="d-flex align-items-center">
<span class="text-muted">
+21 -18
View File
@@ -2,47 +2,50 @@ import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from "@/router/router.js";
import {createPinia} from "pinia";
import 'bootstrap/dist/js/bootstrap.bundle.js'
import {axiosPost} from "@/utilities/request.js";
import {axiosGet, axiosPost} from "@/utilities/request.js";
import {clientStore} from "@/stores/clientStore.js";
const params = new URLSearchParams(window.location.search)
const state = params.get('state')
const code = params.get('code')
const initApp = () => {
if (state && code) {
window.history.replaceState({}, '', window.location.pathname);
}
const initApp = async () => {
debugger
const { default: router } = await import('./router/router.js')
const app = createApp(App)
const serverInformation = await axiosGet("/api/serverInformation", {})
app.use(createPinia())
if (serverInformation){
const store = clientStore()
store.serverInformation = serverInformation.data;
}
app.use(router)
app.mount("#app")
}
function removeSearchString() {
let url = new URL(window.location.href);
url.search = ''; // Remove all query parameters
history.replaceState({}, document.title, url.toString());
}
if (state && code){
axiosPost("/api/signin/oidc", {
await axiosPost("/api/signin/oidc", {
provider: state,
code: code,
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
}).then(data => {
let url = new URL(window.location.href);
url.search = '';
history.replaceState({}, document.title, url.toString());
initApp()
if (!data.status){
}).then(async (data) => {
if (data.status){
window.location.replace(window.location.protocol + '//' + window.location.host + window.location.pathname)
}else {
await initApp()
const store = clientStore()
store.newNotification(data.message, 'danger')
}
})
}else{
initApp()
await initApp()
}
+5
View File
@@ -50,6 +50,11 @@ const router = createRouter({
})
router.beforeEach(async (to, from, next) => {
const store = clientStore()
if (to.path === "/signup" && !store.serverInformation.SignUp.enable){
next('/signin')
store.newNotification("Sign up is disabled. Please contact administrator for more information", "warning")
}
if (to.path === '/signout'){
await axios.get(requestURl('/api/signout')).then(() => {
next('/signin')
+4 -12
View File
@@ -1,12 +1,11 @@
import axios from "axios";
import {useRouter} from "vue-router";
export const requestURl = (url) => {
return import.meta.env.MODE === 'development' ? '/client' + url
: `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
if (import.meta.env.MODE === 'development') {
return '/client' + url;
}
return `./.${url}`;
}
// const router = useRouter()
export const axiosPost = async (URL, body = {}) => {
try{
@@ -14,10 +13,6 @@ export const axiosPost = async (URL, body = {}) => {
return res.data
} catch (error){
console.log(error)
// if (error.status === 401){
// await router.push('/signin')
// }
return undefined
}
}
@@ -28,9 +23,6 @@ export const axiosGet = async (URL, query = {}) => {
return res.data
} catch (error){
console.log(error)
// if (error.status === 401){
// await router.push('/signin')
// }
return undefined
}
}
+1 -1
View File
@@ -40,5 +40,5 @@ export default defineConfig({
}
}
},
base: '/static/dist/WGDashboardClient'
base: './'
})
@@ -1 +1 @@
import{a5 as A,r as n,D as S,g as l,z as v}from"./index-CmClDcBF.js";const b=A("DashboardClientAssignmentStore",()=>{const f=n({}),d=n([]),o=n({}),c=n([]),g=n(!1),r=n(""),i=S(),w=async()=>{await l("/api/clients/allClients",{},s=>{o.value=s.data})},y=async()=>{await l("/api/clients/allClientsRaw",{},s=>{c.value=s.data,console.log(c.value)})},m=s=>Object.values(o.value).flat().find(e=>e.ClientID===s),u=async(s,e)=>{await l("/api/clients/assignedClients",{ConfigurationName:s,Peer:e},a=>{d.value=a.data})};return{assignments:d,getAssignedClients:u,getClients:w,getClientsRaw:y,clients:o,unassignClient:async(s,e,a)=>{g.value=!0,await v("/api/clients/unassignClient",{AssignmentID:a},async t=>{t.status?(i.newMessage("Server","Unassign successfully!","success"),s&&e&&await u(s,e)):(i.newMessage("Server","Unassign Failed. Reason: "+t.message,"success"),console.error("Unassign Failed. Reason: "+t.message)),g.value=!1})},assignClient:async(s,e,a,t=!0)=>{r.value=a,await v("/api/clients/assignClient",{ConfigurationName:s,Peer:e,ClientID:a},async C=>{C.status?(i.newMessage("Server","Assign successfully!","success"),t&&await u(s,e)):(i.newMessage("Server","Assign Failed. Reason: "+C.message,"success"),console.error("Assign Failed. Reason: "+C.message)),r.value=""})},getClientById:m,unassigning:g,assigning:r,clientsRaw:c,allConfigurationsPeers:f,getAllConfigurationsPeers:async()=>{await l("/api/clients/allConfigurationsPeers",{},s=>{f.value=s.data})}}});export{b as D};
import{a5 as A,r as n,D as S,g as l,z as v}from"./index-DM7YJCOo.js";const b=A("DashboardClientAssignmentStore",()=>{const f=n({}),d=n([]),o=n({}),c=n([]),g=n(!1),r=n(""),i=S(),w=async()=>{await l("/api/clients/allClients",{},s=>{o.value=s.data})},y=async()=>{await l("/api/clients/allClientsRaw",{},s=>{c.value=s.data,console.log(c.value)})},m=s=>Object.values(o.value).flat().find(e=>e.ClientID===s),u=async(s,e)=>{await l("/api/clients/assignedClients",{ConfigurationName:s,Peer:e},a=>{d.value=a.data})};return{assignments:d,getAssignedClients:u,getClients:w,getClientsRaw:y,clients:o,unassignClient:async(s,e,a)=>{g.value=!0,await v("/api/clients/unassignClient",{AssignmentID:a},async t=>{t.status?(i.newMessage("Server","Unassign successfully!","success"),s&&e&&await u(s,e)):(i.newMessage("Server","Unassign Failed. Reason: "+t.message,"success"),console.error("Unassign Failed. Reason: "+t.message)),g.value=!1})},assignClient:async(s,e,a,t=!0)=>{r.value=a,await v("/api/clients/assignClient",{ConfigurationName:s,Peer:e,ClientID:a},async C=>{C.status?(i.newMessage("Server","Assign successfully!","success"),t&&await u(s,e)):(i.newMessage("Server","Assign Failed. Reason: "+C.message,"success"),console.error("Assign Failed. Reason: "+C.message)),r.value=""})},getClientById:m,unassigning:g,assigning:r,clientsRaw:c,allConfigurationsPeers:f,getAllConfigurationsPeers:async()=>{await l("/api/clients/allConfigurationsPeers",{},s=>{f.value=s.data})}}});export{b as D};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as r,c as i,b as o,w as e,k as l,j as a,l as _,S as u,h as d,f as t}from"./index-CmClDcBF.js";const m={name:"configuration"},f={class:"mt-md-5 mt-3 text-body"};function p(h,k,x,w,$,v){const n=d("RouterView");return t(),i("div",f,[o(n,null,{default:e(({Component:s,route:c})=>[o(l,{name:"fade2",mode:"out-in"},{default:e(()=>[(t(),a(u,null,{default:e(()=>[(t(),a(_(s),{key:c.path,class:"z-1"}))]),_:2},1024))]),_:2},1024)]),_:1})])}const B=r(m,[["render",p]]);export{B as default};
import{_ as r,c as i,b as o,w as e,k as l,j as a,l as _,S as u,h as d,f as t}from"./index-DM7YJCOo.js";const m={name:"configuration"},f={class:"mt-md-5 mt-3 text-body"};function p(h,k,x,w,$,v){const n=d("RouterView");return t(),i("div",f,[o(n,null,{default:e(({Component:s,route:c})=>[o(l,{name:"fade2",mode:"out-in"},{default:e(()=>[(t(),a(u,null,{default:e(()=>[(t(),a(_(s),{key:c.path,class:"z-1"}))]),_:2},1024))]),_:2},1024)]),_:1})])}const B=r(m,[["render",p]]);export{B as default};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
.fade-enter-active[data-v-9f596f5e]{transition-delay:var(--v0d365bfc)!important}.progress-bar[data-v-851170e4]{width:0;transition:all 1s cubic-bezier(.42,0,.22,1)}.filter a[data-v-7ed053f0]{text-decoration:none}
.fade-enter-active[data-v-9f596f5e]{transition-delay:var(--v0d365bfc)!important}.progress-bar[data-v-01ef60a9]{width:0;transition:all 1s cubic-bezier(.42,0,.22,1)}.filter a[data-v-7ed053f0]{text-decoration:none}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as f,c as i,a as t,b as u,h as w,d as k,m as x,y,n as p,t as v,z as _,D as m,W as b,A as S,f as n,r as D,q as $,F as W,i as V}from"./index-CmClDcBF.js";import{L as C}from"./localeText-TzABauzQ.js";const F={name:"dashboardSettingsInputWireguardConfigurationPath",components:{LocaleText:C},props:{targetData:String,title:String,warning:!1,warningText:""},setup(){const o=m(),s=b(),r=`input_${S()}`;return{store:o,uuid:r,WireguardConfigurationStore:s}},data(){return{value:"",invalidFeedback:"",showInvalidFeedback:!1,isValid:!1,timeout:void 0,changed:!1,updating:!1}},mounted(){this.value=this.store.Configuration.Server[this.targetData]},methods:{async useValidation(){this.changed&&(this.updating=!0,await _("/api/updateDashboardConfigurationItem",{section:"Server",key:this.targetData,value:this.value},o=>{o.status?(this.isValid=!0,this.showInvalidFeedback=!1,this.store.Configuration.Account[this.targetData]=this.value,clearTimeout(this.timeout),this.timeout=setTimeout(()=>this.isValid=!1,5e3),this.WireguardConfigurationStore.getConfigurations(),this.store.newMessage("Server","WireGuard configuration path saved","success")):(this.isValid=!1,this.showInvalidFeedback=!0,this.invalidFeedback=o.message),this.changed=!1,this.updating=!1}))}}},I={class:"card"},T={class:"card-header"},A={class:"my-2"},L={class:"card-body"},M={class:"form-group"},N=["for"],P={class:"d-flex gap-2 align-items-start"},B={class:"flex-grow-1"},G=["id","disabled"],z={class:"invalid-feedback fw-bold"},U=["disabled"],q={key:0,class:"bi bi-save2-fill"},E={key:1,class:"spinner-border spinner-border-sm"},K={key:0,class:"px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1 mb-2"};function j(o,s,r,a,c,g){const d=w("LocaleText");return n(),i("div",I,[t("div",T,[t("h6",A,[u(d,{t:"Path"})])]),t("div",L,[t("div",M,[t("label",{for:this.uuid,class:"text-muted mb-1"},[t("strong",null,[t("small",null,[u(d,{t:this.title},null,8,["t"])])])],8,N),t("div",P,[t("div",B,[x(t("input",{type:"text",class:p(["form-control rounded-3",{"is-invalid":this.showInvalidFeedback,"is-valid":this.isValid}]),id:this.uuid,"onUpdate:modelValue":s[0]||(s[0]=e=>this.value=e),onKeydown:s[1]||(s[1]=e=>this.changed=!0),disabled:this.updating},null,42,G),[[y,this.value]]),t("div",z,v(this.invalidFeedback),1)]),t("button",{onClick:s[2]||(s[2]=e=>this.useValidation()),disabled:!this.changed,class:"ms-auto btn rounded-3 border-success-subtle bg-success-subtle text-success-emphasis"},[this.updating?(n(),i("span",E)):(n(),i("i",q))],8,U)]),r.warning?(n(),i("div",K,[t("small",null,[s[3]||(s[3]=t("i",{class:"bi bi-exclamation-triangle-fill me-2"},null,-1)),u(d,{t:r.warningText},null,8,["t"])])])):k("",!0)])])])}const et=f(F,[["render",j]]),H={class:"card rounded-3"},J={class:"card-header"},O={class:"my-2"},Q={class:"card-body d-flex gap-2"},R={class:"list-group w-100"},X=["onClick"],Y={__name:"dashboardSettingsWireguardConfigurationAutostart",setup(o){const s=m(),r=b(),a=D(s.Configuration.WireGuardConfiguration.autostart),c=$(()=>r.Configurations.map(e=>e.Name)),g=async()=>{await _("/api/updateDashboardConfigurationItem",{section:"WireGuardConfiguration",key:"autostart",value:a.value},async e=>{e.status?(s.newMessage("Server","Start up configurations saved","success"),a.value=e.data):s.newMessage("Server","Start up configurations failed to save","danger")})},d=e=>{a.value.includes(e)?a.value=a.value.filter(h=>h!==e):a.value.push(e),g()};return(e,h)=>(n(),i("div",H,[t("div",J,[t("h6",O,[u(C,{t:"Toggle When Start Up"})])]),t("div",Q,[t("div",R,[(n(!0),i(W,null,V(c.value,l=>(n(),i("button",{type:"button",key:l,onClick:Z=>d(l),class:"list-group-item list-group-item-action py-2 w-100 d-flex align-items-center"},[t("samp",null,v(l),1),t("i",{class:p(["ms-auto",[a.value.includes(l)?"bi-check-circle-fill":"bi-circle"]])},null,2)],8,X))),128))])])]))}},at=f(Y,[["__scopeId","data-v-4aa2aed9"]]);export{et as D,at as a};
import{_ as f,c as i,a as t,b as u,h as w,d as k,m as x,y,n as p,t as v,z as _,D as m,W as b,A as S,f as n,r as D,q as $,F as W,i as V}from"./index-DM7YJCOo.js";import{L as C}from"./localeText-BJvnc1lF.js";const F={name:"dashboardSettingsInputWireguardConfigurationPath",components:{LocaleText:C},props:{targetData:String,title:String,warning:!1,warningText:""},setup(){const o=m(),s=b(),r=`input_${S()}`;return{store:o,uuid:r,WireguardConfigurationStore:s}},data(){return{value:"",invalidFeedback:"",showInvalidFeedback:!1,isValid:!1,timeout:void 0,changed:!1,updating:!1}},mounted(){this.value=this.store.Configuration.Server[this.targetData]},methods:{async useValidation(){this.changed&&(this.updating=!0,await _("/api/updateDashboardConfigurationItem",{section:"Server",key:this.targetData,value:this.value},o=>{o.status?(this.isValid=!0,this.showInvalidFeedback=!1,this.store.Configuration.Account[this.targetData]=this.value,clearTimeout(this.timeout),this.timeout=setTimeout(()=>this.isValid=!1,5e3),this.WireguardConfigurationStore.getConfigurations(),this.store.newMessage("Server","WireGuard configuration path saved","success")):(this.isValid=!1,this.showInvalidFeedback=!0,this.invalidFeedback=o.message),this.changed=!1,this.updating=!1}))}}},I={class:"card"},T={class:"card-header"},A={class:"my-2"},L={class:"card-body"},M={class:"form-group"},N=["for"],P={class:"d-flex gap-2 align-items-start"},B={class:"flex-grow-1"},G=["id","disabled"],z={class:"invalid-feedback fw-bold"},U=["disabled"],q={key:0,class:"bi bi-save2-fill"},E={key:1,class:"spinner-border spinner-border-sm"},K={key:0,class:"px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1 mb-2"};function j(o,s,r,a,c,g){const d=w("LocaleText");return n(),i("div",I,[t("div",T,[t("h6",A,[u(d,{t:"Path"})])]),t("div",L,[t("div",M,[t("label",{for:this.uuid,class:"text-muted mb-1"},[t("strong",null,[t("small",null,[u(d,{t:this.title},null,8,["t"])])])],8,N),t("div",P,[t("div",B,[x(t("input",{type:"text",class:p(["form-control rounded-3",{"is-invalid":this.showInvalidFeedback,"is-valid":this.isValid}]),id:this.uuid,"onUpdate:modelValue":s[0]||(s[0]=e=>this.value=e),onKeydown:s[1]||(s[1]=e=>this.changed=!0),disabled:this.updating},null,42,G),[[y,this.value]]),t("div",z,v(this.invalidFeedback),1)]),t("button",{onClick:s[2]||(s[2]=e=>this.useValidation()),disabled:!this.changed,class:"ms-auto btn rounded-3 border-success-subtle bg-success-subtle text-success-emphasis"},[this.updating?(n(),i("span",E)):(n(),i("i",q))],8,U)]),r.warning?(n(),i("div",K,[t("small",null,[s[3]||(s[3]=t("i",{class:"bi bi-exclamation-triangle-fill me-2"},null,-1)),u(d,{t:r.warningText},null,8,["t"])])])):k("",!0)])])])}const et=f(F,[["render",j]]),H={class:"card rounded-3"},J={class:"card-header"},O={class:"my-2"},Q={class:"card-body d-flex gap-2"},R={class:"list-group w-100"},X=["onClick"],Y={__name:"dashboardSettingsWireguardConfigurationAutostart",setup(o){const s=m(),r=b(),a=D(s.Configuration.WireGuardConfiguration.autostart),c=$(()=>r.Configurations.map(e=>e.Name)),g=async()=>{await _("/api/updateDashboardConfigurationItem",{section:"WireGuardConfiguration",key:"autostart",value:a.value},async e=>{e.status?(s.newMessage("Server","Start up configurations saved","success"),a.value=e.data):s.newMessage("Server","Start up configurations failed to save","danger")})},d=e=>{a.value.includes(e)?a.value=a.value.filter(h=>h!==e):a.value.push(e),g()};return(e,h)=>(n(),i("div",H,[t("div",J,[t("h6",O,[u(C,{t:"Toggle When Start Up"})])]),t("div",Q,[t("div",R,[(n(!0),i(W,null,V(c.value,l=>(n(),i("button",{type:"button",key:l,onClick:Z=>d(l),class:"list-group-item list-group-item-action py-2 w-100 d-flex align-items-center"},[t("samp",null,v(l),1),t("i",{class:p(["ms-auto",[a.value.includes(l)?"bi-check-circle-fill":"bi-circle"]])},null,2)],8,X))),128))])])]))}},at=f(Y,[["__scopeId","data-v-4aa2aed9"]]);export{et as D,at as a};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{I as j,q as w,P as S,J as k,Q as L,u as R}from"./index-CmClDcBF.js";const W=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const X=Object.prototype.toString,Y=t=>X.call(t)==="[object Object]",$=()=>{};function C(t){return Array.isArray(t)?t:[t]}function q(t,a,r){return j(t,a,{...r,immediate:!0})}const I=W?window:void 0;function P(t){var a;const r=S(t);return(a=r?.$el)!==null&&a!==void 0?a:r}function T(...t){const a=(o,u,s,d)=>(o.addEventListener(u,s,d),()=>o.removeEventListener(u,s,d)),r=w(()=>{const o=C(S(t[0])).filter(u=>u!=null);return o.every(u=>typeof u!="string")?o:void 0});return q(()=>{var o,u;return[(o=(u=r.value)===null||u===void 0?void 0:u.map(s=>P(s)))!==null&&o!==void 0?o:[I].filter(s=>s!=null),C(S(r.value?t[1]:t[0])),C(R(r.value?t[2]:t[1])),S(r.value?t[3]:t[2])]},([o,u,s,d],p,c)=>{if(!o?.length||!u?.length||!s?.length)return;const f=Y(d)?{...d}:d,v=o.flatMap(b=>u.flatMap(h=>s.map(y=>a(b,h,y,f))));c(()=>{v.forEach(b=>b())})},{flush:"post"})}function B(t,a,r={}){const{window:o=I,ignore:u=[],capture:s=!0,detectIframe:d=!1,controls:p=!1}=r;if(!o)return p?{stop:$,cancel:$,trigger:$}:$;let c=!0;const f=e=>S(u).some(n=>{if(typeof n=="string")return Array.from(o.document.querySelectorAll(n)).some(i=>i===e.target||e.composedPath().includes(i));{const i=P(n);return i&&(e.target===i||e.composedPath().includes(i))}});function v(e){const n=S(e);return n&&n.$.subTree.shapeFlag===16}function b(e,n){const i=S(e),m=i.$.subTree&&i.$.subTree.children;return m==null||!Array.isArray(m)?!1:m.some(A=>A.el===n.target||n.composedPath().includes(A.el))}const h=e=>{const n=P(t);if(e.target!=null&&!(!(n instanceof Element)&&v(t)&&b(t,e))&&!(!n||n===e.target||e.composedPath().includes(n))){if("detail"in e&&e.detail===0&&(c=!f(e)),!c){c=!0;return}a(e)}};let y=!1;const E=[T(o,"click",e=>{y||(y=!0,setTimeout(()=>{y=!1},0),h(e))},{passive:!0,capture:s}),T(o,"pointerdown",e=>{const n=P(t);c=!f(e)&&!!(n&&!e.composedPath().includes(n))},{passive:!0}),d&&T(o,"blur",e=>{setTimeout(()=>{var n;const i=P(t);((n=o.document.activeElement)===null||n===void 0?void 0:n.tagName)==="IFRAME"&&!i?.contains(o.document.activeElement)&&a(e)},0)},{passive:!0})].filter(Boolean),x=()=>E.forEach(e=>e());return p?{stop:x,cancel:()=>{c=!1},trigger:e=>{c=!0,h(e),c=!1}}:x}function D(t,a={}){const{threshold:r=50,onSwipe:o,onSwipeEnd:u,onSwipeStart:s,passive:d=!0}=a,p=k({x:0,y:0}),c=k({x:0,y:0}),f=w(()=>p.x-c.x),v=w(()=>p.y-c.y),{max:b,abs:h}=Math,y=w(()=>b(h(f.value),h(v.value))>=r),E=L(!1),x=w(()=>y.value?h(f.value)>h(v.value)?f.value>0?"left":"right":v.value>0?"up":"down":"none"),e=l=>[l.touches[0].clientX,l.touches[0].clientY],n=(l,g)=>{p.x=l,p.y=g},i=(l,g)=>{c.x=l,c.y=g},m={passive:d,capture:!d},A=l=>{E.value&&u?.(l,x.value),E.value=!1},O=[T(t,"touchstart",l=>{if(l.touches.length!==1)return;const[g,M]=e(l);n(g,M),i(g,M),s?.(l)},m),T(t,"touchmove",l=>{if(l.touches.length!==1)return;const[g,M]=e(l);i(g,M),m.capture&&!m.passive&&Math.abs(f.value)>Math.abs(v.value)&&l.preventDefault(),!E.value&&y.value&&(E.value=!0),E.value&&o?.(l)},m),T(t,["touchend","touchcancel"],A,m)];return{isSwiping:E,direction:x,coordsStart:p,coordsEnd:c,lengthX:f,lengthY:v,stop:()=>O.forEach(l=>l())}}export{D as a,B as o,P as u};
import{H as I,q as w,P as S,J as k,Q as L,u as R}from"./index-DM7YJCOo.js";const W=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const X=Object.prototype.toString,Y=t=>X.call(t)==="[object Object]",$=()=>{};function C(t){return Array.isArray(t)?t:[t]}function q(t,a,r){return I(t,a,{...r,immediate:!0})}const O=W?window:void 0;function P(t){var a;const r=S(t);return(a=r?.$el)!==null&&a!==void 0?a:r}function T(...t){const a=(o,u,s,d)=>(o.addEventListener(u,s,d),()=>o.removeEventListener(u,s,d)),r=w(()=>{const o=C(S(t[0])).filter(u=>u!=null);return o.every(u=>typeof u!="string")?o:void 0});return q(()=>{var o,u;return[(o=(u=r.value)===null||u===void 0?void 0:u.map(s=>P(s)))!==null&&o!==void 0?o:[O].filter(s=>s!=null),C(S(r.value?t[1]:t[0])),C(R(r.value?t[2]:t[1])),S(r.value?t[3]:t[2])]},([o,u,s,d],p,c)=>{if(!o?.length||!u?.length||!s?.length)return;const f=Y(d)?{...d}:d,v=o.flatMap(b=>u.flatMap(h=>s.map(y=>a(b,h,y,f))));c(()=>{v.forEach(b=>b())})},{flush:"post"})}function B(t,a,r={}){const{window:o=O,ignore:u=[],capture:s=!0,detectIframe:d=!1,controls:p=!1}=r;if(!o)return p?{stop:$,cancel:$,trigger:$}:$;let c=!0;const f=e=>S(u).some(n=>{if(typeof n=="string")return Array.from(o.document.querySelectorAll(n)).some(i=>i===e.target||e.composedPath().includes(i));{const i=P(n);return i&&(e.target===i||e.composedPath().includes(i))}});function v(e){const n=S(e);return n&&n.$.subTree.shapeFlag===16}function b(e,n){const i=S(e),m=i.$.subTree&&i.$.subTree.children;return m==null||!Array.isArray(m)?!1:m.some(A=>A.el===n.target||n.composedPath().includes(A.el))}const h=e=>{const n=P(t);if(e.target!=null&&!(!(n instanceof Element)&&v(t)&&b(t,e))&&!(!n||n===e.target||e.composedPath().includes(n))){if("detail"in e&&e.detail===0&&(c=!f(e)),!c){c=!0;return}a(e)}};let y=!1;const E=[T(o,"click",e=>{y||(y=!0,setTimeout(()=>{y=!1},0),h(e))},{passive:!0,capture:s}),T(o,"pointerdown",e=>{const n=P(t);c=!f(e)&&!!(n&&!e.composedPath().includes(n))},{passive:!0}),d&&T(o,"blur",e=>{setTimeout(()=>{var n;const i=P(t);((n=o.document.activeElement)===null||n===void 0?void 0:n.tagName)==="IFRAME"&&!i?.contains(o.document.activeElement)&&a(e)},0)},{passive:!0})].filter(Boolean),x=()=>E.forEach(e=>e());return p?{stop:x,cancel:()=>{c=!1},trigger:e=>{c=!0,h(e),c=!1}}:x}function D(t,a={}){const{threshold:r=50,onSwipe:o,onSwipeEnd:u,onSwipeStart:s,passive:d=!0}=a,p=k({x:0,y:0}),c=k({x:0,y:0}),f=w(()=>p.x-c.x),v=w(()=>p.y-c.y),{max:b,abs:h}=Math,y=w(()=>b(h(f.value),h(v.value))>=r),E=L(!1),x=w(()=>y.value?h(f.value)>h(v.value)?f.value>0?"left":"right":v.value>0?"up":"down":"none"),e=l=>[l.touches[0].clientX,l.touches[0].clientY],n=(l,g)=>{p.x=l,p.y=g},i=(l,g)=>{c.x=l,c.y=g},m={passive:d,capture:!d},A=l=>{E.value&&u?.(l,x.value),E.value=!1},j=[T(t,"touchstart",l=>{if(l.touches.length!==1)return;const[g,M]=e(l);n(g,M),i(g,M),s?.(l)},m),T(t,"touchmove",l=>{if(l.touches.length!==1)return;const[g,M]=e(l);i(g,M),m.capture&&!m.passive&&Math.abs(f.value)>Math.abs(v.value)&&l.preventDefault(),!E.value&&y.value&&(E.value=!0),E.value&&o?.(l)},m),T(t,["touchend","touchcancel"],A,m)];return{isSwiping:E,direction:x,coordsStart:p,coordsEnd:c,lengthX:f,lengthY:v,stop:()=>j.forEach(l=>l())}}export{D as a,B as o,P as u};
@@ -1 +1 @@
import{_ as e,G as t,c as o,t as a,f as c}from"./index-CmClDcBF.js";const s={name:"localeText",props:{t:""},computed:{getLocaleText(){return t(this.t)}}};function n(r,p,l,_,i,x){return c(),o("span",null,a(this.getLocaleText),1)}const m=e(s,[["render",n]]);export{m as L};
import{_ as e,G as t,c as o,t as a,f as c}from"./index-DM7YJCOo.js";const s={name:"localeText",props:{t:""},computed:{getLocaleText(){return t(this.t)}}};function n(r,p,l,_,i,x){return c(),o("span",null,a(this.getLocaleText),1)}const m=e(s,[["render",n]]);export{m as L};
@@ -1 +1 @@
import{L as l}from"./localeText-TzABauzQ.js";import{d as c}from"./dayjs.min-DFlK6ZAc.js";import{_ as h,c as o,a as e,b as a,w as u,e as p,h as g,t as i,k as f,n as _,f as n}from"./index-CmClDcBF.js";const x={name:"message",methods:{dayjs:c,hide(){this.ct(),this.message.show=!1},show(){this.timeout=setTimeout(()=>{this.message.show=!1},5e3)},ct(){clearTimeout(this.timeout)}},components:{LocaleText:l},props:{message:Object},mounted(){this.show()},data(){return{dismiss:!1,timeout:null}}},v=["id"],b={key:0,class:"d-flex"},w={class:"fw-bold d-block",style:{"text-transform":"uppercase"}},y={class:"ms-auto"},k={key:1},T={class:"card-body d-flex align-items-center gap-3"};function M(C,s,L,j,t,m){const d=g("LocaleText");return n(),o("div",{onMouseenter:s[1]||(s[1]=r=>{t.dismiss=!0,this.ct()}),onMouseleave:s[2]||(s[2]=r=>{t.dismiss=!1,this.show()}),class:"card shadow rounded-3 position-relative message ms-auto",id:this.message.id},[e("div",{class:_([{"text-bg-danger":this.message.type==="danger","text-bg-success":this.message.type==="success","text-bg-warning":this.message.type==="warning"},"card-header pos"])},[a(f,{name:"zoom",mode:"out-in"},{default:u(()=>[t.dismiss?(n(),o("div",k,[e("small",{onClick:s[0]||(s[0]=r=>m.hide()),class:"d-block mx-auto w-100 text-center",style:{cursor:"pointer"}},[s[3]||(s[3]=e("i",{class:"bi bi-x-lg me-2"},null,-1)),a(d,{t:"Dismiss"})])])):(n(),o("div",b,[e("small",w,[a(d,{t:"FROM "}),p(" "+i(this.message.from),1)]),e("small",y,i(m.dayjs().format("hh:mm A")),1)]))]),_:1})],2),e("div",T,[e("div",null,i(this.message.content),1)])],40,v)}const z=h(x,[["render",M],["__scopeId","data-v-94c76b54"]]);export{z as M};
import{L as l}from"./localeText-BJvnc1lF.js";import{d as c}from"./dayjs.min-DZl6XMNW.js";import{_ as h,c as o,a as e,b as a,w as u,e as p,h as g,t as i,k as f,n as _,f as n}from"./index-DM7YJCOo.js";const x={name:"message",methods:{dayjs:c,hide(){this.ct(),this.message.show=!1},show(){this.timeout=setTimeout(()=>{this.message.show=!1},5e3)},ct(){clearTimeout(this.timeout)}},components:{LocaleText:l},props:{message:Object},mounted(){this.show()},data(){return{dismiss:!1,timeout:null}}},v=["id"],b={key:0,class:"d-flex"},w={class:"fw-bold d-block",style:{"text-transform":"uppercase"}},y={class:"ms-auto"},k={key:1},T={class:"card-body d-flex align-items-center gap-3"};function M(C,s,L,j,t,m){const d=g("LocaleText");return n(),o("div",{onMouseenter:s[1]||(s[1]=r=>{t.dismiss=!0,this.ct()}),onMouseleave:s[2]||(s[2]=r=>{t.dismiss=!1,this.show()}),class:"card shadow rounded-3 position-relative message ms-auto",id:this.message.id},[e("div",{class:_([{"text-bg-danger":this.message.type==="danger","text-bg-success":this.message.type==="success","text-bg-warning":this.message.type==="warning"},"card-header pos"])},[a(f,{name:"zoom",mode:"out-in"},{default:u(()=>[t.dismiss?(n(),o("div",k,[e("small",{onClick:s[0]||(s[0]=r=>m.hide()),class:"d-block mx-auto w-100 text-center",style:{cursor:"pointer"}},[s[3]||(s[3]=e("i",{class:"bi bi-x-lg me-2"},null,-1)),a(d,{t:"Dismiss"})])])):(n(),o("div",b,[e("small",w,[a(d,{t:"FROM "}),p(" "+i(this.message.from),1)]),e("small",y,i(m.dayjs().format("hh:mm A")),1)]))]),_:1})],2),e("div",T,[e("div",null,i(this.message.content),1)])],40,v)}const z=h(x,[["render",M],["__scopeId","data-v-94c76b54"]]);export{z as M};
@@ -1 +0,0 @@
.protocolBtnGroup a[data-v-c25dcde1]{transition:all .2s ease-in-out}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.protocolBtnGroup a[data-v-14fcf0ee]{transition:all .2s ease-in-out}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{S as C,e as y,c as w,m as _,a as L,f as S,l as v,i as M,b as k,d as x,g as A,h as F,j as R,M as D,V as P,T as b,k as l,O as E,n as O,F as h,P as f,o as T,p as c,C as V,q as u,r as X}from"./Vector-CuSZivra.js";import{_ as Y,D as G,c as $,d as j,f as q}from"./index-CmClDcBF.js";class r extends C{constructor(t,e){super(),this.flatMidpoint_=null,this.flatMidpointRevision_=-1,this.maxDelta_=-1,this.maxDeltaRevision_=-1,e!==void 0&&!Array.isArray(t[0])?this.setFlatCoordinates(e,t):this.setCoordinates(t,e)}appendCoordinate(t){y(this.flatCoordinates,t),this.changed()}clone(){const t=new r(this.flatCoordinates.slice(),this.layout);return t.applyProperties(this),t}closestPointXY(t,e,o,n){return n<w(this.getExtent(),t,e)?n:(this.maxDeltaRevision_!=this.getRevision()&&(this.maxDelta_=Math.sqrt(_(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,0)),this.maxDeltaRevision_=this.getRevision()),L(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,this.maxDelta_,!1,t,e,o,n))}forEachSegment(t){return S(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t)}getCoordinateAtM(t,e){return this.layout!="XYM"&&this.layout!="XYZM"?null:(e=e!==void 0?e:!1,v(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,e))}getCoordinates(){return M(this.flatCoordinates,0,this.flatCoordinates.length,this.stride)}getCoordinateAt(t,e){return k(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,e,this.stride)}getLength(){return x(this.flatCoordinates,0,this.flatCoordinates.length,this.stride)}getFlatMidpoint(){return this.flatMidpointRevision_!=this.getRevision()&&(this.flatMidpoint_=this.getCoordinateAt(.5,this.flatMidpoint_??void 0),this.flatMidpointRevision_=this.getRevision()),this.flatMidpoint_}getSimplifiedGeometryInternal(t){const e=[];return e.length=A(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,e,0),new r(e,"XY")}getType(){return"LineString"}intersectsExtent(t){return F(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,this.getExtent())}setCoordinates(t,e){this.setLayout(e,t,1),this.flatCoordinates||(this.flatCoordinates=[]),this.flatCoordinates.length=R(this.flatCoordinates,0,t,this.stride),this.changed()}}const B={name:"osmap",props:{type:"",d:Object||Array},data(){return{osmAvailable:!0}},setup(){return{store:G()}},methods:{getLastLonLat(){if(this.type==="traceroute"){const i=this.d.findLast(t=>t.geo&&t.geo.lat&&t.geo.lon);return i?[i.geo.lon,i.geo.lat]:[0,0]}return[this.d.geo.lon,this.d.geo.lat]}},async mounted(){await fetch("https://tile.openstreetmap.org/",{signal:AbortSignal.timeout(1500)}).then(i=>{const t=new D({target:"map",layers:[new b({source:new E})],view:new P({center:l(this.getLastLonLat()),zoom:this.type==="traceroute"?3:10})}),e=[],o=new O;if(this.type==="traceroute")this.d.forEach(s=>{if(s.geo&&s.geo.lat&&s.geo.lon){const a=l([s.geo.lon,s.geo.lat]);e.push(a);const g=this.getLastLonLat(),m=new h({geometry:new f(a),last:s.geo.lon===g[0]&&s.geo.lat===g[1]});o.addFeature(m)}});else{const s=l([this.d.geo.lon,this.d.geo.lat]);e.push(s);const a=new h({geometry:new f(s)});o.addFeature(a)}const n=new r(e),d=new h({geometry:n});o.addFeature(d);const p=new T({source:o,style:function(s){if(s.getGeometry().getType()==="Point")return new c({image:new V({radius:10,fill:new X({color:s.get("last")?"#dc3545":"#0d6efd"}),stroke:new u({color:"white",width:5})})});if(s.getGeometry().getType()==="LineString")return new c({stroke:new u({color:"#0d6efd",width:2})})}});t.addLayer(p)}).catch(i=>{this.osmAvailable=!1})}},z={key:0,id:"map",class:"w-100 rounded-3"};function I(i,t,e,o,n,d){return this.osmAvailable?(q(),$("div",z)):j("",!0)}const H=Y(B,[["render",I]]);export{H as O};
import{S as C,e as y,c as w,m as _,a as L,f as S,l as v,i as M,b as k,d as x,g as A,h as F,j as R,M as D,V as P,T as b,k as l,O as E,n as O,F as h,P as f,o as T,p as c,C as V,q as u,r as X}from"./Vector-CuSZivra.js";import{_ as Y,D as G,c as $,d as j,f as q}from"./index-DM7YJCOo.js";class r extends C{constructor(t,e){super(),this.flatMidpoint_=null,this.flatMidpointRevision_=-1,this.maxDelta_=-1,this.maxDeltaRevision_=-1,e!==void 0&&!Array.isArray(t[0])?this.setFlatCoordinates(e,t):this.setCoordinates(t,e)}appendCoordinate(t){y(this.flatCoordinates,t),this.changed()}clone(){const t=new r(this.flatCoordinates.slice(),this.layout);return t.applyProperties(this),t}closestPointXY(t,e,o,n){return n<w(this.getExtent(),t,e)?n:(this.maxDeltaRevision_!=this.getRevision()&&(this.maxDelta_=Math.sqrt(_(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,0)),this.maxDeltaRevision_=this.getRevision()),L(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,this.maxDelta_,!1,t,e,o,n))}forEachSegment(t){return S(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t)}getCoordinateAtM(t,e){return this.layout!="XYM"&&this.layout!="XYZM"?null:(e=e!==void 0?e:!1,v(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,e))}getCoordinates(){return M(this.flatCoordinates,0,this.flatCoordinates.length,this.stride)}getCoordinateAt(t,e){return k(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,e,this.stride)}getLength(){return x(this.flatCoordinates,0,this.flatCoordinates.length,this.stride)}getFlatMidpoint(){return this.flatMidpointRevision_!=this.getRevision()&&(this.flatMidpoint_=this.getCoordinateAt(.5,this.flatMidpoint_??void 0),this.flatMidpointRevision_=this.getRevision()),this.flatMidpoint_}getSimplifiedGeometryInternal(t){const e=[];return e.length=A(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,e,0),new r(e,"XY")}getType(){return"LineString"}intersectsExtent(t){return F(this.flatCoordinates,0,this.flatCoordinates.length,this.stride,t,this.getExtent())}setCoordinates(t,e){this.setLayout(e,t,1),this.flatCoordinates||(this.flatCoordinates=[]),this.flatCoordinates.length=R(this.flatCoordinates,0,t,this.stride),this.changed()}}const B={name:"osmap",props:{type:"",d:Object||Array},data(){return{osmAvailable:!0}},setup(){return{store:G()}},methods:{getLastLonLat(){if(this.type==="traceroute"){const i=this.d.findLast(t=>t.geo&&t.geo.lat&&t.geo.lon);return i?[i.geo.lon,i.geo.lat]:[0,0]}return[this.d.geo.lon,this.d.geo.lat]}},async mounted(){await fetch("https://tile.openstreetmap.org/",{signal:AbortSignal.timeout(1500)}).then(i=>{const t=new D({target:"map",layers:[new b({source:new E})],view:new P({center:l(this.getLastLonLat()),zoom:this.type==="traceroute"?3:10})}),e=[],o=new O;if(this.type==="traceroute")this.d.forEach(s=>{if(s.geo&&s.geo.lat&&s.geo.lon){const a=l([s.geo.lon,s.geo.lat]);e.push(a);const g=this.getLastLonLat(),m=new h({geometry:new f(a),last:s.geo.lon===g[0]&&s.geo.lat===g[1]});o.addFeature(m)}});else{const s=l([this.d.geo.lon,this.d.geo.lat]);e.push(s);const a=new h({geometry:new f(s)});o.addFeature(a)}const n=new r(e),d=new h({geometry:n});o.addFeature(d);const p=new T({source:o,style:function(s){if(s.getGeometry().getType()==="Point")return new c({image:new V({radius:10,fill:new X({color:s.get("last")?"#dc3545":"#0d6efd"}),stroke:new u({color:"white",width:5})})});if(s.getGeometry().getType()==="LineString")return new c({stroke:new u({color:"#0d6efd",width:2})})}});t.addLayer(p)}).catch(i=>{this.osmAvailable=!1})}},z={key:0,id:"map",class:"w-100 rounded-3"};function I(i,t,e,o,n,d){return this.osmAvailable?(q(),$("div",z)):j("",!0)}const H=Y(B,[["render",I]]);export{H as O};

Some files were not shown because too many files have changed in this diff Show More