Compare commits

..

34 Commits

Author SHA1 Message Date
Donald Zou
64668bc85b Merge branch 'main' into v4.3.3-dev
Some checks failed
Docker Build and Push / docker_build (push) Has been cancelled
Docker Build and Push / docker_scan (push) Has been cancelled
2026-04-02 15:34:12 +08:00
DaanSelen
da16b4ab77 chore: fix #1175 2026-04-02 09:29:09 +02:00
Daan Selen
1e495c546c Revert "Merge branch 'v4.3.3-dev' into main"
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docker Build and Push / docker_build (push) Has been cancelled
Docker Build and Push / docker_scan (push) Has been cancelled
This reverts commit 3586ddce4d, reversing
changes made to 27ec65a970.
2026-03-31 23:15:34 +02:00
DaanSelen
3586ddce4d Merge branch 'v4.3.3-dev' into main 2026-03-31 14:14:11 -07:00
DaanSelen
fb4bd9dd7c bring dev branch up to date from main (#1183)
* 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>

* build(deps): bump @vueuse/shared in /src/static/app

Bumps [@vueuse/shared](https://github.com/vueuse/vueuse/tree/HEAD/packages/shared) 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/shared)

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

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

* build(deps): bump flask from 3.1.2 to 3.1.3 in /src

Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

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

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

* build(deps): bump psycopg[binary] from 3.3.2 to 3.3.3 in /src (#1153)

Bumps [psycopg[binary]](https://github.com/psycopg/psycopg) from 3.3.2 to 3.3.3.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: psycopg[binary]
  dependency-version: 3.3.3
  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.46 to 2.0.48 in /src

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.48.
- [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.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* build(deps): bump requests from 2.32.5 to 2.33.1 in /src

Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.1.
- [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.33.1)

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

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

* chore: change dependabot targeting

* build(deps): bump gunicorn from 25.0.3 to 25.3.0 in /src (#1178)

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

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 25.3.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 requests from 2.32.5 to 2.33.1 in /src

Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.1.
- [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.33.1)

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

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

* build(deps): bump sqlalchemy from 2.0.46 to 2.0.48 in /src

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.48.
- [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.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* build(deps): bump flask from 3.1.2 to 3.1.3 in /src

Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

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

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

* build(deps): bump @vueuse/shared in /src/static/app

Bumps [@vueuse/shared](https://github.com/vueuse/vueuse/tree/HEAD/packages/shared) 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/shared)

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

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

* 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>

* 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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Prokudin <iprok@users.noreply.github.com>
2026-03-31 23:11:44 +02:00
Daan Selen
95f4139889 Merge remote-tracking branch 'origin/dependabot/pip/src/v4.3.3-dev/psycopg-3.3.3' into v4.3.3-dev 2026-03-31 23:07:07 +02:00
Daan Selen
91df681919 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/v4.3.3-dev/cidr-tools-11.3.2' into v4.3.3-dev 2026-03-31 23:06:43 +02:00
Daan Selen
ee8e057aa5 Merge remote-tracking branch 'origin/dependabot/pip/src/psycopg-binary--3.3.3' into v4.3.3-dev 2026-03-31 23:01:22 +02:00
Daan Selen
27ec65a970 Merge remote-tracking branch 'origin/dependabot/pip/src/requests-2.33.1' 2026-03-31 22:09:17 +02:00
Daan Selen
a833e75664 Merge remote-tracking branch 'origin/dependabot/docker/docker/v4.3.3-dev/golang-1.26-alpine3.23' into v4.3.3-dev 2026-03-31 22:07:38 +02:00
dependabot[bot]
55ebf21a99 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-03-31 20:01:36 +00:00
Ivan Prokudin
978946a9b8 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-03-31 21:58:47 +02:00
Daan Selen
9f140c8747 Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/ol-10.8.0' 2026-03-31 21:56:14 +02:00
Daan Selen
9f9cbbe00a Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/src/static/app/vueuse/shared-14.2.1' 2026-03-31 21:56:05 +02:00
Daan Selen
76b702c7db Merge remote-tracking branch 'origin/dependabot/pip/src/flask-3.1.3' 2026-03-31 21:55:50 +02:00
Daan Selen
065dcee42b Merge remote-tracking branch 'origin/dependabot/pip/src/sqlalchemy-2.0.48' 2026-03-31 21:48:03 +02:00
dependabot[bot]
3f65fcf27f 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-03-31 21:41:06 +02:00
dependabot[bot]
447685ec06 build(deps): bump @vueuse/shared in /src/static/app
Bumps [@vueuse/shared](https://github.com/vueuse/vueuse/tree/HEAD/packages/shared) 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/shared)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 21:41:06 +02:00
dependabot[bot]
4513b48651 build(deps): bump flask from 3.1.2 to 3.1.3 in /src
Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 21:41:06 +02:00
dependabot[bot]
13304539e5 build(deps): bump sqlalchemy from 2.0.46 to 2.0.48 in /src
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.48.
- [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.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 21:41:06 +02:00
dependabot[bot]
de6c1751c1 build(deps): bump requests from 2.32.5 to 2.33.1 in /src
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.1.
- [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.33.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 21:41:05 +02:00
dependabot[bot]
c3f0cbbd2f build(deps): bump gunicorn from 25.0.3 to 25.3.0 in /src (#1178)
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 25.0.3 to 25.3.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/25.0.3...25.3.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 25.3.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>
2026-03-31 21:38:07 +02:00
dependabot[bot]
a8b32e4477 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-03-31 19:37:48 +00:00
dependabot[bot]
b35191bfb1 build(deps): bump psycopg from 3.3.2 to 3.3.3 in /src
Bumps [psycopg](https://github.com/psycopg/psycopg) from 3.3.2 to 3.3.3.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 19:36:16 +00:00
dependabot[bot]
55507473cd build(deps): bump golang in /docker
Bumps golang from 1.25-alpine3.23 to 1.26-alpine3.23.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26-alpine3.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 19:36:03 +00:00
Daan Selen
ebb7f49f2d chore: change dependabot targeting 2026-03-31 21:35:20 +02:00
Daan Selen
8315d0ed29 fix: white screen in development image 2026-03-31 21:32:10 +02:00
dependabot[bot]
8f30120978 build(deps): bump requests from 2.32.5 to 2.33.1 in /src
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.1.
- [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.33.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:36:53 +00:00
dependabot[bot]
0a3e66543a build(deps): bump sqlalchemy from 2.0.46 to 2.0.48 in /src
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.48.
- [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.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 18:42:00 +00:00
dependabot[bot]
5fa9cd20a0 build(deps): bump psycopg[binary] from 3.3.2 to 3.3.3 in /src (#1153)
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docker Build and Push / docker_build (push) Has been cancelled
Docker Build and Push / docker_scan (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Bumps [psycopg[binary]](https://github.com/psycopg/psycopg) from 3.3.2 to 3.3.3.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: psycopg[binary]
  dependency-version: 3.3.3
  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>
2026-03-30 20:40:34 +02:00
dependabot[bot]
a8610b9f12 build(deps): bump flask from 3.1.2 to 3.1.3 in /src
Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 22:08:49 +00:00
dependabot[bot]
2e62e85854 build(deps): bump psycopg[binary] from 3.3.2 to 3.3.3 in /src
Bumps [psycopg[binary]](https://github.com/psycopg/psycopg) from 3.3.2 to 3.3.3.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: psycopg[binary]
  dependency-version: 3.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 22:08:44 +00:00
dependabot[bot]
9a3a0accdf build(deps): bump @vueuse/shared in /src/static/app
Bumps [@vueuse/shared](https://github.com/vueuse/vueuse/tree/HEAD/packages/shared) 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/shared)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 20:42:34 +00:00
dependabot[bot]
93804ba476 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-02-16 20:42:27 +00:00
213 changed files with 3977 additions and 3607 deletions

View File

@@ -2,8 +2,4 @@
.github .github
*.md *.md
tests/ tests/
docs/ docs/
src/db
src/wg-dashboard.ini
src/static/app
src/static/client

View File

@@ -7,36 +7,30 @@ version: 2
updates: updates:
- package-ecosystem: "pip" - package-ecosystem: "pip"
directory: "/src" directory: "/src"
target-branch: "development" target-branch: "v4.3.3-dev"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/src/static/app" directory: "/src/static/app"
target-branch: "development" target-branch: "v4.3.3-dev"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/src/static/client"
target-branch: "development"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/.github" directory: "/.github"
target-branch: "development" target-branch: "v4.3.3-dev"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/docker" directory: "/docker"
target-branch: "development" target-branch: "v4.3.3-dev"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "docker-compose" - package-ecosystem: "docker-compose"
directory: "/docker" directory: "/docker"
target-branch: "development" target-branch: "v4.3.3-dev"
schedule: schedule:
interval: "weekly" interval: "weekly"

106
.github/workflows/docker-debug.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
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 }}

View File

@@ -5,7 +5,6 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
- 'development'
- '**dev' - '**dev'
tags: tags:
- '*' - '*'
@@ -104,6 +103,7 @@ jobs:
only-fixed: true only-fixed: true
write-comment: true write-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
exit-code: true
- name: Docker Scout Compare - name: Docker Scout Compare
uses: docker/scout-action@v1 uses: docker/scout-action@v1

View File

@@ -5,6 +5,7 @@
> 🎉 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! > 🎉 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) ![](https://wgdashboard-resources.tor1.cdn.digitaloceanspaces.com/Posters/Banner.png)

View File

@@ -4,7 +4,7 @@
# #
# Pull the current golang-alpine image. # Pull the current golang-alpine image.
FROM golang:1.26-alpine3.23 AS awg-go FROM golang:1.25-alpine AS awg-go
# Install build-dependencies. # Install build-dependencies.
RUN apk add --no-cache \ RUN apk add --no-cache \
@@ -30,7 +30,7 @@ RUN go version && \
# AWG TOOLS BUILDING STAGE # AWG TOOLS BUILDING STAGE
# Base: Alpine # Base: Alpine
# #
FROM alpine:3.23 AS awg-tools FROM alpine:latest AS awg-tools
# Install needed dependencies. # Install needed dependencies.
RUN apk add --no-cache \ RUN apk add --no-cache \
@@ -55,7 +55,7 @@ RUN make && chmod +x wg*
# #
# Use the python-alpine image for building pip dependencies # Use the python-alpine image for building pip dependencies
FROM python:3.14-alpine3.23 AS pip-builder FROM python:3.14-alpine AS pip-builder
ARG TARGETPLATFORM ARG TARGETPLATFORM
@@ -91,7 +91,7 @@ RUN . /opt/wgdashboard/src/venv/bin/activate && \
# #
# Running with the python-alpine image. # Running with the python-alpine image.
FROM python:3.14-alpine3.23 AS final FROM python:3.14-alpine AS final
LABEL maintainer="dselen@nerthus.nl" LABEL maintainer="dselen@nerthus.nl"
# Install only the runtime dependencies # Install only the runtime dependencies
@@ -114,18 +114,15 @@ ENV TZ="Europe/Amsterdam" \
global_dns="9.9.9.9" \ global_dns="9.9.9.9" \
wgd_port="10086" \ wgd_port="10086" \
public_ip="" \ public_ip="" \
WGDASH=/opt/wgdashboard \ WGDASH=/opt/wgdashboard
dynamic_config="true"
# Create directories needed for operation # 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 the venv and source files from local compiled locations or repos
COPY ./src ${WGDASH}/src COPY ./src ${WGDASH}/src
COPY --from=pip-builder /opt/wgdashboard/src/venv /opt/wgdashboard/src/venv COPY --from=pip-builder /opt/wgdashboard/src/venv /opt/wgdashboard/src/venv
COPY ./docker/wg0.conf.template /tmp/wg0.conf.template 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 in the runtime script, essential.
COPY ./docker/entrypoint.sh /entrypoint.sh COPY ./docker/entrypoint.sh /entrypoint.sh

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> - ghcr.io/wgdashboard/wgdashboard:<tag>
- docker.io/donaldzou/wgdashboard:<tag> - docker.io/donaldzou/wgdashboard:<tag>
> tags should be either: latest, main, <version>, <branch-name> (if built) or <commit-sha>. > tags should be either: latest, main, <version> or <commit-sha>.
From there either use the environment variables described below as parameters or use the Docker Compose file: `compose.yaml`.<br> 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> 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>
@@ -95,29 +95,23 @@ Updating the WGDashboard container should be through 'The Docker Way' - by pulli
## ⚙️ Environment Variables ## ⚙️ Environment Variables
| Variable | Accepted Values | Default | Example | Description | | 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. |
| `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. |
| `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. |
| `public_ip` | Public IP address | Retrieved automatically | `253.162.134.73` | Used to generate accurate client configs. Needed if container is NATd. | | `wgd_port` | Any port that is allowed for the process | `10086` | `443` | This port is used to set the WGDashboard web port. |
| `wgd_port` | Any port that is allowed for the process | `10086` | `443` | This port is used to set the WGDashboard web port. | | `username` | Any nonempty string | `-` | `admin` | Username for the WGDashboard web interface account. |
| `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). |
| `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. |
| `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 | `false` | `true` | Autostart the WireGuard client when the container launches. | | `email_server` | SMTP server address | `-` | `smtp.gmail.com` | SMTP server for sending email notifications. |
| `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_port` | SMTP port number | `-` | `587` | Port for connecting to the SMTP server. | | `email_encryption` | `TLS`, `SSL`, etc. | `-` | `TLS` | Encryption method for email communication. |
| `email_encryption` | `TLS`, `SSL`, etc. | `-` | `TLS` | Encryption method for email communication. | | `email_username` | Any non-empty string | `-` | `user@example.com` | Username for SMTP authentication. |
| `email_username` | Any non-empty string | `-` | `user@example.com` | Username for SMTP authentication. | | `email_password` | Any non-empty string | `-` | `app_password` | Password for SMTP authentication. |
| `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_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. |
| `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. |
--- ---
@@ -213,75 +207,6 @@ ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
- Access the web interface via `http://your-ip:10086` (or whichever port you specified in the compose). - 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). - 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: ## Closing remarks:
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl. For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.

View File

@@ -13,7 +13,6 @@ services:
# By default its all disabled, but uncomment the following lines to apply these. (uncommenting is removing the # character) # 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. # Refer to the documentation on https://wgdashboard.dev/ for more info on what everything means.
#environment: #environment:
#- wg_autostart=wg0
#- tz= # <--- Set container timezone, default: Europe/Amsterdam. #- 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. #- 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. #- wgd_port= # <--- Set the port WGDashboard will use for its web-server.

View File

@@ -85,6 +85,8 @@ echo "------------------------- START ----------------------------"
echo "Starting the WGDashboard Docker container." echo "Starting the WGDashboard Docker container."
ensure_installation() { ensure_installation() {
echo "Quick-installing..."
# Make the wgd.sh script executable. # Make the wgd.sh script executable.
chmod +x "${WGDASH}"/src/wgd.sh chmod +x "${WGDASH}"/src/wgd.sh
cd "${WGDASH}"/src || exit cd "${WGDASH}"/src || exit
@@ -100,51 +102,23 @@ ensure_installation() {
echo "Removing clear command from wgd.sh for better Docker logging." echo "Removing clear command from wgd.sh for better Docker logging."
sed -i '/clear/d' ./wgd.sh sed -i '/clear/d' ./wgd.sh
# PERSISTENCE FOR databases directory
# Create required directories and links # Create required directories and links
if [ ! -d "/data/db" ]; then if [ ! -d "/data/db" ]; then
echo "Creating database dir" echo "Creating database dir"
mkdir -p /data/db mkdir -p /data/db
fi fi
if [[ ! -L "${WGDASH}/src/db" ]] && [[ -d "${WGDASH}/src/db" ]]; then if [ ! -d "${WGDASH}/src/db" ]; then
echo "Removing ${WGDASH}/src/db since its not a symbolic link." ln -s /data/db "${WGDASH}/src/db"
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 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 if [ ! -f "${config_file}" ]; then
echo "Creating wg-dashboard.ini file" echo "Creating wg-dashboard.ini file"
touch "${config_file}" touch "${config_file}"
fi fi
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." if [ ! -f "${WGDASH}/src/wg-dashboard.ini" ]; then
rm -fv "${WGDASH}/src/wg-dashboard.ini" ln -s "${config_file}" "${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 fi
# Setup WireGuard if needed # Setup WireGuard if needed
@@ -168,25 +142,14 @@ set_envvars() {
# Check if config file is empty # Check if config file is empty
if [ ! -s "${config_file}" ]; then if [ ! -s "${config_file}" ]; then
echo "Config file is empty. Creating initial structure." 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 fi
echo "Checking basic configuration:" echo "Checking basic configuration:"
set_ini Peers peer_global_dns "${global_dns}" set_ini Peers peer_global_dns "${global_dns}"
if [ -z "${public_ip}" ]; then if [ -z "${public_ip}" ]; then
public_ip=$(curl -s https://ifconfig.me) public_ip=$(curl -s ifconfig.me)
if [ -z "${public_ip}" ]; then echo "Automatically detected public IP: ${public_ip}"
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 fi
set_ini Peers remote_endpoint "${public_ip}" set_ini Peers remote_endpoint "${public_ip}"
@@ -220,24 +183,6 @@ set_envvars() {
set_ini WireGuardConfiguration autostart "${wg_autostart}" set_ini WireGuardConfiguration autostart "${wg_autostart}"
fi 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 (check if any settings need to be configured)
email_vars=("email_server" "email_port" "email_encryption" "email_username" "email_password" "email_from" "email_template") email_vars=("email_server" "email_port" "email_encryption" "email_username" "email_password" "email_from" "email_template")
for var in "${email_vars[@]}"; do for var in "${email_vars[@]}"; do
@@ -262,9 +207,6 @@ set_envvars() {
start_and_monitor() { start_and_monitor() {
printf "\n---------------------- STARTING CORE -----------------------\n" 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. # Due to some instances complaining about this, making sure its there every time.
mkdir -p /dev/net mkdir -p /dev/net
mknod /dev/net/tun c 10 200 mknod /dev/net/tun c 10 200

View File

@@ -4,9 +4,8 @@ from tzlocal import get_localzone
from functools import wraps from functools import wraps
from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for, send_from_directory from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for
import os import os
import mimetypes
from modules.WireguardConfiguration import WireguardConfiguration from modules.WireguardConfiguration import WireguardConfiguration
from modules.DashboardConfig import DashboardConfig from modules.DashboardConfig import DashboardConfig
@@ -54,8 +53,6 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
@client.post(f'{prefix}/api/signup') @client.post(f'{prefix}/api/signup')
def ClientAPI_SignUp(): def ClientAPI_SignUp():
if not dashboardConfig.GetConfig("Clients", "sign_up")[1]:
abort(404)
data = request.get_json() data = request.get_json()
status, msg = dashboardClients.SignUp(**data) status, msg = dashboardClients.SignUp(**data)
return ResponseObject(status, msg) return ResponseObject(status, msg)
@@ -100,7 +97,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
date = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') date = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
emailSender = EmailSender(dashboardConfig) emailSender = EmailSender(dashboardConfig)
if not emailSender.is_ready(): if not emailSender.ready():
return ResponseObject(False, "We can't send you an email due to your Administrator has not setup email service. Please contact your administrator.") 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() data = request.get_json()
@@ -195,26 +192,14 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
}) })
return ResponseObject(status, msg) 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) @client.get(prefix)
def ClientIndex(): def ClientIndex():
app_prefix = dashboardConfig.GetConfig("Server", "app_prefix")[1] return render_template('client.html')
return render_template('client.html', APP_PREFIX=app_prefix)
@client.get(f'{prefix}/api/serverInformation') @client.get(f'{prefix}/api/serverInformation')
def ClientAPI_ServerInformation(): def ClientAPI_ServerInformation():
return ResponseObject(data={ 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') @client.get(f'{prefix}/api/validateAuthentication')
@@ -244,4 +229,4 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
return ResponseObject(status, message) return ResponseObject(status, message)
return client return client

View File

@@ -2,15 +2,13 @@ import logging
import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess
import time, re, uuid, bcrypt, psutil, pyotp, threading import time, re, uuid, bcrypt, psutil, pyotp, threading
import traceback import traceback
from functools import wraps
from urllib.parse import unquote
from uuid import uuid4 from uuid import uuid4
from zipfile import ZipFile from zipfile import ZipFile
from datetime import datetime, timedelta from datetime import datetime, timedelta
import sqlalchemy import sqlalchemy
from jinja2 import Template from jinja2 import Template
from flask import Flask, request, render_template, session, send_file, current_app from flask import Flask, request, render_template, session, send_file
from flask_cors import CORS from flask_cors import CORS
from icmplib import ping, traceroute from icmplib import ping, traceroute
from flask.json.provider import DefaultJSONProvider from flask.json.provider import DefaultJSONProvider
@@ -19,7 +17,8 @@ from itertools import islice
from sqlalchemy import RowMapping from sqlalchemy import RowMapping
from modules.Utilities import ( from modules.Utilities import (
RegexMatch, StringToBoolean, ValidateDNSAddress, RegexMatch, StringToBoolean,
ValidateIPAddressesWithRange, ValidateDNSAddress,
GenerateWireguardPublicKey, GenerateWireguardPrivateKey GenerateWireguardPublicKey, GenerateWireguardPrivateKey
) )
from packaging import version from packaging import version
@@ -31,7 +30,7 @@ from modules.PeerShareLinks import PeerShareLinks
from modules.PeerJobs import PeerJobs from modules.PeerJobs import PeerJobs
from modules.DashboardConfig import DashboardConfig from modules.DashboardConfig import DashboardConfig
from modules.WireguardConfiguration import WireguardConfiguration from modules.WireguardConfiguration import WireguardConfiguration
from modules.AmneziaConfiguration import AmneziaConfiguration from modules.AmneziaWireguardConfiguration import AmneziaWireguardConfiguration
from client import createClientBlueprint from client import createClientBlueprint
@@ -70,29 +69,10 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
response.content_type = "application/json" response.content_type = "application/json"
return response 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 Flask App
''' '''
_, APP_PREFIX_INIT = DashboardConfig().GetConfig("Server", "app_prefix") app = Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
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(): def peerInformationBackgroundThread():
global WireguardConfigurations global WireguardConfigurations
@@ -112,13 +92,11 @@ def peerInformationBackgroundThread():
c.getPeersTransfer() c.getPeersTransfer()
c.getPeersEndpoint() c.getPeersEndpoint()
c.getPeers() c.getPeers()
if DashboardConfig.GetConfig('WireGuardConfiguration', 'peer_tracking')[1] is True: if delay == 6:
print("[WGDashboard] Tracking Peers") if c.configurationInfo.PeerTrafficTracking:
if delay == 6: c.logPeersTraffic()
if c.configurationInfo.PeerTrafficTracking: if c.configurationInfo.PeerHistoricalEndpointTracking:
c.logPeersTraffic() c.logPeersHistoryEndpoint()
if c.configurationInfo.PeerHistoricalEndpointTracking:
c.logPeersHistoryEndpoint()
c.getRestrictedPeersList() c.getRestrictedPeersList()
except Exception as e: except Exception as e:
app.logger.error(f"[WGDashboard] Background Thread #1 Error", e) app.logger.error(f"[WGDashboard] Background Thread #1 Error", e)
@@ -183,10 +161,10 @@ def InitWireguardConfigurationsList(startup: bool = False):
if i in WireguardConfigurations.keys(): if i in WireguardConfigurations.keys():
if WireguardConfigurations[i].configurationFileChanged(): if WireguardConfigurations[i].configurationFileChanged():
with app.app_context(): with app.app_context():
WireguardConfigurations[i] = AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i) WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i)
else: else:
with app.app_context(): with app.app_context():
WireguardConfigurations[i] = AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i, startup=startup) WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i, startup=startup)
except WireguardConfiguration.InvalidConfigurationFileException as e: except WireguardConfiguration.InvalidConfigurationFileException as e:
app.logger.error(f"{i} have an invalid configuration file.") app.logger.error(f"{i} have an invalid configuration file.")
@@ -286,9 +264,7 @@ def auth_req():
("username" not in session or session.get("role") != "admin") ("username" not in session or session.get("role") != "admin")
and (f"{appPrefix}/" != request.path and f"{appPrefix}" != request.path) 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}/client')
and not request.path.startswith(f'{appPrefix}/img') and not request.path.startswith(f'{appPrefix}/static')
and not request.path.startswith(f'{appPrefix}/json')
and not request.path.startswith(f'{appPrefix}/assets')
and request.path not in whiteList and request.path not in whiteList
): ):
response = Flask.make_response(app, { response = Flask.make_response(app, {
@@ -446,11 +422,11 @@ def API_addWireguardConfiguration():
) )
WireguardConfigurations[data['ConfigurationName']] = ( WireguardConfigurations[data['ConfigurationName']] = (
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else ( WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else (
AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName'])) AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName']))
else: else:
WireguardConfigurations[data['ConfigurationName']] = ( WireguardConfigurations[data['ConfigurationName']] = (
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else ( WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else (
AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data))
return ResponseObject() return ResponseObject()
@app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration') @app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration')
@@ -547,7 +523,7 @@ def API_renameWireguardConfiguration():
status, message = rc.renameConfiguration(data.get("NewConfigurationName")) status, message = rc.renameConfiguration(data.get("NewConfigurationName"))
if status: if status:
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"))) 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")))
else: else:
WireguardConfigurations[data.get("ConfigurationName")] = rc WireguardConfigurations[data.get("ConfigurationName")] = rc
return ResponseObject(status, message) return ResponseObject(status, message)
@@ -586,8 +562,8 @@ def API_getAllWireguardConfigurationBackup():
files.sort(key=lambda x: x[1], reverse=True) files.sort(key=lambda x: x[1], reverse=True)
for f, ct in files: for f, ct in files:
if RegexMatch(r"^(.+)_(\d+)\.(conf)$", f): if RegexMatch(r"^(.*)_(.*)\.(conf)$", f):
s = re.search(r"^(.+)_(\d+)\.(conf)$", f) s = re.search(r"^(.*)_(.*)\.(conf)$", f)
name = s.group(1) name = s.group(1)
if name not in existingConfiguration: if name not in existingConfiguration:
if name not in data['NonExistingConfigurations'].keys(): if name not in data['NonExistingConfigurations'].keys():
@@ -730,30 +706,15 @@ def API_updatePeerSettings(configName):
preshared_key = data['preshared_key'] preshared_key = data['preshared_key']
mtu = data['mtu'] mtu = data['mtu']
keepalive = data['keepalive'] keepalive = data['keepalive']
notes = data.get('notes', '')
wireguardConfig = WireguardConfigurations[configName] wireguardConfig = WireguardConfigurations[configName]
foundPeer, peer = wireguardConfig.searchPeer(id) foundPeer, peer = wireguardConfig.searchPeer(id)
if foundPeer: if foundPeer:
if wireguardConfig.Protocol == 'wg': if wireguardConfig.Protocol == 'wg':
status, msg = peer.updatePeer(name, status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses,
private_key, allowed_ip, endpoint_allowed_ip, mtu, keepalive)
preshared_key,
dns_addresses,
allowed_ip,
endpoint_allowed_ip,
mtu,
keepalive,
notes)
else: else:
status, msg = peer.updatePeer(name, status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses,
private_key, allowed_ip, endpoint_allowed_ip, mtu, keepalive, "off")
preshared_key,
dns_addresses,
allowed_ip,
endpoint_allowed_ip,
mtu,
keepalive,
notes)
wireguardConfig.getPeers() wireguardConfig.getPeers()
DashboardWebHooks.RunWebHook('peer_updated', { DashboardWebHooks.RunWebHook('peer_updated', {
"configuration": wireguardConfig.Name, "configuration": wireguardConfig.Name,
@@ -903,7 +864,6 @@ def API_addPeers(configName):
mtu: int = data.get('mtu', None) mtu: int = data.get('mtu', None)
keep_alive: int = data.get('keepalive', None) keep_alive: int = data.get('keepalive', None)
notes: str = data.get('notes', '')
preshared_key: str = data.get('preshared_key', "") preshared_key: str = data.get('preshared_key', "")
if type(mtu) is not int or mtu < 0 or mtu > 1460: if type(mtu) is not int or mtu < 0 or mtu > 1460:
@@ -959,7 +919,7 @@ def API_addPeers(configName):
"endpoint_allowed_ip": endpoint_allowed_ip, "endpoint_allowed_ip": endpoint_allowed_ip,
"mtu": mtu, "mtu": mtu,
"keepalive": keep_alive, "keepalive": keep_alive,
"notes": "" "advanced_security": "off"
}) })
if addedCount == bulkAddAmount: if addedCount == bulkAddAmount:
break break
@@ -1002,11 +962,8 @@ def API_addPeers(configName):
for i in allowed_ips: for i in allowed_ips:
found = False found = False
for subnet in availableIps.keys(): for subnet in availableIps.keys():
try: network = ipaddress.ip_network(subnet, False)
network = ipaddress.ip_network(subnet, False) ap = ipaddress.ip_network(i)
ap = ipaddress.ip_network(i)
except ValueError as e:
return ResponseObject(False, str(e))
if network.version == ap.version and ap.subnet_of(network): if network.version == ap.version and ap.subnet_of(network):
found = True found = True
@@ -1024,13 +981,14 @@ def API_addPeers(configName):
"DNS": dns_addresses, "DNS": dns_addresses,
"mtu": mtu, "mtu": mtu,
"keepalive": keep_alive, "keepalive": keep_alive,
"notes": notes "advanced_security": "off"
}] }]
) )
return ResponseObject(status=status, message=message, data=addedPeers) return ResponseObject(status=status, message=message, data=addedPeers)
except Exception as e: except Exception as e:
app.logger.error("Add peers failed", e) app.logger.error("Add peers failed", e)
return ResponseObject(False, f"Add peers failed.") return ResponseObject(False,
f"Add peers failed. Reason: {message}")
return ResponseObject(False, "Configuration does not exist") return ResponseObject(False, "Configuration does not exist")
@@ -1167,24 +1125,13 @@ def API_GetPeerTraffics():
@app.get(f'{APP_PREFIX}/api/getPeerTrackingTableCounts') @app.get(f'{APP_PREFIX}/api/getPeerTrackingTableCounts')
def API_GetPeerTrackingTableCounts(): def API_GetPeerTrackingTableCounts():
configurationName = request.args.get("configurationName") configurationName = request.args.get("configurationName")
if configurationName and configurationName not in WireguardConfigurations.keys(): if configurationName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist") return ResponseObject(False, "Configuration does not exist")
c = WireguardConfigurations.get(configurationName)
if configurationName: return ResponseObject(data={
c = WireguardConfigurations.get(configurationName) "TrafficTrackingTableSize": c.getTransferTableSize(),
return ResponseObject(data={ "HistoricalTrackingTableSize": c.getHistoricalEndpointTableSize()
"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') @app.get(f'{APP_PREFIX}/api/downloadPeerTrackingTable')
def API_DownloadPeerTackingTable(): def API_DownloadPeerTackingTable():
@@ -1221,75 +1168,52 @@ def API_getDashboardTheme():
def API_getDashboardVersion(): def API_getDashboardVersion():
return ResponseObject(data=DashboardConfig.GetConfig("Server", "version")[1]) return ResponseObject(data=DashboardConfig.GetConfig("Server", "version")[1])
@app.post(f'{APP_PREFIX}/api/PeerScheduleJob') @app.post(f'{APP_PREFIX}/api/savePeerScheduleJob')
@require_fields('Configuration', 'Peer', 'Field', 'Operator', 'Value', 'Action')
def API_savePeerScheduleJob(): def API_savePeerScheduleJob():
data = request.json data = request.json
if "Job" not in data.keys():
configuration = WireguardConfigurations.get(data['Configuration']) 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'])
if configuration is None: if configuration is None:
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist")
f, fp = configuration.searchPeer(job['Peer'])
if not f:
return ResponseObject(False, "Peer does not exist")
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)
peerKey = unquote(data['Peer']) @app.post(f'{APP_PREFIX}/api/deletePeerScheduleJob')
found, _ = configuration.searchPeer(peerKey)
if not found:
return ResponseObject(False, "Peer does not exist", status_code=404)
jobID = data.get('JobID', str(uuid4()))
if len(AllPeerJobs.searchJobById(jobID)) > 0:
return ResponseObject(False, "Job already exists", status_code=409)
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(): def API_deletePeerScheduleJob():
data = request.json data = request.json
if "Job" not in data.keys():
configuration = WireguardConfigurations.get(data['Configuration']) 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'])
if configuration is None: if configuration is None:
return ResponseObject(False, "Configuration does not exist", status_code=404) return ResponseObject(False, "Configuration does not exist")
# f, fp = configuration.searchPeer(job['Peer'])
# if not f:
# return ResponseObject(False, "Peer does not exist")
peerKey = unquote(data['Peer']) s, p = AllPeerJobs.deleteJob(PeerJob(
success, result = AllPeerJobs.deleteJob(PeerJob( job['JobID'], job['Configuration'], job['Peer'], job['Field'], job['Operator'], job['Value'],
data['JobID'], data['Configuration'], peerKey, data.get('Field', ''), job['CreationDate'], job['ExpireDate'], job['Action']))
data.get('Operator', ''), data.get('Value', ''), if s:
datetime.now(), data.get('ExpireDate'), data.get('Action', ''))) return ResponseObject(s)
if success: return ResponseObject(s, message=p)
return ResponseObject(success, message="Job deleted successfully")
return ResponseObject(success, message=result)
@app.get(f'{APP_PREFIX}/api/PeerScheduleJobLogs/<configName>') @app.get(f'{APP_PREFIX}/api/getPeerScheduleJobLogs/<configName>')
def API_getPeerScheduleJobLogs(configName): def API_getPeerScheduleJobLogs(configName):
if configName not in WireguardConfigurations.keys(): if configName not in WireguardConfigurations.keys():
return ResponseObject(False, "Configuration does not exist") return ResponseObject(False, "Configuration does not exist")
@@ -1401,17 +1325,12 @@ def API_traceroute_execute():
data=json.dumps([x['ip'] for x in result])) data=json.dumps([x['ip'] for x in result]))
d = r.json() d = r.json()
for i in range(len(result)): for i in range(len(result)):
result[i]['geo'] = d[i] result[i]['geo'] = d[i]
return ResponseObject(data=result)
except Exception as e: 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") return ResponseObject(data=result, message="Failed to request IP address geolocation")
return ResponseObject(data=result)
except Exception as e: except Exception as exp:
app.logger.error(f"Failed to execute the traceroute: {e}") return ResponseObject(False, exp)
return ResponseObject(data=[], message="Failed to traceroute the given parameter")
else: else:
return ResponseObject(False, "Please provide ipAddress") return ResponseObject(False, "Please provide ipAddress")
@@ -1781,9 +1700,9 @@ Index Page
@app.get(f'{APP_PREFIX}/') @app.get(f'{APP_PREFIX}/')
def index(): def index():
return render_template('index.html', APP_PREFIX=APP_PREFIX) return render_template('index.html')
if __name__ == "__main__": if __name__ == "__main__":
startThreads() startThreads()
DashboardPlugins.startThreads() DashboardPlugins.startThreads()
app.run(host=app_ip, debug=False, port=app_port) app.run(host=app_ip, debug=False, port=app_port)

View File

@@ -1,5 +1,4 @@
import dashboard import dashboard
import os
from datetime import datetime from datetime import datetime
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger, Dash global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger, Dash
app_host, app_port = dashboard.gunicornConfig() app_host, app_port = dashboard.gunicornConfig()
@@ -17,7 +16,7 @@ daemon = True
pidfile = './gunicorn.pid' pidfile = './gunicorn.pid'
wsgi_app = "dashboard:app" wsgi_app = "dashboard:app"
accesslog = f"./log/access_{date}.log" accesslog = f"./log/access_{date}.log"
loglevel = os.environ['log_level'] if 'log_level' in os.environ else 'info' loglevel = "info"
capture_output = True capture_output = True
errorlog = f"./log/error_{date}.log" errorlog = f"./log/error_{date}.log"
pythonpath = "., ./modules" pythonpath = "., ./modules"

View File

@@ -1,120 +0,0 @@
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)
if len(updateAllowedIp.decode().strip("\n")) != 0:
current_app.logger.error(f"Update peer failed when updating Allowed IPs.\nInput: {newAllowedIPs}\nOutput: {updateAllowedIp.decode().strip('\n')}")
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"

View File

@@ -0,0 +1,92 @@
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()

View File

@@ -4,43 +4,28 @@ AmneziaWG Configuration
import random, sqlalchemy, os, subprocess, re, uuid import random, sqlalchemy, os, subprocess, re, uuid
from flask import current_app from flask import current_app
from .PeerJobs import PeerJobs from .PeerJobs import PeerJobs
from .AmneziaPeer import AmneziaPeer from .AmneziaWGPeer import AmneziaWGPeer
from .PeerShareLinks import PeerShareLinks from .PeerShareLinks import PeerShareLinks
from .Utilities import RegexMatch, CheckAddress, CheckPeerKey from .Utilities import RegexMatch
from .WireguardConfiguration import WireguardConfiguration from .WireguardConfiguration import WireguardConfiguration
from .DashboardWebHooks import DashboardWebHooks from .DashboardWebHooks import DashboardWebHooks
class AmneziaConfiguration(WireguardConfiguration): class AmneziaWireguardConfiguration(WireguardConfiguration):
def __init__(self, def __init__(self, DashboardConfig,
DashboardConfig,
AllPeerJobs: PeerJobs, AllPeerJobs: PeerJobs,
AllPeerShareLinks: PeerShareLinks, AllPeerShareLinks: PeerShareLinks,
DashboardWebHooks: DashboardWebHooks, DashboardWebHooks: DashboardWebHooks,
name: str = None, name: str = None, data: dict = None, backup: dict = None, startup: bool = False):
data: dict = None,
backup: dict = None,
startup: bool = False):
self.Jc = 0 self.Jc = 0
self.Jmin = 0 self.Jmin = 0
self.Jmax = 0 self.Jmax = 0
self.S1 = 0 self.S1 = 0
self.S2 = 0 self.S2 = 0
self.S3 = 0
self.S4 = 0
self.H1 = 1 self.H1 = 1
self.H2 = 2 self.H2 = 2
self.H3 = 3 self.H3 = 3
self.H4 = 4 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) super().__init__(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, name, data, backup, startup, wg=False)
@@ -73,68 +58,65 @@ class AmneziaConfiguration(WireguardConfiguration):
"Jmax": self.Jmax, "Jmax": self.Jmax,
"S1": self.S1, "S1": self.S1,
"S2": self.S2, "S2": self.S2,
"S3": self.S3,
"S4": self.S4,
"H1": self.H1, "H1": self.H1,
"H2": self.H2, "H2": self.H2,
"H3": self.H3, "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 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: if dbName is None:
dbName = self.Name dbName = self.Name
self.peersTable = sqlalchemy.Table( self.peersTable = sqlalchemy.Table(
f'{dbName}', self.metadata, *generate_column_obj(), extend_existing=True 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
) )
self.peersRestrictedTable = sqlalchemy.Table( self.peersRestrictedTable = sqlalchemy.Table(
f'{dbName}_restrict_access', self.metadata, *generate_column_obj(), extend_existing=True 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
) )
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( self.peersTransferTable = sqlalchemy.Table(
f'{dbName}_transfer', self.metadata, f'{dbName}_transfer', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
@@ -144,7 +126,38 @@ class AmneziaConfiguration(WireguardConfiguration):
sqlalchemy.Column('cumu_receive', sqlalchemy.Float), sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float), sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float), sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('time', time_col_type, server_default=sqlalchemy.func.now()), 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),
extend_existing=True extend_existing=True
) )
@@ -152,20 +165,15 @@ class AmneziaConfiguration(WireguardConfiguration):
f'{dbName}_history_endpoint', self.metadata, f'{dbName}_history_endpoint', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('time', time_col_type) sqlalchemy.Column('time',
) (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
self.infoTable = sqlalchemy.Table(
'ConfigurationsInfo', self.metadata,
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
sqlalchemy.Column('Info', sqlalchemy.Text),
extend_existing=True extend_existing=True
) )
self.metadata.create_all(self.engine) self.metadata.create_all(self.engine)
def getPeers(self): def getPeers(self):
self.Peers.clear() self.Peers.clear()
if self.configurationFileChanged(): if self.configurationFileChanged():
with open(self.configPath, 'r') as configFile: with open(self.configPath, 'r') as configFile:
p = [] p = []
@@ -203,9 +211,11 @@ class AmneziaConfiguration(WireguardConfiguration):
if tempPeer is None: if tempPeer is None:
tempPeer = { tempPeer = {
"id": i['PublicKey'], "id": i['PublicKey'],
"advanced_security": i.get('AdvancedSecurity', 'off'),
"private_key": "", "private_key": "",
"DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1], "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"), "name": i.get("name"),
"total_receive": 0, "total_receive": 0,
"total_sent": 0, "total_sent": 0,
@@ -219,7 +229,6 @@ class AmneziaConfiguration(WireguardConfiguration):
"cumu_data": 0, "cumu_data": 0,
"mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1], "mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1],
"keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1], "keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1],
"notes": "",
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" "preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
} }
@@ -234,14 +243,14 @@ class AmneziaConfiguration(WireguardConfiguration):
self.peersTable.columns.id == i['PublicKey'] self.peersTable.columns.id == i['PublicKey']
) )
) )
self.Peers.append(AmneziaPeer(tempPeer, self)) self.Peers.append(AmneziaWGPeer(tempPeer, self))
except Exception as e: except Exception as e:
current_app.logger.error(f"{self.Name} getPeers() Error", e) current_app.logger.error(f"{self.Name} getPeers() Error", e)
else: else:
with self.engine.connect() as conn: with self.engine.connect() as conn:
existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall() existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall()
for i in existingPeers: for i in existingPeers:
self.Peers.append(AmneziaPeer(i, self)) self.Peers.append(AmneziaWGPeer(i, self))
def addPeers(self, peers: list) -> tuple[bool, list, str]: def addPeers(self, peers: list) -> tuple[bool, list, str]:
result = { result = {
@@ -249,15 +258,6 @@ class AmneziaConfiguration(WireguardConfiguration):
"peers": [] "peers": []
} }
try: 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: with self.engine.begin() as conn:
for i in peers: for i in peers:
newPeer = { newPeer = {
@@ -278,9 +278,9 @@ class AmneziaConfiguration(WireguardConfiguration):
"cumu_data": 0, "cumu_data": 0,
"mtu": i['mtu'], "mtu": i['mtu'],
"keepalive": i['keepalive'], "keepalive": i['keepalive'],
"notes": i.get('notes', ''),
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["preshared_key"] "preshared_key": i["preshared_key"],
"advanced_security": i['advanced_security']
} }
conn.execute( conn.execute(
self.peersTable.insert().values(newPeer) self.peersTable.insert().values(newPeer)
@@ -293,15 +293,13 @@ class AmneziaConfiguration(WireguardConfiguration):
with open(uid, "w+") as f: with open(uid, "w+") as f:
f.write(p['preshared_key']) f.write(p['preshared_key'])
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(
subprocess.check_output(command, stderr=subprocess.STDOUT) 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)
if presharedKeyExist: if presharedKeyExist:
os.remove(uid) os.remove(uid)
subprocess.check_output(
command = [f"{self.Protocol}-quick", "save", self.Name] f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
subprocess.check_output(command, stderr=subprocess.STDOUT)
self.getPeers() self.getPeers()
for p in peers: for p in peers:
p = self.searchPeer(p['id']) p = self.searchPeer(p['id'])
@@ -313,7 +311,7 @@ class AmneziaConfiguration(WireguardConfiguration):
}) })
except Exception as e: except Exception as e:
current_app.logger.error("Add peers error", e) current_app.logger.error("Add peers error", e)
return False, [], "Internal server error" return False, [], str(e)
return True, result['peers'], "" return True, result['peers'], ""
def getRestrictedPeers(self): def getRestrictedPeers(self):
@@ -321,4 +319,4 @@ class AmneziaConfiguration(WireguardConfiguration):
with self.engine.connect() as conn: with self.engine.connect() as conn:
restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall() restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall()
for i in restricted: for i in restricted:
self.RestrictedPeers.append(AmneziaPeer(i, self)) self.RestrictedPeers.append(AmneziaWGPeer(i, self))

View File

@@ -1,15 +1,14 @@
import configparser import configparser
import os import os
from sqlalchemy_utils import database_exists, create_database from sqlalchemy_utils import database_exists, create_database
from flask import current_app
def ConnectionString(database) -> str: def ConnectionString(database) -> str:
parser = configparser.ConfigParser(strict=False) parser = configparser.ConfigParser(strict=False)
parser.read_file(open('wg-dashboard.ini', "r+")) parser.read_file(open('wg-dashboard.ini', "r+"))
sqlitePath = os.path.join("db") sqlitePath = os.path.join("db")
if not os.path.isdir(sqlitePath): if not os.path.isdir(sqlitePath):
os.mkdir(sqlitePath) os.mkdir(sqlitePath)
if parser.get("Database", "type") == "postgresql": if parser.get("Database", "type") == "postgresql":
cn = f'postgresql+psycopg://{parser.get("Database", "username")}:{parser.get("Database", "password")}@{parser.get("Database", "host")}/{database}' cn = f'postgresql+psycopg://{parser.get("Database", "username")}:{parser.get("Database", "password")}@{parser.get("Database", "host")}/{database}'
elif parser.get("Database", "type") == "mysql": elif parser.get("Database", "type") == "mysql":
@@ -20,6 +19,7 @@ def ConnectionString(database) -> str:
if not database_exists(cn): if not database_exists(cn):
create_database(cn) create_database(cn)
except Exception as e: except Exception as e:
current_app.logger.error("Database error. Terminating...", e)
exit(1) exit(1)
return cn return cn

View File

@@ -8,7 +8,7 @@ import pyotp
import sqlalchemy as db import sqlalchemy as db
import requests import requests
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
from .DashboardClientsTOTP import DashboardClientsTOTP from .DashboardClientsTOTP import DashboardClientsTOTP
from .DashboardOIDC import DashboardOIDC from .DashboardOIDC import DashboardOIDC

View File

@@ -1,7 +1,7 @@
import datetime import datetime
import uuid import uuid
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .DashboardLogger import DashboardLogger from .DashboardLogger import DashboardLogger
import sqlalchemy as db import sqlalchemy as db
from .WireguardConfiguration import WireguardConfiguration from .WireguardConfiguration import WireguardConfiguration

View File

@@ -3,7 +3,7 @@ import hashlib
import uuid import uuid
import sqlalchemy as db import sqlalchemy as db
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
class DashboardClientsTOTP: class DashboardClientsTOTP:

View File

@@ -7,15 +7,19 @@ import sqlalchemy as db
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from flask import current_app from flask import current_app
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .Utilities import (GetRemoteEndpoint, ValidateDNSAddress) from .Utilities import (
GetRemoteEndpoint, ValidateDNSAddress
)
from .DashboardAPIKey import DashboardAPIKey from .DashboardAPIKey import DashboardAPIKey
class DashboardConfig: class DashboardConfig:
DashboardVersion = 'v4.3.3' DashboardVersion = 'v4.3.2'
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.') ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini') ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini')
def __init__(self): def __init__(self):
if not os.path.exists(DashboardConfig.ConfigurationFilePath): if not os.path.exists(DashboardConfig.ConfigurationFilePath):
open(DashboardConfig.ConfigurationFilePath, "x") open(DashboardConfig.ConfigurationFilePath, "x")
@@ -51,8 +55,7 @@ class DashboardConfig:
"peer_display_mode": "grid", "peer_display_mode": "grid",
"remote_endpoint": GetRemoteEndpoint(), "remote_endpoint": GetRemoteEndpoint(),
"peer_MTU": "1420", "peer_MTU": "1420",
"peer_keep_alive": "21", "peer_keep_alive": "21"
"peer_preshared_key_default": "false"
}, },
"Other": { "Other": {
"welcome_session": "true" "welcome_session": "true"
@@ -80,11 +83,9 @@ class DashboardConfig:
}, },
"Clients": { "Clients": {
"enable": "true", "enable": "true",
"sign_up": "true"
}, },
"WireGuardConfiguration": { "WireGuardConfiguration": {
"autostart": "", "autostart": ""
"peer_tracking": "false"
} }
} }
@@ -101,63 +102,12 @@ class DashboardConfig:
self.APIAccessed = False self.APIAccessed = False
self.SetConfig("Server", "version", DashboardConfig.DashboardVersion) 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: def getConnectionString(self, database) -> str or None:
sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db") sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db")
if not os.path.isdir(sqlitePath): if not os.path.isdir(sqlitePath):
os.mkdir(sqlitePath) os.mkdir(sqlitePath)
if self.GetConfig("Database", "type")[1] == "postgresql": if self.GetConfig("Database", "type")[1] == "postgresql":
cn = f'postgresql+psycopg2://{self.GetConfig("Database", "username")[1]}:{self.GetConfig("Database", "password")[1]}@{self.GetConfig("Database", "host")[1]}/{database}' cn = f'postgresql+psycopg2://{self.GetConfig("Database", "username")[1]}:{self.GetConfig("Database", "password")[1]}@{self.GetConfig("Database", "host")[1]}/{database}'
elif self.GetConfig("Database", "type")[1] == "mysql": elif self.GetConfig("Database", "type")[1] == "mysql":
@@ -166,7 +116,7 @@ class DashboardConfig:
cn = f'sqlite:///{os.path.join(sqlitePath, f"{database}.db")}' cn = f'sqlite:///{os.path.join(sqlitePath, f"{database}.db")}'
if not database_exists(cn): if not database_exists(cn):
create_database(cn) create_database(cn)
return cn return cn
def __createAPIKeyTable(self): def __createAPIKeyTable(self):
self.apiKeyTable = db.Table('DashboardAPIKeys', self.dbMetadata, self.apiKeyTable = db.Table('DashboardAPIKeys', self.dbMetadata,

View File

@@ -4,7 +4,7 @@ Dashboard Logger Class
import uuid import uuid
import sqlalchemy as db import sqlalchemy as db
from flask import current_app from flask import current_app
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
class DashboardLogger: class DashboardLogger:

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
import requests import requests
from pydantic import BaseModel, field_serializer from pydantic import BaseModel, field_serializer
import sqlalchemy as db import sqlalchemy as db
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from flask import current_app from flask import current_app
WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated'] WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated']

View File

@@ -1,101 +1,76 @@
import os.path import os.path
import ssl
import smtplib import smtplib
# Email libaries
from email import encoders from email import encoders
from email.header import Header
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import formatdate from email.utils import formataddr
class EmailSender: class EmailSender:
def __init__(self, DashboardConfig): def __init__(self, DashboardConfig):
self.smtp = None
self.DashboardConfig = DashboardConfig self.DashboardConfig = DashboardConfig
if not os.path.exists('./attachments'): if not os.path.exists('./attachments'):
os.mkdir('./attachments') os.mkdir('./attachments')
def Server(self):
return self.DashboardConfig.GetConfig("Email", "server")[1]
def Port(self):
return self.DashboardConfig.GetConfig("Email", "port")[1]
def Encryption(self):
return self.DashboardConfig.GetConfig("Email", "encryption")[1]
def Username(self):
return self.DashboardConfig.GetConfig("Email", "username")[1]
def Password(self):
return self.DashboardConfig.GetConfig("Email", "email_password")[1]
def SendFrom(self):
return self.DashboardConfig.GetConfig("Email", "send_from")[1]
# Thank you, @gdeeble from GitHub
def AuthenticationRequired(self):
return self.DashboardConfig.GetConfig("Email", "authentication_required")[1]
self.refresh_vals() 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 refresh_vals(self) -> None: def send(self, receiver, subject, body, includeAttachment = False, attachmentName = "") -> tuple[bool, str] | tuple[bool, None]:
self.Server = self.DashboardConfig.GetConfig("Email", "server")[1] if self.ready():
self.Port = self.DashboardConfig.GetConfig("Email", "port")[1] 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["To"] = receiver
message.attach(MIMEText(body, "plain"))
self.Encryption = self.DashboardConfig.GetConfig("Email", "encryption")[1] if includeAttachment and len(attachmentName) > 0:
self.AuthRequired = self.DashboardConfig.GetConfig("Email", "authentication_required")[1] attachmentPath = os.path.join('./attachments', attachmentName)
self.Username = self.DashboardConfig.GetConfig("Email", "username")[1] if os.path.exists(attachmentPath):
self.Password = self.DashboardConfig.GetConfig("Email", "email_password")[1] attachment = MIMEBase("application", "octet-stream")
with open(os.path.join('./attachments', attachmentName), 'rb') as f:
self.SendFrom = self.DashboardConfig.GetConfig("Email", "send_from")[1] attachment.set_payload(f.read())
encoders.encode_base64(attachment)
def is_ready(self) -> bool: attachment.add_header("Content-Disposition", f"attachment; filename= {attachmentName}",)
self.refresh_vals() message.attach(attachment)
else:
if self.AuthRequired: self.smtp.close()
ready = all([ return False, "Attachment does not exist"
self.Server, self.Port, self.Encryption, self.smtp.sendmail(self.SendFrom(), receiver, message.as_string())
self.Username, self.Password, self.SendFrom self.smtp.close()
]) return True, None
else: except Exception as e:
ready = all([ return False, f"Send failed | Reason: {e}"
self.Server, self.Port, self.Encryption, self.SendFrom return False, "SMTP not configured"
])
return ready
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"
message = MIMEMultipart()
message['Subject'] = subject
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 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:
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}"
finally:
if smtp:
try:
smtp.quit()
except Exception:
pass

View File

@@ -2,7 +2,7 @@ import uuid
from pydantic import BaseModel, field_serializer from pydantic import BaseModel, field_serializer
import sqlalchemy as db import sqlalchemy as db
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
class NewConfigurationTemplate(BaseModel): class NewConfigurationTemplate(BaseModel):

View File

@@ -10,9 +10,8 @@ from datetime import timedelta
import jinja2 import jinja2
import sqlalchemy as db import sqlalchemy as db
from .PeerJob import PeerJob from .PeerJob import PeerJob
from flask import current_app
from .PeerShareLink import PeerShareLink from .PeerShareLink import PeerShareLink
from .Utilities import GenerateWireguardPublicKey, CheckAddress, ValidateDNSAddress from .Utilities import GenerateWireguardPublicKey, ValidateIPAddressesWithRange, ValidateDNSAddress
class Peer: class Peer:
@@ -35,7 +34,6 @@ class Peer:
self.cumu_data = tableData["cumu_data"] self.cumu_data = tableData["cumu_data"]
self.mtu = tableData["mtu"] self.mtu = tableData["mtu"]
self.keepalive = tableData["keepalive"] self.keepalive = tableData["keepalive"]
self.notes = tableData.get("notes", "")
self.remote_endpoint = tableData["remote_endpoint"] self.remote_endpoint = tableData["remote_endpoint"]
self.preshared_key = tableData["preshared_key"] self.preshared_key = tableData["preshared_key"]
self.jobs: list[PeerJob] = [] self.jobs: list[PeerJob] = []
@@ -51,89 +49,62 @@ class Peer:
def __repr__(self): def __repr__(self):
return str(self.toJson()) return str(self.toJson())
def updatePeer(self, name: str, def updatePeer(self, name: str, private_key: str,
private_key: str,
preshared_key: str, preshared_key: str,
dns_addresses: str, dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
allowed_ip: str, keepalive: int) -> tuple[bool, str] or tuple[bool, None]:
endpoint_allowed_ip: str,
mtu: int,
keepalive: int,
notes: str
) -> tuple[bool, str | None]:
if not self.configuration.getStatus(): if not self.configuration.getStatus():
self.configuration.toggleConfiguration() self.configuration.toggleConfiguration()
# Before we do any compute, let us check if the given endpoint allowed ip is valid at all existingAllowedIps = [item for row in list(
if not CheckAddress(endpoint_allowed_ip): map(lambda x: [q.strip() for q in x.split(',')],
return False, f"Endpoint Allowed IPs format is incorrect" map(lambda y: y.allowed_ip,
list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row]
peers = [] if allowed_ip in existingAllowedIps:
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" return False, "Allowed IP already taken by another peer"
if not ValidateDNSAddress(dns_addresses): if not ValidateIPAddressesWithRange(endpoint_allowed_ip):
return False, f"DNS IP-Address or FQDN is incorrect" return False, f"Endpoint Allowed IPs format is incorrect"
if isinstance(mtu, str): if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses):
return False, f"DNS format is incorrect"
if type(mtu) is str or mtu is None:
mtu = 0 mtu = 0
if isinstance(keepalive, str): if mtu < 0 or mtu > 1460:
keepalive = 0
if mtu not in range(0, 1461):
return False, "MTU format is not correct" return False, "MTU format is not correct"
if type(keepalive) is str or keepalive is None:
keepalive = 0
if keepalive < 0: if keepalive < 0:
return False, "Persistent Keepalive format is not correct" return False, "Persistent Keepalive format is not correct"
if len(private_key) > 0: if len(private_key) > 0:
pubKey = GenerateWireguardPublicKey(private_key) pubKey = GenerateWireguardPublicKey(private_key)
if not pubKey[0] or pubKey[1] != self.id: if not pubKey[0] or pubKey[1] != self.id:
return False, "Private key does not match with the public key" return False, "Private key does not match with the public key"
try: try:
rand = random.Random() rd = random.Random()
uid = str(uuid.UUID(int=rand.getrandbits(128), version=4)) uid = str(uuid.UUID(int=rd.getrandbits(128), version=4))
psk_exist = len(preshared_key) > 0 pskExist = len(preshared_key) > 0
if psk_exist: if pskExist:
with open(uid, "w+") as f: with open(uid, "w+") as f:
f.write(preshared_key) f.write(preshared_key)
newAllowedIPs = allowed_ip.replace(" ", "") newAllowedIPs = allowed_ip.replace(" ", "")
if not CheckAddress(newAllowedIPs): updateAllowedIp = subprocess.check_output(
return False, "Allowed IPs entry format is incorrect" 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)
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 pskExist: os.remove(uid)
if len(updateAllowedIp.decode().strip("\n")) != 0: if len(updateAllowedIp.decode().strip("\n")) != 0:
current_app.logger.error("Update peer failed when updating Allowed IPs") return False, "Update peer failed when updating Allowed IPs"
return False, "Internal server error" saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}",
shell=True, stderr=subprocess.STDOUT)
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'): 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, "Update peer failed when saving the configuration"
return False, "Internal server error"
with self.configuration.engine.begin() as conn: with self.configuration.engine.begin() as conn:
conn.execute( conn.execute(
self.configuration.peersTable.update().values({ self.configuration.peersTable.update().values({
@@ -143,7 +114,6 @@ class Peer:
"endpoint_allowed_ip": endpoint_allowed_ip, "endpoint_allowed_ip": endpoint_allowed_ip,
"mtu": mtu, "mtu": mtu,
"keepalive": keepalive, "keepalive": keepalive,
"notes": notes,
"preshared_key": preshared_key "preshared_key": preshared_key
}).where( }).where(
self.configuration.peersTable.c.id == self.id self.configuration.peersTable.c.id == self.id
@@ -151,8 +121,7 @@ class Peer:
) )
return True, None return True, None
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode('UTF-8')}") return False, exc.output.decode("UTF-8").strip()
return False, "Internal server error"
def downloadPeer(self) -> dict[str, str]: def downloadPeer(self) -> dict[str, str]:
final = { final = {
@@ -163,19 +132,17 @@ class Peer:
if len(filename) == 0: if len(filename) == 0:
filename = "UntitledPeer" filename = "UntitledPeer"
filename = "".join(filename.split(' ')) filename = "".join(filename.split(' '))
filename = f"{filename}"
# use previous filtering code if code below is insufficient or faulty illegal_filename = [".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2", "com3",
filename = re.sub(r'[.,/?<>\\:*|"]', '', filename).rstrip(". ") # remove special characters "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4",
"lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "con", "nul", "prn"]
reserved_pattern = r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$" # match com1-9, lpt1-9, con, nul, prn, aux, nul for i in illegal_filename:
filename = filename.replace(i, "")
if re.match(reserved_pattern, filename, re.IGNORECASE):
filename = f"file_{filename}" # prepend "file_" if it matches
for i in filename: for i in filename:
if re.match("^[a-zA-Z0-9_=+.-]$", i): if re.match("^[a-zA-Z0-9_=+.-]$", i):
final["fileName"] += i final["fileName"] += i
interfaceSection = { interfaceSection = {
"PrivateKey": self.private_key, "PrivateKey": self.private_key,
"Address": self.allowed_ip, "Address": self.allowed_ip,
@@ -188,7 +155,7 @@ class Peer:
if self.configuration.configurationInfo.OverridePeerSettings.DNS else self.DNS if self.configuration.configurationInfo.OverridePeerSettings.DNS else self.DNS
) )
} }
if self.configuration.Protocol == "awg": if self.configuration.Protocol == "awg":
interfaceSection.update({ interfaceSection.update({
"Jc": self.configuration.Jc, "Jc": self.configuration.Jc,
@@ -196,23 +163,12 @@ class Peer:
"Jmax": self.configuration.Jmax, "Jmax": self.configuration.Jmax,
"S1": self.configuration.S1, "S1": self.configuration.S1,
"S2": self.configuration.S2, "S2": self.configuration.S2,
"S3": self.configuration.S3,
"S4": self.configuration.S4,
"H1": self.configuration.H1, "H1": self.configuration.H1,
"H2": self.configuration.H2, "H2": self.configuration.H2,
"H3": self.configuration.H3, "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 = { peerSection = {
"PublicKey": self.configuration.PublicKey, "PublicKey": self.configuration.PublicKey,
"AllowedIPs": ( "AllowedIPs": (
@@ -236,7 +192,7 @@ class Peer:
for (key, val) in combine[s]: for (key, val) in combine[s]:
if val is not None and ((type(val) is str and len(val) > 0) or (type(val) is int and val > 0)): if val is not None and ((type(val) is str and len(val) > 0) or (type(val) is int and val > 0)):
final["file"] += f"{key} = {val}\n" final["file"] += f"{key} = {val}\n"
final["file"] = jinja2.Template(final["file"]).render(configuration=self.configuration) final["file"] = jinja2.Template(final["file"]).render(configuration=self.configuration)
@@ -395,4 +351,4 @@ class Peer:
hours, remainder = divmod(delta.total_seconds(), 3600) hours, remainder = divmod(delta.total_seconds(), 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"

View File

@@ -8,7 +8,7 @@ import sqlalchemy as db
from flask import current_app from flask import current_app
from sqlalchemy import RowMapping from sqlalchemy import RowMapping
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .Log import Log from .Log import Log
class PeerJobLogger: class PeerJobLogger:

View File

@@ -3,7 +3,7 @@ Peer Jobs
""" """
import sqlalchemy import sqlalchemy
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .PeerJob import PeerJob from .PeerJob import PeerJob
from .PeerJobLogger import PeerJobLogger from .PeerJobLogger import PeerJobLogger
import sqlalchemy as db import sqlalchemy as db

View File

@@ -1,4 +1,4 @@
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .PeerShareLink import PeerShareLink from .PeerShareLink import PeerShareLink
import sqlalchemy as db import sqlalchemy as db
from datetime import datetime from datetime import datetime

View File

@@ -1,6 +1,6 @@
import re, ipaddress import re, ipaddress
import subprocess import subprocess
import sqlalchemy
def RegexMatch(regex, text) -> bool: def RegexMatch(regex, text) -> bool:
""" """
@@ -18,18 +18,10 @@ def GetRemoteEndpoint() -> str:
@return: @return:
""" """
import socket import socket
try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.connect(("1.1.1.1", 80)) # Connecting to a public IP
s.connect(("1.1.1.1", 80)) # Connecting to a public IP
wgd_remote_endpoint = s.getsockname()[0] wgd_remote_endpoint = s.getsockname()[0]
return str(wgd_remote_endpoint) 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): def StringToBoolean(value: str):
@@ -41,35 +33,31 @@ def StringToBoolean(value: str):
return (value.strip().replace(" ", "").lower() in return (value.strip().replace(" ", "").lower() in
("yes", "true", "t", "1", 1)) ("yes", "true", "t", "1", 1))
def CheckAddress(ips_str: str) -> bool: def ValidateIPAddressesWithRange(ips: str) -> bool:
if len(ips_str) == 0: s = ips.replace(" ", "").split(",")
return False for ip in s:
for ip in ips_str.split(','):
stripped_ip = ip.strip()
try: try:
# Verify the IP-address, with the strict flag as false also allows for /32 and /128 ipaddress.ip_network(ip)
ipaddress.ip_network(stripped_ip, strict=False) except ValueError as e:
except ValueError:
return False return False
return True return True
def CheckPeerKey(peer_key: str) -> bool: def ValidateIPAddresses(ips) -> bool:
return re.match(r"^[A-Za-z0-9+/]{43}=$", peer_key) s = ips.replace(" ", "").split(",")
for ip in s:
def ValidateDNSAddress(addresses_str: str) -> tuple[bool, str | None]: try:
if len(addresses_str) == 0: ipaddress.ip_address(ip)
return False, "Got an empty list/string to check for valid DNS-addresses" except ValueError as e:
return False
addresses = addresses_str.split(',') return True
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]: def ValidateEndpointAllowedIPs(IPs) -> tuple[bool, str] | tuple[bool, None]:
ips = IPs.replace(" ", "").split(",") ips = IPs.replace(" ", "").split(",")
@@ -113,4 +101,4 @@ def ValidatePasswordStrength(password: str) -> tuple[bool, str] | tuple[bool, No
if not re.search(r'[$&+,:;=?@#|\'<>.\-^*()%!~_-]', password): if not re.search(r'[$&+,:;=?@#|\'<>.\-^*()%!~_-]', password):
return False, "Password must contain at least 1 special character from $&+,:;=?@#|'<>.-^*()%!~_-" return False, "Password must contain at least 1 special character from $&+,:;=?@#|'<>.-^*()%!~_-"
return True, None return True, None

View File

@@ -10,18 +10,13 @@ from datetime import datetime, timedelta
from itertools import islice from itertools import islice
from flask import current_app from flask import current_app
from .DatabaseConnection import ConnectionString from .ConnectionString import ConnectionString
from .DashboardConfig import DashboardConfig from .DashboardConfig import DashboardConfig
from .Peer import Peer from .Peer import Peer
from .PeerJobs import PeerJobs from .PeerJobs import PeerJobs
from .PeerShareLinks import PeerShareLinks from .PeerShareLinks import PeerShareLinks
from .Utilities import StringToBoolean, \ from .Utilities import StringToBoolean, GenerateWireguardPublicKey, RegexMatch, ValidateDNSAddress, \
GenerateWireguardPublicKey, \ ValidateEndpointAllowedIPs
RegexMatch, \
ValidateDNSAddress, \
ValidateEndpointAllowedIPs, \
CheckAddress, \
CheckPeerKey
from .WireguardConfigurationInfo import WireguardConfigurationInfo, PeerGroupsClass from .WireguardConfigurationInfo import WireguardConfigurationInfo, PeerGroupsClass
from .DashboardWebHooks import DashboardWebHooks from .DashboardWebHooks import DashboardWebHooks
@@ -66,14 +61,13 @@ class WireguardConfiguration:
self.Protocol = "wg" if wg else "awg" self.Protocol = "wg" if wg else "awg"
self.AllPeerJobs = AllPeerJobs self.AllPeerJobs = AllPeerJobs
self.DashboardConfig = DashboardConfig self.DashboardConfig = DashboardConfig
self.DashboardConfig.EnsureDatabaseIntegrity({self.Name: self})
self.AllPeerShareLinks = AllPeerShareLinks self.AllPeerShareLinks = AllPeerShareLinks
self.DashboardWebHooks = DashboardWebHooks self.DashboardWebHooks = DashboardWebHooks
self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf') self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf')
self.engine: sqlalchemy.Engine = sqlalchemy.create_engine(ConnectionString("wgdashboard")) self.engine: sqlalchemy.Engine = sqlalchemy.create_engine(ConnectionString("wgdashboard"))
self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData() self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData()
self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1] self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1]
if name is not None: if name is not None:
if data is not None and "Backup" in data.keys(): if data is not None and "Backup" in data.keys():
db = self.__importDatabase( db = self.__importDatabase(
@@ -109,31 +103,16 @@ class WireguardConfiguration:
} }
if self.Protocol == 'awg': if self.Protocol == 'awg':
values = { self.__parser["Interface"]["Jc"] = self.Jc
"Jc": self.Jc, self.__parser["Interface"]["Jc"] = self.Jc
"Jmin": self.Jmin, self.__parser["Interface"]["Jmin"] = self.Jmin
"Jmax": self.Jmax, self.__parser["Interface"]["Jmax"] = self.Jmax
"S1": self.S1, self.__parser["Interface"]["S1"] = self.S1
"S2": self.S2, self.__parser["Interface"]["S2"] = self.S2
"S3": self.S3, self.__parser["Interface"]["H1"] = self.H1
"S4": self.S4, self.__parser["Interface"]["H2"] = self.H2
"H1": self.H1, self.__parser["Interface"]["H3"] = self.H3
"H2": self.H2, self.__parser["Interface"]["H4"] = self.H4
"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(): if "Backup" not in data.keys():
self.createDatabase() self.createDatabase()
@@ -148,11 +127,8 @@ class WireguardConfiguration:
current_app.logger.info(f"Initialized Configuration: {name}") current_app.logger.info(f"Initialized Configuration: {name}")
self.__dumpDatabase() self.__dumpDatabase()
if self.getAutostartStatus() and not self.getStatus() and startup: if self.getAutostartStatus() and not self.getStatus() and startup:
status, ext = self.toggleConfiguration() self.toggleConfiguration()
if not status: current_app.logger.info(f"Autostart Configuration: {name}")
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 self.configurationInfo: WireguardConfigurationInfo | None = None
configurationInfoJson = self.readConfigurationInfo() configurationInfoJson = self.readConfigurationInfo()
@@ -164,6 +140,7 @@ class WireguardConfiguration:
if self.Status: if self.Status:
self.addAutostart() self.addAutostart()
def __getProtocolPath(self) -> str: def __getProtocolPath(self) -> str:
_, path = self.DashboardConfig.GetConfig("Server", "wg_conf_path") if self.Protocol == "wg" \ _, path = self.DashboardConfig.GetConfig("Server", "wg_conf_path") if self.Protocol == "wg" \
@@ -255,50 +232,54 @@ class WireguardConfiguration:
return True return True
def createDatabase(self, dbName = None): 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: if dbName is None:
dbName = self.Name dbName = self.Name
self.peersTable = sqlalchemy.Table( self.peersTable = sqlalchemy.Table(
f'{dbName}', self.metadata, *generate_column_obj(), extend_existing=True 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
) )
self.peersRestrictedTable = sqlalchemy.Table( self.peersRestrictedTable = sqlalchemy.Table(
f'{dbName}_restrict_access', self.metadata, *generate_column_obj(), extend_existing=True 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
) )
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( self.peersTransferTable = sqlalchemy.Table(
f'{dbName}_transfer', self.metadata, f'{dbName}_transfer', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
@@ -308,7 +289,8 @@ class WireguardConfiguration:
sqlalchemy.Column('cumu_receive', sqlalchemy.Float), sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
sqlalchemy.Column('cumu_sent', sqlalchemy.Float), sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
sqlalchemy.Column('cumu_data', sqlalchemy.Float), sqlalchemy.Column('cumu_data', sqlalchemy.Float),
sqlalchemy.Column('time', time_col_type, server_default=sqlalchemy.func.now()), sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP),
server_default=sqlalchemy.func.now()),
extend_existing=True extend_existing=True
) )
@@ -316,9 +298,34 @@ class WireguardConfiguration:
f'{dbName}_history_endpoint', self.metadata, f'{dbName}_history_endpoint', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False), sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('time', time_col_type) sqlalchemy.Column('time',
(sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
extend_existing=True
) )
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( self.infoTable = sqlalchemy.Table(
'ConfigurationsInfo', self.metadata, 'ConfigurationsInfo', self.metadata,
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True), sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
@@ -397,7 +404,6 @@ class WireguardConfiguration:
try: try:
if "[Peer]" not in content: if "[Peer]" not in content:
current_app.logger.info(f"{self.Name} config has no [Peer] section") current_app.logger.info(f"{self.Name} config has no [Peer] section")
self.Peers = []
return return
peerStarts = content.index("[Peer]") peerStarts = content.index("[Peer]")
@@ -433,7 +439,8 @@ class WireguardConfiguration:
"id": i['PublicKey'], "id": i['PublicKey'],
"private_key": "", "private_key": "",
"DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1], "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"), "name": i.get("name"),
"total_receive": 0, "total_receive": 0,
"total_sent": 0, "total_sent": 0,
@@ -447,7 +454,6 @@ class WireguardConfiguration:
"cumu_data": 0, "cumu_data": 0,
"mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1] if len(self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1]) > 0 else None, "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, "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], "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" "preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
} }
@@ -520,15 +526,6 @@ class WireguardConfiguration:
"peers": [] "peers": []
} }
try: 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: with self.engine.begin() as conn:
for i in peers: for i in peers:
newPeer = { newPeer = {
@@ -549,7 +546,6 @@ class WireguardConfiguration:
"cumu_data": 0, "cumu_data": 0,
"mtu": i['mtu'], "mtu": i['mtu'],
"keepalive": i['keepalive'], "keepalive": i['keepalive'],
"notes": i.get("notes", ""),
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
"preshared_key": i["preshared_key"] "preshared_key": i["preshared_key"]
} }
@@ -564,15 +560,12 @@ class WireguardConfiguration:
with open(uid, "w+") as f: with open(uid, "w+") as f:
f.write(p['preshared_key']) f.write(p['preshared_key'])
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(f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
subprocess.check_output(command, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
if presharedKeyExist: if presharedKeyExist:
os.remove(uid) os.remove(uid)
subprocess.check_output(
command = [f"{self.Protocol}-quick", "save", self.Name] f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
subprocess.check_output(command, stderr=subprocess.STDOUT)
self.getPeers() self.getPeers()
for p in peers: for p in peers:
p = self.searchPeer(p['id']) p = self.searchPeer(p['id'])
@@ -584,7 +577,7 @@ class WireguardConfiguration:
}) })
except Exception as e: except Exception as e:
current_app.logger.error("Add peers error", e) current_app.logger.error("Add peers error", e)
return False, [], "Internal server error" return False, [], str(e)
return True, result['peers'], "" return True, result['peers'], ""
def searchPeer(self, publicKey): def searchPeer(self, publicKey):
@@ -622,16 +615,8 @@ class WireguardConfiguration:
with open(uid, "w+") as f: with open(uid, "w+") as f:
f.write(restrictedPeer['preshared_key']) f.write(restrictedPeer['preshared_key'])
newAllowedIPs = restrictedPeer['allowed_ip'].replace(" ", "") 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 ''}",
if not CheckAddress(newAllowedIPs): shell=True, stderr=subprocess.STDOUT)
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) if presharedKeyExist: os.remove(uid)
else: else:
return False, "Failed to allow access of peer " + i return False, "Failed to allow access of peer " + i
@@ -651,9 +636,8 @@ class WireguardConfiguration:
found, pf = self.searchPeer(p) found, pf = self.searchPeer(p)
if found: if found:
try: try:
command = [self.Protocol, "set", self.Name, "peer", pf.id, "remove"] subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove",
subprocess.check_output(command, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
conn.execute( conn.execute(
self.peersRestrictedTable.insert().from_select( self.peersRestrictedTable.insert().from_select(
[c.name for c in self.peersTable.columns], [c.name for c in self.peersTable.columns],
@@ -681,8 +665,9 @@ class WireguardConfiguration:
if not self.__wgSave(): if not self.__wgSave():
return False, "Failed to save configuration through WireGuard" return False, "Failed to save configuration through WireGuard"
self.getRestrictedPeers()
self.getPeers() self.getPeers()
if numOfRestrictedPeers == len(listOfPublicKeys): if numOfRestrictedPeers == len(listOfPublicKeys):
return True, f"Restricted {numOfRestrictedPeers} peer(s)" return True, f"Restricted {numOfRestrictedPeers} peer(s)"
return False, f"Restricted {numOfRestrictedPeers} peer(s) successfully. Failed to restrict {numOfFailedToRestrictPeers} peer(s)" return False, f"Restricted {numOfRestrictedPeers} peer(s) successfully. Failed to restrict {numOfFailedToRestrictPeers} peer(s)"
@@ -734,20 +719,17 @@ class WireguardConfiguration:
def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]: def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]:
try: try:
command = [f"{self.Protocol}-quick", "save", self.Name] subprocess.check_output(f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
subprocess.check_output(command, stderr=subprocess.STDOUT)
return True, None return True, None
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
current_app.logger.error(f"Failed to process command:\n{str(e)}") return False, str(e)
return False, "Internal server error"
def getPeersLatestHandshake(self): def getPeersLatestHandshake(self):
if not self.getStatus(): if not self.getStatus():
self.toggleConfiguration() self.toggleConfiguration()
try: try:
command = [self.Protocol, "show", self.Name, "latest-handshakes"] latestHandshake = subprocess.check_output(f"{self.Protocol} show {self.Name} latest-handshakes",
latestHandshake = subprocess.check_output(command, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return "stopped" return "stopped"
latestHandshake = latestHandshake.decode("UTF-8").split() latestHandshake = latestHandshake.decode("UTF-8").split()
@@ -786,9 +768,8 @@ class WireguardConfiguration:
if not self.getStatus(): if not self.getStatus():
self.toggleConfiguration() self.toggleConfiguration()
# try: # try:
command = [self.Protocol, "show", self.Name, "transfer"] data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} transfer",
data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
data_usage = data_usage.decode("UTF-8").split("\n") data_usage = data_usage.decode("UTF-8").split("\n")
data_usage = [p.split("\t") for p in data_usage] data_usage = [p.split("\t") for p in data_usage]
@@ -802,13 +783,15 @@ class WireguardConfiguration:
) )
).mappings().fetchone() ).mappings().fetchone()
if cur_i is not None: if cur_i is not None:
# print(cur_i is None)
total_sent = cur_i['total_sent'] total_sent = cur_i['total_sent']
# print(cur_i is None)
total_receive = cur_i['total_receive'] total_receive = cur_i['total_receive']
cur_total_sent = float(data_usage[i][2]) / (1024 ** 3) cur_total_sent = float(data_usage[i][2]) / (1024 ** 3)
cur_total_receive = float(data_usage[i][1]) / (1024 ** 3) cur_total_receive = float(data_usage[i][1]) / (1024 ** 3)
cumulative_receive = cur_i['cumu_receive'] + total_receive cumulative_receive = cur_i['cumu_receive'] + total_receive
cumulative_sent = cur_i['cumu_sent'] + total_sent cumulative_sent = cur_i['cumu_sent'] + total_sent
if (total_sent * 0.999 ) <= cur_total_sent and (total_receive * 0.999) <= cur_total_receive: # An accuracy of 1K ppm is sufficient if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
total_sent = cur_total_sent total_sent = cur_total_sent
total_receive = cur_total_receive total_receive = cur_total_receive
else: else:
@@ -843,11 +826,10 @@ class WireguardConfiguration:
if not self.getStatus(): if not self.getStatus():
self.toggleConfiguration() self.toggleConfiguration()
try: try:
command = [self.Protocol, "show", self.Name, "endpoints"] data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} endpoints",
data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return "stopped" return "stopped"
data_usage = data_usage.decode("UTF-8").split() data_usage = data_usage.decode("UTF-8").split()
count = 0 count = 0
with self.engine.begin() as conn: with self.engine.begin() as conn:
@@ -865,17 +847,14 @@ class WireguardConfiguration:
self.getStatus() self.getStatus()
if self.Status: if self.Status:
try: try:
command = [f"{self.Protocol}-quick", "down", self.Name] check = subprocess.check_output(f"{self.Protocol}-quick down {self.Name}",
check = subprocess.check_output(command, stderr=subprocess.STDOUT) shell=True, stderr=subprocess.STDOUT)
self.removeAutostart() self.removeAutostart()
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
return False, str(exc.output.strip().decode("utf-8")) return False, str(exc.output.strip().decode("utf-8"))
else: else:
try: try:
command = [f"{self.Protocol}-quick", "up", self.Name] check = subprocess.check_output(f"{self.Protocol}-quick up {self.Name}", shell=True, stderr=subprocess.STDOUT)
check = subprocess.check_output(command, stderr=subprocess.STDOUT)
self.addAutostart() self.addAutostart()
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
return False, str(exc.output.strip().decode("utf-8")) return False, str(exc.output.strip().decode("utf-8"))
@@ -942,8 +921,8 @@ class WireguardConfiguration:
files.sort(key=lambda x: x[1], reverse=True) files.sort(key=lambda x: x[1], reverse=True)
for f, ct in files: for f, ct in files:
if RegexMatch(rf"^({self.Name})_(\d+)\\.(conf)$", f): if RegexMatch(f"^({self.Name})_(.*)\\.(conf)$", f):
s = re.search(rf"^({self.Name})_(\d+)\\.(conf)$", f) s = re.search(f"^({self.Name})_(.*)\\.(conf)$", f)
date = s.group(2) date = s.group(2)
d = { d = {
"filename": f, "filename": f,
@@ -1015,10 +994,8 @@ class WireguardConfiguration:
with open(self.configPath, 'r') as f: with open(self.configPath, 'r') as f:
original = [l.rstrip("\n") for l in f.readlines()] original = [l.rstrip("\n") for l in f.readlines()]
allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"] allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"]
awgKeys = []
if self.Protocol == 'awg': if self.Protocol == 'awg':
awgKeys = ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5", "J1", "J2", "J3", "Itime"] allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4"]
allowEdit += awgKeys
start = original.index("[Interface]") start = original.index("[Interface]")
try: try:
end = original.index("[Peer]") end = original.index("[Peer]")
@@ -1032,10 +1009,7 @@ class WireguardConfiguration:
if split[0] not in allowEdit: if split[0] not in allowEdit:
new.append(original[line]) new.append(original[line])
for key in allowEdit: for key in allowEdit:
val = str(newData.get(key, "")).strip() new.insert(1, f"{key} = {str(newData[key]).strip()}")
if key in awgKeys and val == "":
continue
new.insert(1, f"{key} = {val}")
new.append("") new.append("")
for line in range(end, len(original)): for line in range(end, len(original)):
new.append(original[line]) new.append(original[line])
@@ -1059,38 +1033,31 @@ class WireguardConfiguration:
return True return True
def renameConfiguration(self, newConfigurationName) -> tuple[bool, str]: 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: try:
if self.getStatus(): if self.getStatus():
self.toggleConfiguration() self.toggleConfiguration()
self.createDatabase(newConfigurationName) self.createDatabase(newConfigurationName)
with self.engine.begin() as conn: 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 `{newConfig}` SELECT * FROM `{oldConfig}`'
)
)
doRenameStatement("")
doRenameStatement("_restrict_access")
doRenameStatement("_deleted")
doRenameStatement("_transfer")
conn.execute( conn.execute(
self.infoTable.update() sqlalchemy.text(
.where(self.infoTable.c.ID == self.Name) f'INSERT INTO "{newConfigurationName}" SELECT * FROM "{self.Name}"'
.values(ID=newConfigurationName) )
)
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.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName) self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName)
shutil.copy( shutil.copy(
self.configPath, self.configPath,
@@ -1098,8 +1065,8 @@ class WireguardConfiguration:
) )
self.deleteConfiguration() self.deleteConfiguration()
except Exception as e: except Exception as e:
current_app.logger.error(f"Failed to rename configuration.\nNew Configuration Name: {newConfigurationName}\nError: {str(e)}") traceback.print_stack()
return False, "Internal server error" return False, str(e)
return True, None return True, None
def getNumberOfAvailableIP(self): def getNumberOfAvailableIP(self):
@@ -1259,6 +1226,7 @@ class WireguardConfiguration:
def __validateOverridePeerSettings(self, key: str, value: str | int) -> tuple[bool, None] | tuple[bool, str]: def __validateOverridePeerSettings(self, key: str, value: str | int) -> tuple[bool, None] | tuple[bool, str]:
status = True status = True
msg = None msg = None
print(value)
if key == "DNS" and value: if key == "DNS" and value:
status, msg = ValidateDNSAddress(value) status, msg = ValidateDNSAddress(value)
elif key == "EndpointAllowedIPs" and value: elif key == "EndpointAllowedIPs" and value:
@@ -1323,4 +1291,4 @@ class WireguardConfiguration:
conn.execute(sqlalchemy.text('VACUUM;')) conn.execute(sqlalchemy.text('VACUUM;'))
except Exception as e: except Exception as e:
return False return False
return True return True

View File

@@ -2,16 +2,16 @@ bcrypt==5.0.0
ifcfg==0.24 ifcfg==0.24
psutil==7.2.2 psutil==7.2.2
pyotp==2.9.0 pyotp==2.9.0
Flask==3.1.2 Flask==3.1.3
flask-cors==6.0.2 flask-cors==6.0.2
icmplib==3.0.4 icmplib==3.0.4
gunicorn==25.0.3 gunicorn==25.3.0
requests==2.32.5 requests==2.33.1
tcconfig==0.30.1 tcconfig==0.30.1
sqlalchemy==2.0.49 sqlalchemy==2.0.48
sqlalchemy_utils==0.42.1 sqlalchemy_utils==0.42.1
psycopg[binary]==3.3.3 psycopg[binary]==3.3.3
PyMySQL==1.1.3 PyMySQL==1.1.2
tzlocal==5.3.1 tzlocal==5.3.1
python-jose==3.5.0 python-jose==3.5.0
pydantic==2.13.3 pydantic==2.12.5

View File

@@ -6,21 +6,14 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="WGDashboard"> <meta name="application-name" content="WGDashboard">
<meta name="apple-mobile-web-app-title" content="WGDashboard"> <meta name="apple-mobile-web-app-title" content="WGDashboard">
<link rel="manifest" href="./json/manifest.json"> <link rel="manifest" href="/json/manifest.json">
<link rel="icon" href="./img/Logo-2-512x512.png"> <link rel="icon" href="/img/Logo-2-512x512.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WGDashboard</title> <title>WGDashboard</title>
<base href="./">
<script>
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
const base = document.querySelector('base');
if (base && isViteDevMode) {
base.href = '/';
}
</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./src/main.js"></script> <script type="module" src="./src/main.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "app", "name": "app",
"version": "4.3.3", "version": "4.3.2",
"private": true, "private": true,
"type": "module", "type": "module",
"module": "es2022", "module": "es2022",
@@ -13,35 +13,35 @@
}, },
"dependencies": { "dependencies": {
"@volar/language-server": "2.4.28", "@volar/language-server": "2.4.28",
"@vue/language-server": "3.2.7", "@vue/language-server": "3.2.4",
"@vuepic/vue-datepicker": "^12.1.0", "@vuepic/vue-datepicker": "^12.1.0",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.0",
"@vueuse/shared": "^14.1.0", "@vueuse/shared": "^14.2.1",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"cidr-tools": "^11.3.3", "cidr-tools": "^11.0.8",
"css-color-converter": "^2.0.0", "css-color-converter": "^2.0.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"electron-builder": "^26.7.0", "electron-builder": "^26.7.0",
"fuse.js": "^7.3.0", "fuse.js": "^7.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"is-cidr": "^6.0.3", "is-cidr": "^6.0.3",
"npm": "^11.8.0", "npm": "^11.12.1",
"ol": "^10.9.0", "ol": "^10.8.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qrcodejs": "^1.0.0", "qrcodejs": "^1.0.0",
"simple-code-editor": "^2.0.9", "simple-code-editor": "^2.0.9",
"uuid": "^14.0.0", "uuid": "^13.0.0",
"vue": "^3.5.32", "vue": "^3.5.28",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
"vue-router": "^5.0.4" "vue-router": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.4",
"vite": "^8.0.9" "vite": "^7.3.1"
}, },
"overrides": { "overrides": {
"tar": "^7.5.6" "tar": "^7.5.6"

View File

@@ -2,7 +2,7 @@
import { ref, reactive } from "vue" import { ref, reactive } from "vue"
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import OidcSettings from "@/components/clientComponents/clientSettingComponents/oidcSettings.vue"; import OidcSettings from "@/components/clientComponents/clientSettingComponents/oidcSettings.vue";
import { fetchGet, fetchPost } from "@/utilities/fetch.js" import { fetchGet } from "@/utilities/fetch.js"
const emits = defineEmits(['close']) const emits = defineEmits(['close'])
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore" import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore"
const dashboardConfigurationStore = DashboardConfigurationStore() const dashboardConfigurationStore = DashboardConfigurationStore()
@@ -12,16 +12,12 @@ const values = reactive({
}) })
const toggling = ref(false) const toggling = ref(false)
const updateSettings = async (key: string) => { const toggleClientSideApp = async () => {
toggling.value = true toggling.value = true
await fetchPost("/api/updateDashboardConfigurationItem", { await fetchGet("/api/clients/toggleStatus", {}, (res) => {
section: "Clients", values.enableClients = res.data
key: key,
value: dashboardConfigurationStore.Configuration.Clients[key]
}, async (res) => {
await dashboardConfigurationStore.getConfiguration()
toggling.value = false
}) })
toggling.value = false
} }
</script> </script>
@@ -41,43 +37,16 @@ const updateSettings = async (key: string) => {
</h6> </h6>
<div class="form-check form-switch ms-auto"> <div class="form-check form-switch ms-auto">
<label class="form-check-label" for="oidc_switch"> <label class="form-check-label" for="oidc_switch">
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.enable ? 'Enabled':'Disabled'"></LocaleText> <LocaleText :t="values.enableClients ? 'Enabled':'Disabled'"></LocaleText>
</label> </label>
<input <input
:disabled="toggling" :disabled="oidcStatusLoading"
v-model="dashboardConfigurationStore.Configuration.Clients.enable" v-model="values.enableClients"
@change="updateSettings('enable')" @change="toggleClientSideApp()"
class="form-check-input" type="checkbox" role="switch" id="oidc_switch"> class="form-check-input" type="checkbox" role="switch" id="oidc_switch">
</div> </div>
</div> </div>
<hr> <OidcSettings mode="Client"></OidcSettings>
<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> </div>
</div> </div>

View File

@@ -197,14 +197,14 @@ const deleteConfigurationModal = ref(false)
v-model="data[key]" v-model="data[key]"
:id="'configuration_' + key"> :id="'configuration_' + key">
</div> </div>
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']" <div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4']"
v-if="configurationInfo.Protocol === 'awg'"> v-if="configurationInfo.Protocol === 'awg'">
<label :for="'configuration_' + key" class="form-label"> <label :for="'configuration_' + key" class="form-label">
<small class="text-muted"> <small class="text-muted">
<LocaleText :t="key"></LocaleText> <LocaleText :t="key"></LocaleText>
</small> </small>
</label> </label>
<input type="text" class="form-control form-control-sm rounded-3" <input type="number" class="form-control form-control-sm rounded-3"
:disabled="saving" :disabled="saving"
v-model="data[key]" v-model="data[key]"
:id="'configuration_' + key"> :id="'configuration_' + key">

View File

@@ -1,31 +0,0 @@
<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>

View File

@@ -6,27 +6,18 @@ export default {
components: {LocaleText}, components: {LocaleText},
props: { props: {
data: Object, data: Object,
saving: Boolean, saving: Boolean
defaultEnabled: Boolean
}, },
data(){ data(){
return{ return{
enable: false 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:{ watch:{
enable(){ enable(){
if (this.enable) { if (this.enable){
if (!this.data.preshared_key){ this.data.preshared_key = window.wireguard.generateKeypair().presharedKey
this.data.preshared_key = window.wireguard.generateKeypair().presharedKey }else {
}
} else {
this.data.preshared_key = "" this.data.preshared_key = ""
} }
} }
@@ -57,4 +48,4 @@ export default {
<style scoped> <style scoped>
</style> </style>

View File

@@ -134,17 +134,12 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<div class="card-footer" role="button" @click="$emit('details')" v-if="!this.Peer.restricted"> <div class="card-footer" role="button" @click="$emit('details')">
<small class="d-flex align-items-center"> <small class="d-flex align-items-center">
<LocaleText t="Details"></LocaleText> <LocaleText t="Details"></LocaleText>
<i class="bi bi-chevron-right ms-auto"></i> <i class="bi bi-chevron-right ms-auto"></i>
</small> </small>
</div> </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> </div>
</template> </template>

View File

@@ -15,7 +15,6 @@ import MtuInput from "@/components/configurationComponents/newPeersComponents/mt
import PersistentKeepAliveInput import PersistentKeepAliveInput
from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue"; from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import NotesInput from "./newPeersComponents/notesInput.vue";
const dashboardStore = DashboardConfigurationStore() const dashboardStore = DashboardConfigurationStore()
const wireguardStore = WireguardConfigurationsStore() const wireguardStore = WireguardConfigurationsStore()
@@ -28,11 +27,11 @@ const peerData = ref({
public_key: "", public_key: "",
DNS: dashboardStore.Configuration.Peers.peer_global_dns, DNS: dashboardStore.Configuration.Peers.peer_global_dns,
endpoint_allowed_ip: dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip, endpoint_allowed_ip: dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip,
notes: "",
keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive), keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive),
mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu), mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu),
preshared_key: "", preshared_key: "",
preshared_key_bulkAdd: Boolean(dashboardStore.Configuration.Peers.peer_preshared_key_default), preshared_key_bulkAdd: false,
advanced_security: "off",
allowed_ips_validation: true, allowed_ips_validation: true,
}) })
const availableIp = ref([]) const availableIp = ref([])
@@ -106,7 +105,6 @@ watch(() => {
<template v-if="!peerData.bulkAdd"> <template v-if="!peerData.bulkAdd">
<hr class="mb-0 mt-2"> <hr class="mb-0 mt-2">
<NameInput :saving="saving" :data="peerData"></NameInput> <NameInput :saving="saving" :data="peerData"></NameInput>
<NotesInput :saving="saving" :data="peerData"></NotesInput>
<PrivatePublicKeyInput :saving="saving" :data="peerData"></PrivatePublicKeyInput> <PrivatePublicKeyInput :saving="saving" :data="peerData"></PrivatePublicKeyInput>
<AllowedIPsInput :availableIp="availableIp" :saving="saving" :data="peerData"></AllowedIPsInput> <AllowedIPsInput :availableIp="availableIp" :saving="saving" :data="peerData"></AllowedIPsInput>
</template> </template>
@@ -120,7 +118,7 @@ watch(() => {
<LocaleText t="Advanced Options"></LocaleText> <LocaleText t="Advanced Options"></LocaleText>
</button> </button>
</h2> </h2>
<div id="peerAddModalAccordionAdvancedOptions" <div id="peerAddModalAccordionAdvancedOptions"
class="accordion-collapse collapse collapsed" data-bs-parent="#peerAddModalAccordion"> class="accordion-collapse collapse collapsed" data-bs-parent="#peerAddModalAccordion">
<div class="accordion-body rounded-bottom-3"> <div class="accordion-body rounded-bottom-3">
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
@@ -128,7 +126,7 @@ watch(() => {
<EndpointAllowedIps :saving="saving" :data="peerData"></EndpointAllowedIps> <EndpointAllowedIps :saving="saving" :data="peerData"></EndpointAllowedIps>
<div class="row gy-3"> <div class="row gy-3">
<div class="col-sm" v-if="!peerData.bulkAdd"> <div class="col-sm" v-if="!peerData.bulkAdd">
<PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd" :defaultEnabled="Boolean(dashboardStore.Configuration.Peers.peer_preshared_key_default)"></PresharedKeyInput> <PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd"></PresharedKeyInput>
</div> </div>
<div class="col-sm"> <div class="col-sm">
@@ -177,4 +175,4 @@ watch(() => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -36,7 +36,8 @@ export default {
keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive), keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive),
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu), mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
preshared_key: "", preshared_key: "",
preshared_key_bulkAdd: Boolean(this.dashboardStore.Configuration.Peers.peer_preshared_key_default), preshared_key_bulkAdd: false,
advanced_security: "off",
}, },
availableIp: undefined, availableIp: undefined,
availableIpSearchString: "", availableIpSearchString: "",
@@ -133,7 +134,7 @@ export default {
<hr class="mb-0 mt-2"> <hr class="mb-0 mt-2">
<div class="row gy-3"> <div class="row gy-3">
<div class="col-sm" v-if="!this.data.bulkAdd"> <div class="col-sm" v-if="!this.data.bulkAdd">
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd" :defaultEnabled="Boolean(this.dashboardStore.Configuration.Peers.peer_preshared_key_default)"></PresharedKeyInput> <PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd"></PresharedKeyInput>
</div> </div>
<div class="col-sm"> <div class="col-sm">
@@ -187,4 +188,4 @@ div{
.card{ .card{
max-height: 100%; max-height: 100%;
} }
</style> </style>

View File

@@ -32,7 +32,6 @@ Chart.register(
import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue"; import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue";
import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue"; import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue";
import PeerEndpoints from "@/components/peerDetailsModalComponents/peerEndpoints.vue"; import PeerEndpoints from "@/components/peerDetailsModalComponents/peerEndpoints.vue";
import { GetLocale } from "@/utilities/locale"
const props = defineProps(['selectedPeer']) const props = defineProps(['selectedPeer'])
const selectedDate = ref(undefined) const selectedDate = ref(undefined)
defineEmits(['close']) defineEmits(['close'])
@@ -50,18 +49,13 @@ defineEmits(['close'])
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button> <button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
</div> </div>
<div class="card-body px-4"> <div class="card-body px-4">
<div class="d-flex justify-content-between align-items-start mb-2 flex-column flex-md-row"> <div>
<div> <p class="mb-0 text-muted"><small>
<p class="mb-0 text-muted"><small><LocaleText t="Peer" /></small></p> <LocaleText t="Peer"></LocaleText>
<h2 :class="{'text-muted': selectedPeer.name.length === 0 }"> </small></p>
{{ selectedPeer.name.length > 0 ? selectedPeer.name : GetLocale("Untitled Peer") }} <h2>
</h2> {{ selectedPeer.name }}
</div> </h2>
<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>
<div class="row mt-3 gy-2 gx-2 mb-2"> <div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-12 col-lg-3"> <div class="col-12 col-lg-3">

View File

@@ -26,7 +26,7 @@ export default {
methods: { methods: {
async fetchLog(){ async fetchLog(){
this.dataLoading = true; this.dataLoading = true;
await fetchGet(`/api/PeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => { await fetchGet(`/api/getPeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
this.data = res.data; this.data = res.data;
this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss") this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss")
this.dataLoading = false; this.dataLoading = false;

View File

@@ -2,7 +2,7 @@
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue"; import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
import {ref} from "vue"; import {ref} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {fetchPost, fetchPut, fetchDelete} from "@/utilities/fetch.js"; import {fetchPost} from "@/utilities/fetch.js";
import { VueDatePicker } from "@vuepic/vue-datepicker"; import { VueDatePicker } from "@vuepic/vue-datepicker";
import dayjs from "dayjs"; import dayjs from "dayjs";
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
@@ -46,8 +46,9 @@ export default {
methods: { methods: {
save(){ save(){
if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){ if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){
const fn = this.newJob ? fetchPost : fetchPut; fetchPost(`/api/savePeerScheduleJob`, {
fn(`/api/PeerScheduleJob`, this.job, (res) => { Job: this.job
}, (res) => {
if (res.status){ if (res.status){
this.edit = false; this.edit = false;
this.store.newMessage("Server", "Peer job saved", "success") this.store.newMessage("Server", "Peer job saved", "success")
@@ -83,7 +84,9 @@ export default {
}, },
delete(){ delete(){
if(this.job.CreationDate){ if(this.job.CreationDate){
fetchDelete(`/api/PeerScheduleJob`, this.job, (res) => { fetchPost(`/api/deletePeerScheduleJob`, {
Job: this.job
}, (res) => {
if (!res.status){ if (!res.status){
this.store.newMessage("Server", res.message, "danger") this.store.newMessage("Server", res.message, "danger")
this.$emit('delete') this.$emit('delete')

View File

@@ -99,17 +99,6 @@ export default {
v-model="this.data.name" v-model="this.data.name"
id="peer_name_textbox" placeholder=""> id="peer_name_textbox" placeholder="">
</div> </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>
<div class="d-flex position-relative"> <div class="d-flex position-relative">
<label for="peer_private_key_textbox" class="form-label"> <label for="peer_private_key_textbox" class="form-label">

View File

@@ -61,7 +61,7 @@ const sendTestEmail = async () => {
<div class="card-header"> <div class="card-header">
<h6 class="my-2 d-flex"> <h6 class="my-2 d-flex">
<i class="bi bi-envelope-fill me-2"></i> <i class="bi bi-envelope-fill me-2"></i>
<LocaleText t="Email Server Settings"></LocaleText> <LocaleText t="Email Account"></LocaleText>
<span class="text-success ms-auto" v-if="emailIsReady"> <span class="text-success ms-auto" v-if="emailIsReady">
<i class="bi bi-check-circle-fill me-2"></i> <i class="bi bi-check-circle-fill me-2"></i>
<LocaleText t="Ready"></LocaleText> <LocaleText t="Ready"></LocaleText>
@@ -115,9 +115,6 @@ const sendTestEmail = async () => {
<select class="form-select rounded-3" <select class="form-select rounded-3"
v-model="store.Configuration.Email.encryption" v-model="store.Configuration.Email.encryption"
id="encryption"> id="encryption">
<option value="IMPLICITTLS">
IMPLICIT TLS
</option>
<option value="STARTTLS"> <option value="STARTTLS">
STARTTLS STARTTLS
</option> </option>
@@ -211,4 +208,4 @@ const sendTestEmail = async () => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -4,69 +4,19 @@ import LocaleText from "@/components/text/localeText.vue";
import ConfigurationTracking import ConfigurationTracking
from "@/components/settingsComponent/dashboardWireguardConfigurationTrackingComponents/configurationTracking.vue"; from "@/components/settingsComponent/dashboardWireguardConfigurationTrackingComponents/configurationTracking.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; 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 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> </script>
<template> <template>
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <div class="card-header">
<h6 class="my-2"> <h6 class="my-2">
<LocaleText t="Peer Tracking"></LocaleText> <LocaleText t="Peer Tracking"></LocaleText>
</h6> </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> </div>
<div class="card-body d-flex flex-column gap-3" v-if="peerTrackingStatus"> <div class="card-body d-flex flex-column gap-3">
<template v-if="!loaded"> <ConfigurationTracking :configuration="configuration" v-for="configuration in store.Configurations"/>
<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>
</div> </div>
</template> </template>

View File

@@ -4,7 +4,7 @@ import {onMounted, ref, watch} from "vue";
import LocaleText from "@/components/text/localeText.vue"; import LocaleText from "@/components/text/localeText.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const props = defineProps(['configuration', 'trackingData']) const props = defineProps(['configuration'])
const sizes = ref({ const sizes = ref({
HistoricalTrackingTableSize: 0, HistoricalTrackingTableSize: 0,
TrafficTrackingTableSize: 0 TrafficTrackingTableSize: 0
@@ -13,15 +13,16 @@ const sizeDataLoaded = ref(false)
const toggling = ref(false) const toggling = ref(false)
await onMounted(async () => { await onMounted(async () => {
sizes.value = props.trackingData[props.configuration.Name] await loadSizeData();
}) })
const loadSizeData = async () => { const loadSizeData = async () => {
sizeDataLoaded.value = false;
await fetchGet("/api/getPeerTrackingTableCounts", { await fetchGet("/api/getPeerTrackingTableCounts", {
configurationName: props.configuration.Name configurationName: props.configuration.Name
}, (res) => { }, (res) => {
sizes.value = res.data; sizes.value = res.data;
sizeDataLoaded.value = true;
}) })
} }
@@ -147,7 +148,8 @@ const deleteRecord = async (key) => {
<hr /> <hr />
<div class="d-flex align-items-start align-items-md-center flex-column flex-md-row gap-2"> <div class="d-flex align-items-start align-items-md-center flex-column flex-md-row gap-2">
<div> <div>
<h6 class="mb-0"> <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">
{{ sizes.HistoricalTrackingTableSize }} <span class="text-muted fw-normal"><LocaleText t="Records"></LocaleText></span> {{ sizes.HistoricalTrackingTableSize }} <span class="text-muted fw-normal"><LocaleText t="Records"></LocaleText></span>
</h6> </h6>
</div> </div>

View File

@@ -22,8 +22,6 @@ import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefau
targetData="peer_mtu" title="MTU"></PeersDefaultSettingsInput> targetData="peer_mtu" title="MTU"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput <PeersDefaultSettingsInput
targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput> targetData="peer_keep_alive" title="Persistent Keepalive"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput
targetData="peer_preshared_key_default" title="Pre-Shared Key Default"></PeersDefaultSettingsInput>
<PeersDefaultSettingsInput <PeersDefaultSettingsInput
targetData="remote_endpoint" title="Peer Remote Endpoint" 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." :warning="true" warningText="This will be changed globally, and will be apply to all peer's QR code and configuration file."
@@ -36,4 +34,4 @@ import PeersDefaultSettingsInput from "@/components/settingsComponent/peersDefau
<style scoped> <style scoped>
</style> </style>

View File

@@ -31,11 +31,6 @@ export default {
mounted() { mounted() {
this.value = this.store.Configuration.Peers[this.targetData]; this.value = this.store.Configuration.Peers[this.targetData];
}, },
computed: {
isBoolean(){
return typeof this.value === "boolean"
}
},
methods:{ methods:{
async useValidation(){ async useValidation(){
if(this.changed){ if(this.changed){
@@ -72,14 +67,7 @@ export default {
<LocaleText :t="this.title"></LocaleText> <LocaleText :t="this.title"></LocaleText>
</small></strong> </small></strong>
</label> </label>
<div v-if="isBoolean" class="form-check form-switch"> <input type="text" class="form-control"
<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}" :class="{'is-invalid': showInvalidFeedback, 'is-valid': isValid}"
:id="this.uuid" :id="this.uuid"
v-model="this.value" v-model="this.value"
@@ -100,4 +88,4 @@ export default {
<style scoped> <style scoped>
</style> </style>

View File

@@ -20,6 +20,7 @@ const squareHeight = computed(() => {
<template> <template>
<div class="flex-grow-1 square rounded-3 border position-relative p-2" <div class="flex-grow-1 square rounded-3 border position-relative p-2"
@mouseenter="show = true" @mouseenter="show = true"
@mouseleave="show = false" @mouseleave="show = false"
:style="{'background-color': `rgb(13 110 253 / ${percentage*10}%)`}"> :style="{'background-color': `rgb(13 110 253 / ${percentage*10}%)`}">

View File

@@ -49,7 +49,7 @@ const data = computed(() => {
<div class="progress" role="progressbar" style="height: 6px"> <div class="progress" role="progressbar" style="height: 6px">
<div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div> <div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div>
</div> </div>
<div class="d-grid mt-2 gap-1" style="grid-template-columns: repeat(10, 1fr)"> <div class="d-flex mt-2 gap-1">
<CpuCore <CpuCore
v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu" v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu"
:key="count" :key="count"
@@ -74,7 +74,7 @@ const data = computed(() => {
<div class="progress" role="progressbar" style="height: 6px"> <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 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>
<div class="d-grid mt-2 gap-1" style="grid-template-columns: repeat(10, 1fr)"> <div class="d-flex mt-2 gap-1">
<StorageMount v-for="(disk, count) in data?.Disks" <StorageMount v-for="(disk, count) in data?.Disks"
v-if="data" v-if="data"
:key="disk.mountPoint" :key="disk.mountPoint"

View File

@@ -27,11 +27,8 @@ export const getUrl = (url) => {
if (apiKey){ if (apiKey){
return `${apiKey.host}${url}` return `${apiKey.host}${url}`
} }
if (import.meta.env.MODE === 'development') { return import.meta.env.MODE === 'development' ? url
return url; : `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
}
// const appPrefix = window.APP_PREFIX || '';
return `./.${url}`;
} }
export const fetchGet = async (url, params=undefined, callback=undefined) => { export const fetchGet = async (url, params=undefined, callback=undefined) => {
@@ -79,50 +76,4 @@ export const fetchPost = async (url, body, callback) => {
console.log("Error:", x) console.log("Error:", x)
router.push({path: '/signin'}) 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'})
})
} }

View File

@@ -43,17 +43,10 @@ export default {
Jmax: 998, Jmax: 998,
S1: 17, S1: 17,
S2: 110, S2: 110,
S3: 1,
S4: 2,
H1: 0, H1: 0,
H2: 0, H2: 0,
H3: 0, H3: 0,
H4: 0, H4: 0
I1: "0",
I2: "0",
I3: "0",
I4: "0",
I5: "0"
}, },
numberOfAvailableIPs: "0", numberOfAvailableIPs: "0",
error: false, error: false,
@@ -66,16 +59,14 @@ export default {
}, },
created() { created() {
this.wireguardGenerateKeypair(); this.wireguardGenerateKeypair();
let hValue = []
// Generate 4 random numbers for H1, H2, H3, H4 while ([...new Set(hValue)].length !== 4){
['H1', 'H2', 'H3', 'H4'].forEach(key => { 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[key] = this.rand(1, 2**31); }
}); this.newConfiguration.H1 = hValue[0]
this.newConfiguration.H2 = hValue[1]
// Initialize I1 to I5 as "0" this.newConfiguration.H3 = hValue[2]
['I1', 'I2', 'I3', 'I4', 'I5'].forEach(key => { this.newConfiguration.H4 = hValue[3]
this.newConfiguration[key] = "0";
});
}, },
methods: { methods: {
rand(min, max){ rand(min, max){
@@ -388,7 +379,7 @@ export default {
<div class="card rounded-3" <div class="card rounded-3"
v-if="this.newConfiguration.Protocol === 'awg'" v-if="this.newConfiguration.Protocol === 'awg'"
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']"> v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4']">
<div class="card-header">{{ key }}</div> <div class="card-header">{{ key }}</div>
<div class="card-body"> <div class="card-body">
<input type="text" <input type="text"

View File

@@ -176,10 +176,7 @@ export default {
<input <input
v-model="this.store.CrossServerConfiguration.Enable" v-model="this.store.CrossServerConfiguration.Enable"
:disabled="loading" :disabled="loading"
class="form-check-input" class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked">
type="checkbox"
role="switch"
id="flexSwitchCheckChecked">
<label class="form-check-label" for="flexSwitchCheckChecked"> <label class="form-check-label" for="flexSwitchCheckChecked">
<LocaleText t="Access Remote Server"></LocaleText> <LocaleText t="Access Remote Server"></LocaleText>
</label> </label>
@@ -187,20 +184,10 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<div class="d-flex container-fluid align-items-center my-1 w-100"> <small class="text-muted pb-3 d-block w-100 text-center mt-3">
<small class="text-muted"> WGDashboard {{ this.version }} | Developed with by
WGDashboard <strong>{{ this.version }}</strong> | Made with by <a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
<a href="https://github.com/WGDashboard" </small>
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"> <div class="messageCentre text-body position-absolute d-flex">
<TransitionGroup name="message" tag="div" <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"> class="position-relative flex-sm-grow-0 flex-grow-1 d-flex align-items-end ms-sm-auto flex-column gap-2">

View File

@@ -176,7 +176,7 @@ const memoryHistoricalChartData = computed(() => {
<div class="card rounded-3 h-100 shadow"> <div class="card rounded-3 h-100 shadow">
<div class="card-body p-4"> <div class="card-body p-4">
<div class="d-flex flex-column gap-3"> <div class="d-flex flex-column gap-3">
<div class="d-flex flex-column gap-3" style="min-height: 130px"> <div class="d-flex flex-column gap-3" style="height: 130px">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<h3 class="text-muted mb-0"> <h3 class="text-muted mb-0">
<i class="bi bi-cpu-fill me-2"></i> <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" role="progressbar" style="height: 10px">
<div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div> <div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div>
</div> </div>
<div class="d-grid gap-1" style="grid-template-columns: repeat(10, 1fr)"> <div class="d-flex gap-1">
<CpuCore <CpuCore
v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu" v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu"
:square="true" :square="true"

View File

@@ -32,7 +32,7 @@ export default defineConfig(({mode}) => {
} }
return { return {
base: "./", base: "/static/dist/WGDashboardAdmin",
plugins: [ plugins: [
vue(), vue(),
], ],

View File

@@ -2,10 +2,9 @@
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WGDashboard Client</title> <title>WGDashboard Client</title>
<base href="./client/">
<style> <style>
*{ *{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
@@ -29,22 +28,15 @@
} }
} }
</style> </style>
<script>
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
const base = document.querySelector('base');
if (base && isViteDevMode) {
base.href = '/';
}
</script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<div id="preloader"> <div id="preloader">
<div id="preloader_placeholder"> <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> </div>
</div> </div>
<script type="module" src="./src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -9,20 +9,20 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "^1.15.1", "axios": "^1.9.0",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"oidc-client-ts": "^3.5.0", "oidc-client-ts": "^3.2.1",
"pinia": "^3.0.4", "pinia": "^3.0.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"uuid": "^14.0.0", "uuid": "^11.1.0",
"vue": "^3.5.32", "vue": "^3.5.13",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^5.2.3",
"vite": "^8.0.8", "vite": "^6.2.4",
"vite-plugin-vue-devtools": "^8.1.1" "vite-plugin-vue-devtools": "^7.7.2"
} }
} }

View File

@@ -1,12 +1,20 @@
<script setup> <script setup async>
import './assets/main.css' import './assets/main.css'
import NotificationList from "@/components/Notification/notificationList.vue"; 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> </script>
<template> <template>
<div data-bs-theme="dark" class="text-body bg-body vw-100 vh-100 bg-body"> <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="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> <Suspense>
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition name="app" type="transition" mode="out-in"> <Transition name="app" type="transition" mode="out-in">

View File

@@ -2,7 +2,7 @@
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import ConfigurationQRCode from "@/components/Configuration/configurationQRCode.vue"; import ConfigurationQRCode from "@/components/Configuration/configurationQRCode.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Duration from 'dayjs/plugin/duration' import Duration from 'dayjs/plugin/Duration'
dayjs.extend(Duration); dayjs.extend(Duration);
const props = defineProps([ const props = defineProps([
'config' 'config'
@@ -113,4 +113,4 @@ const emits = defineEmits(['select'])
background-color: #28a745 !important; background-color: #28a745 !important;
box-shadow: 0 0 0 .2rem #28a74545; box-shadow: 0 0 0 .2rem #28a74545;
} }
</style> </style>

View File

@@ -96,7 +96,7 @@ if (route.query.Email){
</span> </span>
</button> </button>
</form> </form>
<div v-if="store.serverInformation.SignUp.enable"> <div>
<hr class="my-4"> <hr class="my-4">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="text-muted"> <span class="text-muted">

View File

@@ -6,43 +6,43 @@ import router from "@/router/router.js";
import {createPinia} from "pinia"; import {createPinia} from "pinia";
import 'bootstrap/dist/js/bootstrap.bundle.js' import 'bootstrap/dist/js/bootstrap.bundle.js'
import {axiosGet, axiosPost} from "@/utilities/request.js"; import {axiosPost} from "@/utilities/request.js";
import {clientStore} from "@/stores/clientStore.js";
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
const state = params.get('state') const state = params.get('state')
const code = params.get('code') const code = params.get('code')
const initApp = async () => { const initApp = () => {
const app = createApp(App) const app = createApp(App)
const serverInformation = await axiosGet("/api/serverInformation", {})
app.use(createPinia()) app.use(createPinia())
if (serverInformation){
const store = clientStore()
store.serverInformation = serverInformation.data;
}
app.use(router) app.use(router)
app.mount("#app") 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){ if (state && code){
await axiosPost("/api/signin/oidc", { axiosPost("/api/signin/oidc", {
provider: state, provider: state,
code: code, code: code,
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
}).then(async (data) => { }).then(data => {
let url = new URL(window.location.href); let url = new URL(window.location.href);
url.search = ''; url.search = '';
history.replaceState({}, document.title, url.toString()); history.replaceState({}, document.title, url.toString());
await initApp() initApp()
if (!data.status){ if (!data.status){
const store = clientStore() const store = clientStore()
store.newNotification(data.message, 'danger') store.newNotification(data.message, 'danger')
} }
}) })
}else{ }else{
await initApp() initApp()
} }

View File

@@ -50,11 +50,6 @@ const router = createRouter({
}) })
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const store = clientStore() 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'){ if (to.path === '/signout'){
await axios.get(requestURl('/api/signout')).then(() => { await axios.get(requestURl('/api/signout')).then(() => {
next('/signin') next('/signin')

View File

@@ -1,18 +1,23 @@
import axios from "axios"; import axios from "axios";
import {useRouter} from "vue-router";
export const requestURl = (url) => { export const requestURl = (url) => {
if (import.meta.env.MODE === 'development') { return import.meta.env.MODE === 'development' ? '/client' + url
return '/client' + url; : `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
}
return `./.${url}`;
} }
// const router = useRouter()
export const axiosPost = async (URL, body = {}) => { export const axiosPost = async (URL, body = {}) => {
try{ try{
const res = await axios.post(requestURl(URL), body) const res = await axios.post(requestURl(URL), body)
return res.data return res.data
} catch (error){ } catch (error){
console.log(error) console.log(error)
// if (error.status === 401){
// await router.push('/signin')
// }
return undefined return undefined
} }
} }
@@ -23,6 +28,9 @@ export const axiosGet = async (URL, query = {}) => {
return res.data return res.data
} catch (error){ } catch (error){
console.log(error) console.log(error)
// if (error.status === 401){
// await router.push('/signin')
// }
return undefined return undefined
} }
} }

View File

@@ -40,5 +40,5 @@ export default defineConfig({
} }
} }
}, },
base: './' base: '/static/dist/WGDashboardClient'
}) })

View File

@@ -1 +1 @@
import{a5 as A,r as n,D as S,g as l,z as v}from"./index-DXzxfcZW.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,D as S,r as n,g as l,z as v}from"./index-DOH6XKNT.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};

View File

@@ -0,0 +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};

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

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

View File

@@ -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-DXzxfcZW.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-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};

View File

@@ -0,0 +1 @@
import{_ as r,a as e,c as i,d as o,w as t,j as l,i as a,l as _,S as d,k as u}from"./index-DOH6XKNT.js";const m={name:"configuration"},p={class:"mt-md-5 mt-3 text-body"};function f(k,x,h,w,$,v){const n=u("RouterView");return e(),i("div",p,[o(n,null,{default:t(({Component:s,route:c})=>[o(l,{name:"fade2",mode:"out-in"},{default:t(()=>[(e(),a(d,null,{default:t(()=>[(e(),a(_(s),{key:c.path,class:"z-1"}))]),_:2},1024))]),_:2},1024)]),_:1})])}const B=r(m,[["render",f]]);export{B as default};

View File

@@ -1 +1 @@
.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} .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}

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

View File

@@ -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-DXzxfcZW.js";import{L as C}from"./localeText-Dmcj5qqx.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-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};

View File

@@ -0,0 +1 @@
import{_ as f,z as p,D as v,W as _,A as w,a as i,c as n,b as t,d as u,m as k,y as x,n as m,t as b,e as y,k as S,r as D,F as $,h as W,q as V}from"./index-DOH6XKNT.js";import{L as C}from"./localeText-CB2ziPie.js";const F={name:"dashboardSettingsInputWireguardConfigurationPath",components:{LocaleText:C},props:{targetData:String,title:String,warning:!1,warningText:""},setup(){const o=v(),s=_(),r=`input_${w()}`;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 p("/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=S("LocaleText");return i(),n("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,[k(t("input",{type:"text",class:m(["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),[[x,this.value]]),t("div",z,b(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?(i(),n("span",E)):(i(),n("i",q))],8,U)]),r.warning?(i(),n("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"])])])):y("",!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=v(),r=_(),a=D(s.Configuration.WireGuardConfiguration.autostart),c=V(()=>r.Configurations.map(e=>e.Name)),g=async()=>{await p("/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)=>(i(),n("div",H,[t("div",J,[t("h6",O,[u(C,{t:"Toggle When Start Up"})])]),t("div",Q,[t("div",R,[(i(!0),n($,null,W(c.value,l=>(i(),n("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,b(l),1),t("i",{class:m(["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

View File

@@ -1,4 +1,4 @@
import{_ as O,J,W as Y,o as z,H as q,D as M,r as y,K as G,c as h,f as v,a as e,b as s,m as k,y as $,n as B,z as L,t as P,h as Q,a2 as X,d as N,s as D,j as R,F as I,i as H,I as ee,a3 as te,e as V,L as A,E as se,u as K,g as T,p as ne,q as oe,w as U,k as j,a4 as le,T as Z,B as ie,v as ae}from"./index-DXzxfcZW.js";import{L as n}from"./localeText-Dmcj5qqx.js";import{d as de}from"./dayjs.min-C-brjxlJ.js";const re={class:"card rounded-3 flex-grow-1 bg-danger-subtle border-danger-subtle border shadow"},ue={class:"card-body"},ce={class:"d-flex align-items-center gap-3 inputGroup"},me=["value"],fe={class:"mb-0"},ge={class:"d-flex mt-3"},be=["disabled"],ve={__name:"updateConfigurationName",props:{configurationName:String},emits:["close"],setup(d,{emit:r}){const t=d,f=r,o=J({data:"",valid:!1}),g=Y();z(()=>{q(()=>o.data,b=>{o.valid=/^[a-zA-Z0-9_=+.-]{1,15}$/.test(b)&&b.length>0&&!g.Configurations.find(_=>_.Name===b)})});const u=M(),x=y(!1),c=G(),a=async()=>{o.data&&(x.value=!0,clearInterval(u.Peers.RefreshInterval),await L("/api/renameWireguardConfiguration",{ConfigurationName:t.configurationName,NewConfigurationName:o.data},async b=>{b.status?(await g.getConfigurations(),u.newMessage("Server","Configuration renamed","success"),c.push(`/configuration/${o.data}/peers`)):(u.newMessage("Server",b.message,"danger"),x.value=!1)}))};return(b,_)=>(v(),h("div",re,[e("div",ue,[e("p",null,[s(n,{t:"To update this configuration's name, WGDashboard will execute the following operations:"})]),e("ol",null,[e("li",null,[s(n,{t:"Duplicate current configuration's database table and .conf file with the new name"})]),e("li",null,[s(n,{t:"Delete current configuration's database table and .conf file"})])]),e("div",ce,[e("input",{class:"form-control form-control-sm rounded-3",value:d.configurationName,disabled:""},null,8,me),_[3]||(_[3]=e("h3",{class:"mb-0"},[e("i",{class:"bi bi-arrow-right"})],-1)),k(e("input",{class:B(["form-control form-control-sm rounded-3",[o.data?o.valid?"is-valid":"is-invalid":""]]),id:"newConfigurationName","onUpdate:modelValue":_[0]||(_[0]=w=>o.data=w)},null,2),[[$,o.data]])]),e("div",{class:B(["invalid-feedback",{"d-block":!o.valid&&o.data}])},[s(n,{t:"Configuration name is invalid. Possible reasons:"}),e("ul",fe,[e("li",null,[s(n,{t:"Configuration name already exist"})]),e("li",null,[s(n,{t:"Configuration name can only contain 15 lower/uppercase alphabet, numbers, underscore, equal sign, plus sign, period and hyphen."})])])],2),e("div",ge,[e("button",{onClick:_[1]||(_[1]=w=>f("close")),class:"btn btn-sm bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3"},[s(n,{t:"Cancel"})]),e("button",{onClick:_[2]||(_[2]=w=>a()),disabled:!o.data||x.value,class:"btn btn-sm btn-danger rounded-3 ms-auto"},[s(n,{t:"Save"})],8,be)])])]))}},pe=O(ve,[["__scopeId","data-v-33ea9576"]]),he={name:"Dropdown",props:{width:{type:String,default:"80px"},height:{type:String,default:"auto"},title:{type:String,default:""},disabled:{type:Boolean,default:!1},defaultDisplay:{type:Boolean,default:!1}}},ye={class:"title"};function xe(d,r,t,f,o,g){return v(),h("div",{class:B(["dropdown",{disabled:t.disabled}]),onClick:r[0]||(r[0]=(...u)=>d.toggleDropdown&&d.toggleDropdown(...u)),onFocusout:r[1]||(r[1]=(...u)=>d.hideDropdown&&d.hideDropdown(...u)),tabindex:"0"},[e("div",ye,[e("div",null,P(t.title),1)])],34)}const _e=O(he,[["render",xe]]),we={components:{Dropdown:_e},name:"CodeEditor",props:{lineNums:{type:Boolean,default:!1},modelValue:{type:String},value:{type:String},theme:{type:String,default:"github-dark"},tabSpaces:{type:Number,default:2},wrap:{type:Boolean,default:!1},readOnly:{type:Boolean,default:!1},autofocus:{type:Boolean,default:!1},header:{type:Boolean,default:!0},width:{type:String,default:"540px"},height:{type:String,default:"auto"},maxWidth:{type:String},minWidth:{type:String},maxHeight:{type:String},minHeight:{type:String},borderRadius:{type:String,default:"12px"},languages:{type:Array,default:function(){return[["javascript","JS"]]}},langListWidth:{type:String,default:"110px"},langListHeight:{type:String,default:"auto"},langListDisplay:{type:Boolean,default:!1},displayLanguage:{type:Boolean,default:!0},zIndex:{type:String,default:"0"},fontSize:{type:String,default:"17px"},padding:{type:String,default:"20px"}},directives:{highlight:{mounted(d,r){d.textContent=r.value},updated(d,r){d.scrolling?d.scrolling=!1:d.textContent=r.value}}},data(){return{scrollBarWidth:0,scrollBarHeight:0,top:0,left:0,languageClass:"hljs language-"+this.languages[0][0],languageTitle:this.languages[0][1]?this.languages[0][1]:this.languages[0][0],content:this.value,cursorPosition:0,insertTab:!1,lineNum:0,lineNumsWidth:0,scrolling:!1,textareaHeight:0,showLineNums:this.wrap?!1:this.lineNums}},computed:{tabWidth(){let d="";for(let r=0;r<this.tabSpaces;r++)d+=" ";return d},contentValue(){return this.modelValue==null?this.content+` import{_ as O,W as Y,o as z,H as q,D as M,K as G,a as v,c as h,b as e,d as s,m as k,y as $,n as B,J,r as y,z as L,t as P,k as Q,a2 as X,s as D,i as R,e as N,F as I,h as H,I as ee,a3 as te,f as V,L as A,E as se,u as K,g as T,p as ne,q as oe,w as U,j,a4 as le,T as Z,B as ie,v as ae}from"./index-DOH6XKNT.js";import{L as n}from"./localeText-CB2ziPie.js";import{d as de}from"./dayjs.min-CMJwD6qc.js";const re={class:"card rounded-3 flex-grow-1 bg-danger-subtle border-danger-subtle border shadow"},ue={class:"card-body"},ce={class:"d-flex align-items-center gap-3 inputGroup"},me=["value"],fe={class:"mb-0"},ge={class:"d-flex mt-3"},be=["disabled"],ve={__name:"updateConfigurationName",props:{configurationName:String},emits:["close"],setup(d,{emit:r}){const t=d,f=r,o=J({data:"",valid:!1}),g=Y();z(()=>{q(()=>o.data,b=>{o.valid=/^[a-zA-Z0-9_=+.-]{1,15}$/.test(b)&&b.length>0&&!g.Configurations.find(_=>_.Name===b)})});const u=M(),x=y(!1),c=G(),a=async()=>{o.data&&(x.value=!0,clearInterval(u.Peers.RefreshInterval),await L("/api/renameWireguardConfiguration",{ConfigurationName:t.configurationName,NewConfigurationName:o.data},async b=>{b.status?(await g.getConfigurations(),u.newMessage("Server","Configuration renamed","success"),c.push(`/configuration/${o.data}/peers`)):(u.newMessage("Server",b.message,"danger"),x.value=!1)}))};return(b,_)=>(v(),h("div",re,[e("div",ue,[e("p",null,[s(n,{t:"To update this configuration's name, WGDashboard will execute the following operations:"})]),e("ol",null,[e("li",null,[s(n,{t:"Duplicate current configuration's database table and .conf file with the new name"})]),e("li",null,[s(n,{t:"Delete current configuration's database table and .conf file"})])]),e("div",ce,[e("input",{class:"form-control form-control-sm rounded-3",value:d.configurationName,disabled:""},null,8,me),_[3]||(_[3]=e("h3",{class:"mb-0"},[e("i",{class:"bi bi-arrow-right"})],-1)),k(e("input",{class:B(["form-control form-control-sm rounded-3",[o.data?o.valid?"is-valid":"is-invalid":""]]),id:"newConfigurationName","onUpdate:modelValue":_[0]||(_[0]=w=>o.data=w)},null,2),[[$,o.data]])]),e("div",{class:B(["invalid-feedback",{"d-block":!o.valid&&o.data}])},[s(n,{t:"Configuration name is invalid. Possible reasons:"}),e("ul",fe,[e("li",null,[s(n,{t:"Configuration name already exist"})]),e("li",null,[s(n,{t:"Configuration name can only contain 15 lower/uppercase alphabet, numbers, underscore, equal sign, plus sign, period and hyphen."})])])],2),e("div",ge,[e("button",{onClick:_[1]||(_[1]=w=>f("close")),class:"btn btn-sm bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3"},[s(n,{t:"Cancel"})]),e("button",{onClick:_[2]||(_[2]=w=>a()),disabled:!o.data||x.value,class:"btn btn-sm btn-danger rounded-3 ms-auto"},[s(n,{t:"Save"})],8,be)])])]))}},pe=O(ve,[["__scopeId","data-v-33ea9576"]]),he={name:"Dropdown",props:{width:{type:String,default:"80px"},height:{type:String,default:"auto"},title:{type:String,default:""},disabled:{type:Boolean,default:!1},defaultDisplay:{type:Boolean,default:!1}}},ye={class:"title"};function xe(d,r,t,f,o,g){return v(),h("div",{class:B(["dropdown",{disabled:t.disabled}]),onClick:r[0]||(r[0]=(...u)=>d.toggleDropdown&&d.toggleDropdown(...u)),onFocusout:r[1]||(r[1]=(...u)=>d.hideDropdown&&d.hideDropdown(...u)),tabindex:"0"},[e("div",ye,[e("div",null,P(t.title),1)])],34)}const _e=O(he,[["render",xe]]),we={components:{Dropdown:_e},name:"CodeEditor",props:{lineNums:{type:Boolean,default:!1},modelValue:{type:String},value:{type:String},theme:{type:String,default:"github-dark"},tabSpaces:{type:Number,default:2},wrap:{type:Boolean,default:!1},readOnly:{type:Boolean,default:!1},autofocus:{type:Boolean,default:!1},header:{type:Boolean,default:!0},width:{type:String,default:"540px"},height:{type:String,default:"auto"},maxWidth:{type:String},minWidth:{type:String},maxHeight:{type:String},minHeight:{type:String},borderRadius:{type:String,default:"12px"},languages:{type:Array,default:function(){return[["javascript","JS"]]}},langListWidth:{type:String,default:"110px"},langListHeight:{type:String,default:"auto"},langListDisplay:{type:Boolean,default:!1},displayLanguage:{type:Boolean,default:!0},zIndex:{type:String,default:"0"},fontSize:{type:String,default:"17px"},padding:{type:String,default:"20px"}},directives:{highlight:{mounted(d,r){d.textContent=r.value},updated(d,r){d.scrolling?d.scrolling=!1:d.textContent=r.value}}},data(){return{scrollBarWidth:0,scrollBarHeight:0,top:0,left:0,languageClass:"hljs language-"+this.languages[0][0],languageTitle:this.languages[0][1]?this.languages[0][1]:this.languages[0][0],content:this.value,cursorPosition:0,insertTab:!1,lineNum:0,lineNumsWidth:0,scrolling:!1,textareaHeight:0,showLineNums:this.wrap?!1:this.lineNums}},computed:{tabWidth(){let d="";for(let r=0;r<this.tabSpaces;r++)d+=" ";return d},contentValue(){return this.modelValue==null?this.content+`
`:this.modelValue+` `:this.modelValue+`
`},scroll(){return this.height!="auto"}},methods:{updateValue(d){this.modelValue==null?this.content=d.target.value:this.$emit("update:modelValue",d.target.value)},changeLang(d){this.languageTitle=d[1]?d[1]:d[0],this.languageClass="language-"+d[0],this.$emit("lang",d[0])},tab(){if(document.execCommand("insertText"))document.execCommand("insertText",!1,this.tabWidth);else{const d=this.$refs.textarea.selectionStart;this.content=this.content.substring(0,d)+this.tabWidth+this.content.substring(d),this.cursorPosition=d+this.tabWidth.length,this.insertTab=!0}},calcScrollDistance(d){this.$refs.code.scrolling=!0,this.scrolling=!0,this.top=-d.target.scrollTop,this.left=-d.target.scrollLeft},resizer(){new ResizeObserver(t=>{this.scrollBarWidth=t[0].target.offsetWidth-t[0].target.clientWidth,this.scrollBarHeight=t[0].target.offsetHeight-t[0].target.clientHeight,this.textareaHeight=t[0].target.offsetHeight}).observe(this.$refs.textarea);const r=new ResizeObserver(t=>{this.lineNumsWidth=t[0].target.offsetWidth});this.$refs.lineNums&&r.observe(this.$refs.lineNums)},copy(){document.execCommand("copy")?(this.$refs.textarea.select(),document.execCommand("copy"),window.getSelection().removeAllRanges()):navigator.clipboard.writeText(this.$refs.textarea.value)},getLineNum(){const d=this.$refs.textarea.value;let r=0,t=d.indexOf(` `},scroll(){return this.height!="auto"}},methods:{updateValue(d){this.modelValue==null?this.content=d.target.value:this.$emit("update:modelValue",d.target.value)},changeLang(d){this.languageTitle=d[1]?d[1]:d[0],this.languageClass="language-"+d[0],this.$emit("lang",d[0])},tab(){if(document.execCommand("insertText"))document.execCommand("insertText",!1,this.tabWidth);else{const d=this.$refs.textarea.selectionStart;this.content=this.content.substring(0,d)+this.tabWidth+this.content.substring(d),this.cursorPosition=d+this.tabWidth.length,this.insertTab=!0}},calcScrollDistance(d){this.$refs.code.scrolling=!0,this.scrolling=!0,this.top=-d.target.scrollTop,this.left=-d.target.scrollLeft},resizer(){new ResizeObserver(t=>{this.scrollBarWidth=t[0].target.offsetWidth-t[0].target.clientWidth,this.scrollBarHeight=t[0].target.offsetHeight-t[0].target.clientHeight,this.textareaHeight=t[0].target.offsetHeight}).observe(this.$refs.textarea);const r=new ResizeObserver(t=>{this.lineNumsWidth=t[0].target.offsetWidth});this.$refs.lineNums&&r.observe(this.$refs.lineNums)},copy(){document.execCommand("copy")?(this.$refs.textarea.select(),document.execCommand("copy"),window.getSelection().removeAllRanges()):navigator.clipboard.writeText(this.$refs.textarea.value)},getLineNum(){const d=this.$refs.textarea.value;let r=0,t=d.indexOf(`
`);for(;t!==-1;)r++,t=d.indexOf(` `);for(;t!==-1;)r++,t=d.indexOf(`

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

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