Compare commits

..

256 Commits

Author SHA1 Message Date
Christoph Haas
ea26e56994 fix delayed setup of external auth providers (#529)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-09-21 21:16:12 +02:00
h44z
61bf349813 add user's display-name to peer view (#525) (#534)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-09-21 13:02:12 +02:00
Osvaldo-Net
80693400be Soporte para el idioma español. (#530) (#532)
* Add files via upload

* Update index.js

* Update App.vue

* Update es.json
2025-09-21 12:44:59 +02:00
Christoph Haas
afb38b685c improve logging of LDAP login process (#529)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-09-17 22:33:54 +02:00
h44z
7cd7d13dc7 fix peer creation if custom public key is set (#523) (#528)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-09-15 22:54:34 +02:00
dependabot[bot]
d945e313b2 chore(deps): bump github.com/go-webauthn/webauthn from 0.13.4 to 0.14.0 (#526)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.13.4 to 0.14.0.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.13.4...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.14.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>
2025-09-15 22:50:10 +02:00
dependabot[bot]
c5fe82ab11 chore(deps): bump gorm.io/gorm from 1.30.5 to 1.31.0 in the gorm group (#527)
Bumps the gorm group with 1 update: [gorm.io/gorm](https://github.com/go-gorm/gorm).


Updates `gorm.io/gorm` from 1.30.5 to 1.31.0
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.30.5...v1.31.0)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-version: 1.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 22:48:44 +02:00
h44z
765fb09770 Mikrotik improvements (#521)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
* allow to specify ignored interfaces (#514)

* only set endpoint info for "responder" peers (#516)
2025-09-09 21:43:16 +02:00
Christoph Haas
6d2a5fa6de chore: update Go dependencies
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-09-08 19:25:47 +02:00
dependabot[bot]
891d499a18 chore(deps): bump actions/setup-python from 5 to 6 in the actions group (#520)
Bumps the actions group with 1 update: [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 19:24:25 +02:00
Christoph Haas
db357b82d0 update doc for disable_admin_user flag (#515) 2025-09-08 19:16:52 +02:00
Victor LEFEBVRE
b61d84ec4f allow disabling local admin user (#515)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-09-08 10:39:10 +02:00
dependabot[bot]
d311313cb4 chore(deps): bump the patch group across 1 directory with 2 updates (#510)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the patch group with 2 updates in the / directory: [github.com/go-pkgz/routegroup](https://github.com/go-pkgz/routegroup) and [gorm.io/gorm](https://github.com/go-gorm/gorm).


Updates `github.com/go-pkgz/routegroup` from 1.5.2 to 1.5.3
- [Release notes](https://github.com/go-pkgz/routegroup/releases)
- [Commits](https://github.com/go-pkgz/routegroup/compare/v1.5.2...v1.5.3)

Updates `gorm.io/gorm` from 1.30.1 to 1.30.2
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.30.1...v1.30.2)

---
updated-dependencies:
- dependency-name: github.com/go-pkgz/routegroup
  dependency-version: 1.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: gorm.io/gorm
  dependency-version: 1.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 20:17:13 +02:00
h44z
0cbca61c15 ensure that LDAP filter values are escaped (#512) 2025-09-03 19:37:34 +02:00
h44z
c79a6c83a8 allow setting the DisplayName property for newly provisioned peers (#507) (#511) 2025-09-03 19:34:58 +02:00
dependabot[bot]
098a9fe23e chore(deps): bump golang from 1.24-alpine to 1.25-alpine (#505)
Bumps golang from 1.24-alpine to 1.25-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 19:14:17 +02:00
dependabot[bot]
41cab5f7ea chore(deps): bump the actions group with 2 updates (#499)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

Updates `actions/download-artifact` from 4 to 5
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 21:47:25 +02:00
dependabot[bot]
708c558211 chore(deps): bump github.com/go-pkgz/routegroup in the patch group (#498)
Bumps the patch group with 1 update: [github.com/go-pkgz/routegroup](https://github.com/go-pkgz/routegroup).


Updates `github.com/go-pkgz/routegroup` from 1.5.1 to 1.5.2
- [Release notes](https://github.com/go-pkgz/routegroup/releases)
- [Commits](https://github.com/go-pkgz/routegroup/compare/v1.5.1...v1.5.2)

---
updated-dependencies:
- dependency-name: github.com/go-pkgz/routegroup
  dependency-version: 1.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 21:47:15 +02:00
h44z
99df4ca3cd ensure hooks run after restart (#494) (#497) 2025-08-12 21:47:04 +02:00
h44z
9884d8c002 fix migration tool (#495) (#496)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-08-11 19:05:33 +02:00
Christoph Haas
b099e8abfa ensure that v2 (or just 2) tags are only published for stable releases (#493) 2025-08-11 18:11:32 +02:00
h44z
112f6bfb77 Mikrotik integration (#467)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Allow MikroTik routes as WireGuard backends
2025-08-10 14:42:02 +02:00
Christoph Haas
a86f83a219 ensure that deleted peers are restored once the interface is re-enabled 2025-08-10 14:18:43 +02:00
Christoph Haas
131413b470 chore: update routegroup dependency 2025-08-10 10:59:28 +02:00
Christoph Haas
2246829151 chore: update deps (except routegroup, it breaks the api)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-08-09 15:56:02 +02:00
Christoph Haas
c20f17cddf fix multi-peer generation, fix prefix handling (#491) 2025-08-09 15:55:29 +02:00
Marcin Woźniak
3f76aa416f chore(logs): added more debug logs and reformated those files using gofmt (#490)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-07-27 23:32:34 +02:00
h44z
6a8b28df88 add dark mode (#482) (#489)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-07-27 00:15:27 +02:00
dependabot[bot]
ffef1f7b12 chore(deps): bump gorm.io/driver/sqlserver in the gorm group (#488)
Bumps the gorm group with 1 update: [gorm.io/driver/sqlserver](https://github.com/go-gorm/sqlserver).


Updates `gorm.io/driver/sqlserver` from 1.6.0 to 1.6.1
- [Commits](https://github.com/go-gorm/sqlserver/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: gorm.io/driver/sqlserver
  dependency-version: 1.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-26 22:28:52 +02:00
dependabot[bot]
dc002b156b chore(deps): bump github.com/alexedwards/scs/v2 from 2.8.0 to 2.9.0 (#487)
Bumps [github.com/alexedwards/scs/v2](https://github.com/alexedwards/scs) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/alexedwards/scs/releases)
- [Commits](https://github.com/alexedwards/scs/compare/v2.8.0...v2.9.0)

---
updated-dependencies:
- dependency-name: github.com/alexedwards/scs/v2
  dependency-version: 2.9.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>
2025-07-26 12:58:07 +02:00
Christoph Haas
1794b8653a add retry handling for auth provider setup (#484)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-07-19 23:29:05 +02:00
dependabot[bot]
a6d985d3ce chore(deps): bump the patch group across 1 directory with 4 updates (#485)
Bumps the patch group with 2 updates in the / directory: [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) and [github.com/swaggo/swag](https://github.com/swaggo/swag).


Updates `github.com/go-webauthn/webauthn` from 0.13.0 to 0.13.4
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.13.0...v0.13.4)

Updates `github.com/swaggo/swag` from 1.16.4 to 1.16.5
- [Release notes](https://github.com/swaggo/swag/releases)
- [Changelog](https://github.com/swaggo/swag/blob/master/.goreleaser.yml)
- [Commits](https://github.com/swaggo/swag/compare/v1.16.4...v1.16.5)

Updates `golang.org/x/crypto` from 0.39.0 to 0.40.0
- [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.40.0)

Updates `golang.org/x/sys` from 0.33.0 to 0.34.0
- [Commits](https://github.com/golang/sys/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: github.com/swaggo/swag
  dependency-version: 1.16.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: golang.org/x/crypto
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch
- dependency-name: golang.org/x/sys
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-19 22:36:11 +02:00
dependabot[bot]
a7bd3b3f95 chore(deps): bump github.com/go-playground/validator/v10 (#480)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.26.0 to 10.27.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.26.0...v10.27.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.27.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>
2025-07-19 22:20:23 +02:00
h44z
f286840964 fix oauth domain check (#474) (#476)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-06-29 20:00:15 +02:00
h44z
edb88b5768 new webhook models (#444) (#471)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
warning: existing webhook receivers need to be adapted to the new models
2025-06-29 19:49:01 +02:00
h44z
588bbca141 only execute interface hooks if the state has changed (#469) (#472) 2025-06-29 19:48:46 +02:00
h44z
f08740991b support for raw-wireguard and wg-quick style peer configurations (#441) (#473) 2025-06-29 19:47:53 +02:00
h44z
dd28a8dddf allow to hide login form (#459) (#470)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
use the `hide_login_form` parameter in the `auth` settings to configure this feature
2025-06-27 13:50:38 +02:00
dependabot[bot]
f994700caf chore(deps): bump alpine from 3.19 to 3.22 (#465)
Bumps alpine from 3.19 to 3.22.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.22'
  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>
2025-06-27 12:38:18 +02:00
h44z
be29abd29a add webhook event for peer state change (#444) (#468)
* add webhook event for peer state change (#444)

new event types: connect and disconnect

example payload:

```json
{
  "event": "connect",
  "entity": "peer",
  "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
  "payload": {
    "PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
    "IsConnected": true,
    "IsPingable": false,
    "LastPing": null,
    "BytesReceived": 1860,
    "BytesTransmitted": 10824,
    "LastHandshake": "2025-06-26T23:04:33.325216659+02:00",
    "Endpoint": "10.55.66.77:33874",
    "LastSessionStart": "2025-06-26T22:50:40.10221606+02:00"
  }
}
```

* add webhook docs (#444)
2025-06-27 12:37:10 +02:00
h44z
94785c10ec use website title in mail templates (#448) (#466)
* use website title in mail templates (#448)

* change button font color to white (#448)
2025-06-27 11:45:44 +02:00
HPPinata
3a732fd3e5 add docker to dependabot (#463)
Some checks failed
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2025-06-26 20:20:15 +02:00
S.J. Louw
f0be66aea4 Option to limit peer count that a normal user can create (#457) 2025-06-26 20:17:45 +02:00
dependabot[bot]
cbf8c5bca9 chore(deps): bump the gorm group across 1 directory with 4 updates (#453)
Bumps the gorm group with 3 updates in the / directory: [gorm.io/driver/mysql](https://github.com/go-gorm/mysql), [gorm.io/driver/postgres](https://github.com/go-gorm/postgres) and [gorm.io/driver/sqlserver](https://github.com/go-gorm/sqlserver).


Updates `gorm.io/driver/mysql` from 1.5.7 to 1.6.0
- [Commits](https://github.com/go-gorm/mysql/compare/v1.5.7...v1.6.0)

Updates `gorm.io/driver/postgres` from 1.5.11 to 1.6.0
- [Commits](https://github.com/go-gorm/postgres/compare/v1.5.11...v1.6.0)

Updates `gorm.io/driver/sqlserver` from 1.5.4 to 1.6.0
- [Commits](https://github.com/go-gorm/sqlserver/compare/v1.5.4...v1.6.0)

Updates `gorm.io/gorm` from 1.26.1 to 1.30.0
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.26.1...v1.30.0)

---
updated-dependencies:
- dependency-name: gorm.io/driver/mysql
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
- dependency-name: gorm.io/driver/postgres
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
- dependency-name: gorm.io/driver/sqlserver
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
- dependency-name: gorm.io/gorm
  dependency-version: 1.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-26 20:17:12 +02:00
dependabot[bot]
b6bfa1f6de chore(deps): bump golang.org/x/crypto in the golang group (#454)
Bumps the golang group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.38.0 to 0.39.0
- [Commits](https://github.com/golang/crypto/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-26 20:10:32 +02:00
dependabot[bot]
0c8d6223ce chore(deps): bump github.com/go-webauthn/webauthn from 0.12.3 to 0.13.0 (#440)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.12.3 to 0.13.0.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.12.3...v0.13.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.13.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>
2025-06-26 20:09:49 +02:00
Christoph Haas
e3b65ca337 improve logging of OAuth login issues, decrease auth-code exchange timeout (#451)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-06-10 17:51:45 +02:00
Christoph Haas
61d8aa6589 fix self-provisioned peer-generation (#452) 2025-06-08 11:17:04 +02:00
Christoph Haas
7fd2bbad02 Merge branch 'passkey_support' 2025-05-17 19:22:44 +02:00
Christoph Haas
75a5f3d815 add/improve documentation 2025-05-16 14:58:05 +02:00
Christoph Haas
e9005b1b90 add minimum password length check 2025-05-16 09:55:35 +02:00
Christoph Haas
8816165260 fix duplicate creation of default peer (#437) 2025-05-15 17:59:00 +02:00
Christoph Haas
ab9995350f sanitize external_url, remove trailing slashes 2025-05-15 17:58:34 +02:00
Christoph Haas
7df4e4b813 fix minor frontend glitches 2025-05-13 20:18:17 +02:00
dependabot[bot]
657c4307b3 chore(deps): bump gorm.io/gorm from 1.25.12 to 1.26.1 in the gorm group (#415)
Bumps the gorm group with 1 update: [gorm.io/gorm](https://github.com/go-gorm/gorm).


Updates `gorm.io/gorm` from 1.25.12 to 1.26.1
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.12...v1.26.1)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 20:13:29 +02:00
dependabot[bot]
b918fb6522 chore(deps): bump github.com/vishvananda/netlink in the patch group (#434)
Bumps the patch group with 1 update: [github.com/vishvananda/netlink](https://github.com/vishvananda/netlink).


Updates `github.com/vishvananda/netlink` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/vishvananda/netlink/releases)
- [Commits](https://github.com/vishvananda/netlink/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/vishvananda/netlink
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 20:12:52 +02:00
Christoph Haas
78deede360 Separate tag-based and branch-based documentation deployment
Updated the workflow to deploy documentation with `latest` alias only on tagged refs, while regular deployments occur for branches without updating aliases. This ensures proper versioning and prevents unintended alias updates.
2025-05-13 20:01:33 +02:00
Christoph Haas
a8fb4365cf Separate tag-based and branch-based documentation deployment
Updated the workflow to deploy documentation with `latest` alias only on tagged refs, while regular deployments occur for branches without updating aliases. This ensures proper versioning and prevents unintended alias updates.
2025-05-13 19:34:19 +02:00
Christoph Haas
1394be2341 add webauthn (passkey) support 2025-05-12 22:53:43 +02:00
dependabot[bot]
0102588d23 chore(deps): bump golang.org/x/crypto in the golang group (#433)
Bumps the golang group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.37.0 to 0.38.0
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 22:53:05 +02:00
Christoph Haas
6a96925be7 add API endpoints to prepare fresh interfaces and peers (#432) 2025-05-09 16:19:36 +02:00
Rafael Alexandre
f018babca7 update portuguese translations (#430)
Signed-off-by: Rafael Alexandre <r.alexandre99@gmail.com>
2025-05-09 15:45:13 +02:00
Christoph Haas
c6253e7c15 clarify Docker image version tags, remove stable and legacy builds (#191) 2025-05-09 15:42:08 +02:00
dependabot[bot]
2a1d82251e chore(deps): bump the golang group with 2 updates (#429)
Bumps the golang group with 2 updates: [golang.org/x/oauth2](https://github.com/golang/oauth2) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/oauth2` from 0.29.0 to 0.30.0
- [Commits](https://github.com/golang/oauth2/compare/v0.29.0...v0.30.0)

Updates `golang.org/x/sys` from 0.32.0 to 0.33.0
- [Commits](https://github.com/golang/sys/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 18:33:44 +02:00
Christoph Haas
99d6ce73ad update documentation for allowed_domains in oauth and oidc (#416) 2025-05-05 18:33:05 +02:00
Vladimir Dombrovski
3eb84f0ee9 Enable allowed_domains in oauth and oidc providers (#416)
* Enable allowed_domains in oauth and oidc providers

Signed-off-by: Vladimir DOMBROVSKI <vladimir.dombrovski@bso.co>

* Domain check code cleanup

* Run gofmt on domain validation code

---------

Signed-off-by: Vladimir DOMBROVSKI <vladimir.dombrovski@bso.co>
2025-05-05 18:26:19 +02:00
Christoph Haas
d8a57edef9 fix Docker image tagging 2025-05-05 18:18:56 +02:00
Christoph Haas
8271dd7c1f Merge branch 'prepare V2 release' 2025-05-04 20:19:19 +02:00
Christoph Haas
4ca37089bc fix browser warnings, update vite 2025-05-04 20:14:40 +02:00
Christoph Haas
8e5d5138c0 fix bootswatch 5.3.5 issue 2025-05-04 20:05:38 +02:00
Christoph Haas
c73286e11a improve german translations 2025-05-04 19:44:25 +02:00
Christoph Haas
b4aa6f8ef3 fix gorm error if no encryption is used (#427) 2025-05-04 17:42:13 +02:00
Christoph Haas
432c627f9b further improve documentation and examples (#423) 2025-05-04 14:48:34 +02:00
Christoph Haas
cd60761ea7 improve docs 2025-05-04 11:16:46 +02:00
Christoph Haas
2c8304417b prepare for v2 release 2025-05-04 11:00:12 +02:00
Christoph Haas
020ebb64e7 docs: add another listening-address example 2025-05-04 09:26:56 +02:00
Christoph Haas
923d4a6188 docs: add reverse-proxy example, improve docker examples, fix slow_query_threshold documentation; feat: allow config.yml and config.yaml as configuration files 2025-05-03 22:21:56 +02:00
Dominik Lakatoš
2b46dca770 generating WG keypair in browser using Web Crypto API (#422) 2025-05-03 07:58:41 +02:00
Christoph Haas
b9c4ca04f5 allow to encrypt keys in db, add browser-only key generator, add hints that private keys are stored on the server (#420) 2025-05-02 18:48:35 +02:00
Christoph Haas
dddf0c475b build v2 tags for release-candidate versions 2025-05-02 10:51:28 +02:00
Christoph Haas
fe60a5ab9b update documentation for Docker usage (#419) 2025-05-02 10:42:33 +02:00
Christoph Haas
e176e07f7d update documentation for Docker usage (#419), include wireguard-tools in Docker image 2025-05-02 10:29:04 +02:00
Christoph Haas
b06c03ef8e fix missing error check (#419) 2025-05-01 19:12:19 +02:00
Christoph Haas
6b0b78d749 docs: add note about running wireguard in Docker (#156) 2025-04-30 22:42:04 +02:00
Vladimir Dombrovski
62f3c8d4a1 Implement EditableKeys parameter (#417)
Signed-off-by: Vladimir DOMBROVSKI <vladimir.dombrovski@bso.co>
2025-04-30 22:05:40 +02:00
acc0mplish
fbcb22198c Added Korean translations (#414) 2025-04-24 14:54:45 +02:00
Rafael Alexandre
2c443a4a9b add portuguese translations (#412)
Signed-off-by: Rafael Alexandre <r.alexandre99@gmail.com>
2025-04-22 22:44:05 +02:00
Christoph
059234d416 never publish pointer payloads on message bus (#411) 2025-04-21 16:42:35 +02:00
Christoph
e2966d32ea fix user creation (#411) 2025-04-21 15:29:53 +02:00
Christoph
9354a1d9d3 add simple webhook feature for peer, interface and user events (#398) 2025-04-19 21:29:26 +02:00
Christoph
e75a32e4d0 improve docs regarding external_url (#406) 2025-04-19 18:01:02 +02:00
Christoph
1d94f6baaf change tagged-input-field component, allow to paste multiple values (#365) 2025-04-19 17:43:51 +02:00
Christoph
6681dfa96f generate interface and peer configuration filenames in backend only (#395) 2025-04-19 13:12:31 +02:00
Christoph
a60feb7fc9 fix incorrect documentation for ldap providers (#408) 2025-04-19 12:21:45 +02:00
Christoph
37904f96fb run initial LDAP sync on startup (#407) 2025-04-19 12:12:45 +02:00
Christoph
1e9ee25e49 chore: update dependencies 2025-04-19 12:07:09 +02:00
dependabot[bot]
30eac7c44a chore(deps): bump the golang group with 3 updates (#400)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the golang group with 3 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/oauth2](https://github.com/golang/oauth2) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/crypto` from 0.36.0 to 0.37.0
- [Commits](https://github.com/golang/crypto/compare/v0.36.0...v0.37.0)

Updates `golang.org/x/oauth2` from 0.28.0 to 0.29.0
- [Commits](https://github.com/golang/oauth2/compare/v0.28.0...v0.29.0)

Updates `golang.org/x/sys` from 0.31.0 to 0.32.0
- [Commits](https://github.com/golang/sys/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 18:31:59 +02:00
dependabot[bot]
801ce76616 chore(deps): bump github.com/coreos/go-oidc/v3 from 3.13.0 to 3.14.1 (#399)
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.13.0 to 3.14.1.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.13.0...v3.14.1)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-version: 3.14.1
  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>
2025-04-07 18:31:22 +02:00
dependabot[bot]
5f9c3bab3e chore(deps): bump github.com/go-playground/validator/v10 (#396)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.25.0 to 10.26.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.25.0...v10.26.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  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>
2025-04-03 06:46:31 +02:00
dependabot[bot]
e19f42b1eb chore(deps): bump github.com/go-pkgz/routegroup from 1.3.1 to 1.4.1 (#397)
Bumps [github.com/go-pkgz/routegroup](https://github.com/go-pkgz/routegroup) from 1.3.1 to 1.4.1.
- [Release notes](https://github.com/go-pkgz/routegroup/releases)
- [Commits](https://github.com/go-pkgz/routegroup/compare/v1.3.1...v1.4.1)

---
updated-dependencies:
- dependency-name: github.com/go-pkgz/routegroup
  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>
2025-04-03 06:33:48 +02:00
Christoph Haas
34fb373659 chore: update frontend dependencies 2025-03-30 23:21:29 +02:00
Christoph Haas
b938bc8c4c fix: fix peer audit event 2025-03-30 23:16:10 +02:00
Christoph Haas
87bf5da5bd fix: fix session handling (remove IdleTimeout) 2025-03-30 23:14:49 +02:00
Christoph Haas
3723e4cc75 fix: fix csrf token handling after login 2025-03-29 17:21:54 +01:00
Christoph Haas
6cbccf6d43 feat: add simple audit ui 2025-03-29 16:42:31 +01:00
Christoph Haas
a49cfa6343 chore: update dependencies 2025-03-23 23:12:47 +01:00
Christoph Haas
fe681c015c Merge branch 'master' into chore-code-cleanup
# Conflicts:
#	go.mod
#	go.sum
2025-03-23 23:10:34 +01:00
Christoph Haas
7d0da4e7ad chore: use interfaces for all other services 2025-03-23 23:09:47 +01:00
dependabot[bot]
3218bdd6fb chore(deps): bump the patch group across 1 directory with 2 updates (#391)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-03-17 19:04:11 +01:00
dependabot[bot]
12ccd6e32d chore(deps): bump the golang group with 3 updates (#389) 2025-03-17 18:57:42 +01:00
Christoph Haas
02ed7b19df chore: use interfaces for web related services 2025-03-09 21:48:38 +01:00
Christoph Haas
678b6c6456 Merge branch 'master' into chore-code-cleanup
# Conflicts:
#	go.mod
#	go.sum
2025-03-09 21:17:47 +01:00
Christoph Haas
0206952182 chore: replace gin with standard lib net/http 2025-03-09 21:16:42 +01:00
klmmr
53bae9d194 config: validate mail configuration certificates by default (#388)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Before this commit, the default was to not validate TLS certificates of
the SMTP server. This is perhaps a rather unexpected default and can be
considered insecure. This commit activates mail server TLS cert validation
by default.

This change might break some users' email configuration, if they did not
explicitly set the `mail.cert_validation` config variable. Nonetheless,
I think that the secure option should be the default option (e.g.,
to prevent man-in-the-middle attacks and breaching mail server login
credentials).

Signed-off-by: klmmr <35450576+klmmr@users.noreply.github.com>
2025-03-05 19:20:57 +01:00
Dmytro Bondar
f616a9f5f4 chore(deps): update frontend packages (#387)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-03-04 22:23:37 +01:00
Dmytro Bondar
bf5453c264 chore(deps): update Go version to 1.24 in Dockerfile and go.mod
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-03-04 08:48:57 +01:00
dependabot[bot]
fd631d3b9f chore(deps): bump github.com/a8m/envsubst in the patch group
Bumps the patch group with 1 update: [github.com/a8m/envsubst](https://github.com/a8m/envsubst).


Updates `github.com/a8m/envsubst` from 1.4.2 to 1.4.3
- [Release notes](https://github.com/a8m/envsubst/releases)
- [Commits](https://github.com/a8m/envsubst/compare/v1.4.2...v1.4.3)

---
updated-dependencies:
- dependency-name: github.com/a8m/envsubst
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 08:48:57 +01:00
dependabot[bot]
9680e8350c chore(deps): bump the golang group with 2 updates (#384)
Bumps the golang group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/oauth2](https://github.com/golang/oauth2).


Updates `golang.org/x/crypto` from 0.34.0 to 0.35.0
- [Commits](https://github.com/golang/crypto/compare/v0.34.0...v0.35.0)

Updates `golang.org/x/oauth2` from 0.26.0 to 0.27.0
- [Commits](https://github.com/golang/oauth2/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-04 08:39:33 +01:00
Christoph Haas
7473132932 chore: replace logrus with standard lib log/slog 2025-03-02 08:51:13 +01:00
Christoph Haas
5c51573874 chore: update to yaml v3 2025-02-28 16:15:22 +01:00
Christoph Haas
fdb436b135 chore: get rid of static code warnings 2025-02-28 16:11:55 +01:00
Christoph Haas
e24acfa57d chore: cleanup code formatting 2025-02-28 08:37:55 +01:00
Dmytro Bondar
10332c7f9a feat(helm): add optional volumeName to persistence configuration #379 (#380)
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-02-27 22:58:15 +01:00
Christoph Haas
f7d7038829 chore: update to Go 1.24, improve oauth admin mapping tests 2025-02-27 22:32:11 +01:00
Christoph Haas
66ccdc29e9 fix qr-code generation for large configurations (#374)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-02-26 22:59:11 +01:00
Christoph Haas
40b4538e78 implement checkall checkbox (#372) 2025-02-26 22:24:37 +01:00
Christoph Haas
986f6fdead fix peer creation for client interface (#371) 2025-02-26 22:02:53 +01:00
dependabot[bot]
dabdf111f9 chore(deps): bump github.com/prometheus/client_golang (#377)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  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>
2025-02-26 21:57:06 +01:00
dependabot[bot]
b074af6dc5 chore(deps): bump golang.org/x/crypto in the golang group (#376)
Bumps the golang group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.33.0 to 0.34.0
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 21:56:54 +01:00
klmmr
eeb0c87c68 ldap-sync: fix creation of only one user per LDAP sync (#375)
Before this fix, a too early `return` statement terminated the
`updateLdapUsers()` function, whenever one not already existing user was
created. Therefore, in each LDAP sync a maximum of one new user could be
created (i.e., it took x LDAP sync cycles until x new LDAP users are
registered in wg-portal). Depending on the LDAP `sync_interval` this can
take a long time and produces unecessary long waiting times until users
are available in wg-portal.

Removing the early return statement, and move the remainder of the
function into an `else` statement, so that all new users can be
added in a single LDAP sync.

Also adding a debug statement to better trace the behavior.

Signed-off-by: klmmr <35450576+klmmr@users.noreply.github.com>
2025-02-26 21:56:22 +01:00
dependabot[bot]
67f076effe chore(deps): bump github.com/yeqown/go-qrcode/v2 in the patch group (#370)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the patch group with 1 update: [github.com/yeqown/go-qrcode/v2](https://github.com/yeqown/go-qrcode).


Updates `github.com/yeqown/go-qrcode/v2` from 2.2.4 to 2.2.5
- [Release notes](https://github.com/yeqown/go-qrcode/releases)
- [Commits](https://github.com/yeqown/go-qrcode/compare/v2.2.4...v2.2.5)

---
updated-dependencies:
- dependency-name: github.com/yeqown/go-qrcode/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 19:15:27 +01:00
Christoph Haas
f6d7a851d1 frontend: fix locked user display (#367)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-02-17 08:18:36 +01:00
Christoph Haas
fc712ebf42 api: fix ExpiredAt format (#368) 2025-02-17 08:03:43 +01:00
Christoph Haas
43163273fa api: remove IsAdmin from required attributes (#366) 2025-02-17 07:43:31 +01:00
dependabot[bot]
5697c2b7f2 chore(deps): bump the golang group with 3 updates (#363)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the golang group with 3 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/oauth2](https://github.com/golang/oauth2) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/crypto` from 0.32.0 to 0.33.0
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.33.0)

Updates `golang.org/x/oauth2` from 0.25.0 to 0.26.0
- [Commits](https://github.com/golang/oauth2/compare/v0.25.0...v0.26.0)

Updates `golang.org/x/sys` from 0.29.0 to 0.30.0
- [Commits](https://github.com/golang/sys/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 18:19:02 +01:00
Christoph Haas
e983a7b8f3 automatic API access for default admin (#357)
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-02-07 22:42:48 +01:00
Christoph Haas
c33eaba1c0 remove unsupported validator (#360) 2025-02-07 22:21:16 +01:00
Dmytro Bondar
3774257abb Added Ukrainian translations (#361)
Signed-off-by: Dmytro Bondar <git@bonddim.dev>
2025-02-07 22:04:26 +01:00
klmmr
588f09bdaa [DOCS] Fix example config wrt. admin_value_regex and admin_group_regex (#362)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-02-07 17:59:58 +01:00
JBSAN3
7557a6ef5a Add French in to translations (#359)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-02-06 15:06:39 +01:00
dependabot[bot]
3478645317 chore(deps): bump github.com/prometheus-community/pro-bing (#356)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-02-03 20:42:25 +01:00
Dmytro Bondar
a950dd76ba Added issue and pull request templates (#355)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-28 21:43:31 +01:00
dependabot[bot]
8c0ecec485 chore(deps): bump github.com/prometheus-community/pro-bing (#354) 2025-01-28 19:05:32 +01:00
Christoph Haas
d01d865b4d fix self provisioning feature (#272)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-26 11:35:24 +01:00
Christoph Haas
1b8cdc3417 automatically append listening port to endpoint address (#352) 2025-01-26 09:52:09 +01:00
Christoph Haas
d35889de73 remove external google fonts (#107)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-25 23:06:44 +01:00
Dmytro Bondar
0b18b5efd6 [chart] Fix default configurations (#350)
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-24 12:48:36 +01:00
Dmytro Bondar
2cf2341e4c [chart] Update helm chart (#349)
Some checks are pending
Chart / lint-test (push) Waiting to run
Chart / publish (push) Waiting to run
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-23 13:42:51 +01:00
Dmytro Bondar
043d25a08f [docs] big bang update (#348)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
* [docs] big bang update

* Simplified polluted README.md by moving parts to the documentation
* Removed duplicates with `pymdownx.snippets` extension
* Enabled code copy
* Extended "Getting Started"
* Added "Monitoring" page
* Separated "Upgrade" page
* Added default config yaml to docs

Signed-off-by: Dmytro Bondar <git@bonddim.dev>

* Update sources.md

Co-authored-by: h44z <christoph.h@sprinternet.at>

---------

Signed-off-by: Dmytro Bondar <git@bonddim.dev>
Co-authored-by: h44z <christoph.h@sprinternet.at>
2025-01-23 08:06:55 +01:00
Christoph Haas
f6c8cd5ea8 allow LDAP users (and linked peers) to be automatically re-enabled (#345)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-21 18:03:30 +01:00
Christoph Haas
a04eaa4bfb fix user group parsing for OAuth login (#317) 2025-01-21 17:33:01 +01:00
Dmytro Bondar
7a0a2117f5 Remove Swagger Authorize button from published docs (#347)
* Remove Swagger *Authorize* button from published docs

* Ignore mkdocs output dir

* tidy mods
2025-01-21 12:31:28 +01:00
Dmytro Bondar
2cea2e477a Show version on frontend (#346) 2025-01-21 12:27:25 +01:00
Christoph Haas
c2658534b0 chore: publish more docker version tags, migrate to semver
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-18 19:09:43 +01:00
Christoph Haas
2030c59362 chore: publish more docker version tags, migrate to semver 2025-01-18 19:02:36 +01:00
Christoph Haas
e31c170f48 Revert "chore: publish more docker version tags, migrate to semver"
This reverts commit 075fd0171e.
2025-01-18 18:51:04 +01:00
Christoph Haas
49a987cbce Revert "chore: publish more docker version tags, migrate to semver"
This reverts commit 3526240faf.
2025-01-18 18:51:04 +01:00
Christoph Haas
3526240faf chore: publish more docker version tags, migrate to semver 2025-01-18 18:24:01 +01:00
Christoph Haas
075fd0171e chore: publish more docker version tags, migrate to semver 2025-01-18 18:10:51 +01:00
Christoph Haas
c73ce0288e fix disabling of missing ldap users (#344) and allow deletion of all user types 2025-01-18 17:39:18 +01:00
Christoph Haas
31c0daeba8 fix .gitignore
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-18 12:13:09 +01:00
Christoph Haas
662e9c0549 Improve admin privilege handling for OAuth. Update documentation. 2025-01-18 11:55:56 +01:00
Christoph Haas
6523a87dfb fix peer disable if ldap user is disabled (#343)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-17 21:59:15 +01:00
Christoph Haas
7ccec5db8d add swagger doc to mkdocs/website 2025-01-17 21:47:54 +01:00
Christoph Haas
c211c56f75 chore: update dependencies
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-13 22:18:27 +01:00
Christoph Haas
17844ed929 fix update of userdata after OAuth login (#317, #160) 2025-01-13 22:14:00 +01:00
Christoph Haas
2d78fe33b8 add metric endpoint to public API (#72, #80)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-11 23:42:05 +01:00
Christoph Haas
63d85d8123 code cleanup 2025-01-11 22:56:25 +01:00
Christoph Haas
26d3257516 update userdata after OAuth login (#317, #160)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-11 18:55:23 +01:00
h44z
d596f578f6 API - CRUD for peers, interfaces and users (#340)
Public REST API implementation to handle peers, interfaces and users. It also includes some simple provisioning endpoints.

The Swagger API documentation is available under /api/v1/doc.html
2025-01-11 18:44:55 +01:00
h44z
ad267ed0a8 Update README.md, fix build badge
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-05 13:53:28 +01:00
h44z
624988aef1 Create SECURITY.md 2025-01-05 13:49:35 +01:00
Christoph Haas
3020fbca4e fix change of peer identifier (public key) (#265) 2025-01-05 11:30:34 +01:00
Christoph Haas
6d86f15ff8 implement/fix peer and user disable event (#337, #273)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-05 10:06:34 +01:00
Christoph Haas
62dbdfe0f9 fix plain oauth login (#317)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-04 14:25:13 +01:00
Christoph Haas
378252ba2f sec: validate return url 2025-01-04 13:43:18 +01:00
Christoph
0664bd0ad0 chore: update dependencies
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2024-12-27 15:10:04 +01:00
dependabot[bot]
877cdae587 Bump github.com/go-ldap/ldap/v3 from 3.4.8 to 3.4.9 in the patch group (#335)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Bumps the patch group with 1 update: [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap).


Updates `github.com/go-ldap/ldap/v3` from 3.4.8 to 3.4.9
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.8...v3.4.9)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 18:15:22 +01:00
dependabot[bot]
edb5c82a66 Bump golang.org/x/crypto from 0.30.0 to 0.31.0 in the golang group (#334)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
Bumps the golang group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.30.0 to 0.31.0
- [Commits](https://github.com/golang/crypto/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 15:53:48 +01:00
Dmytro Bondar
0ea24e313d feat: handle missing config file gracefully with a warning (#331)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-12-10 15:17:31 +01:00
dependabot[bot]
983568b36a Bump the golang group with 2 updates (#332)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
Bumps the golang group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/crypto` from 0.29.0 to 0.30.0
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0)

Updates `golang.org/x/sys` from 0.27.0 to 0.28.0
- [Commits](https://github.com/golang/sys/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 19:25:44 +01:00
Dmytro Bondar
81ff0cde60 chore: update vue-i18n and related dependencies to version 9.14.2 (#330)
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
Resolved XSS vulnerability https://github.com/advisories/GHSA-9r9m-ffp6-9x4v
2024-12-04 12:22:24 +01:00
Dmytro Bondar
0f27443ffc chart: update monitoring resources (#329)
### Monitoring settings:
* Disabled rendering of Prometheus resources by default.
* Added a new relabeling configuration to replace the `instance` label based on the `app_kubernetes_io_name` label. Instance will be displayed as Helm release name instead of pod IP address.

### Grafana dashboard configuration:
* Changed the `interval` field for several metrics queries from `"$__rate_interval"` to `"$interval"`.
* Updated the `refresh` interval from `30s` to `1m`.
* Added a new variable for `Step Interval` with default value `2m`.

### Chart version update:
* Updated the chart version from `0.4.0` to `0.5.0`.

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-12-04 12:22:03 +01:00
dependabot[bot]
ca6070689e Bump gorm.io/driver/postgres from 1.5.10 to 1.5.11 in the gorm group (#328)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
Bumps the gorm group with 1 update: [gorm.io/driver/postgres](https://github.com/go-gorm/postgres).


Updates `gorm.io/driver/postgres` from 1.5.10 to 1.5.11
- [Commits](https://github.com/go-gorm/postgres/compare/v1.5.10...v1.5.11)

---
updated-dependencies:
- dependency-name: gorm.io/driver/postgres
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gorm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-04 09:56:49 +01:00
Dmytro Bondar
ba9b6c39e0 docs: build multi-version documentation (#327)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
* Build multi-version docs with 'mike'

Signed-off-by: Dmytro Bondar <git@bonddim.com>

* Revert remote_branch option

---------

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-12-03 19:04:43 +01:00
Dmytro Bondar
afcba8d43e chore: update dependencies (#325)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
* Update go version and dependencies

Signed-off-by: Dmytro Bondar <git@bonddim.com>

* Group dependabot updates

Signed-off-by: Dmytro Bondar <git@bonddim.com>

* Lock file maintenance

Signed-off-by: Dmytro Bondar <git@bonddim.com>

* Rename vite.config.js

Keep CJS as default, opt-in to ESM if needed
More: https://vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated

Signed-off-by: Dmytro Bondar <git@bonddim.com>

---------

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-11-27 18:24:23 +01:00
Dmytro Bondar
90a570bd66 fix: enhance PrivateString Scan method to support []byte input (#324)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-11-26 21:09:39 +01:00
h44z
f7c3bdf456 Update docker-compose.yml (fixes #308)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2024-11-24 18:53:28 +01:00
ClarkQAQ
486a6ac038 feat(translations): chinese translations (#316)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
*  feat(translations): chinese translations

*  feat(App.vue): append dropdown item
2024-11-01 18:05:38 +01:00
Christoph
bf9183256a chore: update dependencies, refactor option types
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2024-10-15 15:44:47 +02:00
Christoph Haas
6bb683047e chore: update go dependencies
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
Docker / release (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
2024-10-09 22:41:01 +02:00
dependabot[bot]
5a289276f4 Bump docker/build-push-action from 5 to 6 in the actions group (#310)
Bumps the actions group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `docker/build-push-action` from 5 to 6
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 22:37:56 +02:00
Dmytro Bondar
d8eac37302 Updated metrics, added dashboard for Grafana (#311)
* Updated metrics, added dashboard for Grafana

* Remove unused interfce metric

* Set default scrape interval
2024-10-09 22:33:50 +02:00
Ryazanov Alexander Mihailovich
386597e057 UX: Config downloads without .txt extension (#314)
* refactor: change mime type in PeerViewModal.vue

* refactor: change download type in InterfaceView.vue
2024-10-09 22:32:32 +02:00
Dmytro Bondar
f22a7e4a2e feat: Metrics for Prometheus (#309)
* feat: prometheus metrics

* Added Prometheus resources support to helm chart
2024-09-29 22:10:50 +02:00
Christoph Haas
ae1be0e367 chore: update go dependencies 2024-09-23 22:04:24 +02:00
Dmytro Bondar
7a08c14de4 chore: CI files update (#306)
* Update Dockerfile

- Fix FromAsCasing
- Copy `cmd` and `internal` directories only for backend

* Export binaries from docker images

* Create release with assets from workflow

* Remove circleci config

* fix chart publishing
2024-09-23 21:54:22 +02:00
Dmytro Bondar
2c01f42369 feat: substitute environment variables in config file (#305)
* feat: use envsubst to substitute env variables in config file

* Remove output config to log

* Update readme
2024-09-23 21:48:11 +02:00
Dmytro Bondar
3196010a58 feat: Added peers sorting on views (#302)
* Added peers sorting for Interface and Profile views

* Use ip-address package to sort with IPv6 addresses only

* Add RX/TX column and fix add-peer button title
2024-09-23 21:44:43 +02:00
Dmytro Bondar
6ffe1a90ae feat: TLS support for web (#301)
* Added TLS support for web

- Added optional configurations `cert_file` and `key_file` to run web server with https

Signed-off-by: Dmytro Bondar <git@bonddim.com>

* Helm chart update

- Refactored Ingress to use one host only (`config.web.external_url` is required)
- Added Certificate resource template (secret is mounted to container into `/app/certs/`)
- Added support for service with mixed protocols (exposes UI and Wireguard ports on same IP)
- Added helm-docs target to makefile
- Changed pod labels to use selectorLabels
- Removed default probes (app runs without healthy web)
- Removed sections from README

Signed-off-by: Dmytro Bondar <git@bonddim.com>

* Fix chart workflow path filter

* Fix chart lint issue

* Skip clean-up tested chart

* Try k3d cluster

---------

Signed-off-by: Dmytro Bondar <git@bonddim.com>
2024-09-22 13:25:08 +02:00
dithmer
e3d05a4678 Fix not loading stats on interface change (#294)
To fix that, the method peers.LoadStats() is simply also called together
with peers.LoadPeers() when the @change handler is activated on the
interface selector element.

Co-authored-by: Tim Dithmer <tim.dithmer@suresecure.de>
2024-09-22 13:20:47 +02:00
dithmer
deff2334ac Fix the wrong default hooks for PreDown and PostDown on Peer Preparing by using the correct properties of the iface (#293)
Co-authored-by: Tim Dithmer <tim.dithmer@suresecure.de>
2024-09-22 11:55:41 +02:00
congnvp
4f1044a963 Add Vietnamese in to translations (#291) 2024-09-22 11:54:41 +02:00
Dmytro Bondar
2428dedc42 fix: autosave wireguard conf files (#303)
* fix: autosave wireguard conf files

- Fix subscription to Interface and Peer updates topics
- Remove admin permissions validation
- Update file on peer deletion
- Change save condition to configured storage path only, as initialized interface is not nil

* Added  comment to peer config for prometheus exporter
2024-09-22 11:53:42 +02:00
Dmytro Bondar
605841f2a0 fix: LDAP sync interval (#304)
Configurable LDAP sync interval for each LDAP provider
2024-09-22 11:49:23 +02:00
Christoph Haas
a46dabc1d3 #282: change default peer mask to /32 2024-08-13 22:49:58 +02:00
Christoph Haas
3f72de6af4 chore: update dependencies 2024-08-13 22:38:01 +02:00
dependabot[bot]
f1f5280cbc Bump the actions group across 1 directory with 3 updates (#279)
Bumps the actions group with 3 updates in the / directory: [actions/setup-python](https://github.com/actions/setup-python), [docker/login-action](https://github.com/docker/login-action) and [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `actions/setup-python` from 4 to 5
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

Updates `docker/login-action` from 1 to 3
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v3)

Updates `docker/build-push-action` from 5 to 6
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-04 23:09:44 +02:00
Christoph Haas
48f4b6cb0e chore: update dependencies 2024-07-04 22:44:50 +02:00
Ryazanov Alexander Mihailovich
58294a3c2a QoL frontend improvements (#267)
* refactor: add space for language title

* feat: add overflow-break to text-wrap class

* feat: add public key text wrapping for interfaces

* refactor: get rid of getlang function and set locale during initialization

fix: show english flag when locale does not match

* refactor: bind header logo alt attribute to companyName

* refactor: add language name to german

* refactor: add language name to russian

* refactor: add language name to english
2024-07-04 22:40:16 +02:00
Dmytro Bondar
6f52cb2ada Init Helm chart (#255)
* Initial chart version

* Add CI/CD for chart

* Fix admin creds template

* Add command, args, env, envFrom

* Render volumes and volumeMounts with tpl

* Change persistance accessMode type

* Add update strategy config

* Use custom types in docs

* Add startup probe config

* Fix web.external_url config
2024-07-04 22:37:30 +02:00
Christoph Haas
85381121ee update to go 1.22 2024-04-29 23:28:32 +02:00
Dmytro Bondar
a6d985c2fe Remove CodeQL workflow file (#256) 2024-04-29 23:21:00 +02:00
Christoph Haas
aebf80bf68 update dependencies 2024-04-29 23:18:16 +02:00
muellpanda
e72ba87619 Remove 'hidden'-attribute from email button (#251) 2024-04-29 23:10:31 +02:00
Christoph Haas
288b7794ca fix default peer creation on login (#189) 2024-04-02 22:29:10 +02:00
Christoph Haas
95e10dcc24 execute interface hooks if interface settings have changed (#224) 2024-04-02 20:51:09 +02:00
Christoph Haas
c17c182926 update all external dependencies 2024-04-02 20:02:36 +02:00
Christoph Haas
d8c1b67a2e make mkdocs build again 2024-03-29 16:47:35 +01:00
Christoph Haas
c325e4590b update mkdocs.yml 2024-03-29 16:42:05 +01:00
dependabot[bot]
a3f5ec1311 Bump gorm.io/driver/mysql from 1.5.2 to 1.5.6 (#232)
Bumps [gorm.io/driver/mysql](https://github.com/go-gorm/mysql) from 1.5.2 to 1.5.6.
- [Commits](https://github.com/go-gorm/mysql/compare/v1.5.2...v1.5.6)

---
updated-dependencies:
- dependency-name: gorm.io/driver/mysql
  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>
2024-03-29 16:02:14 +01:00
dependabot[bot]
2f7819ca9b Bump github.com/glebarez/sqlite from 1.10.0 to 1.11.0 (#227)
Bumps [github.com/glebarez/sqlite](https://github.com/glebarez/sqlite) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/glebarez/sqlite/releases)
- [Commits](https://github.com/glebarez/sqlite/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/glebarez/sqlite
  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>
2024-03-29 16:02:05 +01:00
dependabot[bot]
86fbff886f Bump gorm.io/gorm from 1.25.7 to 1.25.8 (#228)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.7 to 1.25.8.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.7...v1.25.8)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  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>
2024-03-29 16:01:55 +01:00
dependabot[bot]
52c3bc8d92 Bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#229)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  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>
2024-03-29 16:01:43 +01:00
dependabot[bot]
ea055f3428 Bump golang.org/x/oauth2 from 0.15.0 to 0.18.0 (#226)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.15.0 to 0.18.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.15.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  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>
2024-03-29 16:00:03 +01:00
Ruoxi Wang
1d862c01d5 Implement custom Value and Scan methods for PrivateString type (#231) 2024-03-29 15:52:14 +01:00
dependabot[bot]
38310d6ff2 Bump github.com/prometheus-community/pro-bing from 0.3.0 to 0.4.0 (#220)
Bumps [github.com/prometheus-community/pro-bing](https://github.com/prometheus-community/pro-bing) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/prometheus-community/pro-bing/releases)
- [Changelog](https://github.com/prometheus-community/pro-bing/blob/main/.goreleaser.yaml)
- [Commits](https://github.com/prometheus-community/pro-bing/compare/v0.3.0...v0.4.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus-community/pro-bing
  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>
2024-03-13 23:13:37 +01:00
dependabot[bot]
68903597eb Bump gorm.io/gorm from 1.25.5 to 1.25.7 (#222)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.5 to 1.25.7.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.5...v1.25.7)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  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>
2024-03-13 23:13:06 +01:00
dependabot[bot]
2cfd565e3f Bump gorm.io/driver/sqlserver from 1.5.2 to 1.5.3 (#223)
Bumps [gorm.io/driver/sqlserver](https://github.com/go-gorm/sqlserver) from 1.5.2 to 1.5.3.
- [Commits](https://github.com/go-gorm/sqlserver/compare/v1.5.2...v1.5.3)

---
updated-dependencies:
- dependency-name: gorm.io/driver/sqlserver
  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>
2024-03-13 23:12:05 +01:00
dependabot[bot]
6f617d6e86 Bump github.com/swaggo/swag from 1.16.2 to 1.16.3 (#221)
Bumps [github.com/swaggo/swag](https://github.com/swaggo/swag) from 1.16.2 to 1.16.3.
- [Release notes](https://github.com/swaggo/swag/releases)
- [Changelog](https://github.com/swaggo/swag/blob/master/.goreleaser.yml)
- [Commits](https://github.com/swaggo/swag/compare/v1.16.2...v1.16.3)

---
updated-dependencies:
- dependency-name: github.com/swaggo/swag
  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>
2024-03-13 23:11:50 +01:00
dependabot[bot]
349a6befa1 Bump github.com/gin-contrib/cors from 1.5.0 to 1.7.0 (#219)
Bumps [github.com/gin-contrib/cors](https://github.com/gin-contrib/cors) from 1.5.0 to 1.7.0.
- [Release notes](https://github.com/gin-contrib/cors/releases)
- [Changelog](https://github.com/gin-contrib/cors/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/cors/compare/v1.5.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/cors
  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>
2024-03-13 23:10:08 +01:00
Christoph Haas
2de438add8 Merge branch 'pr216' 2024-03-12 22:45:32 +01:00
Christoph Haas
e565e26c65 Merge branch 'pr214' 2024-03-12 22:33:15 +01:00
Christoph Haas
acc785e4ca small cleanup 2024-03-12 22:32:05 +01:00
Mehrdad Tahernia
c89f201c78 fix issue 211: DNS Search Domain not applying (#217)
Added the DnsSearchStr to the template to include the dns search domain in the generated config file
2024-03-11 16:03:03 +01:00
Dmytro Bondar
3279cb2204 Fix installation button translations (#215) 2024-03-11 16:02:16 +01:00
Dmytro Bondar
6fb6dc0d23 Remove builded frontend from repo 2024-03-04 11:57:19 +01:00
Dmytro Bondar
8279aba15e Add docs to .dockerignore 2024-03-04 11:43:35 +01:00
Dmytro Bondar
c8989e1ca3 Add curl,iptables,nftables into final image 2024-03-04 11:43:35 +01:00
Dmytro Bondar
9e1b6b6d91 Refactor docker-publish workflow 2024-03-04 11:43:30 +01:00
Dmytro Bondar
180b43608d Update actions versions 2024-03-04 11:27:12 +01:00
Dmytro Bondar
f76b59286e Use golang-alpine image for builds 2024-03-04 11:27:12 +01:00
Dmytro Bondar
c970b81d84 Add frontend to Dockerfile and use cross-platform build 2024-03-04 11:27:02 +01:00
Dmytro Bondar
c37a85fa0b Fix CVE-2024-23331 https://github.com/advisories/GHSA-c24v-8rfc-w8vw 2024-03-04 09:55:13 +01:00
Dmytro Bondar
5dcb3eca6d Add dependabot config for actions and go 2024-03-04 09:55:13 +01:00
sh0rch
1287215837 another Minor fixes for greater compatibility with the original code. 2024-02-29 08:18:40 +03:00
sh0rch
26cd286c57 Minor fixes for greater compatibility with the original code. 2024-02-29 07:59:27 +03:00
sh0rch
eae1bc765d Brought into working condition for LDAP authentication. 2024-02-29 07:30:22 +03:00
sh0rch
0ade556e80 Brought into working condition for LDAP authentication. 2024-02-29 07:17:17 +03:00
Christoph Haas
1b4b5ff161 fix REST API permission checks (#209) 2024-01-31 21:14:36 +01:00
Christoph Haas
81e696fc7d update frontend dependencies 2023-12-23 13:36:42 +01:00
Christoph Haas
ebe902d119 update backend dependencies 2023-12-23 13:30:31 +01:00
HPPinata
4a0fcfbf60 Use Alpine base image (#205)
wgquick (apparently) depends on bash and openresolv.

Without those packages removing an Interface is impossible and causes an error message to appear in the webui:
- No Bash: exec complains about "bash no such file in $PATH"
- No openresolv: resolvconf -f -d *interface* returns with error 127

Not having a Shell in the container also makes debugging a lot more annoying.

This enables deletion of interfaces in the webui, eases debugging and only adds about 3MB in size.
2023-12-23 13:08:33 +01:00
h44z
1f47075020 Update social links 2023-11-03 09:03:54 +01:00
Christoph Haas
faf454f649 add github pages site 2023-11-02 21:44:14 +01:00
h44z
35939c92c9 Create CNAME 2023-11-01 10:18:55 +01:00
821 changed files with 43040 additions and 29270 deletions

View File

@@ -1,67 +0,0 @@
version: 2.1
jobs:
build-latest:
steps:
- checkout
- restore_cache:
keys:
- go-mod-latest-v4-{{ checksum "go.sum" }}
- run:
name: Build Frontend
command: |
make frontend
- run:
name: Install Dependencies
command: |
make build-dependencies
- save_cache:
key: go-mod-latest-v4-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
name: Build AMD64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-amd64
- run:
name: Install Cross-Platform Dependencies
command: |
sudo apt-get update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm64
- run:
name: Build ARM
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm
- store_artifacts:
path: ~/repo/dist
- run:
name: "Publish Release on GitHub"
command: |
if [ ! -z "${CIRCLE_TAG}" ]; then
go install github.com/tcnksm/ghr@latest
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist
fi
working_directory: ~/repo
docker:
- image: cimg/go:1.21-node
workflows:
build-and-release:
jobs:
#--------------- BUILD ---------------#
- build-latest:
filters:
tags:
only: /^v.*/

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
# Ignore everything
*
# Allow backend files
!cmd/
!internal/
!go.mod
!go.sum
# Allow frontend files
!frontend/
# Ignore node_modules
**/node_modules/

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve WG-Portal
labels: bug
---
<!-- Tip: you can use code blocks
for better formatting of yaml config or logs
```yaml
# config.yaml
```
```console
logs here
``` -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Steps to reproduce**
<!--Steps to reproduce the bug should be clear and easily reproducible to help people
gain an understanding of the problem.-->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Additional context**
<!-- Add any other context about the problem here. -->
- Application version: v
- Install method: binary/docker/helm/sources
<!-- - OS: -->

View File

@@ -0,0 +1,18 @@
---
name: Feature request
about: Suggest an idea for this project
labels: 'enhancement'
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

35
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns:
- "*"
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
groups:
golang:
patterns:
- golang.org*
gorm:
patterns:
- gorm.io*
patch:
update-types:
- patch
- package-ecosystem: "docker"
directory: /
schedule:
interval: weekly

18
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,18 @@
## Problem Statement
What is the problem you're trying to solve?
## Related Issue
Fixes #...
## Proposed Changes
How do you like to solve the issue and why?
## Checklist
- [ ] Commits are signed with `git commit --signoff`
- [ ] Changes have reasonable test coverage
- [ ] Tests pass with `make test`
- [ ] Helm docs are up-to-date with `make helm-docs`

75
.github/workflows/chart.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
# Publish chart to the GitHub Container Registry (GHCR) on push to master
# Run the following tests on PRs:
# - Check if chart's documentation is up to date
# - Check chart linting
# - Check chart installation in a Kind cluster
# - Check chart packaging
name: Chart
on:
pull_request:
branches: [master]
paths: ['deploy/helm/**']
push:
branches: [master]
paths: ['deploy/helm/**']
jobs:
lint-test:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Check docs
run: |
make helm-docs
if ! git diff --exit-code; then
echo "error::Documentation is not up to date. Please run helm-docs and commit changes."
exit 1
fi
# ct lint requires Python 3.x to run following packages:
# - yamale (https://github.com/23andMe/Yamale)
# - yamllint (https://github.com/adrienverge/yamllint)
- uses: actions/setup-python@v6
with:
python-version: '3.x'
- uses: helm/chart-testing-action@v2
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml
- uses: nolar/setup-k3d-k3s@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run chart-testing (install)
run: ct install --config ct.yaml
- name: Check chart packaging
run: helm package deploy/helm
publish:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
permissions:
packages: write
steps:
- uses: actions/checkout@v5
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Package helm chart
run: helm package deploy/helm
- name: Push chart to GHCR
run: helm push wg-portal-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts

View File

@@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 15 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,30 +1,24 @@
name: Docker name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on: on:
pull_request:
branches: [master]
push: push:
branches: [ master, stable ] branches: [master]
# Publish vX.X.X tags as releases. # Publish vX.X.X tags as releases.
tags: [ 'v*.*.*' ] tags: ["v*.*.*"]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
jobs: jobs:
build-dockerhub: build-n-push:
name: Push Docker image to Docker Hub name: Build and Push
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@@ -34,106 +28,94 @@ jobs:
- name: Get Version - name: Get Version
shell: bash shell: bash
run: | run: echo "BUILD_VERSION=${GITHUB_REF_NAME}-${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
id: get_version
- name: Log in to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: wgportal/wg-portal images: |
wgportal/wg-portal
ghcr.io/${{ github.repository }}
flavor: | flavor: |
latest=true latest=auto
prefix= prefix=
suffix= suffix=
tags: | tags: |
type=ref,event=tag
type=ref,event=branch type=ref,event=branch
# semver tags, without v prefix
type=semver,pattern={{version}} type=semver,pattern={{version}}
# major and major.minor tags are not available for alpha or beta releases
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}} type=semver,pattern=v{{major}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: | build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }} BUILD_VERSION=${{ env.BUILD_VERSION }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
build-github: - name: Export binaries from images
name: Push Docker image to Github Container Registry uses: docker/build-push-action@v6
runs-on: ubuntu-latest with:
permissions: context: .
contents: read platforms: linux/amd64,linux/arm64,linux/arm/v7
packages: write target: binaries
outputs: type=local,dest=./binaries
build-args: |
BUILD_VERSION=${{ env.BUILD_VERSION }}
steps: - name: Rename binaries
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get Version
shell: bash
run: | run: |
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})" for file in binaries/linux*/wg-portal; do
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)" mv $file binaries/wg-portal_$(basename $(dirname $file))
id: get_version done
# Login against a Docker registry except on PR - name: Upload binaries
# https://github.com/docker/login-action uses: actions/upload-artifact@v4
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} name: binaries
username: ${{ github.actor }} path: binaries/wg-portal_linux*
password: ${{ secrets.GITHUB_TOKEN }} retention-days: 10
# Extract metadata (tags, labels) for Docker release:
# https://github.com/docker/metadata-action if: startsWith(github.ref, 'refs/tags/v')
- name: Extract Docker metadata runs-on: ubuntu-latest
id: meta needs: build-n-push
uses: docker/metadata-action@v5 permissions:
contents: write
steps:
- name: Download binaries
uses: actions/download-artifact@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} name: binaries
flavor: |
latest=true
prefix=
suffix=
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern=v{{major}}
# Build and push Docker image with Buildx (don't push on PR) - name: Create GitHub Release
# https://github.com/docker/build-push-action uses: softprops/action-gh-release@v2
- name: Build and push Docker image
uses: docker/build-push-action@v5
with: with:
context: . files: 'wg-portal_linux*'
push: ${{ github.event_name != 'pull_request' }} generate_release_notes: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }}

40
.github/workflows/pages.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: github-pages
on:
push:
branches: [master]
tags:
- 'v*'
- '!v*-alpha*'
- '!v*-beta*'
- '!v*-rc*'
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Install dependencies
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
- name: Publish documentation
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
run: mike deploy --push ${{ github.ref_name }}
env:
GIT_COMMITTER_NAME: "github-actions[bot]"
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
- name: Publish latest documentation
if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
env:
GIT_COMMITTER_NAME: "github-actions[bot]"
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"

13
.gitignore vendored
View File

@@ -32,7 +32,12 @@ ssh.key
.testCoverage.txt .testCoverage.txt
wg_portal.db wg_portal.db
sqlite.db sqlite.db
go.sum /config.yml
swagger.json /config.yaml
swagger.yaml /config/
/config.yml venv/
.cache/
# ignore local frontend dist directory
internal/app/api/core/frontend-dist
# mkdocs output directory
site/

View File

@@ -1,56 +1,69 @@
# Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Dockerfile References: https://docs.docker.com/engine/reference/builder/
# This dockerfile uses a multi-stage build system to reduce the image footprint. # This dockerfile uses a multi-stage build system to reduce the image footprint.
######- ######
# Start from the latest golang base image as builder image (only used to compile the code) # Build frontend
######- ######
FROM golang:1.21 as builder FROM --platform=${BUILDPLATFORM} node:lts-alpine AS frontend
# Set the working directory
ARG BUILD_IDENTIFIER
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
ARG BUILD_VERSION
ENV ENV_BUILD_VERSION=$BUILD_VERSION
# populated by BuildKit
ARG TARGETPLATFORM
ENV ENV_TARGETPLATFORM=$TARGETPLATFORM
RUN mkdir /build
# Copy the source from the current directory to the Working Directory inside the container
ADD . /build/
# Set the Current Working Directory inside the container
WORKDIR /build WORKDIR /build
# Download dependencies
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
# Set dist output directory
ENV DIST_OUT_DIR="dist"
# Copy the sources to the working directory
COPY frontend .
# Build the frontend
RUN npm run build
# Build the Go app ######
RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platform $ENV_TARGETPLATFORM"; make build # Build backend
######
FROM --platform=${BUILDPLATFORM} golang:1.25-alpine AS builder
# Set the working directory
WORKDIR /build
# Download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the sources to the working directory
COPY ./cmd ./cmd
COPY ./internal ./internal
# Copy the frontend build result
COPY --from=frontend /build/dist/ ./internal/app/api/core/frontend-dist/
# Set the build version from arguments
ARG BUILD_VERSION
# Split to cross-platform build
ARG TARGETARCH
# Build the application
RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o /build/dist/wg-portal \
-ldflags "-w -s -extldflags '-static' -X 'github.com/h44z/wg-portal/internal.Version=${BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
######- ######
# Here starts the main image # Export binaries
######- ######
FROM scratch FROM scratch AS binaries
COPY --from=builder /build/dist/wg-portal /
######
# Final image
######
FROM alpine:3.22
# Install OS-level dependencies
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
# Setup timezone # Setup timezone
ENV TZ=Europe/Vienna ENV TZ=UTC
# Import linux stuff from builder.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy binaries # Copy binaries
COPY --from=builder /build/dist/wg-portal /app/wg-portal COPY --from=builder /build/dist/wg-portal /app/wg-portal
# Set the Current Working Directory inside the container # Set the Current Working Directory inside the container
WORKDIR /app WORKDIR /app
# Expose default ports for metrics, web and wireguard
# by default, the web-portal is reachable on port 8888 EXPOSE 8787/tcp
EXPOSE 8888/tcp EXPOSE 8888/tcp
EXPOSE 51820/udp
# the database and config file can be mounted from the host # the database and config file can be mounted from the host
VOLUME [ "/app/data", "/app/config" ] VOLUME [ "/app/data", "/app/config" ]
# Command to run the executable # Command to run the executable
ENTRYPOINT [ "/app/wg-portal" ] ENTRYPOINT [ "/app/wg-portal" ]

View File

@@ -127,4 +127,15 @@ build-docker:
docker build --progress=plain \ docker build --progress=plain \
--build-arg BUILD_IDENTIFIER=${ENV_BUILD_IDENTIFIER} --build-arg BUILD_VERSION=${ENV_BUILD_VERSION} \ --build-arg BUILD_IDENTIFIER=${ENV_BUILD_IDENTIFIER} --build-arg BUILD_VERSION=${ENV_BUILD_VERSION} \
--build-arg TARGETPLATFORM=unknown . \ --build-arg TARGETPLATFORM=unknown . \
-t h44z/wg-portal:local -t h44z/wg-portal:local
#< helm-docs: Generate the helm chart documentation
.PHONY: helm-docs
helm-docs:
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
#< run-mkdocs: Run a local instance of MkDocs
.PHONY: run-mkdocs
run-mkdocs:
python -m venv venv; source venv/bin/activate; pip install mike cairosvg mkdocs-material mkdocs-minify-plugin mkdocs-swagger-ui-tag
venv/bin/mkdocs serve

226
README.md
View File

@@ -1,208 +1,68 @@
# WireGuard Portal (v2 - testing) # WireGuard Portal v2
[![Build Status](https://travis-ci.com/h44z/wg-portal.svg?token=q4pSqaqT58Jzpxdx62xk&branch=master)](https://travis-ci.com/h44z/wg-portal) [![Build Status](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml/badge.svg?event=push)](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
![GitHub last commit](https://img.shields.io/github/last-commit/h44z/wg-portal) ![GitHub last commit](https://img.shields.io/github/last-commit/h44z/wg-portal/master)
[![Go Report Card](https://goreportcard.com/badge/github.com/h44z/wg-portal)](https://goreportcard.com/report/github.com/h44z/wg-portal) [![Go Report Card](https://goreportcard.com/badge/github.com/h44z/wg-portal)](https://goreportcard.com/report/github.com/h44z/wg-portal)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/h44z/wg-portal) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/h44z/wg-portal)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/wgportal/wg-portal/) [![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/wgportal/wg-portal/)
> :warning: **IMPORTANT** Version 2 is currently under development and may contain bugs. It is currently not advised to use this version ## Introduction
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead. <!-- Text from this line # is included in docs/documentation/overview.md -->
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
connections. connections.
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP (Active Directory or OpenLDAP) as a user source for authentication and profile data. The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Postgres), OAuth or LDAP
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
## Features ## Features
* Self-hosted - the whole application is a single binary
* Responsive web UI written in Vue.JS
* Automatically select IP from the network pool assigned to client
* QR-Code for convenient mobile client configuration
* Sent email to client with QR-code and client config
* Enable / Disable clients seamlessly
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth or LDAP)
* IPv6 ready
* Docker ready
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Peer Expiry Feature
* Handle route and DNS settings like wg-quick does
* ~~REST API for management and client deployment~~ (coming soon)
![Screenshot](screenshot.png) * Self-hosted - the whole application is a single binary
* Responsive multi-language web UI written in Vue.js
* Automatically selects IP from the network pool assigned to the client
* QR-Code for convenient mobile client configuration
* Sends email to the client with QR-code and client config
* Enable / Disable clients seamlessly
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth, or LDAP), Passkey support
* IPv6 ready
* Docker ready
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Supports multiple WireGuard backends (wgctrl or MikroTik [BETA])
* Peer Expiry Feature
* Handles route and DNS settings like wg-quick does
* Exposes Prometheus metrics for monitoring and alerting
* REST API for management and client deployment
* Webhook for custom actions on peer, interface, or user updates
<!-- Text to this line # is included in docs/documentation/overview.md -->
![Screenshot](docs/assets/images/screenshot.png)
## Configuration ## Documentation
You can configure WireGuard Portal using a yaml configuration file.
The filepath of the yaml configuration file defaults to **config/config.yml** in the working directory of the executable.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
For example: `WG_PORTAL_CONFIG=/home/test/config.yml ./wg-portal-amd64`.
By default, WireGuard Portal uses a SQLite database. The database is stored in **data/sqlite.db** in the working directory of the executable. For the complete documentation visit [wgportal.org](https://wgportal.org).
### Configuration Options
The following configuration options are available:
| configuration key | parent key | default_value | description |
|---------------------------|------------|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
| create_default_peer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
| log_level | advanced | warn | The loglevel, can be one of: trace, debug, info, warn, error. |
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
| log_json | advanced | false | Logs in JSON format. |
| ldap_sync_interval | advanced | 15m | The time interval after which users will be synchronized from LDAP. |
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
| use_ip_v6 | advanced | true | Enable IPv6 support. |
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
| data_collection_interval | statistics | 10m | The interval between the data collection cycles. |
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
| host | mail | 127.0.0.1 | The mail-server address. |
| port | mail | 25 | The mail-server SMTP port. |
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
| username | mail | | The SMTP user name. |
| password | mail | | The SMTP password. |
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
| callback_url_prefix | auth | /api/v0 | OAuth callback URL prefix. The full callback URL will look like: https://wg.portal.local/callback_url_prefix/provider_name/callback |
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
| client_id | auth/oidc | | The OAuth client id. |
| client_secret | auth/oidc | | The OAuth client secret. |
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
| base_url | auth/oauth | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
| client_id | auth/oauth | | The OAuth client id. |
| client_secret | auth/oauth | | The OAuth client secret. |
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
| token_url | auth/oauth | | The URL for the token endpoint. |
| redirect_url | auth/oauth | | The redirect URL. |
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
| scopes | auth/oauth | | OAuth scopes. |
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
| tls_key_path | auth/ldap | | A path to the TLS key. |
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
| bind_pass | auth/ldap | | The bind password. |
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
| synchronize | auth/ldap | | Periodically synchronize users (name, department, phone, status, ...) to the WireGuard Portal database. |
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| debug | database | false | Debug database statements (log each statement). |
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
| request_logging | web | false | Log all HTTP requests. |
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
| listening_address | web | :8888 | The listening port of the web server. |
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
| session_secret | web | very_secret | The session secret for the web frontend. |
| csrf_secret | web | extremely_secret | The CSRF secret. |
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
## Upgrading from V1
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
The configuration (config.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
To upgrade from a previous SQLite database, start wg-portal like:
```shell
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
```
You can also specify the database type using the parameter **-migrateFromType**, supported types: mysql, mssql, postgres or sqlite.
For example:
```shell
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
```
The upgrade will transform the old, existing database and store the values in the new database specified in config.yml.
Ensure that the new database does not contain any data!
## V2 TODOs
* Public REST API
* Translations
* Documentation
* Audit UI
## Building
To build a standalone application, use the Makefile provided in the repository.
Go version 1.20 or higher has to be installed to build WireGuard Portal.
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
```shell
# build the frontend (optional)
make frontend
# build the binary
make build
```
## What is out of scope ## What is out of scope
* Automatic generation or application of any `iptables` or `nftables` rules.
* Support for operating systems other than linux.
* Automatic import of private keys of an existing WireGuard setup.
* Automatic generation or application of any `iptables` or `nftables` rules.
* Support for operating systems other than linux.
* Automatic import of private keys of an existing WireGuard setup.
## Application stack ## Application stack
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling * [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go * [Bootstrap](https://getbootstrap.com/), for the HTML templates
* [Bootstrap](https://getbootstrap.com/), for the HTML templates * [Vue.js](https://vuejs.org/), for the frontend
* [Vue.JS](https://vuejs.org/), for the frontend
## License ## License
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT * MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
> [!IMPORTANT]
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.

33
SECURITY.md Normal file
View File

@@ -0,0 +1,33 @@
# Security Policy
If you believe you've found a security issue in one of the supported versions of *WireGuard Portal*, please report it to us as described below.
## Supported Versions
| Version | Supported |
|---------|--------------------|
| v2.x | :white_check_mark: |
| v1.x | :white_check_mark: |
## Reporting a Vulnerability
Please do not report security vulnerabilities through public GitHub issues.
Instead, we encourage you to submit a report through GitHub [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
If you prefer to submit a report without logging in to GitHub, please email *info (at) wgportal.org*.
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
We prefer all communications to be in English.
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
- Type of issue (e.g. SQL injection, cross-site scripting, ...)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
Thank you for helping keep *WireGuard Portal* and its users safe!

View File

@@ -7,11 +7,15 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/sirupsen/logrus"
"github.com/swaggo/swag" "github.com/swaggo/swag"
"github.com/swaggo/swag/gen" "github.com/swaggo/swag/gen"
"gopkg.in/yaml.v3"
) )
var apiRootPath = "/internal/app/api"
var apiDocPath = "core/assets/doc"
var apiMkDocPath = "/docs/documentation/rest-api"
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go // this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
func main() { func main() {
wd, err := os.Getwd() // should be the project root wd, err := os.Getwd() // should be the project root
@@ -19,10 +23,9 @@ func main() {
panic(err) panic(err)
} }
apiBasePath := filepath.Join(wd, "/internal/app/api") apiBasePath := filepath.Join(wd, apiRootPath)
apis := []string{"v0"} apis := []string{"v0", "v1"}
hasError := false
for _, apiVersion := range apis { for _, apiVersion := range apis {
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers") apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
@@ -33,16 +36,20 @@ func main() {
err := generateApi(apiBasePath, apiPath, apiVersion) err := generateApi(apiBasePath, apiPath, apiVersion)
if err != nil { if err != nil {
hasError = true log.Fatalf("failed to generate API docs for %s: %v", apiVersion, err)
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err) }
// copy the latest version of the API docs for mkdocs
if apiVersion == apis[len(apis)-1] {
if err = copyDocForMkdocs(wd, apiBasePath, apiVersion); err != nil {
log.Printf("failed to copy API docs for mkdocs: %v", err)
} else {
log.Println("Copied API docs " + apiVersion + " for mkdocs")
}
} }
log.Println("Generated swagger docs for API", apiVersion) log.Println("Generated swagger docs for API", apiVersion)
} }
if hasError {
os.Exit(1)
}
} }
func generateApi(basePath, apiPath, version string) error { func generateApi(basePath, apiPath, version string) error {
@@ -51,10 +58,10 @@ func generateApi(basePath, apiPath, version string) error {
Excludes: "", Excludes: "",
MainAPIFile: "base.go", MainAPIFile: "base.go",
PropNamingStrategy: swag.PascalCase, PropNamingStrategy: swag.PascalCase,
OutputDir: filepath.Join(basePath, "core/assets/doc"), OutputDir: filepath.Join(basePath, apiDocPath),
OutputTypes: []string{"json", "yaml"}, OutputTypes: []string{"json", "yaml"},
ParseVendor: false, ParseVendor: false,
ParseDependency: true, ParseDependency: 3,
MarkdownFilesDir: "", MarkdownFilesDir: "",
ParseInternal: true, ParseInternal: true,
GeneratedTime: false, GeneratedTime: false,
@@ -68,3 +75,43 @@ func generateApi(basePath, apiPath, version string) error {
return nil return nil
} }
func copyDocForMkdocs(workingDir, basePath, version string) error {
srcPath := filepath.Join(basePath, apiDocPath, fmt.Sprintf("%s_swagger.yaml", version))
dstPath := filepath.Join(workingDir, apiMkDocPath, "swagger.yaml")
// copy the file
input, err := os.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("error while reading swagger doc: %w", err)
}
output, err := removeAuthorizeButton(input)
if err != nil {
return fmt.Errorf("error while removing authorize button: %w", err)
}
err = os.WriteFile(dstPath, output, 0644)
if err != nil {
return fmt.Errorf("error while writing swagger doc: %w", err)
}
return nil
}
func removeAuthorizeButton(input []byte) ([]byte, error) {
var swagger map[string]any
err := yaml.Unmarshal(input, &swagger)
if err != nil {
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
}
delete(swagger, "securityDefinitions")
output, err := yaml.Marshal(&swagger)
if err != nil {
return nil, fmt.Errorf("error while marshalling swagger file: %w", err)
}
return output, nil
}

View File

@@ -2,81 +2,104 @@ package main
import ( import (
"context" "context"
"log/slog"
"os"
"syscall"
"time"
"github.com/go-playground/validator/v10"
evbus "github.com/vardius/message-bus"
"gorm.io/gorm/schema"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/adapters"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/app/api/core" "github.com/h44z/wg-portal/internal/app/api/core"
backendV0 "github.com/h44z/wg-portal/internal/app/api/v0/backend"
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers" handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
"github.com/h44z/wg-portal/internal/app/audit" "github.com/h44z/wg-portal/internal/app/audit"
"github.com/h44z/wg-portal/internal/app/auth" "github.com/h44z/wg-portal/internal/app/auth"
"github.com/h44z/wg-portal/internal/app/configfile" "github.com/h44z/wg-portal/internal/app/configfile"
"github.com/h44z/wg-portal/internal/app/mail" "github.com/h44z/wg-portal/internal/app/mail"
"github.com/h44z/wg-portal/internal/app/route" "github.com/h44z/wg-portal/internal/app/route"
"github.com/h44z/wg-portal/internal/app/users" "github.com/h44z/wg-portal/internal/app/users"
"github.com/h44z/wg-portal/internal/app/webhooks"
"github.com/h44z/wg-portal/internal/app/wireguard" "github.com/h44z/wg-portal/internal/app/wireguard"
"os"
"strings"
"syscall"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/adapters"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
"github.com/sirupsen/logrus"
evbus "github.com/vardius/message-bus"
) )
// main entry point for WireGuard Portal // main entry point for WireGuard Portal
func main() { func main() {
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
logrus.Infof("Starting WireGuard Portal V2...") slog.Info("Starting WireGuard Portal V2...", "version", internal.Version)
logrus.Infof("WireGuard Portal version: %s", internal.Version)
cfg, err := config.GetConfig() cfg, err := config.GetConfig()
internal.AssertNoError(err) internal.AssertNoError(err)
setupLogging(cfg) internal.SetupLogging(cfg.Advanced.LogLevel, cfg.Advanced.LogPretty, cfg.Advanced.LogJson)
cfg.LogStartupValues() cfg.LogStartupValues()
dbEncryptedSerializer := app.NewGormEncryptedStringSerializer(cfg.Database.EncryptionPassphrase)
schema.RegisterSerializer("encstr", dbEncryptedSerializer)
rawDb, err := adapters.NewDatabase(cfg.Database) rawDb, err := adapters.NewDatabase(cfg.Database)
internal.AssertNoError(err) internal.AssertNoError(err)
database, err := adapters.NewSqlRepository(rawDb) database, err := adapters.NewSqlRepository(rawDb)
internal.AssertNoError(err) internal.AssertNoError(err)
wireGuard := adapters.NewWireGuardRepository() wireGuard, err := wireguard.NewControllerManager(cfg)
internal.AssertNoError(err)
wgQuick := adapters.NewWgQuickRepo() wgQuick := adapters.NewWgQuickRepo()
mailer := adapters.NewSmtpMailRepo(cfg.Mail) mailer := adapters.NewSmtpMailRepo(cfg.Mail)
metricsServer := adapters.NewMetricsServer(cfg)
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath) cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
internal.AssertNoError(err) internal.AssertNoError(err)
shouldExit, err := app.HandleProgramArgs(cfg, rawDb) shouldExit, err := app.HandleProgramArgs(rawDb)
switch { switch {
case shouldExit && err == nil: case shouldExit && err == nil:
return return
case shouldExit && err != nil: case shouldExit:
logrus.Errorf("Failed to process program args: %v", err) slog.Error("Failed to process program args", "error", err)
os.Exit(1) os.Exit(1)
case !shouldExit: default:
internal.AssertNoError(err) internal.AssertNoError(err)
} }
queueSize := 100 queueSize := 100
eventBus := evbus.New(queueSize) eventBus := evbus.New(queueSize)
auditManager := audit.NewManager(database)
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
internal.AssertNoError(err)
auditRecorder.StartBackgroundJobs(ctx)
userManager, err := users.NewUserManager(cfg, eventBus, database, database) userManager, err := users.NewUserManager(cfg, eventBus, database, database)
internal.AssertNoError(err) internal.AssertNoError(err)
userManager.StartBackgroundJobs(ctx)
authenticator, err := auth.NewAuthenticator(&cfg.Auth, eventBus, userManager) authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
internal.AssertNoError(err)
authenticator.StartBackgroundJobs(ctx)
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager)
internal.AssertNoError(err) internal.AssertNoError(err)
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database) wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
internal.AssertNoError(err) internal.AssertNoError(err)
wireGuardManager.StartBackgroundJobs(ctx)
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard) statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
internal.AssertNoError(err) internal.AssertNoError(err)
statisticsCollector.StartBackgroundJobs(ctx)
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem) cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
internal.AssertNoError(err) internal.AssertNoError(err)
@@ -84,62 +107,89 @@ func main() {
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database) mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
internal.AssertNoError(err) internal.AssertNoError(err)
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
internal.AssertNoError(err)
auditRecorder.StartBackgroundJobs(ctx)
routeManager, err := route.NewRouteManager(cfg, eventBus, database) routeManager, err := route.NewRouteManager(cfg, eventBus, database)
internal.AssertNoError(err) internal.AssertNoError(err)
routeManager.StartBackgroundJobs(ctx) routeManager.StartBackgroundJobs(ctx)
backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager, webhookManager, err := webhooks.NewManager(cfg, eventBus)
statisticsCollector, cfgFileManager, mailManager)
internal.AssertNoError(err) internal.AssertNoError(err)
err = backend.Startup(ctx) webhookManager.StartBackgroundJobs(ctx)
err = app.Initialize(cfg, wireGuardManager, userManager)
internal.AssertNoError(err) internal.AssertNoError(err)
apiFrontend := handlersV0.NewRestApi(cfg, backend) validatorManager := validator.New()
webSrv, err := core.NewServer(cfg, apiFrontend) // region API v0 (SPA frontend)
apiV0Session := handlersV0.NewSessionWrapper(cfg)
apiV0Auth := handlersV0.NewAuthenticationHandler(authenticator, apiV0Session)
apiV0BackendUsers := backendV0.NewUserService(cfg, userManager, wireGuardManager)
apiV0BackendInterfaces := backendV0.NewInterfaceService(cfg, wireGuardManager, cfgFileManager)
apiV0BackendPeers := backendV0.NewPeerService(cfg, wireGuardManager, cfgFileManager, mailManager)
apiV0EndpointAuth := handlersV0.NewAuthEndpoint(cfg, apiV0Auth, apiV0Session, validatorManager, authenticator,
webAuthn)
apiV0EndpointAudit := handlersV0.NewAuditEndpoint(cfg, apiV0Auth, auditManager)
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
apiFrontend := handlersV0.NewRestApi(apiV0Session,
apiV0EndpointAuth,
apiV0EndpointAudit,
apiV0EndpointUsers,
apiV0EndpointInterfaces,
apiV0EndpointPeers,
apiV0EndpointConfig,
apiV0EndpointTest,
)
// endregion API v0 (SPA frontend)
// region API v1 (User REST API)
apiV1Auth := handlersV1.NewAuthenticationHandler(userManager)
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1Auth, validatorManager, apiV1BackendUsers)
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1Auth, validatorManager, apiV1BackendPeers)
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1Auth, validatorManager, apiV1BackendInterfaces)
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1Auth, validatorManager,
apiV1BackendProvisioning)
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1Auth, validatorManager, apiV1BackendMetrics)
apiV1 := handlersV1.NewRestApi(
apiV1EndpointUsers,
apiV1EndpointPeers,
apiV1EndpointInterfaces,
apiV1EndpointProvisioning,
apiV1EndpointMetrics,
)
// endregion API v1 (User REST API)
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
internal.AssertNoError(err) internal.AssertNoError(err)
go metricsServer.Run(ctx)
go webSrv.Run(ctx, cfg.Web.ListeningAddress) go webSrv.Run(ctx, cfg.Web.ListeningAddress)
slog.Info("Application startup complete")
// wait until context gets cancelled // wait until context gets cancelled
<-ctx.Done() <-ctx.Done()
logrus.Infof("Stopping WireGuard Portal") slog.Info("Stopping WireGuard Portal")
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
logrus.Infof("Stopped WireGuard Portal") slog.Info("Stopped WireGuard Portal")
}
func setupLogging(cfg *config.Config) {
switch strings.ToLower(cfg.Advanced.LogLevel) {
case "trace":
logrus.SetLevel(logrus.TraceLevel)
case "debug":
logrus.SetLevel(logrus.DebugLevel)
case "info", "information":
logrus.SetLevel(logrus.InfoLevel)
case "warn", "warning":
logrus.SetLevel(logrus.WarnLevel)
case "error":
logrus.SetLevel(logrus.ErrorLevel)
default:
logrus.SetLevel(logrus.WarnLevel)
}
switch {
case cfg.Advanced.LogJson:
logrus.SetFormatter(&logrus.JSONFormatter{
PrettyPrint: cfg.Advanced.LogPretty,
})
case cfg.Advanced.LogPretty:
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
DisableColors: false,
})
}
} }

View File

@@ -1,27 +1,34 @@
# More information about the configuration can be found in the documentation: https://wgportal.org/master/documentation/overview/
advanced: advanced:
log_level: trace log_level: trace
core: core:
admin_user: test@test.de admin_user: test@test.de
admin_password: secret admin_password: secret
create_default_peer: true
create_default_peer_on_creation: false
web: web:
external_url: http://localhost:8888 external_url: http://localhost:8888
request_logging: true request_logging: true
webhook:
url: ""
authentication: ""
timeout: 10s
auth: auth:
callback_url_prefix: http://localhost:8888/api/v0
ldap: ldap:
- id: ldap1 - id: ldap1
provider_name: company ldap provider_name: company ldap
display_name: Login with</br>LDAP
url: ldap://ldap.yourcompany.local:389 url: ldap://ldap.yourcompany.local:389
bind_user: ldap_wireguard@yourcompany.local bind_user: ldap_wireguard@yourcompany.local
bind_pass: super_Secret_PASSWORD bind_pass: super_Secret_PASSWORD
base_dn: DC=YOURCOMPANY,DC=LOCAL base_dn: DC=YOURCOMPANY,DC=LOCAL
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
synchronize: false sync_interval: 0 # sync disabled
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
registration_enabled: true registration_enabled: true
oidc: oidc:
@@ -44,4 +51,46 @@ auth:
extra_scopes: extra_scopes:
- https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile - https://www.googleapis.com/auth/userinfo.profile
registration_enabled: true registration_enabled: true
oauth:
- id: google_plain_oauth
provider_name: google3
display_name: Login with</br>Google3
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
field_map:
email: email
firstname: name
user_identifier: sub
is_admin: this-attribute-must-be-true
registration_enabled: true
- id: google_plain_oauth_with_groups
provider_name: google4
display_name: Login with</br>Google4
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
- i-want-some-groups
field_map:
email: email
firstname: name
user_identifier: sub
user_groups: groups
admin_mapping:
admin_value_regex: ^true$
admin_group_regex: ^admin-group-name$
registration_enabled: true
log_user_info: true

5
ct.yaml Normal file
View File

@@ -0,0 +1,5 @@
# See https://github.com/helm/chart-testing#configuration
remote: origin
chart-dirs: deploy
target-branch: master
validate-maintainers: false

23
deploy/helm/.helmignore Normal file
View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

25
deploy/helm/Chart.yaml Normal file
View File

@@ -0,0 +1,25 @@
apiVersion: v2
name: wg-portal
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
# Version is set to ensure compatibility with the chart's Ingress resource.
kubeVersion: ">=1.19.0"
type: application
home: https://wgportal.org
icon: https://wgportal.org/latest/assets/images/logo.svg
sources:
- https://github.com/h44z/wg-portal
annotations:
artifacthub.io/category: networking
artifacthub.io/changes: ""
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.7.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v2"

124
deploy/helm/README.md Normal file
View File

@@ -0,0 +1,124 @@
# wg-portal
![Version: 0.7.1](https://img.shields.io/badge/Version-0.7.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2](https://img.shields.io/badge/AppVersion-v2-informational?style=flat-square)
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
**Homepage:** <https://wgportal.org>
## Source Code
* <https://github.com/h44z/wg-portal>
## Requirements
Kubernetes: `>=1.19.0`
## Installing the Chart
To install the chart with the release name `wg-portal`:
```console
helm install wg-portal oci://ghcr.io/h44z/charts/wg-portal
```
This command deploy wg-portal on the Kubernetes cluster in the default configuration.
The [Values](#values) section lists the parameters that can be configured during installation.
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
| fullnameOverride | string | `""` | Fully override resource names |
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
| config.advanced | tpl/object | `{}` | [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options. |
| config.auth | tpl/object | `{}` | [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options. |
| config.core | tpl/object | `{}` | [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br> If external admins in `auth` are defined and there are no `admin_user` and `admin_password` defined here, the default admin account will be disabled. |
| config.database | tpl/object | `{}` | [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options |
| config.mail | tpl/object | `{}` | [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options |
| config.statistics | tpl/object | `{}` | [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options |
| config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
| image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion |
| imagePullSecrets | list | `[]` | Image pull secrets |
| podAnnotations | tpl/object | `{}` | Extra annotations to add to the pod |
| podLabels | object | `{}` | Extra labels to add to the pod |
| podSecurityContext | object | `{}` | Pod Security Context |
| securityContext.capabilities.add | list | `["NET_ADMIN"]` | Add capabilities to the container |
| initContainers | tpl/list | `[]` | Pod init containers |
| sidecarContainers | tpl/list | `[]` | Pod sidecar containers |
| dnsPolicy | string | `"ClusterFirst"` | Set DNS policy for the pod. Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`. |
| restartPolicy | string | `"Always"` | Restart policy for all containers within the pod. Valid values are `Always`, `OnFailure` or `Never`. |
| hostNetwork | string | `false`. | Use the host's network namespace. |
| resources | object | `{}` | Resources requests and limits |
| command | list | `[]` | Overwrite pod command |
| args | list | `[]` | Additional pod arguments |
| env | tpl/list | `[]` | Additional environment variables |
| envFrom | tpl/list | `[]` | Additional environment variables from a secret or configMap |
| livenessProbe | object | `{}` | Liveness probe configuration |
| readinessProbe | object | `{}` | Readiness probe configuration |
| startupProbe | object | `{}` | Startup probe configuration |
| volumes | tpl/list | `[]` | Additional volumes |
| volumeMounts | tpl/list | `[]` | Additional volumeMounts |
| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | Node Selector configuration |
| tolerations | list | `[]` | Tolerations configuration |
| affinity | object | `{}` | Affinity configuration |
| service.mixed.enabled | bool | `false` | Whether to create a single service for the web and wireguard interfaces |
| service.mixed.type | string | `"LoadBalancer"` | Service type |
| service.web.annotations | object | `{}` | Annotations for the web service |
| service.web.type | string | `"ClusterIP"` | Web service type |
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
| service.metrics.port | int | `8787` | |
| ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created |
| ingress.className | string | `""` | Ingress class name |
| ingress.annotations | object | `{}` | Ingress annotations |
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |
| certificate.issuer.name | string | `""` | Certificate issuer name |
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |
| certificate.duration | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.renewBefore | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.commonName | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.emailAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.ipAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.keystores | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.privateKey | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.secretTemplate | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.subject | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.uris | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| certificate.usages | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
| persistence.enabled | bool | `false` | Specifies whether an persistent volume should be created |
| persistence.annotations | object | `{}` | Persistent Volume Claim annotations |
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
| persistence.size | string | `"1Gi"` | Persistent Volume size |
| persistence.volumeName | string | `""` | Persistent Volume Name (optional) |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
| serviceAccount.annotations | object | `{}` | Service account annotations |
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
| monitoring.enabled | bool | `false` | Enable Prometheus monitoring. |
| monitoring.apiVersion | string | `"monitoring.coreos.com/v1"` | API version of the Prometheus resource. Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. |
| monitoring.kind | string | `"PodMonitor"` | Kind of the Prometheus resource. Could be `PodMonitor` or `ServiceMonitor`. |
| monitoring.labels | object | `{}` | Resource labels. |
| monitoring.annotations | object | `{}` | Resource annotations. |
| monitoring.interval | string | `1m` | Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used. |
| monitoring.metricRelabelings | list | `[]` | Relabelings to samples before ingestion. |
| monitoring.relabelings | list | `[]` | Relabelings to samples before scraping. |
| monitoring.scrapeTimeout | string | `""` | Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. |
| monitoring.jobLabel | string | `""` | The label to use to retrieve the job name from. |
| monitoring.podTargetLabels | object | `{}` | Transfers labels on the Kubernetes Pod onto the target. |
| monitoring.dashboard.enabled | bool | `false` | Enable Grafana dashboard. |
| monitoring.dashboard.annotations | object | `{}` | Annotations for the dashboard ConfigMap. |
| monitoring.dashboard.labels | object | `{}` | Additional labels for the dashboard ConfigMap. |
| monitoring.dashboard.namespace | string | `""` | Dashboard ConfigMap namespace Overrides the namespace for the dashboard ConfigMap. |

View File

@@ -0,0 +1,27 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
## Installing the Chart
To install the chart with the release name `wg-portal`:
```console
helm install wg-portal oci://ghcr.io/h44z/charts/wg-portal
```
This command deploy wg-portal on the Kubernetes cluster in the default configuration.
The [Values](#values) section lists the parameters that can be configured during installation.
{{ template "chart.valuesSection" . }}

View File

@@ -0,0 +1,917 @@
{
"annotations": {},
"description": "WireGuard Portal Dashboard",
"panels": [
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"disableTextWrap": false,
"editorMode": "code",
"exemplar": false,
"expr": "sum by (instance, interface) (wireguard_interface_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"})",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"interval": "",
"legendFormat": "Received {{interface}}",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface) (wireguard_interface_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"})",
"hide": false,
"instant": false,
"legendFormat": "Sent {{interface}}",
"range": true,
"refId": "B"
}
],
"title": "Interface Bytes Total",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"id": 13,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface) (rate(wireguard_interface_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "",
"legendFormat": "Received {{interface}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface) (rate(wireguard_interface_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "",
"legendFormat": "Sent {{interface}}",
"range": true,
"refId": "B"
}
],
"title": "Interface Bandwidth",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (name, instance, interface) (rate(wireguard_peer_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "$interval",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Peer Receive Bandwidth",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": 3600000,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 9
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"expr": "sum by (instance, interface, name) (rate(wireguard_peer_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
"hide": false,
"instant": false,
"interval": "$interval",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Peer Transmit Bandwidth",
"type": "timeseries"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 60,
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 1
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "bool_yes_no"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 24,
"x": 0,
"y": 18
},
"id": 12,
"options": {
"colWidth": 0.85,
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false
},
"rowHeight": 0.85,
"showValue": "never",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(name) (wireguard_peer_up{instance=\"$instance\", interface=~\"$interface\"})",
"instant": false,
"interval": "$interval",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Peer Connection History",
"type": "status-history"
},
{
"datasource": {
"default": false,
"type": "prometheus",
"uid": "${datasource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic-by-name"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto",
"wrapText": false
},
"filterable": false,
"inspect": false
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "dark-red",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": "/(Time|instance|interface|name)\\s\\d*/"
},
"properties": [
{
"id": "custom.hidden",
"value": true
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/Received|Transmitted/"
},
"properties": [
{
"id": "unit",
"value": "bytes"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Last Handshake"
},
"properties": [
{
"id": "unit",
"value": "s"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Connected"
},
"properties": [
{
"id": "mappings",
"value": [
{
"options": {
"0": {
"color": "red",
"index": 0,
"text": "No"
},
"1": {
"color": "green",
"index": 1,
"text": "Yes"
}
},
"type": "value"
}
]
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
}
]
}
]
},
"gridPos": {
"h": 14,
"w": 24,
"x": 0,
"y": 29
},
"id": 11,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"enablePagination": false,
"fields": [],
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Sent"
}
]
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"disableTextWrap": false,
"editorMode": "code",
"exemplar": false,
"expr": "sum by(id, instance, interface, name, addresses) (increase(wireguard_peer_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__range]))",
"format": "table",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"interval": "",
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"disableTextWrap": false,
"editorMode": "code",
"exemplar": false,
"expr": "sum by(id, instance, interface, name) (increase(wireguard_peer_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__range]))",
"format": "table",
"fullMetaSearch": false,
"includeNullMetadata": true,
"instant": false,
"interval": "",
"legendFormat": "__auto",
"range": true,
"refId": "B",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "time()-sum(wireguard_peer_last_handshake_seconds{instance=\"$instance\", interface=~\"$interface\"}) by(id, instance, interface, name) ",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "__auto",
"range": false,
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum(wireguard_peer_up{instance=\"$instance\", interface=~\"$interface\"}) by(id, instance, interface, name) ",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "__auto",
"range": false,
"refId": "D"
}
],
"title": "Peer Info",
"transformations": [
{
"id": "joinByField",
"options": {
"byField": "id",
"mode": "outer"
}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time 1": false,
"Time 2": false,
"Time 3": false,
"Time 4": false
},
"includeByName": {},
"indexByName": {
"Time 1": 8,
"Time 2": 9,
"Time 3": 10,
"Time 4": 11,
"Value #A": 4,
"Value #B": 5,
"Value #C": 6,
"Value #D": 7,
"addresses": 2,
"id": 3,
"instance 1": 12,
"instance 2": 13,
"instance 3": 16,
"instance 4": 19,
"interface 1": 0,
"interface 2": 14,
"interface 3": 17,
"interface 4": 20,
"name 1": 1,
"name 2": 15,
"name 3": 18,
"name 4": 21
},
"renameByName": {
"Value #A": "Received",
"Value #B": "Transmitted",
"Value #C": "Last Handshake",
"Value #D": "Connected",
"addresses": "IP Addresses",
"id": "Public Key",
"interface": "Interface",
"interface 1": "Interface",
"name": "Name",
"name 1": "Name"
}
}
}
],
"type": "table"
}
],
"refresh": "1m",
"tags": [
"wireguard",
"vpn"
],
"templating": {
"list": [
{
"current": {},
"hide": 0,
"includeAll": false,
"label": "Prometheus",
"multi": false,
"name": "datasource",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "label_values(wireguard_interface_sent_bytes_total,instance)",
"hide": 0,
"includeAll": false,
"label": "Instance",
"multi": false,
"name": "instance",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(wireguard_interface_sent_bytes_total,instance)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"definition": "label_values(wireguard_interface_sent_bytes_total{instance=\"$instance\"},interface)",
"hide": 0,
"includeAll": true,
"label": "Interface",
"multi": true,
"name": "interface",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(wireguard_interface_sent_bytes_total{instance=\"$instance\"},interface)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"current": {
"text": "2m",
"value": "2m"
},
"description": "",
"label": "Step Interval",
"name": "interval",
"options": [
{
"selected": false,
"text": "30s",
"value": "30s"
},
{
"selected": false,
"text": "1m",
"value": "1m"
},
{
"selected": true,
"text": "2m",
"value": "2m"
},
{
"selected": false,
"text": "5m",
"value": "5m"
},
{
"selected": false,
"text": "10m",
"value": "10m"
}
],
"query": "30s,1m,2m,5m,10m",
"type": "custom"
}
]
},
"time": {
"from": "now-12h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "WireGuard Portal",
"uid": "wireguard-portal",
"weekStart": ""
}

View File

@@ -0,0 +1,24 @@
{{- $serviceName := printf "%s-web" (include "wg-portal.fullname" .) -}}
{{- $servicePort := .Values.service.web.port }}
{{- if not .Values.ingress.enabled }}
Get the application URL by running these commands:
{{- if eq "ClusterIP" .Values.service.web.type }}
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ $serviceName }} {{ $servicePort }}:{{ $servicePort }}
Visit http://127.0.0.1:{{ $servicePort }} to use your application
{{- else if eq "LoadBalancer" .Values.service.web.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ $serviceName }}'
export SERVICE_IP=$(kubectl get --namespace {{ .Release.Namespace }} svc {{ $serviceName }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ $servicePort }}
{{- else if eq "NodePort" .Values.service.web.type }}
export NODE_IP=$(kubectl get --namespace {{ .Release.Namespace }} nodes -o jsonpath="{.items[0].status.addresses[0].address}")
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} svc {{ $serviceName }} -o jsonpath="{.spec.ports[0].nodePort}" )
echo http://$NODE_IP:$NODE_PORT
{{- end }}
{{- else }}
Visit http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}{{ .Values.ingress.path }} to use your application
{{- end }}

View File

@@ -0,0 +1,132 @@
{{/*
Expand the name of the chart
*/}}
{{- define "wg-portal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "wg-portal.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label
*/}}
{{- define "wg-portal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "wg-portal.labels" -}}
helm.sh/chart: {{ include "wg-portal.chart" . }}
{{ include "wg-portal.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "wg-portal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "wg-portal.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "wg-portal.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "wg-portal.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Disables default admin credentials
If external auth is enabled and has admin group mappings,
the admin_user will be set to blank (disabled).
*/}}
{{- define "wg-portal.admin" -}}
{{- $externalAdmin := false -}}
{{- with .Values.config.auth -}}
{{- range (default list .ldap) -}}
{{- if hasKey . "admin_group" -}}
{{- $externalAdmin = true -}}
{{- end -}}
{{- end }}
{{- range (concat (default list .oidc) (default list .oauth)) -}}
{{- if hasKey .field_map "is_admin" -}}
{{- $externalAdmin = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $externalAdmin -}}
admin_user: ""
{{- end -}}
{{- end -}}
{{/*
Define PersistentVolumeClaim spec
*/}}
{{- define "wg-portal.pvc" -}}
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . }}
{{- end }}
{{- with .Values.persistence.volumeName }}
volumeName: {{ . }}
{{- end }}
{{- end -}}
{{/*
Define hostname
*/}}
{{- define "wg-portal.hostname" -}}
{{- if .Values.config.web.external_url -}}
{{- (urlParse (tpl .Values.config.web.external_url .)).hostname -}}
{{- end -}}
{{- end -}}
{{/*
wg-portal.util.merge will merge two YAML templates or dict with template and output the result.
This takes an array of three values:
- the top context
- the template name or dict of the overrides (destination)
- the template name of the base (source)
{{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") }}
{{- include "wg-portal.util.merge" (list $ "wg-portal.destTemplate" "wg-portal.sourceTemplate") }}
*/}}
{{- define "wg-portal.util.merge" -}}
{{- $top := first . -}}
{{- $overrides := index . 1 -}}
{{- $base := fromYaml (include (index . 2) $top) | default (dict) -}}
{{- if kindIs "string" $overrides -}}
{{- $overrides = fromYaml (include $overrides $top) | default (dict) -}}
{{- end -}}
{{- toYaml (merge $overrides $base) -}}
{{- end -}}

View File

@@ -0,0 +1,119 @@
{{- define "wg-portal.podTemplate" -}}
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
kubectl.kubernetes.io/default-container: {{ .Chart.Name }}
{{- with .Values.podAnnotations }}
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") | nindent 4 }}
spec:
{{- with .Values.affinity }}
affinity: {{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
containers:
{{- with .Values.sidecarContainers }}
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag}}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.command }}
command: {{ . }}
{{- end }}
{{- with .Values.args }}
args: {{ . }}
{{- end }}
{{- with .Values.env }}
env: {{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
{{- with .Values.envFrom }}
envFrom: {{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
ports:
- name: metrics
containerPort: {{ .Values.service.metrics.port}}
protocol: TCP
- name: web
containerPort: {{ .Values.service.web.port }}
protocol: TCP
{{- range $index, $port := .Values.service.wireguard.ports }}
- name: wg{{ $index }}
containerPort: {{ $port }}
protocol: UDP
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.startupProbe }}
startupProbe: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.securityContext }}
securityContext: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.resources}}
resources: {{- toYaml . | nindent 8 }}
{{- end }}
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: data
mountPath: /app/data
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
- name: certs
mountPath: /app/certs
{{- end }}
{{- with .Values.volumeMounts }}
{{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
{{- with .Values.dnsPolicy }}
dnsPolicy: {{ . }}
{{- end }}
{{- with .Values.hostNetwork }}
hostNetwork: {{ . }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.initContainers }}
initContainers: {{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.restartPolicy }}
restartPolicy: {{ . }}
{{- end }}
serviceAccountName: {{ include "wg-portal.serviceAccountName" . }}
{{- with .Values.podSecurityContext }}
securityContext: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 4 }}
{{- end }}
volumes:
- name: config
secret:
secretName: {{ include "wg-portal.fullname" . }}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
- name: certs
secret:
secretName: {{ include "wg-portal.fullname" . }}-tls
{{- end }}
{{- if not .Values.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if eq .Values.workloadType "Deployment" }}
- name: data
persistentVolumeClaim:
claimName: {{ include "wg-portal.fullname" . }}
{{- end }}
{{- with .Values.volumes }}
{{- tpl (toYaml .) $ | nindent 4 }}
{{- end }}
{{- end -}}

View File

@@ -0,0 +1,66 @@
{{/*
Define the service template
{{- include "wg-portal.service" (dict "context" $ "scope" .Values.service.<name> "ports" list "name" "<name>") -}}
*/}}
{{- define "wg-portal.service.tpl" -}}
apiVersion: v1
kind: Service
metadata:
{{- with .scope.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.labels" .context | nindent 4 }}
name: {{ include "wg-portal.fullname" .context }}{{ ternary "" (printf "-%s" .name) (empty .name) }}
spec:
{{- with .scope.clusterIP }}
clusterIP: {{ . }}
{{- end }}
{{- with .scope.externalIPs }}
externalIPs: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .scope.externalName }}
externalName: {{ . }}
{{- end }}
{{- with .scope.externalTrafficPolicy }}
externalTrafficPolicy: {{ . }}
{{- end }}
{{- with .scope.healthCheckNodePort }}
healthCheckNodePort: {{ . }}
{{- end }}
{{- with .scope.loadBalancerIP }}
loadBalancerIP: {{ . }}
{{- end }}
{{- with .scope.loadBalancerSourceRanges }}
loadBalancerSourceRanges: {{ toYaml . | nindent 4 }}
{{- end }}
ports: {{- toYaml .ports | nindent 4 }}
{{- with .scope.publishNotReadyAddresses }}
publishNotReadyAddresses: {{ . }}
{{- end }}
{{- with .scope.sessionAffinity }}
sessionAffinity: {{ . }}
{{- end }}
{{- with .scope.sessionAffinityConfig }}
sessionAffinityConfig: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .scope.topologyKeys }}
topologyKeys: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .scope.type }}
type: {{ . }}
{{- end }}
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
{{- end -}}
{{/*
Define the service port template for the web port
*/}}
{{- define "wg-portal.service.webPort" -}}
name: web
port: {{ .Values.service.web.port }}
protocol: TCP
targetPort: web
{{- if semverCompare ">=1.20-0" .Capabilities.KubeVersion.Version }}
appProtocol: {{ ternary "https" .Values.service.web.appProtocol .Values.certificate.enabled }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,54 @@
{{/* https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources */}}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) -}}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
secretName: {{ include "wg-portal.fullname" . }}-tls
{{- with .Values.certificate.secretTemplate }}
secretTemplate: {{ toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.privateKey }}
privateKey: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.keystores }}
keystores: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.duration }}
duration: {{ . }}
{{- end }}
{{- with .Values.certificate.renewBefore }}
renewBefore: {{ . }}
{{- end }}
{{- with .Values.certificate.usages }}
usages: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.subject }}
subject: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.commonName }}
commonName: {{ . }}
{{- end }}
dnsNames:
- {{ include "wg-portal.hostname" . }}
{{- with .Values.certificate.uris }}
uris: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.emailAddresses }}
emailAddresses: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.ipAddresses }}
ipAddresses: {{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.certificate.otherNames }}
otherNames: {{- toYaml . | nindent 4 }}
{{- end }}
issuerRef:
{{- with .Values.certificate.issuer.group }}
group: {{ . }}
{{- end }}
kind: {{ .Values.certificate.issuer.kind }}
name: {{ .Values.certificate.issuer.name }}
{{- end -}}

View File

@@ -0,0 +1,14 @@
{{- with .Values.monitoring.dashboard -}}
{{- if .enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
{{- with .annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }}
name: {{ printf "grafana-dashboards-%s" (include "wg-portal.fullname" $) }}
namespace: {{ default $.Release.Namespace .namespace }}
data: {{ ($.Files.Glob "files/dashboard.json").AsConfig | nindent 2 }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,17 @@
{{- if eq .Values.workloadType "Deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
{{- with .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ . }}
{{- end }}
{{- with .Values.strategy }}
strategy: {{- toYaml . | nindent 4 }}
{{- end }}
selector:
matchLabels: {{- include "wg-portal.selectorLabels" . | nindent 6 }}
template: {{- include "wg-portal.podTemplate" . | nindent 4 }}
{{- end -}}

View File

@@ -0,0 +1,4 @@
{{- range .Values.extraDeploy -}}
{{- tpl (toYaml .) $ }}
---
{{- end -}}

View File

@@ -0,0 +1,30 @@
{{- $hostname := include "wg-portal.hostname" . -}}
{{- if and .Values.ingress.enabled $hostname -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
{{- with .Values.ingress.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ $hostname }}
http:
paths:
- path: {{ default "/" (urlParse (tpl .Values.config.web.external_url .)).path }}
pathType: {{ default "ImplementationSpecific" .pathType }}
backend:
service:
name: {{ include "wg-portal.fullname" . }}
port:
name: web
{{- if .Values.ingress.tls }}
tls:
- hosts:
- {{ $hostname | quote }}
secretName: {{ include "wg-portal.fullname" . }}-tls
{{- end }}
{{- end }}

View File

@@ -0,0 +1,44 @@
{{- with .Values.monitoring -}}
{{- if and .enabled ($.Capabilities.APIVersions.Has .apiVersion) -}}
{{- $endpointsKey := (eq .kind "PodMonitor") | ternary "podMetricsEndpoints" "endpoints" -}}
apiVersion: {{ .apiVersion }}
kind: {{ .kind }}
metadata:
{{- with .annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }}
name: {{ include "wg-portal.fullname" $ }}
spec:
namespaceSelector:
matchNames:
- {{ $.Release.Namespace }}
selector:
matchLabels:
{{- include "wg-portal.selectorLabels" $ | nindent 6 }}
{{ $endpointsKey }}:
- port: metrics
path: /metrics
interval: {{ coalesce .interval ($.Values.config.statistics).data_collection_interval "1m" }}
{{- with .metricRelabelings }}
metricRelabelings: {{- toYaml . | nindent 8 }}
{{- end }}
relabelings:
- action: replace
sourceLabels:
- __meta_kubernetes_pod_label_app_kubernetes_io_name
targetLabel: instance
{{- with .relabelings }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .scrapeTimeout }}
scrapeTimeout: {{ . }}
{{- end }}
{{- with .jobLabel }}
jobLabel: {{ . }}
{{- end }}
{{- with .podTargetLabels }}
podTargetLabels: {{- toYaml . | nindent 2 }}
{{- end }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,11 @@
{{- if and .Values.persistence.enabled (eq .Values.workloadType "Deployment") -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
{{- with .Values.persistence.annotations }}
annotations: {{- toYaml . | nindent 4}}
{{- end }}
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec: {{- include "wg-portal.pvc" . | nindent 2 }}
{{- end -}}

View File

@@ -0,0 +1,42 @@
{{- $advanced := dict "start_listen_port" (.Values.service.wireguard.ports | sortAlpha | first | int) -}}
{{- $statistics := dict "listening_address" (printf ":%v" .Values.service.metrics.port) -}}
{{- $web:= dict "listening_address" (printf ":%v" .Values.service.web.port) -}}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
{{- $_ := set $web "cert_file" "/app/certs/tls.crt" }}
{{- $_ := set $web "key_file" "/app/certs/tls.key" }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
stringData:
config.yml: |
{{- with mustMerge $advanced .Values.config.advanced }}
advanced: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.auth }}
auth: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with mustMerge .Values.config.core (include "wg-portal.admin" . | fromYaml) }}
core: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.database }}
database: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.mail }}
mail: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with mustMerge $statistics .Values.config.statistics }}
statistics: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}
{{- with mustMerge $web .Values.config.web }}
web: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }}

View File

@@ -0,0 +1,20 @@
{{- $portsWeb := list (include "wg-portal.service.webPort" . | fromYaml) -}}
{{- $ports := list -}}
{{- range $idx, $port := .Values.service.wireguard.ports -}}
{{- $name := printf "wg%d" $idx -}}
{{- $ports = append $ports (dict "name" $name "port" $port "protocol" "UDP" "targetPort" $name) -}}
{{- end -}}
{{- if .Values.service.mixed.enabled -}}
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.mixed "ports" (concat $portsWeb $ports)) }}
{{- else }}
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.web "ports" $portsWeb) }}
---
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.wireguard "ports" $ports "name" "wireguard") }}
{{- end -}}
{{- if and .Values.monitoring.enabled (eq .Values.monitoring.kind "ServiceMonitor") }}
---
{{- $portsMetrics := list (dict "name" "metrics" "port" .Values.service.metrics.port "protocol" "TCP" "targetPort" "metrics") -}}
{{- include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.metrics "ports" $portsWeb "name" "metrics") }}
{{- end -}}

View File

@@ -0,0 +1,10 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "wg-portal.serviceAccountName" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,24 @@
{{- if eq .Values.workloadType "StatefulSet" -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "wg-portal.fullname" . }}
labels: {{- include "wg-portal.labels" . | nindent 4 }}
spec:
{{- with .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ . }}
{{- end }}
{{- with .Values.strategy }}
updateStrategy: {{- toYaml . | nindent 4 }}
{{- end }}
serviceName: {{ template "wg-portal.fullname" . }}-web
selector:
matchLabels: {{- include "wg-portal.selectorLabels" . | nindent 6 }}
template: {{- include "wg-portal.podTemplate" . | nindent 4 }}
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec: {{- include "wg-portal.pvc" . | nindent 8 }}
{{- end -}}
{{- end -}}

248
deploy/helm/values.yaml Normal file
View File

@@ -0,0 +1,248 @@
# Default values for wg-portal.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# -- Partially override resource names (adds suffix)
nameOverride: ""
# -- Fully override resource names
fullnameOverride: ""
# -- Array of extra objects to deploy with the release
extraDeploy: []
config:
# -- (tpl/object) [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options.
advanced: {}
# -- (tpl/object) [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options.
auth: {}
# -- (tpl/object) [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br>
# If external admins in `auth` are defined and
# there are no `admin_user` and `admin_password` defined here,
# the default admin account will be disabled.
core: {}
# -- (tpl/object) [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options
database: {}
# -- (tpl/object) [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options
mail: {}
# -- (tpl/object) [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options
statistics: {}
# -- (tpl/object) [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br>
# `listening_address` will be set automatically from `service.web.port`.
# `external_url` is required to enable ingress and certificate resources.
web: {}
# -- The number of old ReplicaSets to retain to allow rollback.
# @default -- `10`
revisionHistoryLimit: ""
# -- Workload type - `Deployment` or `StatefulSet`
workloadType: Deployment
# -- Update strategy for the workload
# Valid values are:
# `RollingUpdate` or `Recreate` for Deployment,
# `RollingUpdate` or `OnDelete` for StatefulSet
strategy:
type: RollingUpdate
image:
# -- Image repository
repository: ghcr.io/h44z/wg-portal
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion
tag: ""
# -- Image pull secrets
imagePullSecrets: []
# -- (tpl/object) Extra annotations to add to the pod
podAnnotations: {}
# -- Extra labels to add to the pod
podLabels: {}
# -- Pod Security Context
podSecurityContext: {}
# Container Security Context
securityContext:
capabilities:
# -- Add capabilities to the container
add:
- NET_ADMIN
# -- (tpl/list) Pod init containers
initContainers: []
# -- (tpl/list) Pod sidecar containers
sidecarContainers: []
# -- Set DNS policy for the pod.
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
# @default -- `"ClusterFirst"`
dnsPolicy: ""
# -- Restart policy for all containers within the pod.
# Valid values are `Always`, `OnFailure` or `Never`.
# @default -- `"Always"`
restartPolicy: ""
# -- Use the host's network namespace.
# @default -- `false`.
hostNetwork: ""
# -- Resources requests and limits
resources: {}
# -- Overwrite pod command
command: []
# -- Additional pod arguments
args: []
# -- (tpl/list) Additional environment variables
env: []
# -- (tpl/list) Additional environment variables from a secret or configMap
envFrom: []
# -- Liveness probe configuration
livenessProbe: {}
# -- Readiness probe configuration
readinessProbe: {}
# -- Startup probe configuration
startupProbe: {}
# -- (tpl/list) Additional volumes
volumes: []
# -- (tpl/list) Additional volumeMounts
volumeMounts: []
# -- Node Selector configuration
nodeSelector:
kubernetes.io/os: linux
# -- Tolerations configuration
tolerations: []
# -- Affinity configuration
affinity: {}
service:
mixed:
# -- Whether to create a single service for the web and wireguard interfaces
enabled: false
# -- Service type
type: LoadBalancer
web:
# -- Annotations for the web service
annotations: {}
# -- Web service type
type: ClusterIP
# -- Web service port
# Used for the web interface listener
port: 8888
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
appProtocol: http
wireguard:
# -- Annotations for the WireGuard service
annotations: {}
# -- Wireguard service type
type: LoadBalancer
# -- Wireguard service ports.
# Exposes the WireGuard ports for created interfaces.
# Lowerest port is selected as start port for the first interface.
# Increment next port by 1 for each additional interface.
ports:
- 51820
metrics:
port: 8787
ingress:
# -- Specifies whether an ingress resource should be created
enabled: false
# -- Ingress class name
className: ""
# -- Ingress annotations
annotations: {}
# -- Ingress TLS configuration.
# Enable certificate resource or add ingress annotation to create required secret
tls: false
certificate:
# -- Specifies whether a certificate resource should be created.
# If enabled, certificate will be used for the web.
enabled: false
issuer:
# -- Certificate issuer name
name: ""
# -- Certificate issuer kind (ClusterIssuer or Issuer)
kind: ""
# -- Certificate issuer group
group: cert-manager.io
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
duration: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
renewBefore: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
commonName: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
emailAddresses: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
ipAddresses: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
keystores: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
privateKey: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
secretTemplate: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
subject: {}
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
uris: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
usages: []
persistence:
# -- Specifies whether an persistent volume should be created
enabled: false
# -- Persistent Volume Claim annotations
annotations: {}
# -- Persistent Volume storage class.
# If undefined (the default) cluster's default provisioner will be used.
storageClass: ""
# -- Persistent Volume Access Mode
accessMode: ReadWriteOnce
# -- Persistent Volume size
size: 1Gi
# -- Persistent Volume Name (optional)
volumeName: ""
serviceAccount:
# -- Specifies whether a service account should be created
create: true
# -- Service account annotations
annotations: {}
# -- Automatically mount a ServiceAccount's API credentials
automount: false
# -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
monitoring:
# -- Enable Prometheus monitoring.
enabled: false
# -- API version of the Prometheus resource.
# Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus.
apiVersion: monitoring.coreos.com/v1
# -- Kind of the Prometheus resource.
# Could be `PodMonitor` or `ServiceMonitor`.
kind: PodMonitor
# -- Resource labels.
labels: {}
# -- Resource annotations.
annotations: {}
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
# @default -- `1m`
interval: ""
# -- Relabelings to samples before ingestion.
metricRelabelings: []
# -- Relabelings to samples before scraping.
relabelings: []
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
scrapeTimeout: ""
# -- The label to use to retrieve the job name from.
jobLabel: ""
# -- Transfers labels on the Kubernetes Pod onto the target.
podTargetLabels: {}
dashboard:
# -- Enable Grafana dashboard.
enabled: false
# -- Annotations for the dashboard ConfigMap.
annotations: {}
# -- Additional labels for the dashboard ConfigMap.
labels: {}
# -- Dashboard ConfigMap namespace
# Overrides the namespace for the dashboard ConfigMap.
namespace: ""

View File

@@ -1,5 +1,4 @@
--- ---
version: '3.6'
services: services:
wg-portal: wg-portal:
image: wgportal/wg-portal:v2 image: wgportal/wg-portal:v2
@@ -11,8 +10,10 @@ services:
max-file: "3" max-file: "3"
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
# Use host network mode for WireGuard and the UI. Ensure that access to the UI is properly secured.
network_mode: "host" network_mode: "host"
volumes: volumes:
# left side is the host path, right side is the container path
- /etc/wireguard:/etc/wireguard - /etc/wireguard:/etc/wireguard
- ./data:/app/data - ./data:/app/data
- ./config:/app/config - ./config:/app/config

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
wgportal.org

BIN
docs/assets/images/dashboard.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>WireGuard icon</title><path d="M23.98 11.645S24.533 0 11.735 0C.418 0 .064 11.17.064 11.17S-1.6 24 11.997 24C25.04 24 23.98 11.645 23.98 11.645zM8.155 7.576c2.4-1.47 5.469-.571 6.618 1.638.218.419.246 1.063.108 1.503-.477 1.516-1.601 2.366-3.145 2.728.455-.39.817-.832.933-1.442a2.112 2.112 0 0 0-.364-1.677 2.14 2.14 0 0 0-2.465-.75c-.95.36-1.47 1.228-1.377 2.294.087.99.839 1.632 2.245 1.876-.21.111-.372.193-.53.281a5.113 5.113 0 0 0-1.644 1.43c-.143.192-.24.208-.458.075-2.827-1.729-3.009-6.067.078-7.956zM6.04 18.258c-.455.116-.895.286-1.359.438.227-1.532 2.021-2.943 3.539-2.782a3.91 3.91 0 0 0-.74 2.072c-.504.093-.98.155-1.44.272zM15.703 3.3c.448.017.898.01 1.347.02a2.324 2.324 0 0 1 .334.047 3.249 3.249 0 0 1-.34.434c-.16.15-.341.296-.573.069-.055-.055-.187-.042-.283-.044-.447-.005-.894-.02-1.34-.003a8.323 8.323 0 0 0-1.154.118c-.072.013-.178.25-.146.338.078.207.191.435.359.567.619.49 1.277.928 1.9 1.413.604.472 1.167.99 1.51 1.7.446.928.46 1.9.267 2.877-.322 1.63-1.147 2.98-2.483 3.962-.538.395-1.205.62-1.821.903-.543.25-1.1.465-1.644.712-.98.446-1.53 1.51-1.369 2.615.149 1.015 1.04 1.862 2.059 2.037 1.223.21 2.486-.586 2.785-1.83.336-1.397-.423-2.646-1.845-3.024l-.256-.066c.38-.17.708-.291 1.012-.458q.793-.437 1.558-.925c.15-.096.231-.096.36.014.977.846 1.56 1.898 1.724 3.187.27 2.135-.74 4.096-2.646 5.101-2.948 1.555-6.557-.215-7.208-3.484-.558-2.8 1.418-5.34 3.797-5.83 1.023-.211 1.958-.637 2.685-1.425.47-.508.697-.944.775-1.141a3.165 3.165 0 0 0 .217-1.158 2.71 2.71 0 0 0-.237-.992c-.248-.566-1.2-1.466-1.435-1.656l-2.24-1.754c-.079-.065-.168-.06-.36-.047-.23.016-.815.048-1.067-.018.204-.155.76-.38 1-.56-.726-.49-1.554-.314-2.315-.46.176-.328 1.046-.831 1.541-.888a7.323 7.323 0 0 0-.135-.822c-.03-.111-.154-.22-.263-.283-.262-.154-.541-.281-.843-.434a1.755 1.755 0 0 1 .906-.28 3.385 3.385 0 0 1 .908.088c.54.123.97.042 1.399-.324-.338-.136-.676-.26-1.003-.407a9.843 9.843 0 0 1-.942-.493c.85.118 1.671.437 2.54.32l.022-.118-2.018-.47c1.203-.11 2.323-.128 3.384.388.299.146.61.266.897.432.14.08.233.24.348.365.09.098.164.23.276.29.424.225.89.234 1.366.223l.01-.16c.479.15 1.017.702 1.017 1.105-.776 0-1.55-.003-2.325.004-.083 0-.165.061-.247.094.078.046.155.128.235.131z M14.703 2.153a.118.118 0 0 0-.016.19.179.179 0 0 0 .246.065c.075-.038.148-.078.238-.125-.072-.062-.13-.114-.19-.163-.106-.087-.193-.032-.278.033z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1,217 @@
Below are some sample YAML configurations demonstrating how to override some default values.
## Basic
```yaml
core:
admin_user: test@example.com
admin_password: password
admin_api_token: super-s3cr3t-api-token-or-a-UUID
import_existing: false
create_default_peer: true
self_provisioning_allowed: true
backend:
# default backend decides where new interfaces are created
default: mikrotik
mikrotik:
- id: mikrotik # unique id, not "local"
display_name: RouterOS RB5009 # optional nice name
api_url: https://10.10.10.10/rest
api_user: wgportal
api_password: a-super-secret-password
api_verify_tls: false # set to false only if using self-signed during testing
api_timeout: 30s # maximum request duration
concurrency: 5 # limit parallel REST calls to device
debug: false # verbose logging for this backend
ignored_interfaces: # ignore these interfaces during import
- wgTest1
- wgTest2
web:
site_title: My WireGuard Server
site_company_name: My Company
listening_address: :8080
external_url: https://my.external-domain.com
csrf_secret: super-s3cr3t-csrf
session_secret: super-s3cr3t-session
request_logging: true
advanced:
log_level: trace
log_pretty: true
log_json: false
config_storage_path: /etc/wireguard
expiry_check_interval: 5m
database:
debug: true
type: sqlite
dsn: data/sqlite.db
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
auth:
webauthn:
enabled: true
```
## LDAP Authentication and Synchronization
```yaml
# ... (basic configuration)
auth:
ldap:
# a sample LDAP provider with user sync enabled
- id: ldap
provider_name: Active Directory
url: ldap://srv-ad1.company.local:389
bind_user: ldap_wireguard@company.local
bind_pass: super-s3cr3t-ldap
base_dn: DC=COMPANY,DC=LOCAL
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
sync_interval: 15m
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
disable_missing: true
field_map:
user_identifier: sAMAccountName
email: mail
firstname: givenName
lastname: sn
phone: telephoneNumber
department: department
memberof: memberOf
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
registration_enabled: true
log_user_info: true
```
## OpenID Connect (OIDC) Authentication
```yaml
# ... (basic configuration)
auth:
oidc:
# A sample Entra ID provider with environment variable substitution.
# Only users with an @outlook.com email address are allowed to register or login.
- id: azure
provider_name: azure
display_name: Login with</br>Entra ID
registration_enabled: true
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
client_id: "${AZURE_CLIENT_ID}"
client_secret: "${AZURE_CLIENT_SECRET}"
allowed_domains:
- "outlook.com"
extra_scopes:
- profile
- email
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
- id: oidc-with-admin-attribute
provider_name: google
display_name: Login with</br>Google
base_url: https://accounts.google.com
client_id: the-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
field_map:
user_identifier: sub
email: email
firstname: given_name
lastname: family_name
phone: phone_number
department: department
is_admin: wg_admin
admin_mapping:
admin_value_regex: ^true$
registration_enabled: true
log_user_info: true
# a sample provider where users in the group `the-admin-group` are considered as admins
- id: oidc-with-admin-group
provider_name: google2
display_name: Login with</br>Google2
base_url: https://accounts.google.com
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
field_map:
user_identifier: sub
email: email
firstname: given_name
lastname: family_name
phone: phone_number
department: department
user_groups: groups
admin_mapping:
admin_group_regex: ^the-admin-group$
registration_enabled: true
log_user_info: true
```
## Plain OAuth2 Authentication
```yaml
# ... (basic configuration)
auth:
oauth:
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
# are considered as admins
- id: google_plain_oauth-with-admin-attribute
provider_name: google3
display_name: Login with</br>Google3
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
field_map:
user_identifier: sub
email: email
firstname: name
is_admin: this-attribute-must-be-true
admin_mapping:
admin_value_regex: ^(True|true)$
registration_enabled: true
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
# users in the group `admin-group-name` are considered as admins
- id: google_plain_oauth_with_groups
provider_name: google4
display_name: Login with</br>Google4
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
- i-want-some-groups
field_map:
email: email
firstname: name
user_identifier: sub
is_admin: this-attribute-must-be-true
user_groups: groups
admin_mapping:
admin_value_regex: ^true$
admin_group_regex: ^admin-group-name$
registration_enabled: true
log_user_info: true
```
For more information, check out the usage documentation (e.g. [General Configuration](../usage/general.md) or [Backends Configuration](../usage/backends.md)).

View File

@@ -0,0 +1,766 @@
This page provides an overview of **all available configuration options** for WireGuard Portal.
You can supply these configurations in a **YAML** file when starting the Portal.
The path of the configuration file defaults to `config/config.yaml` (or `config/config.yml`) in the working directory of the executable.
It is possible to override the configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
For example: `WG_PORTAL_CONFIG=/etc/wg-portal/config.yaml ./wg-portal`.
Also, environment variable substitution in the config file is supported. Refer to the [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
Configuration examples are available on the [Examples](./examples.md) page.
<details>
<summary>Default configuration</summary>
```yaml
core:
admin_user: admin@wgportal.local
admin_password: wgportal-default
admin_api_token: ""
disable_admin_user: false
editable_keys: true
create_default_peer: false
create_default_peer_on_creation: false
re_enable_peer_after_user_enable: true
delete_peer_after_user_deleted: false
self_provisioning_allowed: false
import_existing: true
restore_state: true
backend:
default: local
advanced:
log_level: info
log_pretty: false
log_json: false
start_listen_port: 51820
start_cidr_v4: 10.11.12.0/24
start_cidr_v6: fdfd:d3ad:c0de:1234::0/64
use_ip_v6: true
config_storage_path: ""
expiry_check_interval: 15m
rule_prio_offset: 20000
route_table_offset: 20000
api_admin_only: true
limit_additional_user_peers: 0
database:
debug: false
slow_query_threshold: "0"
type: sqlite
dsn: data/sqlite.db
encryption_passphrase: ""
statistics:
use_ping_checks: true
ping_check_workers: 10
ping_unprivileged: false
ping_check_interval: 1m
data_collection_interval: 1m
collect_interface_data: true
collect_peer_data: true
collect_audit_data: true
listening_address: :8787
mail:
host: 127.0.0.1
port: 25
encryption: none
cert_validation: true
username: ""
password: ""
auth_type: plain
from: Wireguard Portal <noreply@wireguard.local>
link_only: false
auth:
oidc: []
oauth: []
ldap: []
webauthn:
enabled: true
min_password_length: 16
hide_login_form: false
web:
listening_address: :8888
external_url: http://localhost:8888
site_company_name: WireGuard Portal
site_title: WireGuard Portal
session_identifier: wgPortalSession
session_secret: very_secret
csrf_secret: extremely_secret
request_logging: false
expose_host_info: false
cert_file: ""
key_File: ""
webhook:
url: ""
authentication: ""
timeout: 10s
```
</details>
Below you will find sections like
[`core`](#core),
[`backend`](#backend),
[`advanced`](#advanced),
[`database`](#database),
[`statistics`](#statistics),
[`mail`](#mail),
[`auth`](#auth),
[`web`](#web) and
[`webhook`](#webhook).
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
---
## Core
These are the primary configuration options that control fundamental WireGuard Portal behavior.
More advanced options are found in the subsequent `Advanced` section.
### `admin_user`
- **Default:** `admin@wgportal.local`
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
### `admin_password`
- **Default:** `wgportal-default`
- **Description:** The administrator password. The default password should be changed immediately!
- **Important:** The password should be strong and secure. The minimum password length is specified in [auth.min_password_length](#min_password_length). By default, it is 16 characters.
### `disable_admin_user`
- **Default:** `false`
- **Description:** If `true`, no admin user is created. This is useful if you plan to manage users exclusively through external authentication providers such as LDAP or OAuth.
### `admin_api_token`
- **Default:** *(empty)*
- **Description:** An API token for the admin user. If a token is provided, the REST API can be accessed using this token. If empty, the API is initially disabled for the admin user.
### `editable_keys`
- **Default:** `true`
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
### `create_default_peer`
- **Default:** `false`
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
### `create_default_peer_on_creation`
- **Default:** `false`
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
### `re_enable_peer_after_user_enable`
- **Default:** `true`
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
### `delete_peer_after_user_deleted`
- **Default:** `false`
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
### `self_provisioning_allowed`
- **Default:** `false`
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
### `import_existing`
- **Default:** `true`
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
### `restore_state`
- **Default:** `true`
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
---
## Backend
Configuration options for the WireGuard backend, which manages the WireGuard interfaces and peers.
The current MikroTik backend is in **BETA** and may not support all features.
### `default`
- **Default:** `local`
- **Description:** The default backend to use for managing WireGuard interfaces.
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
### `ignored_local_interfaces`
- **Default:** *(empty)*
- **Description:** A list of interface names to exclude when enumerating local interfaces.
This is useful if you want to prevent certain interfaces from being imported from the local system.
### Mikrotik
The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces.
Below are the properties for each entry inside `backend.mikrotik`:
#### `id`
- **Default:** *(empty)*
- **Description:** A unique identifier for this backend.
This value can be referenced by `backend.default` to use this backend as default.
The identifier must be unique across all backends and must not use the reserved keyword `local`.
#### `display_name`
- **Default:** *(empty)*
- **Description:** A human-friendly display name for this backend. If omitted, the `id` will be used as the display name.
#### `api_url`
- **Default:** *(empty)*
- **Description:** Base URL of the MikroTik REST API, including scheme and path, e.g., `https://10.10.10.10:8729/rest`.
#### `api_user`
- **Default:** *(empty)*
- **Description:** Username for authenticating against the MikroTik API.
Ensure that the user has sufficient permissions to manage WireGuard interfaces and peers.
#### `api_password`
- **Default:** *(empty)*
- **Description:** Password for the specified API user.
#### `api_verify_tls`
- **Default:** `false`
- **Description:** Whether to verify the TLS certificate of the MikroTik API endpoint. Set to `false` to allow self-signed certificates (not recommended for production).
#### `api_timeout`
- **Default:** `30s`
- **Description:** Timeout for API requests to the MikroTik device. Uses Go duration format (e.g., `10s`, `1m`). If omitted, a default of 30 seconds is used.
#### `concurrency`
- **Default:** `5`
- **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used.
#### `ignored_interfaces`
- **Default:** *(empty)*
- **Description:** A list of interface names to exclude during interface enumeration.
This is useful if you want to prevent specific interfaces from being imported from the MikroTik device.
#### `debug`
- **Default:** `false`
- **Description:** Enable verbose debug logging for the MikroTik backend.
For more details on configuring the MikroTik backend, see the [Backends](../usage/backends.md) documentation.
---
## Advanced
Additional or more specialized configuration options for logging and interface creation details.
### `log_level`
- **Default:** `info`
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
### `log_pretty`
- **Default:** `false`
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
### `log_json`
- **Default:** `false`
- **Description:** If `true`, log messages are structured in JSON format.
### `start_listen_port`
- **Default:** `51820`
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
### `start_cidr_v4`
- **Default:** `10.11.12.0/24`
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
### `start_cidr_v6`
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
### `use_ip_v6`
- **Default:** `true`
- **Description:** Enable or disable IPv6 support.
### `config_storage_path`
- **Default:** *(empty)*
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
### `expiry_check_interval`
- **Default:** `15m`
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `rule_prio_offset`
- **Default:** `20000`
- **Description:** Offset for IP route rule priorities when configuring routing.
### `route_table_offset`
- **Default:** `20000`
- **Description:** Offset for IP route table IDs when configuring routing.
### `api_admin_only`
- **Default:** `true`
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
### `limit_additional_user_peers`
- **Default:** `0`
- **Description:** Limit additional peers a normal user can create. `0` means unlimited.
---
## Database
Configuration for the underlying database used by WireGuard Portal.
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
If sensitive values (like private keys) should be stored in an encrypted format, set the `encryption_passphrase` option.
### `debug`
- **Default:** `false`
- **Description:** If `true`, logs all database statements (verbose).
### `slow_query_threshold`
- **Default:** "0"
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration). The value must be a string.
### `type`
- **Default:** `sqlite`
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
### `dsn`
- **Default:** `data/sqlite.db`
- **Description:** The Data Source Name (DSN) for connecting to the database.
For example:
```text
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
```
### `encryption_passphrase`
- **Default:** *(empty)*
- **Description:** Passphrase for encrypting sensitive values such as private keys in the database. Encryption is only applied if this passphrase is set.
**Important:** Once you enable encryption by setting this passphrase, you cannot disable it or change it afterward.
New or updated records will be encrypted; existing data remains in plaintext until its next modified.
---
## Statistics
Controls how WireGuard Portal collects and reports usage statistics, including ping checks and Prometheus metrics.
### `use_ping_checks`
- **Default:** `true`
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
### `ping_check_workers`
- **Default:** `10`
- **Description:** Number of parallel worker processes for ping checks.
### `ping_unprivileged`
- **Default:** `false`
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
### `ping_check_interval`
- **Default:** `1m`
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `data_collection_interval`
- **Default:** `1m`
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `collect_interface_data`
- **Default:** `true`
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
### `collect_peer_data`
- **Default:** `true`
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
### `collect_audit_data`
- **Default:** `true`
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
### `listening_address`
- **Default:** `:8787`
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787` or `127.0.0.1:8787`).
---
## Mail
Options for configuring email notifications or sending peer configurations via email.
### `host`
- **Default:** `127.0.0.1`
- **Description:** Hostname or IP of the SMTP server.
### `port`
- **Default:** `25`
- **Description:** Port number for the SMTP server.
### `encryption`
- **Default:** `none`
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
### `cert_validation`
- **Default:** `true`
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
### `username`
- **Default:** *(empty)*
- **Description:** Optional SMTP username for authentication.
### `password`
- **Default:** *(empty)*
- **Description:** Optional SMTP password for authentication.
### `auth_type`
- **Default:** `plain`
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
### `from`
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
- **Description:** The default "From" address when sending emails.
### `link_only`
- **Default:** `false`
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
---
## Auth
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), **Passkeys** (`webauthn`) and **LDAP** (`ldap`).
Each can have multiple providers configured. Below are the relevant keys.
Some core authentication options are shared across all providers, while others are specific to each provider type.
### `min_password_length`
- **Default:** `16`
- **Description:** Minimum password length for local authentication. This is not enforced for LDAP authentication.
The default admin password strength is also enforced by this setting.
- **Important:** The password should be strong and secure. It is recommended to use a password with at least 16 characters, including uppercase and lowercase letters, numbers, and special characters.
### `hide_login_form`
- **Default:** `false`
- **Description:** If `true`, the login form is hidden and only the OIDC, OAuth, LDAP, or WebAuthn providers are shown. This is useful if you want to enforce a specific authentication method.
If no social login providers are configured, the login form is always shown, regardless of this setting.
- **Important:** You can still access the login form by adding the `?all` query parameter to the login URL (e.g. https://wg.portal/#/login?all).
---
### OIDC
The `oidc` array contains a list of OpenID Connect providers.
Below are the properties for each OIDC provider entry inside `auth.oidc`:
#### `provider_name`
- **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
#### `display_name`
- **Default:** *(empty)*
- **Description:** A user-friendly name shown on the login page (e.g., "Login with Google").
#### `base_url`
- **Default:** *(empty)*
- **Description:** The OIDC providers base URL (e.g., `https://accounts.google.com`).
#### `client_id`
- **Default:** *(empty)*
- **Description:** The OAuth client ID from the OIDC provider.
#### `client_secret`
- **Default:** *(empty)*
- **Description:** The OAuth client secret from the OIDC provider.
#### `extra_scopes`
- **Default:** *(empty)*
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
#### `allowed_domains`
- **Default:** *(empty)*
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical OIDC Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
| `lastname` | `family_name` | The users last (family) name, typically provided by the IdP in the `family_name` claim. |
| `phone` | `phone_number` | The users phone number. This may require additional scopes/permissions from the IdP to access. |
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
#### `admin_mapping`
- **Default:** *(empty)*
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
#### `registration_enabled`
- **Default:** *(empty)*
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
---
### OAuth
The `oauth` array contains a list of plain OAuth2 providers.
Below are the properties for each OAuth provider entry inside `auth.oauth`:
#### `provider_name`
- **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
#### `display_name`
- **Default:** *(empty)*
- **Description:** A user-friendly name shown on the login page.
#### `client_id`
- **Default:** *(empty)*
- **Description:** The OAuth client ID for the provider.
#### `client_secret`
- **Default:** *(empty)*
- **Description:** The OAuth client secret for the provider.
#### `auth_url`
- **Default:** *(empty)*
- **Description:** URL of the authentication endpoint.
#### `token_url`
- **Default:** *(empty)*
- **Description:** URL of the token endpoint.
#### `user_info_url`
- **Default:** *(empty)*
- **Description:** URL of the user information endpoint.
#### `scopes`
- **Default:** *(empty)*
- **Description:** A list of OAuth scopes.
#### `allowed_domains`
- **Default:** *(empty)*
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
| `lastname` | `family_name` | The users last (family) name, typically provided by the IdP in the `family_name` claim. |
| `phone` | `phone_number` | The users phone number. This may require additional scopes/permissions from the IdP to access. |
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
#### `admin_mapping`
- **Default:** *(empty)*
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
#### `registration_enabled`
- **Default:** *(empty)*
- **Description:** If `true`, new users are created automatically on successful login.
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, logs user info at the trace level upon login.
---
### LDAP
The `ldap` array contains a list of LDAP authentication providers.
Below are the properties for each LDAP provider entry inside `auth.ldap`:
#### `provider_name`
- **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
#### `url`
- **Default:** *(empty)*
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
#### `start_tls`
- **Default:** *(empty)*
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
#### `cert_validation`
- **Default:** *(empty)*
- **Description:** If `true`, validate the LDAP servers TLS certificate.
#### `tls_certificate_path`
- **Default:** *(empty)*
- **Description:** Path to a TLS certificate if needed for LDAP connections.
#### `tls_key_path`
- **Default:** *(empty)*
- **Description:** Path to the corresponding TLS certificate key.
#### `base_dn`
- **Default:** *(empty)*
- **Description:** The base DN for user searches (e.g., `DC=COMPANY,DC=LOCAL`).
#### `bind_user`
- **Default:** *(empty)*
- **Description:** The bind user for LDAP (e.g., `company\\ldap_wireguard` or `ldap_wireguard@company.local`).
#### `bind_pass`
- **Default:** *(empty)*
- **Description:** The bind password for LDAP authentication.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps LDAP attributes to WireGuard Portal fields.
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|----------------------------|----------------------------|--------------------------------------------------------------|
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
| email | mail / userPrincipalName | Stores the user's primary email address. |
| firstname | givenName | Contains the user's first (given) name. |
| lastname | sn | Contains the user's last (surname) name. |
| phone | telephoneNumber / mobile | Holds the user's phone or mobile number. |
| department | departmentNumber / ou | Specifies the department or organizational unit of the user. |
| memberof | memberOf | Lists the groups and roles to which the user belongs. |
#### `login_filter`
- **Default:** *(empty)*
- **Description:** An LDAP filter to restrict which users can log in. Use `{{login_identifier}}` to insert the username.
For example:
```text
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
```
- **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user.
If the filter returns multiple or no users, the login will fail.
#### `admin_group`
- **Default:** *(empty)*
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.
For example:
```text
CN=WireGuardAdmins,OU=Some-OU,DC=YOURDOMAIN,DC=LOCAL
```
#### `sync_interval`
- **Default:** *(empty)*
- **Description:** How frequently (in duration, e.g. `30m`) to synchronize users from LDAP. Empty or `0` disables sync. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
Only users that match the `sync_filter` are synchronized, if `disable_missing` is `true`, users not found in LDAP are disabled.
#### `sync_filter`
- **Default:** *(empty)*
- **Description:** An LDAP filter to select which users get synchronized into WireGuard Portal.
For example:
```text
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
```
#### `disable_missing`
- **Default:** *(empty)*
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
#### `auto_re_enable`
- **Default:** *(empty)*
- **Description:** If `true`, users that where disabled because they were missing (see `disable_missing`) will be re-enabled once they are found again.
#### `registration_enabled`
- **Default:** *(empty)*
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
---
### WebAuthn (Passkeys)
The `webauthn` section contains configuration options for WebAuthn authentication (passkeys).
#### `enabled`
- **Default:** `true`
- **Description:** If `true`, Passkey authentication is enabled. If `false`, WebAuthn is disabled.
Users are encouraged to use Passkeys for secure authentication instead of passwords.
If a passkey is registered, the password login is still available as a fallback. Ensure that the password is strong and secure.
## Web
The web section contains configuration options for the web server, including the listening address, session management, and CSRF protection.
It is important to specify a valid `external_url` for the web server, especially if you are using a reverse proxy.
Without a valid `external_url`, the login process may fail due to CSRF protection.
### `listening_address`
- **Default:** `:8888`
- **Description:** The listening address and port for the web server (e.g., `:8888` to bind on all interfaces or `127.0.0.1:8888` to bind only on the loopback interface).
Ensure that access to WireGuard Portal is protected against unauthorized access, especially if binding to all interfaces.
### `external_url`
- **Default:** `http://localhost:8888`
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
### `site_company_name`
- **Default:** `WireGuard Portal`
- **Description:** The company name that is shown at the bottom of the web frontend.
### `site_title`
- **Default:** `WireGuard Portal`
- **Description:** The title that is shown in the web frontend.
### `session_identifier`
- **Default:** `wgPortalSession`
- **Description:** The session identifier for the web frontend.
### `session_secret`
- **Default:** `very_secret`
- **Description:** The session secret for the web frontend.
### `csrf_secret`
- **Default:** `extremely_secret`
- **Description:** The CSRF secret.
### `request_logging`
- **Default:** `false`
- **Description:** Log all HTTP requests.
### `expose_host_info`
- **Default:** `false`
- **Description:** Expose the hostname and version of the WireGuard Portal server in an HTTP header. This is useful for debugging but may expose sensitive information.
### `cert_file`
- **Default:** *(empty)*
- **Description:** (Optional) Path to the TLS certificate file.
### `key_file`
- **Default:** *(empty)*
- **Description:** (Optional) Path to the TLS certificate key file.
---
## Webhook
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal.
Further details can be found in the [usage documentation](../usage/webhooks.md).
### `url`
- **Default:** *(empty)*
- **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled.
### `authentication`
- **Default:** *(empty)*
- **Description:** The Authorization header for the webhook endpoint. The value is send as-is in the header. For example: `Bearer <token>`.
### `timeout`
- **Default:** `10s`
- **Description:** The timeout for the webhook request. If the request takes longer than this, it is aborted.

View File

@@ -0,0 +1,42 @@
Starting from v2, each [release](https://github.com/h44z/wg-portal/releases) includes compiled binaries for supported platforms.
These binary versions can be manually downloaded and installed.
## Download
Make sure that you download the correct binary for your architecture. The available binaries are:
- `wg-portal_linux_amd64` - Linux x86_64
- `wg-portal_linux_arm64` - Linux ARM 64-bit
- `wg-portal_linux_arm_v7` - Linux ARM 32-bit
With `curl`:
```shell
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
```
With `wget`:
```shell
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
```
with `gh cli`:
```shell
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
```
## Install
```shell
sudo mkdir -p /opt/wg-portal
sudo install wg-portal /opt/wg-portal/
```
## Unreleased versions (master branch builds)
Unreleased versions can be fetched directly from the artifacts section of the [GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster).

View File

@@ -0,0 +1,161 @@
## Image Usage
The WireGuard Portal Docker image is available on both [Docker Hub](https://hub.docker.com/r/wgportal/wg-portal) and [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
It is built on the official Alpine Linux base image and comes pre-packaged with all necessary WireGuard dependencies.
This container allows you to establish WireGuard VPN connections without relying on a host system that supports WireGuard or using the `linuxserver/wireguard` Docker image.
The recommended method for deploying WireGuard Portal is via Docker Compose for ease of configuration and management.
A sample docker-compose.yml (managing WireGuard interfaces directly on the host) is provided below:
```yaml
--8<-- "docker-compose.yml::19"
```
By default, the webserver for the UI is listening on port **8888** on all available interfaces.
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
## WireGuard Interface Handling
WireGuard Portal supports managing WireGuard interfaces through three distinct deployment methods, providing flexibility based on your system architecture and operational preferences:
- **Directly on the host system**:
WireGuard Portal can control WireGuard interfaces natively on the host, without using containers.
This setup is ideal for environments where direct access to system networking is preferred.
To use this method, you need to set the network mode to `host` in your docker-compose.yml file.
```yaml
services:
wg-portal:
...
network_mode: "host"
...
```
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
- **Within the WireGuard Portal Docker container**:
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
```yaml
services:
wg-portal:
image: wgportal/wg-portal:v2
container_name: wg-portal
...
cap_add:
- NET_ADMIN
ports:
# host port : container port
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
- "51820:51820/udp"
# Web UI port
- "8888:8888/tcp"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
# host path : container path
- ./wg/data:/app/data
- ./wg/config:/app/config
```
- **Via a separate Docker container**:
WireGuard Portal can interface with and control WireGuard running in another Docker container, such as the [linuxserver/wireguard](https://docs.linuxserver.io/images/docker-wireguard/) image.
This method is useful in setups that already use `linuxserver/wireguard` or where you want to isolate the VPN backend from the portal frontend.
For this, you need to set the network mode to `service:wireguard` in your docker-compose.yml file, `wireguard` is the service name of your WireGuard container.
```yaml
services:
wg-portal:
image: wgportal/wg-portal:v2
container_name: wg-portal
...
cap_add:
- NET_ADMIN
network_mode: "service:wireguard" # So we ensure to stay on the same network as the wireguard container.
volumes:
# host path : container path
- ./wg/etc:/etc/wireguard
- ./wg/data:/app/data
- ./wg/config:/app/config
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
# host port : container port
- "51820:51820/udp" # WireGuard port, needs to match the port in wg-portal interface config
- "8888:8888/tcp" # Noticed that the port of the web UI is exposed in the wireguard container.
volumes:
- ./wg/etc:/config/wg_confs # We share the configuration (wgx.conf) between wg-portal and wireguard
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
```
As the `linuxserver/wireguard` image uses _wg-quick_ to manage the interfaces, you need to have at least the following configuration set for WireGuard Portal:
```yaml
core:
# The WireGuard container uses wg-quick to manage the WireGuard interfaces - this conflicts with WireGuard Portal during startup.
# To avoid this, we need to set the restore_state option to false so that wg-quick can create the interfaces.
restore_state: false
# Usually, there are no existing interfaces in the WireGuard container, so we can set this to false.
import_existing: false
advanced:
# WireGuard Portal needs to export the WireGuard configuration as wg-quick config files so that the WireGuard container can use them.
config_storage_path: /etc/wireguard/
```
## Image Versioning
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
There are three types of tags in the repository:
#### Semantic versioned tags
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
These are official releases of WireGuard Portal. For production deployments of WireGuard Portal, we strongly recommend using one of these versioned tags instead of the latest or canary tags.
There are different types of these tags:
- Major version tags: `v2` or `2`. These tags always refer to the latest image for WireGuard Portal version **2**.
- Minor version tags: `v2.x` or `2.0`. These tags always refer to the latest image for WireGuard Portal version **2.x**.
- Specific version tags (patch version): `v2.0.0` or `2.0.0`. These tags denote a very specific release. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases). Once these tags for a specific version show up in the Docker repository, they will never change.
#### The `latest` tag
The lastest tag is the latest stable release of WireGuard Portal. For version **2**, this is the same as the `v2` tag.
#### The `master` tag
This is the most recent build to the main branch! It changes a lot and is very unstable.
We recommend that you don't use it except for development purposes or to test the latest features.
## Configuration
You can configure WireGuard Portal using a YAML configuration file.
The filepath of the YAML configuration file defaults to `/app/config/config.yaml`.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
By default, WireGuard Portal uses an SQLite database. The database is stored in `/app/data/sqlite.db`.
You should mount those directories as a volume:
- `/app/data`
- `/app/config`
A detailed description of the configuration options can be found [here](../configuration/overview.md).
If you want to access configuration files in wg-quick format, you can mount the `/etc/wireguard` directory inside the container to a location of your choice.
Also enable the `config_storage_path` option in the configuration file:
```yaml
advanced:
config_storage_path: /etc/wireguard
```

View File

@@ -0,0 +1 @@
--8<-- "./deploy/helm/README.md:16"

View File

@@ -0,0 +1,98 @@
## Reverse Proxy for HTTPS
For production deployments, always serve the WireGuard Portal over HTTPS. You have two options to secure your connection:
### Reverse Proxy
Let a frontend proxy handle HTTPS for you. This also frees you from managing certificates manually and is therefore the preferred option.
You can use Nginx, Traefik, Caddy or any other proxy.
Below is an example using a Docker Compose stack with [Traefik](https://traefik.io/traefik/).
It exposes the WireGuard Portal on `https://wg.domain.com` and redirects initial HTTP traffic to HTTPS.
```yaml
services:
reverse-proxy:
image: traefik:v3.3
restart: unless-stopped
command:
#- '--log.level=DEBUG'
- '--providers.docker.endpoint=unix:///var/run/docker.sock'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--entrypoints.websecure.http3'
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge=true'
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge.entrypoint=web'
- '--certificatesresolvers.letsencryptresolver.acme.email=your.email@domain.com'
- '--certificatesresolvers.letsencryptresolver.acme.storage=/letsencrypt/acme.json'
#- '--certificatesresolvers.letsencryptresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory' # just for testing
ports:
- 80:80 # for HTTP
- 443:443/tcp # for HTTPS
- 443:443/udp # for HTTP/3
volumes:
- acme-certs:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- 'traefik.enable=true'
# HTTP Catchall for redirecting HTTP -> HTTPS
- 'traefik.http.routers.dashboard-catchall.rule=Host(`wg.domain.com`) && PathPrefix(`/`)'
- 'traefik.http.routers.dashboard-catchall.entrypoints=web'
- 'traefik.http.routers.dashboard-catchall.middlewares=redirect-to-https'
- 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
wg-portal:
image: wgportal/wg-portal:v2
container_name: wg-portal
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
cap_add:
- NET_ADMIN
ports:
# host port : container port
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
- "51820:51820/udp"
# Web UI port (only available on localhost, Traefik will handle the HTTPS)
- "127.0.0.1:8888:8888/tcp"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
# host path : container path
- ./wg/data:/app/data
- ./wg/config:/app/config
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.wgportal.rule=Host(`wg.domain.com`)'
- 'traefik.http.routers.wgportal.entrypoints=websecure'
- 'traefik.http.routers.wgportal.tls.certresolver=letsencryptresolver'
- 'traefik.http.routers.wgportal.service=wgportal'
- 'traefik.http.services.wgportal.loadbalancer.server.port=8888'
volumes:
acme-certs:
```
The WireGuard Portal configuration must be updated accordingly so that the correct external URL is set for the web interface:
```yaml
web:
external_url: https://wg.domain.com
```
### Built-in TLS
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
In your `config.yaml`, under the `web` section, point to your certificate and key files:
```yaml
web:
cert_file: /path/to/your/fullchain.pem
key_file: /path/to/your/privkey.pem
```
The web server will then use these files to serve HTTPS traffic directly instead of HTTP.

View File

@@ -0,0 +1,26 @@
To build the application from source files, use the Makefile provided in the repository.
## Requirements
- [Git](https://git-scm.com/downloads)
- [Make](https://www.gnu.org/software/make/)
- [Go](https://go.dev/dl/): `>=1.24.0`
- [Node.js with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
## Build
```shell
# Get source code
git clone https://github.com/h44z/wg-portal -b ${WG_PORTAL_VERSION:-master} --depth 1
cd wg-portal
# Build the frontend
make frontend
# Build the backend
make build
```
## Install
Compiled binary will be available in `./dist` directory.
For installation instructions, check the [Binaries](./binaries.md) section.

View File

@@ -0,0 +1,32 @@
By default, WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled.
## Exposed Metrics
| Metric | Type | Description |
|--------------------------------------------|-------|------------------------------------------------|
| `wireguard_interface_received_bytes_total` | gauge | Bytes received through the interface. |
| `wireguard_interface_sent_bytes_total` | gauge | Bytes sent through the interface. |
| `wireguard_peer_last_handshake_seconds` | gauge | Seconds from the last handshake with the peer. |
| `wireguard_peer_received_bytes_total` | gauge | Bytes received from the peer. |
| `wireguard_peer_sent_bytes_total` | gauge | Bytes sent to the peer. |
| `wireguard_peer_up` | gauge | Peer connection state (boolean: 1/0). |
## Prometheus Config
Add the following scrape job to your Prometheus config file:
```yaml
# prometheus.yaml
scrape_configs:
- job_name: wg-portal
scrape_interval: 60s
static_configs:
- targets:
- localhost:8787 # Change localhost to IP Address or hostname with WG-Portal
```
# Grafana Dashboard
You may import [`dashboard.json`](https://github.com/h44z/wg-portal/blob/master/deploy/helm/files/dashboard.json) into your Grafana instance.
![Dashboard](../../assets/images/dashboard.png)

View File

@@ -0,0 +1 @@
--8<-- "README.md:12:41"

View File

@@ -0,0 +1 @@
<swagger-ui src="./swagger.yaml"/>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
Major upgrades between different versions may require special procedures, which are described in the following sections.
## Upgrade from v1 to v2
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
The configuration (config.yaml) for WireGuard Portal must be updated and valid before starting the upgrade.
To upgrade from a previous SQLite database, start wg-portal like:
```shell
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
```
You can also specify the database type using the parameter **-migrateFromType**.
Supported database types: `mysql`, `mssql`, `postgres` or `sqlite`.
For example:
```shell
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom='user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local'
```
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yaml** configuration file.
Ensure that the new database does not contain any data!
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
```yaml
services:
wg-portal:
image: wgportal/wg-portal:v2
# ... other settings
restart: no
command: ["-migrateFrom=/app/data/old_wg_portal.db"]
```

View File

@@ -0,0 +1,57 @@
# Backends
WireGuard Portal can manage WireGuard interfaces and peers on different backends.
Each backend represents a system where interfaces actually live.
You can register multiple backends and choose which one to use per interface.
A global default backend determines where newly created interfaces go (unless you explicitly choose another in the UI).
**Supported backends:**
- **Local** (default): Manages interfaces on the host running WireGuard Portal (Linux WireGuard via wgctrl). Use this when the portal should directly configure wg devices on the same server.
- **MikroTik** RouterOS (_beta_): Manages interfaces and peers on MikroTik devices via the RouterOS REST API. Use this to control WG interfaces on RouterOS v7+.
How backend selection works:
- The default backend is configured at `backend.default` (_local_ or the id of a defined MikroTik backend).
New interfaces created in the UI will use this backend by default.
- Each interface stores its backend. You can select a different backend when creating a new interface.
## Configuring MikroTik backends (RouterOS v7+)
> :warning: The MikroTik backend is currently marked beta. While basic functionality is implemented, some advanced features are not yet implemented or contain bugs. Please test carefully before using in production.
The MikroTik backend uses the [REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API) under a base URL ending with /rest.
You can register one or more MikroTik devices as backends for a single WireGuard Portal instance.
### Prerequisites on MikroTik:
- RouterOS v7 with WireGuard support.
- REST API enabled and reachable over HTTP(S). A typical base URL is https://<router-address>:8729/rest or https://<router-address>/rest depending on your service setup.
- A dedicated RouterOS user with the following group permissions:
- **api** (for logging in via REST API)
- **rest-api** (for logging in via REST API)
- **read** (to read interface and peer data)
- **write** (to create/update interfaces and peers)
- **test** (to perform ping checks)
- **sensitive** (to read private keys)
- TLS certificate on the device is recommended. If you use a self-signed certificate during testing, set `api_verify_tls`: _false_ in wg-portal (not recommended for production).
Example WireGuard Portal configuration (config/config.yaml):
```yaml
backend:
# default backend decides where new interfaces are created
default: mikrotik-prod
mikrotik:
- id: mikrotik-prod # unique id, not "local"
display_name: RouterOS RB5009 # optional nice name
api_url: https://10.10.10.10/rest
api_user: wgportal
api_password: a-super-secret-password
api_verify_tls: true # set to false only if using self-signed during testing
api_timeout: 30s # maximum request duration
concurrency: 5 # limit parallel REST calls to device
debug: false # verbose logging for this backend
```
### Known limitations:
- The MikroTik backend is still in beta. Some features may not work as expected.
- Not all WireGuard Portal features are supported yet (e.g., no support for interface hooks)

View File

@@ -0,0 +1,57 @@
This documentation section describes the general usage of WireGuard Portal.
If you are looking for specific setup instructions, please refer to the *Getting Started* and [*Configuration*](../configuration/overview.md) sections,
for example, using a [Docker](../getting-started/docker.md) deployment.
## Basic Concepts
WireGuard Portal is a web-based configuration portal for WireGuard server management. It allows managing multiple WireGuard interfaces and users from a single web UI.
WireGuard Interfaces can be categorized into three types:
- **Server**: A WireGuard server interface that to which multiple peers can connect. In this mode, it is possible to specify default settings for all peers, such as the IP address range, DNS servers, and MTU size.
- **Client**: A WireGuard client interface that can be used to connect to a WireGuard server. Usually, such an interface has exactly one peer.
- **Unknown**: This is the default type for imported interfaces. It is encouraged to change the type to either `Server` or `Client` after importing the interface.
## Accessing the Web UI
The web UI should be accessed via the URL specified in the `external_url` property of the configuration file.
By default, WireGuard Portal listens on port `8888` for HTTP connections. Check the [Security](security.md) section for more information on securing the web UI.
So the default URL to access the web UI is:
```
http://localhost:8888
```
A freshly set-up WireGuard Portal instance will have a default admin user with the username `admin@wgportal.local` and the password `wgportal-default`.
You can and should override the default credentials in the configuration file. Make sure to change the default password immediately after the first login!
### Basic UI Description
![WireGuard Portal Web UI](../../assets/images/landing_page.png)
As seen in the screenshot above, the web UI is divided into several sections which are accessible via the navigation bar on the top of the screen.
1. **Home**: The landing page of WireGuard Portal. It provides a staring point for the user to access the different sections of the web UI. It also provides quick links to WireGuard Client downloads or official documentation.
2. **Interfaces**: This section allows you to manage the WireGuard interfaces. You can add, edit, or delete interfaces, as well as view their status and statistics. Peers for each interface can be managed here as well.
3. **Users**: This section allows you to manage the users of WireGuard Portal. You can add, edit, or delete users, as well as view their status and statistics.
4. **Key Generator**: This section allows you to generate WireGuard keys locally on your browser. The generated keys are never sent to the server. This is useful if you want to generate keys for a new peer without having to store the private keys in the database.
5. **Profile / Settings**: This section allows you to access your own profile page, settings, and audit logs.
### Interface View
![WireGuard Portal Interface View](../../assets/images/interface_view.png)
The interface view provides an overview of the WireGuard interfaces and peers configured in WireGuard Portal.
The most important elements are:
1. **Interface Selector**: This dropdown allows you to select the WireGuard interface you want to manage.
All further actions will be performed on the selected interface.
2. **Create new Interface**: This button allows you to create a new WireGuard interface.
3. **Interface Overview**: This section provides an overview of the selected WireGuard interface. It shows the interface type, number of peers, and other important information.
4. **List of Peers**: This section provides a list of all peers associated with the selected WireGuard interface. You can view, add, edit, or delete peers from this list.
5. **Add new Peer**: This button allows you to add a new peer to the selected WireGuard interface.
6. **Add multiple Peers**: This button allows you to add multiple peers to the selected WireGuard interface.
This is useful if you want to add a large number of peers at once.

View File

@@ -0,0 +1,37 @@
WireGuard Portal lets you hook up any LDAP server such as Active Directory or OpenLDAP for both authentication and user sync.
You can even register multiple LDAP servers side-by-side. When someone logs in via LDAP, their specific provider is remembered,
so there's no risk of cross-provider conflicts. Details on the log-in process can be found in the [Security](security.md#ldap-authentication) documentation.
If you enable LDAP synchronization, all users within the LDAP directory will be created automatically in the WireGuard Portal database if they do not exist.
If a user is disabled or deleted in LDAP, the user will be disabled in WireGuard Portal as well.
The synchronization process can be fine-tuned by multiple parameters, which are described below.
## LDAP Synchronization
WireGuard Portal can automatically synchronize users from LDAP to the database.
To enable this feature, set the `sync_interval` property in the LDAP provider configuration to a value greater than "0".
The value is a string representing a duration, such as "15m" for 15 minutes or "1h" for 1 hour (check the [exact format definition](https://pkg.go.dev/time#ParseDuration) for details).
The synchronization process will run in the background and synchronize users from LDAP to the database at the specified interval.
Also make sure that the `sync_filter` property is a well-formed LDAP filter, or synchronization will fail.
### Limiting Synchronization to Specific Users
Use the `sync_filter` property in your LDAP provider block to restrict which users get synchronized.
It accepts any valid LDAP search filter, only entries matching that filter will be pulled into the portal's database.
For example, to import only users with a `mail` attribute:
```yaml
auth:
ldap:
- id: ldap
# ... other settings
sync_filter: (mail=*)
```
### Disable Missing Users
If you set the `disable_missing` property to `true`, any user that is not found in LDAP during synchronization will be disabled in WireGuard Portal.
All peers associated with that user will also be disabled.
If you want a user and its peers to be automatically re-enabled once they are found in LDAP again, set the `auto_re_enable` property to `true`.
This will only re-enable the user if they where disabled by the synchronization process. Manually disabled users will not be re-enabled.

View File

@@ -0,0 +1,160 @@
This section describes the security features available to administrators for hardening WireGuard Portal and protecting its data.
## Authentication
WireGuard Portal supports multiple authentication methods, including:
- Local user accounts
- LDAP authentication
- OAuth and OIDC authentication
- Passkey authentication (WebAuthn)
Users can have two roles which limit their permissions in WireGuard Portal:
- **User**: Can manage their own account and peers.
- **Admin**: Can manage all users and peers, including the ability to manage WireGuard interfaces.
### Password Security
WireGuard Portal supports username and password authentication for both local and LDAP-backed accounts.
Local users are stored in the database, while LDAP users are authenticated against an external LDAP server.
On initial startup, WireGuard Portal automatically creates a local admin account with the password `wgportal-default`.
> :warning: This password must be changed immediately after the first login.
The minimum password length for all local users can be configured in the [`auth`](../configuration/overview.md#auth)
section of the configuration file. The default value is **16** characters, see [`min_password_length`](../configuration/overview.md#min_password_length).
The minimum password length is also enforced for the default admin user.
### Passkey (WebAuthn) Authentication
Besides the standard authentication mechanisms, WireGuard Portal supports Passkey authentication.
This feature is enabled by default and can be configured in the [`webauthn`](../configuration/overview.md#webauthn-passkeys) section of the configuration file.
Users can register multiple Passkeys to their account. These Passkeys can be used to log in to the web UI as long as the user is not locked.
> :warning: Passkey authentication does not disable password authentication. The password can still be used to log in (e.g., as a fallback).
To register a Passkey, open the settings page *(1)* in the web UI and click on the "Register Passkey" *(2)* button.
![Passkey UI](../../assets/images/passkey_setup.png)
### OAuth and OIDC Authentication
WireGuard Portal supports OAuth and OIDC authentication. You can use any OAuth or OIDC provider that supports the authorization code flow,
such as Google, GitHub, or Keycloak.
For OAuth or OIDC to work, you need to configure the [`external_url`](../configuration/overview.md#external_url) property in the [`web`](../configuration/overview.md#web) section of the configuration file.
If you are planning to expose the portal to the internet, make sure that the `external_url` is configured to use HTTPS.
To add OIDC or OAuth authentication to WireGuard Portal, create a Client-ID and Client-Secret in your OAuth provider and
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
#### Limiting Login to Specific Domains
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth or OIDC providers.
This property is a comma-separated list of domains that are allowed to log in. The user's email address is checked against this list.
For example, if you want to allow only users with an email address ending in `outlook.com` to log in, set the property as follows:
```yaml
auth:
oidc:
- provider_name: "oidc1"
# ... other settings
allowed_domains:
- "outlook.com"
```
#### Limit Login to Existing Users
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth or OIDC providers.
If registration is enabled, new users will be created in the database when they log in for the first time.
#### Admin Mapping
You can map users to admin roles based on their attributes in the OAuth or OIDC provider. To do this, set the `admin_mapping` property for the provider.
Administrative access can either be mapped by a specific attribute or by group membership.
**Attribute specific mapping** can be achieved by setting the `admin_value_regex` and the `is_admin` property.
The `admin_value_regex` property is a regular expression that is matched against the value of the `is_admin` attribute.
The user is granted admin access if the regex matches the attribute value.
Example:
```yaml
auth:
oidc:
- provider_name: "oidc1"
# ... other settings
field_map:
is_admin: "wg_admin_prop"
admin_mapping:
admin_value_regex: "^true$"
```
The example above will grant admin access to users with the `wg_admin_prop` attribute set to `true`.
**Group membership mapping** can be achieved by setting the `admin_group_regex` and `user_groups` property.
The `admin_group_regex` property is a regular expression that is matched against the group names of the user.
The user is granted admin access if the regex matches any of the group names.
Example:
```yaml
auth:
oidc:
- provider_name: "oidc1"
# ... other settings
field_map:
user_groups: "groups"
admin_mapping:
admin_group_regex: "^the-admin-group$"
```
The example above will grant admin access to users who are members of the `the-admin-group` group.
### LDAP Authentication
WireGuard Portal supports LDAP authentication. You can use any LDAP server that supports the LDAP protocol, such as Active Directory or OpenLDAP.
Multiple LDAP servers can be configured in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
WireGuard Portal remembers the authentication provider of the user and therefore avoids conflicts between multiple LDAP providers.
To configure LDAP authentication, create a new [`ldap`](../configuration/overview.md#ldap) authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
#### Limiting Login to Specific Users
You can limit the login to specific users by setting the `login_filter` property for LDAP provider. This filter uses the LDAP search filter syntax.
The username can be inserted into the query by placing the `{{login_identifier}}` placeholder in the filter. This placeholder will then be replaced with the username entered by the user during login.
For example, if you want to allow only users with the `objectClass` attribute set to `organizationalPerson` to log in, set the property as follows:
```yaml
auth:
ldap:
- provider_name: "ldap1"
# ... other settings
login_filter: "(&(objectClass=organizationalPerson)(uid={{login_identifier}}))"
```
The `login_filter` should always be designed to return at most one user.
#### Limit Login to Existing Users
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for LDAP providers.
If registration is enabled, new users will be created in the database when they log in for the first time.
#### Admin Mapping
You can map users to admin roles based on their group membership in the LDAP server. To do this, set the `admin_group` and `memberof` property for the provider.
The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin.
All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access.
## UI and API Access
WireGuard Portal provides a web UI and a REST API for user interaction. It is important to secure these interfaces to prevent unauthorized access and data breaches.
### HTTPS
It is recommended to use HTTPS for all communication with the portal to prevent eavesdropping.
Event though, WireGuard Portal supports HTTPS out of the box, it is recommended to use a reverse proxy like Nginx or Traefik to handle SSL termination and other security features.
A detailed explanation is available in the [Reverse Proxy](../getting-started/reverse-proxy.md) section.

View File

@@ -0,0 +1,285 @@
Webhooks allow WireGuard Portal to notify external services about events such as user creation, device changes, or configuration updates. This enables integration with other systems and automation workflows.
When webhooks are configured and a specified event occurs, WireGuard Portal sends an HTTP **POST** request to the configured webhook URL.
The payload contains event-specific data in JSON format.
## Configuration
All available configuration options for webhooks can be found in the [configuration overview](../configuration/overview.md#webhook).
A basic webhook configuration looks like this:
```yaml
webhook:
url: https://your-service.example.com/webhook
```
### Security
Webhooks can be secured by using a shared secret. This secret is included in the `Authorization` header of the webhook request, allowing your service to verify the authenticity of the request.
You can set the shared secret in the webhook configuration:
```yaml
webhook:
url: https://your-service.example.com/webhook
secret: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
```
You should also make sure that your webhook endpoint is secured with HTTPS to prevent eavesdropping and tampering.
## Available Events
WireGuard Portal supports various events that can trigger webhooks. The following events are available:
- `create`: Triggered when a new entity is created.
- `update`: Triggered when an existing entity is updated.
- `delete`: Triggered when an entity is deleted.
- `connect`: Triggered when a user connects to the VPN.
- `disconnect`: Triggered when a user disconnects from the VPN.
The following entity models are supported for webhook events:
- `user`: WireGuard Portal users support creation, update, or deletion events.
- `peer`: Peers support creation, update, or deletion events. Via the `peer_metric` entity, you can also receive connection status updates.
- `peer_metric`: Peer metrics support connection status updates, such as when a peer connects or disconnects.
- `interface`: WireGuard interfaces support creation, update, or deletion events.
## Payload Structure
All webhook events send a JSON payload containing relevant data. The structure of the payload depends on the event type and entity involved.
A common shell structure for webhook payloads is as follows:
```json
{
"event": "create", // The event type, e.g. "create", "update", "delete", "connect", "disconnect"
"entity": "user", // The entity type, e.g. "user", "peer", "peer_metric", "interface"
"identifier": "the-user-identifier", // Unique identifier of the entity, e.g. user ID or peer ID
"payload": {
// The payload of the event, e.g. a Peer model.
// Detailed model descriptions are provided below.
}
}
```
### Payload Models
All payload models are encoded as JSON objects. Fields with empty values might be omitted in the payload.
#### User Payload (entity: `user`)
| JSON Field | Type | Description |
|----------------|-------------|-----------------------------------|
| CreatedBy | string | Creator identifier |
| UpdatedBy | string | Last updater identifier |
| CreatedAt | time.Time | Time of creation |
| UpdatedAt | time.Time | Time of last update |
| Identifier | string | Unique user identifier |
| Email | string | User email |
| Source | string | Authentication source |
| ProviderName | string | Name of auth provider |
| IsAdmin | bool | Whether user has admin privileges |
| Firstname | string | User's first name (optional) |
| Lastname | string | User's last name (optional) |
| Phone | string | Contact phone number (optional) |
| Department | string | User's department (optional) |
| Notes | string | Additional notes (optional) |
| Disabled | *time.Time | When user was disabled |
| DisabledReason | string | Reason for deactivation |
| Locked | *time.Time | When user account was locked |
| LockedReason | string | Reason for being locked |
#### Peer Payload (entity: `peer`)
| JSON Field | Type | Description |
|----------------------|------------|----------------------------------------|
| CreatedBy | string | Creator identifier |
| UpdatedBy | string | Last updater identifier |
| CreatedAt | time.Time | Creation timestamp |
| UpdatedAt | time.Time | Last update timestamp |
| Endpoint | string | Peer endpoint address |
| EndpointPublicKey | string | Public key of peer endpoint |
| AllowedIPsStr | string | Allowed IPs |
| ExtraAllowedIPsStr | string | Extra allowed IPs |
| PresharedKey | string | Pre-shared key for encryption |
| PersistentKeepalive | int | Keepalive interval in seconds |
| DisplayName | string | Display name of the peer |
| Identifier | string | Unique identifier |
| UserIdentifier | string | Associated user ID (optional) |
| InterfaceIdentifier | string | Interface this peer is attached to |
| Disabled | *time.Time | When the peer was disabled |
| DisabledReason | string | Reason for being disabled |
| ExpiresAt | *time.Time | Expiration date |
| Notes | string | Notes for this peer |
| AutomaticallyCreated | bool | Whether peer was auto-generated |
| PrivateKey | string | Peer private key |
| PublicKey | string | Peer public key |
| InterfaceType | string | Type of the peer interface |
| Addresses | []string | IP addresses |
| CheckAliveAddress | string | Address used for alive checks |
| DnsStr | string | DNS servers |
| DnsSearchStr | string | DNS search domains |
| Mtu | int | MTU (Maximum Transmission Unit) |
| FirewallMark | uint32 | Firewall mark (optional) |
| RoutingTable | string | Custom routing table (optional) |
| PreUp | string | Command before bringing up interface |
| PostUp | string | Command after bringing up interface |
| PreDown | string | Command before bringing down interface |
| PostDown | string | Command after bringing down interface |
#### Interface Payload (entity: `interface`)
| JSON Field | Type | Description |
|----------------------------|------------|----------------------------------------|
| CreatedBy | string | Creator identifier |
| UpdatedBy | string | Last updater identifier |
| CreatedAt | time.Time | Creation timestamp |
| UpdatedAt | time.Time | Last update timestamp |
| Identifier | string | Unique identifier |
| PrivateKey | string | Private key for the interface |
| PublicKey | string | Public key for the interface |
| ListenPort | int | Listening port |
| Addresses | []string | IP addresses |
| DnsStr | string | DNS servers |
| DnsSearchStr | string | DNS search domains |
| Mtu | int | MTU (Maximum Transmission Unit) |
| FirewallMark | uint32 | Firewall mark |
| RoutingTable | string | Custom routing table |
| PreUp | string | Command before bringing up interface |
| PostUp | string | Command after bringing up interface |
| PreDown | string | Command before bringing down interface |
| PostDown | string | Command after bringing down interface |
| SaveConfig | bool | Whether to save config to file |
| DisplayName | string | Human-readable name |
| Type | string | Type of interface |
| DriverType | string | Driver used |
| Disabled | *time.Time | When the interface was disabled |
| DisabledReason | string | Reason for being disabled |
| PeerDefNetworkStr | string | Default peer network configuration |
| PeerDefDnsStr | string | Default peer DNS servers |
| PeerDefDnsSearchStr | string | Default peer DNS search domains |
| PeerDefEndpoint | string | Default peer endpoint |
| PeerDefAllowedIPsStr | string | Default peer allowed IPs |
| PeerDefMtu | int | Default peer MTU |
| PeerDefPersistentKeepalive | int | Default keepalive value |
| PeerDefFirewallMark | uint32 | Default firewall mark for peers |
| PeerDefRoutingTable | string | Default routing table for peers |
| PeerDefPreUp | string | Default peer pre-up command |
| PeerDefPostUp | string | Default peer post-up command |
| PeerDefPreDown | string | Default peer pre-down command |
| PeerDefPostDown | string | Default peer post-down command |
#### Peer Metrics Payload (entity: `peer_metric`)
| JSON Field | Type | Description |
|------------|------------|----------------------------|
| Status | PeerStatus | Current status of the peer |
| Peer | Peer | Peer data |
`PeerStatus` sub-structure:
| JSON Field | Type | Description |
|------------------|------------|------------------------------|
| UpdatedAt | time.Time | Time of last status update |
| IsConnected | bool | Is peer currently connected |
| IsPingable | bool | Can peer be pinged |
| LastPing | *time.Time | Time of last successful ping |
| BytesReceived | uint64 | Bytes received from peer |
| BytesTransmitted | uint64 | Bytes sent to peer |
| Endpoint | string | Last known endpoint |
| LastHandshake | *time.Time | Last successful handshake |
| LastSessionStart | *time.Time | Time the last session began |
### Example Payloads
The following payload is an example of a webhook event when a peer connects to the VPN:
```json
{
"event": "connect",
"entity": "peer_metric",
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"payload": {
"Status": {
"UpdatedAt": "2025-06-27T22:20:08.734900034+02:00",
"IsConnected": true,
"IsPingable": false,
"BytesReceived": 212,
"BytesTransmitted": 2884,
"Endpoint": "10.55.66.77:58756",
"LastHandshake": "2025-06-27T22:19:46.580842776+02:00",
"LastSessionStart": "2025-06-27T22:19:46.580842776+02:00"
},
"Peer": {
"CreatedBy": "admin@wgportal.local",
"UpdatedBy": "admin@wgportal.local",
"CreatedAt": "2025-06-26T21:43:49.251839574+02:00",
"UpdatedAt": "2025-06-27T22:18:39.67763985+02:00",
"Endpoint": "10.55.66.1:51820",
"EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=",
"AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64",
"ExtraAllowedIPsStr": "",
"PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=",
"PersistentKeepalive": 16,
"DisplayName": "Peer Fb5TaziA",
"Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"UserIdentifier": "admin@wgportal.local",
"InterfaceIdentifier": "wgTesting",
"AutomaticallyCreated": false,
"PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=",
"PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"InterfaceType": "client",
"Addresses": [
"10.11.12.10/32",
"fdfd:d3ad:c0de:1234::a/128"
],
"CheckAliveAddress": "",
"DnsStr": "",
"DnsSearchStr": "",
"Mtu": 1420
}
}
}
```
Here is another example of a webhook event when a peer is updated:
```json
{
"event": "update",
"entity": "peer",
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"payload": {
"CreatedBy": "admin@wgportal.local",
"UpdatedBy": "admin@wgportal.local",
"CreatedAt": "2025-06-26T21:43:49.251839574+02:00",
"UpdatedAt": "2025-06-27T22:18:39.67763985+02:00",
"Endpoint": "10.55.66.1:51820",
"EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=",
"AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64",
"ExtraAllowedIPsStr": "",
"PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=",
"PersistentKeepalive": 16,
"DisplayName": "Peer Fb5TaziA",
"Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"UserIdentifier": "admin@wgportal.local",
"InterfaceIdentifier": "wgTesting",
"AutomaticallyCreated": false,
"PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=",
"PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"InterfaceType": "client",
"Addresses": [
"10.11.12.10/32",
"fdfd:d3ad:c0de:1234::a/128"
],
"CheckAliveAddress": "",
"DnsStr": "",
"DnsSearchStr": "",
"Mtu": 1420
}
}
```

4
docs/index.md Normal file
View File

@@ -0,0 +1,4 @@
---
template: layouts/home.html
title: WireGuard Portal
---

View File

@@ -0,0 +1,49 @@
/* This file is used for extra styles that are not part of the theme */
span.title {
font-weight: bold;
}
span.em {
font-weight: bold;
}
.separator {
border-bottom: 1px solid #e3e8ee;
}
a.field {
font-weight: 600;
/* color: #3c4257; */
font-size: .8rem;
}
span.parent-field {
font-weight: 600;
color:#a3acb9;
font-size: .85em;
}
span.type {
color: #8792a2;
font-size: .7rem;
margin-right: 4px;
}
span.version {
color: #8792a2;
font-size: .7rem;
float: right;
}
span.faint {
color: #8792a2;
}
.md-social__link svg {
fill: rgb(61, 61, 61);
}
.md-tabs__link {
font-size: 0.8rem;
}

View File

@@ -0,0 +1,433 @@
{% extends "main.html" %}
<!-- Render hero under tabs -->
{% block tabs %}
{{ super() }}
<!-- Additional styles for landing page -->
<style>
/* Apply box shadow on smaller screens that don't display tabs */
@media only screen and (max-width: 1220px) {
.md-header {
box-shadow: 0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);
transition: color 250ms,background-color 250ms,box-shadow 250ms;
}
}
/* Hide main content for now */
.md-content {
display: none;
}
/* Hide table of contents */
@media screen and (min-width: 60em) {
.md-sidebar--secondary {
display: none;
}
}
/* Hide navigation */
@media screen and (min-width: 76.25em) {
.md-sidebar--primary {
display: none;
}
}
/* Get started button */
.md-typeset .md-button--primary {
color: var(--md-primary-fg-color);
background-color: var(--md-primary-bg-color);
border-color: var(--md-primary-bg-color);
}
.md-typeset .md-button--primary:hover {
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color);
border-color: var(--md-primary-bg-color);
}
.tx-hero {
max-width: 700px;
display: flex;
padding: .4rem;
margin: 0 auto;
text-align: center;
}
.tx-hero h1 {
font-weight: 700;
font-size: 38px;
line-height: 46px;
color: rgb(38, 38, 38);
}
.tx-hero p {
color: rgb(92, 92, 92);
font-weight: 400;
font-size: 20px;
line-height: 32px;
}
.tx-hero__image {
max-width: 1000px;
min-width: 600px;
width: 100%;
height: auto;
margin: 0 auto;
display: flex;
align-items: stretch;
}
.tx-hero__image img {
width: 100%;
height: 100%;
min-width: 0;
}
/* Secondary content styles */
.secondary-section {
background: rgb(245, 245, 245) none repeat scroll 0% 0%;
border-top: 1px solid rgb(222, 222, 222);
border-bottom: 1px solid rgb(222, 222, 222)
}
@media screen and (max-width: 1012px) {
.secondary-section {
display: block;
}
}
.secondary-section .g {
position: relative;
margin-left: auto;
margin-right: auto;
padding: 0px 40px;
max-width: 1280px;
}
.secondary-section .g .section {
font-size: 18px;
font-weight: 400;
line-height: 30px;
letter-spacing: normal;
padding: 88px 0px 116px;
}
.secondary-section .g .section.follow {
padding-top: 0px;
}
.secondary-section .g .section .component-wrapper {
display: flex;
-moz-box-align: center;
align-items: center;
}
@media screen and (max-width: 1012px) {
.secondary-section .g .section .component-wrapper {
display: block;
}
}
.secondary-section .g .section .component-wrapper h3 {
color: rgb(38, 38, 38);
font-size: 36px;
font-weight: 700;
line-height: 46px;
letter-spacing: normal;
margin-bottom: 12px;
}
.secondary-section .g .section .component-wrapper h4 {
color: rgb(38, 38, 38);
}
.secondary-section .g .section .component-wrapper p {
color: rgb(92, 92, 92);
font-size: 18px;
font-weight: 400;
line-height: 30px;
letter-spacing: normal;
margin-bottom: 16px;
}
.secondary-section .g .section .component-wrapper .image-wrapper {
margin-bottom: 12px;
overflow: hidden;
border-radius: 8px;
margin-top: 48px;
border: 1px solid rgb(222, 222, 222);
box-shadow: rgba(202, 202, 202, 0.15) 0px 0px 0px 6px;
max-width: 600px;
width: 100%;
height: auto;
margin: 0 auto;
display: flex;
align-items: stretch;
}
.image-wrapper img {
width: 100%;
height: 100%;
min-width: 0;
}
.secondary-section .g .section .component-wrapper .first-column {
padding-right: 100px;
flex: 0 1 auto;
height: auto;
width: 50%;
}
@media screen and (max-width: 1012px) {
.secondary-section .g .section .component-wrapper .first-column {
padding-right: 0px;
width: 100%;
margin-bottom: 32px;
}
}
.secondary-section .g .section .component-wrapper .second-column {
flex: 0 1 auto;
height: auto;
width: 50%;
}
@media screen and (max-width: 1012px) {
.secondary-section .g .section .component-wrapper .second-column {
width: 100%;
margin-bottom: 32px;
}
}
.secondary-section .g .section .component-wrapper .responsive-grid {
display: grid;
width: 100%;
grid-template-columns: repeat(1, 1fr);
gap: 2rem;
}
@media screen and (min-width: 64rem) {
.secondary-section .g .section .component-wrapper .responsive-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.secondary-section .g .section .component-wrapper .responsive-grid a.card-wrapper {
text-decoration: none;
transition: none;
background: none;
padding: 0;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card {
position: relative;
background-color: #fff none repeat scroll 0% 0%;
padding: 1.5rem;
display: flex;
flex-direction: row;
-moz-box-align: center;
align-items: center;
height: 100%;
-moz-box-pack: start;
justify-content: flex-start;
box-shadow: rgba(0, 0, 0, 0.09) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.15) 0px 0.25rem 0.5rem 0px;
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) 0s;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card:hover {
box-shadow: rgba(0, 0, 0, 0.2) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.26) 0px 0.25rem 0.5rem 0px;
}
@media screen and (min-width: 75rem) {
.secondary-section .g .section .component-wrapper .responsive-grid .card {
padding: 2rem 2.5rem;
}
}
@media screen and (min-width: 36rem) {
.secondary-section .g .section .component-wrapper .responsive-grid .card {
padding: 1rem 1.5rem;
}
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .logo {
margin-right: 0.75rem;
width: 1.2rem;
min-width: 1.2rem;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content {
display: flex;
flex: 1 1 0%;
flex-direction: column;
width: 100%;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content h5 {
color: rgb(61, 61, 61);
margin: 0;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content p {
margin-top: 0.25em;
margin-bottom: 0;
color: rgb(92, 92, 92);
font-size: 0.65rem;
font-weight: 300;
line-height: normal;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content code {
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
padding: 2px 6px;
border-radius: 4px;
}
.component-wrapper span.em {
color: rgb(61, 61, 61);
}
.component-wrapper a {
transition: color 125ms;
color: rgb(61, 61, 61);
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
padding: 2px 6px;
margin: 0px 1px;
border-radius: 4px;
display: inline;
cursor: pointer;
font-weight: 600;
}
.component-wrapper a:hover {
color: var(--md-typeset-a-color);
background: var(--md-accent-fg-color--transparent);
}
</style>
<!-- Hero for landing page -->
<div class="md-container tx-hero">
<div class="md-grid md-typeset">
<div class="md-main__inner">
<div>
<h1>A beautiful and simple UI to manage your WireGuard peers and interfaces</h1>
<p>WireGuard Portal is an open source web-based user interface that makes it easy to setup and manage
WireGuard VPN connections. It's built on top of WireGuard's official <span class="em">wgctrl</span> library.</p>
</p>
<a
href="documentation/overview/"
title="Get Started"
class="md-button md-button--primary"
>
Get started
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" style="margin-left:2px"><path d="M1 5.16772H9.5M9.5 5.16772L6.5 1.66772M9.5 5.16772L6.5 8.66772" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</a>
</div>
</div>
</div>
</div>
<div class="md-container">
<div class="tx-hero__image">
<img
src="{{config.site_url}}/assets/images/screenshot.png"
alt=""
draggable="false"
>
</div>
</div>
<div class="md-container secondary-section">
<div class="g">
<!-- Architecture as building blocks -->
<div class="section">
<div class="component-wrapper">
<div class="first-column">
<h3>More information about WireGuard</h3>
<p>
WireGuard® is an extremely <span class="em">simple</span> yet <span class="em">fast</span> and modern
VPN that utilizes <span class="em">state-of-the-art cryptography</span>.
</p>
<p>
WireGuard uses state-of-the-art <a href="https://www.wireguard.com/protocol/">cryptography</a> and still
manages to be as easy to configure and deploy as SSH.
A combination of extremely high-speed cryptographic primitives and the fact that WireGuard lives inside
the Linux kernel means that secure networking can be very high-speed.
It is suitable for both small embedded devices like smartphones and fully loaded backbone routers.
</p>
</div>
<div class="second-column">
<div class="image-wrapper">
<img
src="{{config.site_url}}/assets/images/wg-tool.png"
alt=""
draggable="false"
>
</div>
</div>
</div>
<div class="component-wrapper" style="display: block;">
<h4>Explore official documentation</h4>
<!-- Arch as code -->
<div class="responsive-grid">
<a class="card-wrapper" href="https://www.wireguard.com/">
<div class="card">
<div class="logo">
<span class="twemoji">
{% include ".icons/octicons/file-code-24.svg" %}
</span>
</div>
<div class="card-content">
<h5>Official Website</h5>
<p>
If you'd like a general conceptual overview of what WireGuard is about, read onward here.
</p>
</div>
</div>
</a>
<!-- Networking -->
<a class="card-wrapper" href="https://www.wireguard.com/protocol/">
<div class="card">
<div class="logo">
<span class="twemoji">
{% include ".icons/fontawesome/solid/network-wired.svg" %}
</span>
</div>
<div class="card-content">
<h5>Protocol & Cryptography</h5>
<p>
WireGuard uses state-of-the-art cryptography, like the Noise protocol framework, Curve25519, ChaCha20, Poly1305, BLAKE2, SipHash24, HKDF, and secure trusted constructions.
</p>
</div>
</div>
</a>
<!-- Customize -->
<a class="card-wrapper" href="https://www.wireguard.com/install/">
<div class="card">
<div class="logo">
<span class="twemoji">
{% include ".icons/fontawesome/solid/puzzle-piece.svg" %}
</span>
</div>
<div class="card-content">
<h5>Client Installation</h5>
<p>
You may progress to installation and reading the quickstart instructions on how to use it.
</p>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- Content -->
{% block content %}{% endblock %}
<!-- Application footer -->
{% block footer %}
{{ super() }}
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block extrahead %}
{% if page and page.meta and page.meta.title %}
<meta property="og:title" content="{{ page.meta.title }}" />
{% endif %}
{% if page and page.meta and page.meta.image %}
<meta property="og:image" content="{{ page.meta.image }}" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="{{ page.meta.image_width }}" />
<meta property="og:image:height" content="{{ page.meta.image_height }}" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:title" content="{{ page.meta.twitter_title }}" />
<meta property="twitter:image" content="{{ page.meta.image }}" />
<meta property="twitter:image:alt" content="{{ page.meta.image_alt }}" />
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% import "partials/language.html" as lang with context %}
<!-- Application footer -->
<footer class="md-footer">
<!-- Further information -->
<div class="md-footer-meta md-typeset" style="background-color: #fff;">
<div class="md-footer-meta__inner md-grid" style="background-color: #fff;">
<!-- Copyright and theme information -->
<div class="md-footer-copyright">
{% if config.copyright %}
<div class="md-footer-copyright__highlight" style="color: rgb(38, 38, 38);">
{{ config.copyright }}
</div>
{% endif %}
<div style="color: rgb(38, 38, 38);">
Made with
<a
href="https://squidfunk.github.io/mkdocs-material/"
target="_blank" rel="noopener"
style="color: black;"
>
Material for MkDocs
</a>
</div>
</div>
<!-- Social links -->
{% include "partials/social.html" %}
</div>
</div>
</footer>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-bs-theme="light">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link href="/favicon.ico" rel="icon" /> <link href="/favicon.ico" rel="icon" />
@@ -24,7 +24,7 @@
<div id="toasts"></div> <div id="toasts"></div>
<!-- main application --> <!-- main application -->
<div id="app"></div> <div id="app" class="d-flex flex-column flex-grow-1"></div>
<!-- vue teleport will add modals and dialogs here --> <!-- vue teleport will add modals and dialogs here -->
<div id="modals"></div> <div id="modals"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -8,24 +8,28 @@
"preview": "vite preview --port 5050" "preview": "vite preview --port 5050"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0", "@fontsource/nunito-sans": "^5.2.5",
"@kyvg/vue3-notification": "^2.9.1", "@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.0", "@simplewebauthn/browser": "^13.1.0",
"bootswatch": "^5.3.0", "@vojtechlanka/vue-tags-input": "^3.1.1",
"flag-icons": "^6.7.0", "bootstrap": "^5.3.7",
"is-cidr": "^5.0.3", "bootswatch": "^5.3.7",
"is-ip": "^5.0.0", "flag-icons": "^7.3.2",
"pinia": "^2.1.4", "ip-address": "^10.0.1",
"prismjs": "^1.29.0", "is-cidr": "^5.1.1",
"vue": "^3.3.4", "is-ip": "^5.0.1",
"vue-i18n": "^9.2.2", "pinia": "^3.0.2",
"prismjs": "^1.30.0",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.2", "vue-router": "^4.5.0"
"vue3-tags-input": "^1.0.12"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^5.2.3",
"vite": "^4.3.9" "sass-embedded": "^1.86.3",
"vite": "6.3.4"
} }
} }

View File

@@ -1,9 +1,10 @@
<script setup> <script setup>
import { RouterLink, RouterView } from 'vue-router'; import { RouterLink, RouterView } from 'vue-router';
import {computed, getCurrentInstance, onMounted, ref} from "vue"; import { computed, getCurrentInstance, onMounted, ref } from "vue";
import {authStore} from "./stores/auth"; import { authStore } from "./stores/auth";
import {securityStore} from "./stores/security"; import { securityStore } from "./stores/security";
import {settingsStore} from "@/stores/settings"; import { settingsStore } from "@/stores/settings";
import { Notifications } from "@kyvg/vue3-notification";
const appGlobal = getCurrentInstance().appContext.config.globalProperties const appGlobal = getCurrentInstance().appContext.config.globalProperties
const auth = authStore() const auth = authStore()
@@ -13,6 +14,10 @@ const settings = settingsStore()
onMounted(async () => { onMounted(async () => {
console.log("Starting WireGuard Portal frontend..."); console.log("Starting WireGuard Portal frontend...");
// restore theme from localStorage
const theme = localStorage.getItem('wgTheme') || 'light';
document.documentElement.setAttribute('data-bs-theme', theme);
await sec.LoadSecurityProperties(); await sec.LoadSecurityProperties();
await auth.LoadProviders(); await auth.LoadProviders();
@@ -39,19 +44,55 @@ const switchLanguage = function (lang) {
} }
} }
const switchTheme = function (theme) {
if (document.documentElement.getAttribute('data-bs-theme') !== theme) {
localStorage.setItem('wgTheme', theme);
document.documentElement.setAttribute('data-bs-theme', theme);
}
}
const languageFlag = computed(() => { const languageFlag = computed(() => {
// `this` points to the component instance // `this` points to the component instance
let lang = appGlobal.$i18n.locale.toLowerCase(); let lang = appGlobal.$i18n.locale.toLowerCase();
if (lang === "en") { if (!appGlobal.$i18n.availableLocales.includes(lang)) {
lang = "us"; lang = appGlobal.$i18n.fallbackLocale;
} }
return "fi-" + lang; const langMap = {
en: "us",
pt: "pt",
uk: "ua",
zh: "cn",
ko: "kr",
es: "es",
};
return "fi-" + (langMap[lang] || lang);
}) })
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME); const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
const wgVersion = ref(WGPORTAL_VERSION); const wgVersion = ref(WGPORTAL_VERSION);
const currentYear = ref(new Date().getFullYear()) const currentYear = ref(new Date().getFullYear())
const userDisplayName = computed(() => {
let displayName = "Unknown";
if (auth.IsAuthenticated) {
if (auth.User.Firstname === "" && auth.User.Lastname === "") {
displayName = auth.User.Identifier;
} else if (auth.User.Firstname === "" && auth.User.Lastname !== "") {
displayName = auth.User.Lastname;
} else if (auth.User.Firstname !== "" && auth.User.Lastname === "") {
displayName = auth.User.Firstname;
} else if (auth.User.Firstname !== "" && auth.User.Lastname !== "") {
displayName = auth.User.Firstname + " " + auth.User.Lastname;
}
}
// pad string to 20 characters so that the menu is always the same size on desktop
if (displayName.length < 20 && window.innerWidth > 992) {
displayName = displayName.padStart(20, "\u00A0");
}
return displayName;
})
</script> </script>
<template> <template>
@@ -59,7 +100,7 @@ const currentYear = ref(new Date().getFullYear())
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/"><img alt="WireGuard Portal" src="/img/header-logo.png" /></a> <a class="navbar-brand" href="/"><img :alt="companyName" src="/img/header-logo.png" /></a>
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" <button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button"> data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@@ -76,24 +117,43 @@ const currentYear = ref(new Date().getFullYear())
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item"> <li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink> <RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
</li> </li>
<li class="nav-item">
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
</li>
</ul> </ul>
<div class="navbar-nav d-flex justify-content-end"> <div class="navbar-nav d-flex justify-content-end">
<div v-if="auth.IsAuthenticated" class="nav-item dropdown"> <div v-if="auth.IsAuthenticated" class="nav-item dropdown">
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" <a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a> href="#" role="button">{{ userDisplayName }}</a>
<div class="dropdown-menu"> <div class="dropdown-menu">
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink> <RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly') || settings.Setting('WebAuthnEnabled')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
<RouterLink :to="{ name: 'audit' }" class="dropdown-item" v-if="auth.IsAdmin"><i class="fas fa-file-shield"></i> {{ $t('menu.audit') }}</RouterLink>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"> <a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
<i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}
</a>
</div> </div>
</div> </div>
<div v-if="!auth.IsAuthenticated" class="nav-item"> <div v-if="!auth.IsAuthenticated" class="nav-item">
<RouterLink :to="{ name: 'login' }" class="nav-link"> <RouterLink :to="{ name: 'login' }" class="nav-link"><i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}</RouterLink>
<i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }} </div>
</RouterLink> <div class="nav-item dropdown" data-bs-theme="light">
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="theme-menu" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme">
<i class="fa-solid fa-circle-half-stroke"></i>
<span class="d-lg-none ms-2">Toggle theme</span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('light')" aria-pressed="false">
<i class="fa-solid fa-sun"></i><span class="ms-2">Light</span>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('dark')" aria-pressed="true">
<i class="fa-solid fa-moon"></i><span class="ms-2">Dark</span>
</button>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
@@ -111,10 +171,20 @@ const currentYear = ref(new Date().getFullYear())
<div class="col-6 text-end"> <div class="col-6 text-end">
<div :aria-label="$t('menu.lang')" class="btn-group" role="group"> <div :aria-label="$t('menu.lang')" class="btn-group" role="group">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0" data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button> <button aria-expanded="false" aria-haspopup="true" class="btn flag-button pe-0"
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style=""> <div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a> <a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ko')"><span class="fi fi-kr"></span> 한국어</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('pt')"><span class="fi fi-pt"></span> Português</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('es')"><span class="fi fi-es"></span> Español</a>
</div> </div>
</div> </div>
</div> </div>
@@ -125,4 +195,30 @@ const currentYear = ref(new Date().getFullYear())
</template> </template>
<style> <style>
</style> .flag-button:active,.flag-button:hover,.flag-button:focus,.flag-button:checked,.flag-button:disabled,.flag-button:not(:disabled) {
border: 1px solid transparent!important;
}
[data-bs-theme=dark] .form-select {
color: #0c0c0c!important;
background-color: #c1c1c1!important;
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")!important;
}
[data-bs-theme=dark] .form-control {
color: #0c0c0c!important;
background-color: #c1c1c1!important;
}
[data-bs-theme=dark] .form-control:focus {
color: #0c0c0c!important;
background-color: #c1c1c1!important;
}
[data-bs-theme=dark] .badge.bg-light {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
color: var(--bs-badge-color)!important;
}
[data-bs-theme=dark] span.input-group-text {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
color: var(--bs-badge-color)!important;
}
</style>

View File

@@ -1,5 +1,107 @@
a.disabled { a.disabled {
pointer-events: none; pointer-events: none;
cursor: default; cursor: default;
color: #888888; color: #888888;
}
.text-wrap {
overflow-break: anywhere;
}
.asc::after {
content: " ↑";
}
.desc::after {
content: " ↓";
}
/* style the background and the text color of the input ... */
.vue-tags-input {
max-width: 100% !important;
background-color: #f7f7f9 !important;
padding: 0 0;
}
.vue-tags-input .ti-input {
padding: 0 0;
border: none !important;
transition: border-bottom 200ms ease;
}
.vue-tags-input .ti-new-tag-input {
background: transparent;
color: var(--bs-body-color);
padding: 0.75rem 1.5rem !important;
}
/* style the placeholders color across all browser */
.vue-tags-input ::-webkit-input-placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input .ti-input::placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input ::-moz-placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input :-ms-input-placeholder {
color: var(--bs-secondary-color);
}
.vue-tags-input :-moz-placeholder {
color: var(--bs-secondary-color);
}
/* default styles for all the tags */
.vue-tags-input .ti-tag {
position: relative;
background: #ffffff;
border: 2px solid var(--bs-body-color);
margin: 6px;
color: var(--bs-body-color);
}
[data-bs-theme=dark] .vue-tags-input .ti-tag {
position: relative;
background: #3c3c3c;
border: 2px solid var(--bs-body-color);
margin: 6px;
color: var(--bs-body-color);
}
/* the styles if a tag is invalid */
.vue-tags-input .ti-tag.ti-invalid {
background-color: #e88a74;
}
/* if the user input is invalid, the input color should be red */
.vue-tags-input .ti-new-tag-input.ti-invalid {
color: #e88a74;
}
/* if a tag or the user input is a duplicate, it should be crossed out */
.vue-tags-input .ti-duplicate span,
.vue-tags-input .ti-new-tag-input.ti-duplicate {
text-decoration: line-through;
}
/* if the user presses backspace, the complete tag should be crossed out, to mark it for deletion */
.vue-tags-input .ti-tag:after {
transition: transform .2s;
position: absolute;
content: '';
height: 2px;
width: 108%;
left: -4%;
top: calc(50% - 1px);
background-color: #000;
transform: scaleX(0);
}
.vue-tags-input .ti-deletion-mark:after {
transform: scaleX(1);
} }

View File

@@ -0,0 +1,20 @@
// disable external web fonts
$web-font-path: false;
@import "bootswatch/dist/lux/variables";
@import "bootstrap/scss/bootstrap";
@import "bootswatch/dist/lux/bootswatch";
// fix strange border width bug in bootswatch 5.3
:root {
--bs-border-width: 1px;
}
// for future use, once bootswatch supports @use
/*
@use "bootswatch/dist/lux/_variables.scss" as lux-vars with (
$web-font-path: false
);
@use "bootstrap/scss/bootstrap" as bs;
@use "bootswatch/dist/lux/_bootswatch.scss" as lux-theme;
*/

View File

@@ -4,17 +4,19 @@ import {interfaceStore} from "@/stores/interfaces";
import {computed, ref, watch} from "vue"; import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from 'vue3-tags-input'; import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators'; import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import {isIP} from 'is-ip'; import {isIP} from 'is-ip';
import { freshInterface } from '@/helpers/models'; import { freshInterface } from '@/helpers/models';
import {peerStore} from "@/stores/peers"; import {peerStore} from "@/stores/peers";
import {settingsStore} from "@/stores/settings";
const { t } = useI18n() const { t } = useI18n()
const interfaces = interfaceStore() const interfaces = interfaceStore()
const peers = peerStore() const peers = peerStore()
const settings = settingsStore()
const props = defineProps({ const props = defineProps({
interfaceId: String, interfaceId: String,
@@ -38,7 +40,36 @@ const title = computed(() => {
return t("modals.interface-edit.headline-new") return t("modals.interface-edit.headline-new")
}) })
const currentTags = ref({
Addresses: "",
Dns: "",
DnsSearch: "",
PeerDefNetwork: "",
PeerDefAllowedIPs: "",
PeerDefDns: "",
PeerDefDnsSearch: ""
})
const formData = ref(freshInterface()) const formData = ref(freshInterface())
const isSaving = ref(false)
const isDeleting = ref(false)
const isApplyingDefaults = ref(false)
const isBackendValid = computed(() => {
if (!props.visible || !selectedInterface.value) {
return true // if modal is not visible or no interface is selected, we don't care about backend validity
}
let backendId = selectedInterface.value.Backend
let valid = false
let availableBackends = settings.Setting('AvailableBackends') || []
availableBackends.forEach(backend => {
if (backend.Id === backendId) {
valid = true
}
})
return valid
})
// functions // functions
@@ -52,6 +83,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = interfaces.Prepared.Identifier formData.value.Identifier = interfaces.Prepared.Identifier
formData.value.DisplayName = interfaces.Prepared.DisplayName formData.value.DisplayName = interfaces.Prepared.DisplayName
formData.value.Mode = interfaces.Prepared.Mode formData.value.Mode = interfaces.Prepared.Mode
formData.value.Backend = interfaces.Prepared.Backend
formData.value.PublicKey = interfaces.Prepared.PublicKey formData.value.PublicKey = interfaces.Prepared.PublicKey
formData.value.PrivateKey = interfaces.Prepared.PrivateKey formData.value.PrivateKey = interfaces.Prepared.PrivateKey
@@ -90,6 +122,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = selectedInterface.value.Identifier formData.value.Identifier = selectedInterface.value.Identifier
formData.value.DisplayName = selectedInterface.value.DisplayName formData.value.DisplayName = selectedInterface.value.DisplayName
formData.value.Mode = selectedInterface.value.Mode formData.value.Mode = selectedInterface.value.Mode
formData.value.Backend = selectedInterface.value.Backend
formData.value.PublicKey = selectedInterface.value.PublicKey formData.value.PublicKey = selectedInterface.value.PublicKey
formData.value.PrivateKey = selectedInterface.value.PrivateKey formData.value.PrivateKey = selectedInterface.value.PrivateKey
@@ -137,97 +170,99 @@ function close() {
function handleChangeAddresses(tags) { function handleChangeAddresses(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if(isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.Addresses = tags formData.value.Addresses = tags.map(tag => tag.text)
} }
} }
function handleChangeDns(tags) { function handleChangeDns(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(!isIP(tag)) { if(!isIP(tag.text)) {
validInput = false validInput = false
notify({ notify({
title: "Invalid IP", title: "Invalid IP",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.Dns = tags formData.value.Dns = tags.map(tag => tag.text)
} }
} }
function handleChangeDnsSearch(tags) { function handleChangeDnsSearch(tags) {
formData.value.DnsSearch = tags formData.value.DnsSearch = tags.map(tag => tag.text)
} }
function handleChangePeerDefNetwork(tags) { function handleChangePeerDefNetwork(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if(isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.PeerDefNetwork = tags formData.value.PeerDefNetwork = tags.map(tag => tag.text)
} }
} }
function handleChangePeerDefAllowedIPs(tags) { function handleChangePeerDefAllowedIPs(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if(isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.PeerDefAllowedIPs = tags formData.value.PeerDefAllowedIPs = tags.map(tag => tag.text)
} }
} }
function handleChangePeerDefDns(tags) { function handleChangePeerDefDns(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(!isIP(tag)) { if(!isIP(tag.text)) {
validInput = false validInput = false
notify({ notify({
title: "Invalid IP", title: "Invalid IP",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if(validInput) {
formData.value.PeerDefDns = tags formData.value.PeerDefDns = tags.map(tag => tag.text)
} }
} }
function handleChangePeerDefDnsSearch(tags) { function handleChangePeerDefDnsSearch(tags) {
formData.value.PeerDefDnsSearch = tags formData.value.PeerDefDnsSearch = tags.map(tag => tag.text)
} }
async function save() { async function save() {
if (isSaving.value) return
isSaving.value = true
try { try {
if (props.interfaceId!=='#NEW#') { if (props.interfaceId!=='#NEW#') {
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value) await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
@@ -242,6 +277,8 @@ async function save() {
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isSaving.value = false
} }
} }
@@ -250,6 +287,8 @@ async function applyPeerDefaults() {
return; // do nothing for new interfaces return; // do nothing for new interfaces
} }
if (isApplyingDefaults.value) return
isApplyingDefaults.value = true
try { try {
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value) await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
@@ -267,10 +306,14 @@ async function applyPeerDefaults() {
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isApplyingDefaults.value = false
} }
} }
async function del() { async function del() {
if (isDeleting.value) return
isDeleting.value = true
try { try {
await interfaces.DeleteInterface(selectedInterface.value.Identifier) await interfaces.DeleteInterface(selectedInterface.value.Identifier)
close() close()
@@ -281,6 +324,8 @@ async function del() {
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isDeleting.value = false
} }
} }
@@ -305,13 +350,22 @@ async function del() {
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text"> <input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
</div> </div>
<div class="form-group"> <div class="row">
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label> <div class="form-group col-md-6">
<select v-model="formData.Mode" class="form-select"> <label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option> <select v-model="formData.Mode" class="form-select">
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option> <option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option> <option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
</select> <option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4" for="ifaceBackendSelector">{{ $t('modals.interface-edit.backend.label') }}</label>
<select id="ifaceBackendSelector" v-model="formData.Backend" class="form-select" aria-describedby="backendHelp">
<option v-for="backend in settings.Setting('AvailableBackends')" :value="backend.Id">{{ backend.Id === 'local' ? $t(backend.Name) : backend.Name }}</option>
</select>
<small v-if="!isBackendValid" id="backendHelp" class="form-text text-warning">{{ $t('modals.interface-edit.backend.invalid-label') }}</small>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
@@ -322,22 +376,26 @@ async function del() {
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend> <legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email"> <input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="text">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email"> <input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="text">
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend> <legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Addresses" <vue-tags-input class="form-control" v-model="currentTags.Addresses"
:placeholder="$t('modals.interface-edit.ip.placeholder')" :tags="formData.Addresses.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.ip.placeholder')"
:validate="validateCIDR" :validation="validateCIDR()"
@on-tags-changed="handleChangeAddresses"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeAddresses"/>
</div> </div>
<div v-if="formData.Mode==='server'" class="form-group"> <div v-if="formData.Mode==='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
@@ -345,31 +403,41 @@ async function del() {
</div> </div>
<div v-if="formData.Mode!=='server'" class="form-group"> <div v-if="formData.Mode!=='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Dns" <vue-tags-input class="form-control" v-model="currentTags.Dns"
:placeholder="$t('modals.interface-edit.dns.placeholder')" :tags="formData.Dns.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.dns.placeholder')"
:validate="validateIP" :validation="validateIP()"
@on-tags-changed="handleChangeDns"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDns"/>
</div> </div>
<div v-if="formData.Mode!=='server'" class="form-group"> <div v-if="formData.Mode!=='server'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.DnsSearch" <vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
:placeholder="$t('modals.interface-edit.dns-search.placeholder')" :tags="formData.DnsSearch.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.dns-search.placeholder')"
:validate="validateDomain" :validation="validateDomain()"
@on-tags-changed="handleChangeDnsSearch"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDnsSearch"/>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number"> <input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6" v-if="formData.Backend==='local'">
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number"> <input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
</div> </div>
<div class="form-group col-md-6" v-else>
</div>
</div> </div>
<div class="row"> <div class="row" v-if="formData.Backend==='local'">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text"> <input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
@@ -420,36 +488,52 @@ async function del() {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork" <vue-tags-input class="form-control" v-model="currentTags.PeerDefNetwork"
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')" :tags="formData.PeerDefNetwork.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
:validate="validateCIDR" :validation="validateCIDR()"
@on-tags-changed="handleChangePeerDefNetwork"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefNetwork"/>
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small> <small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs" <vue-tags-input class="form-control" v-model="currentTags.PeerDefAllowedIPs"
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')" :tags="formData.PeerDefAllowedIPs.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
:validate="validateCIDR" :validation="validateCIDR()"
@on-tags-changed="handleChangePeerDefAllowedIPs"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefAllowedIPs"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns" <vue-tags-input class="form-control" v-model="currentTags.PeerDefDns"
:placeholder="$t('modals.interface-edit.dns.placeholder')" :tags="formData.PeerDefDns.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.dns.placeholder')"
:validate="validateIP" :validation="validateIP()"
@on-tags-changed="handleChangePeerDefDns"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefDns"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label> <label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch" <vue-tags-input class="form-control" v-model="currentTags.PeerDefDnsSearch"
:placeholder="$t('modals.interface-edit.dns-search.placeholder')" :tags="formData.PeerDefDnsSearch.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.interface-edit.dns-search.placeholder')"
:validate="validateDomain" :validation="validateDomain()"
@on-tags-changed="handleChangePeerDefDnsSearch"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangePeerDefDnsSearch"/>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
@@ -493,16 +577,25 @@ async function del() {
</fieldset> </fieldset>
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end"> <fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
<hr class="mt-4"> <hr class="mt-4">
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults">{{ $t('modals.interface-edit.button-apply-defaults') }}</button> <button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults" :disabled="isApplyingDefaults">
<span v-if="isApplyingDefaults" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('modals.interface-edit.button-apply-defaults') }}
</button>
</fieldset> </fieldset>
</div> </div>
</div> </div>
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button> <button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.delete') }}
</button>
</div> </div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button> <button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button> <button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template> </template>
</Modal> </Modal>

View File

@@ -1,20 +1,22 @@
<script setup> <script setup>
import Modal from "./Modal.vue"; import Modal from "./Modal.vue";
import {peerStore} from "@/stores/peers"; import { peerStore } from "@/stores/peers";
import {interfaceStore} from "@/stores/interfaces"; import { interfaceStore } from "@/stores/interfaces";
import {computed, ref, watch} from "vue"; import { computed, ref, watch } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from "vue3-tags-input"; import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators'; import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import {isIP} from 'is-ip'; import { isIP } from 'is-ip';
import { freshPeer, freshInterface } from '@/helpers/models'; import { freshPeer, freshInterface } from '@/helpers/models';
import { profileStore } from "@/stores/profile";
const { t } = useI18n() const { t } = useI18n()
const peers = peerStore() const peers = peerStore()
const interfaces = interfaceStore() const interfaces = interfaceStore()
const profile = profileStore()
const props = defineProps({ const props = defineProps({
peerId: String, peerId: String,
@@ -24,7 +26,16 @@ const props = defineProps({
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
const selectedPeer = computed(() => { const selectedPeer = computed(() => {
return peers.Find(props.peerId) let p = peers.Find(props.peerId)
if (!p) {
if (!!props.peerId || props.peerId.length) {
p = profile.peers.find((p) => p.Identifier === props.peerId)
} else {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
}
return p
}) })
const selectedInterface = computed(() => { const selectedInterface = computed(() => {
@@ -54,126 +65,133 @@ const title = computed(() => {
} }
}) })
const currentTags = ref({
Addresses: "",
AllowedIPs: "",
ExtraAllowedIPs: "",
Dns: "",
DnsSearch: ""
})
const formData = ref(freshPeer()) const formData = ref(freshPeer())
const isSaving = ref(false)
const isDeleting = ref(false)
// functions // functions
watch(() => props.visible, async (newValue, oldValue) => { watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown if (oldValue === false && newValue === true) { // if modal is shown
console.log(selectedInterface.value) if (!selectedPeer.value) {
console.log(selectedPeer.value) await peers.PreparePeer(selectedInterface.value.Identifier)
if (!selectedPeer.value) {
await peers.PreparePeer(selectedInterface.value.Identifier)
formData.value.Identifier = peers.Prepared.Identifier formData.value.Identifier = peers.Prepared.Identifier
formData.value.DisplayName = peers.Prepared.DisplayName formData.value.DisplayName = peers.Prepared.DisplayName
formData.value.UserIdentifier = peers.Prepared.UserIdentifier formData.value.UserIdentifier = peers.Prepared.UserIdentifier
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
formData.value.Disabled = peers.Prepared.Disabled formData.value.Disabled = peers.Prepared.Disabled
formData.value.ExpiresAt = peers.Prepared.ExpiresAt formData.value.ExpiresAt = peers.Prepared.ExpiresAt
formData.value.Notes = peers.Prepared.Notes formData.value.Notes = peers.Prepared.Notes
formData.value.Endpoint = peers.Prepared.Endpoint formData.value.Endpoint = peers.Prepared.Endpoint
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
formData.value.AllowedIPs = peers.Prepared.AllowedIPs formData.value.AllowedIPs = peers.Prepared.AllowedIPs
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
formData.value.PresharedKey = peers.Prepared.PresharedKey formData.value.PresharedKey = peers.Prepared.PresharedKey
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
formData.value.PrivateKey = peers.Prepared.PrivateKey formData.value.PrivateKey = peers.Prepared.PrivateKey
formData.value.PublicKey = peers.Prepared.PublicKey formData.value.PublicKey = peers.Prepared.PublicKey
formData.value.Mode = peers.Prepared.Mode formData.value.Mode = peers.Prepared.Mode
formData.value.Addresses = peers.Prepared.Addresses formData.value.Addresses = peers.Prepared.Addresses
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
formData.value.Dns = peers.Prepared.Dns formData.value.Dns = peers.Prepared.Dns
formData.value.DnsSearch = peers.Prepared.DnsSearch formData.value.DnsSearch = peers.Prepared.DnsSearch
formData.value.Mtu = peers.Prepared.Mtu formData.value.Mtu = peers.Prepared.Mtu
formData.value.FirewallMark = peers.Prepared.FirewallMark formData.value.FirewallMark = peers.Prepared.FirewallMark
formData.value.RoutingTable = peers.Prepared.RoutingTable formData.value.RoutingTable = peers.Prepared.RoutingTable
formData.value.PreUp = peers.Prepared.PreUp formData.value.PreUp = peers.Prepared.PreUp
formData.value.PostUp = peers.Prepared.PostUp formData.value.PostUp = peers.Prepared.PostUp
formData.value.PreDown = peers.Prepared.PreDown formData.value.PreDown = peers.Prepared.PreDown
formData.value.PostDown = peers.Prepared.PostDown formData.value.PostDown = peers.Prepared.PostDown
} else { // fill existing data } else { // fill existing data
formData.value.Identifier = selectedPeer.value.Identifier formData.value.Identifier = selectedPeer.value.Identifier
formData.value.DisplayName = selectedPeer.value.DisplayName formData.value.DisplayName = selectedPeer.value.DisplayName
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
formData.value.Disabled = selectedPeer.value.Disabled formData.value.Disabled = selectedPeer.value.Disabled
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
formData.value.Notes = selectedPeer.value.Notes formData.value.Notes = selectedPeer.value.Notes
formData.value.Endpoint = selectedPeer.value.Endpoint formData.value.Endpoint = selectedPeer.value.Endpoint
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
formData.value.PresharedKey = selectedPeer.value.PresharedKey formData.value.PresharedKey = selectedPeer.value.PresharedKey
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
formData.value.PrivateKey = selectedPeer.value.PrivateKey formData.value.PrivateKey = selectedPeer.value.PrivateKey
formData.value.PublicKey = selectedPeer.value.PublicKey formData.value.PublicKey = selectedPeer.value.PublicKey
formData.value.Mode = selectedPeer.value.Mode formData.value.Mode = selectedPeer.value.Mode
formData.value.Addresses = selectedPeer.value.Addresses formData.value.Addresses = selectedPeer.value.Addresses
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
formData.value.Dns = selectedPeer.value.Dns formData.value.Dns = selectedPeer.value.Dns
formData.value.DnsSearch = selectedPeer.value.DnsSearch formData.value.DnsSearch = selectedPeer.value.DnsSearch
formData.value.Mtu = selectedPeer.value.Mtu formData.value.Mtu = selectedPeer.value.Mtu
formData.value.FirewallMark = selectedPeer.value.FirewallMark formData.value.FirewallMark = selectedPeer.value.FirewallMark
formData.value.RoutingTable = selectedPeer.value.RoutingTable formData.value.RoutingTable = selectedPeer.value.RoutingTable
formData.value.PreUp = selectedPeer.value.PreUp formData.value.PreUp = selectedPeer.value.PreUp
formData.value.PostUp = selectedPeer.value.PostUp formData.value.PostUp = selectedPeer.value.PostUp
formData.value.PreDown = selectedPeer.value.PreDown formData.value.PreDown = selectedPeer.value.PreDown
formData.value.PostDown = selectedPeer.value.PostDown formData.value.PostDown = selectedPeer.value.PostDown
if (!formData.value.Endpoint.Overridable || if (!formData.value.Endpoint.Overridable ||
!formData.value.EndpointPublicKey.Overridable || !formData.value.EndpointPublicKey.Overridable ||
!formData.value.AllowedIPs.Overridable || !formData.value.AllowedIPs.Overridable ||
!formData.value.PersistentKeepalive.Overridable || !formData.value.PersistentKeepalive.Overridable ||
!formData.value.Dns.Overridable || !formData.value.Dns.Overridable ||
!formData.value.DnsSearch.Overridable || !formData.value.DnsSearch.Overridable ||
!formData.value.Mtu.Overridable || !formData.value.Mtu.Overridable ||
!formData.value.FirewallMark.Overridable || !formData.value.FirewallMark.Overridable ||
!formData.value.RoutingTable.Overridable || !formData.value.RoutingTable.Overridable ||
!formData.value.PreUp.Overridable || !formData.value.PreUp.Overridable ||
!formData.value.PostUp.Overridable || !formData.value.PostUp.Overridable ||
!formData.value.PreDown.Overridable || !formData.value.PreDown.Overridable ||
!formData.value.PostDown.Overridable) { !formData.value.PostDown.Overridable) {
formData.value.IgnoreGlobalSettings = true formData.value.IgnoreGlobalSettings = true
}
}
} }
} }
}
}
) )
watch(() => formData.value.IgnoreGlobalSettings, async (newValue, oldValue) => { watch(() => formData.value.IgnoreGlobalSettings, async (newValue, oldValue) => {
formData.value.Endpoint.Overridable = !newValue formData.value.Endpoint.Overridable = !newValue
formData.value.EndpointPublicKey.Overridable = !newValue formData.value.EndpointPublicKey.Overridable = !newValue
formData.value.AllowedIPs.Overridable = !newValue formData.value.AllowedIPs.Overridable = !newValue
formData.value.PersistentKeepalive.Overridable = !newValue formData.value.PersistentKeepalive.Overridable = !newValue
formData.value.Dns.Overridable = !newValue formData.value.Dns.Overridable = !newValue
formData.value.DnsSearch.Overridable = !newValue formData.value.DnsSearch.Overridable = !newValue
formData.value.Mtu.Overridable = !newValue formData.value.Mtu.Overridable = !newValue
formData.value.FirewallMark.Overridable = !newValue formData.value.FirewallMark.Overridable = !newValue
formData.value.RoutingTable.Overridable = !newValue formData.value.RoutingTable.Overridable = !newValue
formData.value.PreUp.Overridable = !newValue formData.value.PreUp.Overridable = !newValue
formData.value.PostUp.Overridable = !newValue formData.value.PostUp.Overridable = !newValue
formData.value.PreDown.Overridable = !newValue formData.value.PreDown.Overridable = !newValue
formData.value.PostDown.Overridable = !newValue formData.value.PostDown.Overridable = !newValue
} }
) )
watch(() => formData.value.Disabled, async (newValue, oldValue) => { watch(() => formData.value.Disabled, async (newValue, oldValue) => {
if (oldValue && !newValue && formData.value.ExpiresAt) { if (oldValue && !newValue && formData.value.ExpiresAt) {
formData.value.ExpiresAt = "" // reset expiry date formData.value.ExpiresAt = "" // reset expiry date
} }
} }
) )
function close() { function close() {
@@ -184,104 +202,110 @@ function close() {
function handleChangeAddresses(tags) { function handleChangeAddresses(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if (isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if (validInput) {
formData.value.Addresses = tags formData.value.Addresses = tags.map(tag => tag.text)
} }
} }
function handleChangeAllowedIPs(tags) { function handleChangeAllowedIPs(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if (isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if (validInput) {
formData.value.AllowedIPs.Value = tags formData.value.AllowedIPs.Value = tags.map(tag => tag.text)
} }
} }
function handleChangeExtraAllowedIPs(tags) { function handleChangeExtraAllowedIPs(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(isCidr(tag) === 0) { if (isCidr(tag.text) === 0) {
validInput = false validInput = false
notify({ notify({
title: "Invalid CIDR", title: "Invalid CIDR",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if (validInput) {
formData.value.ExtraAllowedIPs = tags formData.value.ExtraAllowedIPs = tags.map(tag => tag.text)
} }
} }
function handleChangeDns(tags) { function handleChangeDns(tags) {
let validInput = true let validInput = true
tags.forEach(tag => { tags.forEach(tag => {
if(!isIP(tag)) { if (!isIP(tag.text)) {
validInput = false validInput = false
notify({ notify({
title: "Invalid IP", title: "Invalid IP",
text: tag + " is not a valid IP address", text: tag.text + " is not a valid IP address",
type: 'error', type: 'error',
}) })
} }
}) })
if(validInput) { if (validInput) {
formData.value.Dns.Value = tags formData.value.Dns.Value = tags.map(tag => tag.text)
} }
} }
function handleChangeDnsSearch(tags) { function handleChangeDnsSearch(tags) {
formData.value.DnsSearch.Value = tags formData.value.DnsSearch.Value = tags.map(tag => tag.text)
} }
async function save() { async function save() {
if (isSaving.value) return
isSaving.value = true
try { try {
if (props.peerId!=='#NEW#') { if (props.peerId !== '#NEW#') {
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value) await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
} else { } else {
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value) await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
} }
close() close()
} catch (e) { } catch (e) {
console.log(e)
notify({ notify({
title: "Failed to save peer!", title: "Failed to save peer!",
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isSaving.value = false
} }
} }
async function del() { async function del() {
if (isDeleting.value) return
isDeleting.value = true
try { try {
await peers.DeletePeer(selectedPeer.value.Identifier) await peers.DeletePeer(selectedPeer.value.Identifier)
close() close()
} catch (e) { } catch (e) {
console.log(e)
notify({ notify({
title: "Failed to delete peer!", title: "Failed to delete peer!",
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isDeleting.value = false
} }
} }
@@ -294,87 +318,117 @@ async function del() {
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend> <legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')" v-model="formData.DisplayName"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')"
v-model="formData.DisplayName">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.linked-user.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.linked-user.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.linked-user.placeholder')" v-model="formData.UserIdentifier"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.linked-user.placeholder')"
v-model="formData.UserIdentifier">
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend> <legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
<div class="form-group" v-if="selectedInterface.Mode==='server'"> <div class="form-group" v-if="selectedInterface.Mode === 'server'">
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required v-model="formData.PrivateKey"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
v-model="formData.PrivateKey">
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required v-model="formData.PublicKey"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
v-model="formData.PublicKey">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')" v-model="formData.PresharedKey"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
v-model="formData.PresharedKey">
</div> </div>
<div class="form-group" v-if="formData.Mode==='client'"> <div class="form-group" v-if="formData.Mode === 'client'">
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint-public-key.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint-public-key.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint-public-key.placeholder')" v-model="formData.EndpointPublicKey.Value"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint-public-key.placeholder')"
v-model="formData.EndpointPublicKey.Value">
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend> <legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
<div class="form-group" v-if="selectedInterface.Mode==='client'"> <div class="form-group" v-if="selectedInterface.Mode === 'client'">
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')" v-model="formData.Endpoint.Value"> <input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')"
v-model="formData.Endpoint.Value">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Addresses" <vue-tags-input class="form-control" v-model="currentTags.Addresses"
:tags="formData.Addresses.map(str => ({ text: str }))"
:placeholder="$t('modals.peer-edit.ip.placeholder')" :placeholder="$t('modals.peer-edit.ip.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateCIDR()"
:validate="validateCIDR" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangeAddresses"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeAddresses" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value" <vue-tags-input class="form-control" v-model="currentTags.AllowedIPs"
:tags="formData.AllowedIPs.Value.map(str => ({ text: str }))"
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')" :placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
:add-tag-on-keys="[13, 188, 32, 9]" :validation="validateCIDR()"
:validate="validateCIDR" :add-on-key="[13, 188, 32, 9]"
@on-tags-changed="handleChangeAllowedIPs"/> :save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeAllowedIPs" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs" <vue-tags-input class="form-control" v-model="currentTags.ExtraAllowedIPs"
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')" :tags="formData.ExtraAllowedIPs.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
:validate="validateCIDR" :validation="validateCIDR()"
@on-tags-changed="handleChangeExtraAllowedIPs"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeExtraAllowedIPs" />
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small> <small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Dns.Value" <vue-tags-input class="form-control" v-model="currentTags.Dns"
:placeholder="$t('modals.peer-edit.dns.placeholder')" :tags="formData.Dns.Value.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.peer-edit.dns.placeholder')"
:validate="validateIP" :validation="validateIP()"
@on-tags-changed="handleChangeDns"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDns" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value" <vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
:placeholder="$t('modals.peer-edit.dns-search.label')" :tags="formData.DnsSearch.Value.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.peer-edit.dns-search.label')"
:validate="validateDomain" :validation="validateDomain()"
@on-tags-changed="handleChangeDnsSearch"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeDnsSearch" />
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')" v-model="formData.PersistentKeepalive.Value"> <input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')"
v-model="formData.PersistentKeepalive.Value">
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')" v-model="formData.Mtu.Value"> <input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
v-model="formData.Mtu.Value">
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -382,19 +436,23 @@ async function del() {
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend> <legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea> <textarea v-model="formData.PreUp.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea> <textarea v-model="formData.PostUp.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea> <textarea v-model="formData.PreDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea> <textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
@@ -403,7 +461,7 @@ async function del() {
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" v-model="formData.Disabled"> <input class="form-check-input" type="checkbox" v-model="formData.Disabled">
<label class="form-check-label" >{{ $t('modals.peer-edit.disabled.label') }}</label> <label class="form-check-label">{{ $t('modals.peer-edit.disabled.label') }}</label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" v-model="formData.IgnoreGlobalSettings"> <input class="form-check-input" type="checkbox" v-model="formData.IgnoreGlobalSettings">
@@ -412,20 +470,26 @@ async function del() {
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label> <label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01" v-model="formData.ExpiresAt"> <input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01"
v-model="formData.ExpiresAt">
</div> </div>
</div> </div>
</fieldset> </fieldset>
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button v-if="props.peerId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button> <button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.delete') }}
</button>
</div> </div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button> <button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button> <button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template> </template>
</Modal> </Modal>
</template> </template>
<style> <style></style>
</style>

View File

@@ -5,7 +5,7 @@ import {interfaceStore} from "@/stores/interfaces";
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from "vue3-tags-input"; import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
import { freshInterface } from '@/helpers/models'; import { freshInterface } from '@/helpers/models';
const { t } = useI18n() const { t } = useI18n()
@@ -32,11 +32,13 @@ const selectedInterface = computed(() => {
function freshForm() { function freshForm() {
return { return {
Identifiers: [], Identifiers: [],
Suffix: "", Prefix: "",
} }
} }
const currentTag = ref("")
const formData = ref(freshForm()) const formData = ref(freshForm())
const isSaving = ref(false)
const title = computed(() => { const title = computed(() => {
if (!props.visible) { if (!props.visible) {
@@ -55,16 +57,19 @@ function close() {
} }
function handleChangeUserIdentifiers(tags) { function handleChangeUserIdentifiers(tags) {
formData.value.Identifiers = tags formData.value.Identifiers = tags.map(tag => tag.text)
} }
async function save() { async function save() {
if (isSaving.value) return
isSaving.value = true
if (formData.value.Identifiers.length === 0) { if (formData.value.Identifiers.length === 0) {
notify({ notify({
title: "Missing Identifiers", title: "Missing Identifiers",
text: "At least one identifier is required to create a new peer.", text: "At least one identifier is required to create a new peer.",
type: 'error', type: 'error',
}) })
isSaving.value = false
return return
} }
@@ -78,6 +83,8 @@ async function save() {
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isSaving.value = false
} }
} }
@@ -89,21 +96,28 @@ async function save() {
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
<vue3-tags-input class="form-control" :tags="formData.Identifiers" <vue-tags-input class="form-control" v-model="currentTag"
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')" :tags="formData.Identifiers.map(str => ({ text: str }))"
:add-tag-on-keys="[13, 188, 32, 9]" :placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
@on-tags-changed="handleChangeUserIdentifiers"/> :add-on-key="[13, 188, 32, 9]"
:save-on-key="[13, 188, 32, 9]"
:allow-edit-tags="true"
:separators="[',', ';', ' ']"
@tags-changed="handleChangeUserIdentifiers"/>
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small> <small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label> <label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix"> <input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Prefix">
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small> <small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
</div> </div>
</fieldset> </fieldset>
</template> </template>
<template #footer> <template #footer>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button> <button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button> <button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template> </template>
</Modal> </Modal>

View File

@@ -1,19 +1,23 @@
<script setup> <script setup>
import Modal from "./Modal.vue"; import Modal from "./Modal.vue";
import {peerStore} from "@/stores/peers"; import { peerStore } from "@/stores/peers";
import {interfaceStore} from "@/stores/interfaces"; import { interfaceStore } from "@/stores/interfaces";
import {computed, ref, watch} from "vue"; import { computed, ref, watch } from "vue";
import {useI18n} from "vue-i18n"; import { useI18n } from "vue-i18n";
import {freshInterface, freshPeer, freshStats} from '@/helpers/models'; import { freshInterface, freshPeer, freshStats } from '@/helpers/models';
import Prism from "vue-prism-component"; import Prism from "vue-prism-component";
import {notify} from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import {settingsStore} from "@/stores/settings"; import { settingsStore } from "@/stores/settings";
import { profileStore } from "@/stores/profile";
import { base64_url_encode } from '@/helpers/encoding';
import { apiWrapper } from "@/helpers/fetch-wrapper";
const { t } = useI18n() const { t } = useI18n()
const settings = settingsStore() const settings = settingsStore()
const peers = peerStore() const peers = peerStore()
const interfaces = interfaceStore() const interfaces = interfaceStore()
const profile = profileStore()
const props = defineProps({ const props = defineProps({
peerId: String, peerId: String,
@@ -32,9 +36,12 @@ const selectedPeer = computed(() => {
let p = peers.Find(props.peerId) let p = peers.Find(props.peerId)
if (!p) { if (!p) {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions if (!!props.peerId || props.peerId.length) {
p = profile.peers.find((p) => p.Identifier === props.peerId)
} else {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
} }
return p return p
}) })
@@ -42,9 +49,13 @@ const selectedStats = computed(() => {
let s = peers.Statistics(props.peerId) let s = peers.Statistics(props.peerId)
if (!s) { if (!s) {
s = freshStats() // dummy peer to avoid 'undefined' exceptions if (!!props.peerId || props.peerId.length) {
} s = profile.Statistics(props.peerId)
} else {
s = freshStats() // dummy stats to avoid 'undefined' exceptions
}
}
return s return s
}) })
@@ -54,7 +65,6 @@ const selectedInterface = computed(() => {
if (!i) { if (!i) {
i = freshInterface() // dummy interface to avoid 'undefined' exceptions i = freshInterface() // dummy interface to avoid 'undefined' exceptions
} }
return i return i
}) })
@@ -69,29 +79,27 @@ const title = computed(() => {
} }
}) })
const configStyle = ref("wgquick")
watch(() => props.visible, async (newValue, oldValue) => { watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown if (oldValue === false && newValue === true) { // if modal is shown
await peers.LoadPeerConfig(selectedPeer.value.Identifier) await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
configString.value = peers.configuration configString.value = peers.configuration
} }
} })
)
watch(() => configStyle.value, async () => {
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
configString.value = peers.configuration
})
function download() { function download() {
// credit: https://www.bitdegree.org/learn/javascript-download // credit: https://www.bitdegree.org/learn/javascript-download
let filename = 'WireGuard-Tunnel.conf'
if (selectedPeer.value.DisplayName) {
filename = selectedPeer.value.DisplayName
.replace(/ /g,"_")
.replace(/[^a-zA-Z0-9-_]/g,"")
.substring(0, 16)
+ ".conf"
}
let text = configString.value let text = configString.value
let element = document.createElement('a') let element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)) element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('download', filename) element.setAttribute('download', selectedPeer.value.Filename)
element.style.display = 'none' element.style.display = 'none'
document.body.appendChild(element) document.body.appendChild(element)
@@ -101,7 +109,7 @@ function download() {
} }
function email() { function email() {
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), [selectedPeer.value.Identifier]).catch(e => { peers.MailPeerConfig(settings.Setting("MailLinkOnly"), configStyle.value, [selectedPeer.value.Identifier]).catch(e => {
notify({ notify({
title: "Failed to send mail with peer configuration!", title: "Failed to send mail with peer configuration!",
text: e.toString(), text: e.toString(),
@@ -110,33 +118,54 @@ function email() {
}) })
} }
function ConfigQrUrl() {
if (props.peerId.length) {
return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}?style=${configStyle.value}`)
}
return ''
}
</script> </script>
<template> <template>
<Modal :title="title" :visible="visible" @close="close"> <Modal :title="title" :visible="visible" @close="close">
<template #default> <template #default>
<div class="d-flex justify-content-end align-items-center mb-1">
<span class="me-2">{{ $t('modals.peer-view.style-label') }}: </span>
<div class="btn-group btn-switch-group" role="group" aria-label="Configuration Style">
<input type="radio" class="btn-check" name="configstyle" id="raw" value="raw" autocomplete="off" checked="" v-model="configStyle">
<label class="btn btn-outline-dark btn-sm" for="raw">Raw</label>
<input type="radio" class="btn-check" name="configstyle" id="wgquick" value="wgquick" autocomplete="off" checked="" v-model="configStyle">
<label class="btn btn-outline-dark btn-sm" for="wgquick">WG-Quick</label>
</div>
</div>
<div class="accordion" id="peerInformation"> <div class="accordion" id="peerInformation">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails" aria-expanded="true" aria-controls="collapseDetails"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails"
aria-expanded="true" aria-controls="collapseDetails">
{{ $t('modals.peer-view.section-info') }} {{ $t('modals.peer-view.section-info') }}
</button> </button>
</h2> </h2>
<div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails" data-bs-parent="#peerInformation" style=""> <div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails"
data-bs-parent="#peerInformation" style="">
<div class="accordion-body"> <div class="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<ul> <ul>
<li>{{ $t('modals.peer-view.identifier') }}: {{ selectedPeer.PublicKey }}</li> <li>{{ $t('modals.peer-view.identifier') }}: {{ selectedPeer.PublicKey }}</li>
<li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li> <li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip"
class="badge rounded-pill bg-light">{{ ip }}</span></li>
<li>{{ $t('modals.peer-view.user') }}: {{ selectedPeer.UserIdentifier }}</li> <li>{{ $t('modals.peer-view.user') }}: {{ selectedPeer.UserIdentifier }}</li>
<li v-if="selectedPeer.Notes">{{ $t('modals.peer-view.notes') }}: {{ selectedPeer.Notes }}</li> <li v-if="selectedPeer.Notes">{{ $t('modals.peer-view.notes') }}: {{ selectedPeer.Notes }}</li>
<li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{ selectedPeer.ExpiresAt }}</li> <li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{ selectedPeer.DisabledReason }}</li> selectedPeer.ExpiresAt }}</li>
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{
selectedPeer.DisabledReason }}</li>
</ul> </ul>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code"> <img class="config-qr-img" :src="ConfigQrUrl()" loading="lazy" alt="Configuration QR Code">
</div> </div>
</div> </div>
</div> </div>
@@ -144,16 +173,20 @@ function email() {
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingStatus"> <h2 class="accordion-header" id="headingStatus">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus">
{{ $t('modals.peer-view.section-status') }} {{ $t('modals.peer-view.section-status') }}
</button> </button>
</h2> </h2>
<div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus" data-bs-parent="#peerInformation" style=""> <div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus"
data-bs-parent="#peerInformation" style="">
<div class="accordion-body"> <div class="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h4>{{ $t('modals.peer-view.traffic') }}</h4> <h4>{{ $t('modals.peer-view.traffic') }}</h4>
<p><i class="fas fa-long-arrow-alt-down" :title="$t('modals.peer-view.download')"></i> {{ selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up" :title="$t('modals.peer-view.upload')"></i> {{ selectedStats.BytesTransmitted }} Bytes</p> <p><i class="fas fa-long-arrow-alt-down" :title="$t('modals.peer-view.download')"></i> {{
selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up"
:title="$t('modals.peer-view.upload')"></i> {{ selectedStats.BytesTransmitted }} Bytes</p>
<h4>{{ $t('modals.peer-view.connection-status') }}</h4> <h4>{{ $t('modals.peer-view.connection-status') }}</h4>
<ul> <ul>
<li>{{ $t('modals.peer-view.pingable') }}: {{ selectedStats.IsPingable }}</li> <li>{{ $t('modals.peer-view.pingable') }}: {{ selectedStats.IsPingable }}</li>
@@ -166,13 +199,15 @@ function email() {
</div> </div>
</div> </div>
</div> </div>
<div v-if="selectedInterface.Mode==='server'" class="accordion-item"> <div v-if="selectedInterface.Mode === 'server'" class="accordion-item">
<h2 class="accordion-header" id="headingConfig"> <h2 class="accordion-header" id="headingConfig">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
{{ $t('modals.peer-view.section-config') }} {{ $t('modals.peer-view.section-config') }}
</button> </button>
</h2> </h2>
<div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig" data-bs-parent="#peerInformation" style=""> <div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig"
data-bs-parent="#peerInformation" style="">
<div class="accordion-body"> <div class="accordion-body">
<Prism language="ini" :code="configString"></Prism> <Prism language="ini" :code="configString"></Prism>
</div> </div>
@@ -182,18 +217,25 @@ function email() {
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-download') }}</button> <button @click.prevent="download" type="button" class="btn btn-primary me-1">{{
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-email') }}</button> $t('modals.peer-view.button-download') }}</button>
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{
$t('modals.peer-view.button-email') }}</button>
</div> </div>
<button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button> <button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button>
</template> </template>
</Modal> </Modal></template>
</template>
<style> <style>
.config-qr-img { .config-qr-img {
max-width: 100%; max-width: 100%;
} }
.btn-switch-group .btn {
border-width: 1px;
padding: 5px;
line-height: 1;
}
</style> </style>

View File

@@ -5,10 +5,12 @@ import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import {freshUser} from "@/helpers/models"; import {freshUser} from "@/helpers/models";
import {settingsStore} from "@/stores/settings";
const { t } = useI18n() const { t } = useI18n()
const users = userStore() const users = userStore()
const settings = settingsStore()
const props = defineProps({ const props = defineProps({
userId: String, userId: String,
@@ -32,6 +34,32 @@ const title = computed(() => {
}) })
const formData = ref(freshUser()) const formData = ref(freshUser())
const isSaving = ref(false)
const isDeleting = ref(false)
const passwordWeak = computed(() => {
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
})
const formValid = computed(() => {
if (formData.value.Source !== 'db') {
return true // nothing to validate
}
if (props.userId !== '#NEW#' && passwordWeak.value) {
return false
}
if (props.userId === '#NEW#' && (!formData.value.Password || formData.value.Password.length < 1)) {
return false
}
if (props.userId === '#NEW#' && passwordWeak.value) {
return false
}
if (!formData.value.Identifier || formData.value.Identifier.length < 1) {
return false
}
return true
})
// functions // functions
@@ -51,6 +79,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Notes = selectedUser.value.Notes formData.value.Notes = selectedUser.value.Notes
formData.value.Password = "" formData.value.Password = ""
formData.value.Disabled = selectedUser.value.Disabled formData.value.Disabled = selectedUser.value.Disabled
formData.value.Locked = selectedUser.value.Locked
} }
} }
} }
@@ -62,6 +91,8 @@ function close() {
} }
async function save() { async function save() {
if (isSaving.value) return
isSaving.value = true
try { try {
if (props.userId!=='#NEW#') { if (props.userId!=='#NEW#') {
await users.UpdateUser(selectedUser.value.Identifier, formData.value) await users.UpdateUser(selectedUser.value.Identifier, formData.value)
@@ -75,10 +106,14 @@ async function save() {
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isSaving.value = false
} }
} }
async function del() { async function del() {
if (isDeleting.value) return
isDeleting.value = true
try { try {
await users.DeleteUser(selectedUser.value.Identifier) await users.DeleteUser(selectedUser.value.Identifier)
close() close()
@@ -88,6 +123,8 @@ async function del() {
text: e.toString(), text: e.toString(),
type: 'error', type: 'error',
}) })
} finally {
isDeleting.value = false
} }
} }
@@ -108,7 +145,8 @@ async function del() {
</div> </div>
<div v-if="formData.Source==='db'" class="form-group"> <div v-if="formData.Source==='db'" class="form-group">
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label> <label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :placeholder="$t('modals.user-edit.password.placeholder')" type="text"> <input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :class="{ 'is-invalid': passwordWeak, 'is-valid': formData.Password !== '' && !passwordWeak }" :placeholder="$t('modals.user-edit.password.placeholder')" type="password">
<div class="invalid-feedback">{{ $t('modals.user-edit.password.too-weak') }}</div>
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small> <small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
</div> </div>
</fieldset> </fieldset>
@@ -165,9 +203,15 @@ async function del() {
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button> <button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.delete') }}
</button>
</div> </div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button> <button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid || isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button> <button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template> </template>
</Modal> </Modal>

View File

@@ -0,0 +1,308 @@
<script setup>
import Modal from "./Modal.vue";
import { peerStore } from "@/stores/peers";
import { computed, ref, watch } from "vue";
import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification";
import { freshPeer, freshInterface } from '@/helpers/models';
import { profileStore } from "@/stores/profile";
const { t } = useI18n()
const peers = peerStore()
const profile = profileStore()
const props = defineProps({
peerId: String,
visible: Boolean,
})
const emit = defineEmits(['close'])
const selectedPeer = computed(() => {
let p = peers.Find(props.peerId)
if (!p) {
if (!!props.peerId || props.peerId.length) {
p = profile.peers.find((p) => p.Identifier === props.peerId)
} else {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
}
return p
})
const selectedInterface = computed(() => {
let iId = profile.selectedInterfaceId;
let i = freshInterface() // dummy interface to avoid 'undefined' exceptions
if (iId) {
i = profile.interfaces.find((i) => i.Identifier === iId)
}
return i
})
const title = computed(() => {
if (!props.visible) {
return ""
}
if (selectedPeer.value) {
return t("modals.peer-edit.headline-edit-peer") + " " + selectedPeer.value.Identifier
}
return t("modals.peer-edit.headline-new-peer")
})
const formData = ref(freshPeer())
const isSaving = ref(false)
const isDeleting = ref(false)
// functions
watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown
if (!selectedPeer.value) {
await peers.PreparePeer(selectedInterface.value.Identifier)
formData.value.Identifier = peers.Prepared.Identifier
formData.value.DisplayName = peers.Prepared.DisplayName
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
formData.value.Disabled = peers.Prepared.Disabled
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
formData.value.Notes = peers.Prepared.Notes
formData.value.Endpoint = peers.Prepared.Endpoint
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
formData.value.PresharedKey = peers.Prepared.PresharedKey
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
formData.value.PrivateKey = peers.Prepared.PrivateKey
formData.value.PublicKey = peers.Prepared.PublicKey
formData.value.Mode = peers.Prepared.Mode
formData.value.Addresses = peers.Prepared.Addresses
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
formData.value.Dns = peers.Prepared.Dns
formData.value.DnsSearch = peers.Prepared.DnsSearch
formData.value.Mtu = peers.Prepared.Mtu
formData.value.FirewallMark = peers.Prepared.FirewallMark
formData.value.RoutingTable = peers.Prepared.RoutingTable
formData.value.PreUp = peers.Prepared.PreUp
formData.value.PostUp = peers.Prepared.PostUp
formData.value.PreDown = peers.Prepared.PreDown
formData.value.PostDown = peers.Prepared.PostDown
} else { // fill existing data
formData.value.Identifier = selectedPeer.value.Identifier
formData.value.DisplayName = selectedPeer.value.DisplayName
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
formData.value.Disabled = selectedPeer.value.Disabled
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
formData.value.Notes = selectedPeer.value.Notes
formData.value.Endpoint = selectedPeer.value.Endpoint
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
formData.value.PresharedKey = selectedPeer.value.PresharedKey
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
formData.value.PrivateKey = selectedPeer.value.PrivateKey
formData.value.PublicKey = selectedPeer.value.PublicKey
formData.value.Mode = selectedPeer.value.Mode
formData.value.Addresses = selectedPeer.value.Addresses
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
formData.value.Dns = selectedPeer.value.Dns
formData.value.DnsSearch = selectedPeer.value.DnsSearch
formData.value.Mtu = selectedPeer.value.Mtu
formData.value.FirewallMark = selectedPeer.value.FirewallMark
formData.value.RoutingTable = selectedPeer.value.RoutingTable
formData.value.PreUp = selectedPeer.value.PreUp
formData.value.PostUp = selectedPeer.value.PostUp
formData.value.PreDown = selectedPeer.value.PreDown
formData.value.PostDown = selectedPeer.value.PostDown
if (!formData.value.Endpoint.Overridable ||
!formData.value.EndpointPublicKey.Overridable ||
!formData.value.AllowedIPs.Overridable ||
!formData.value.PersistentKeepalive.Overridable ||
!formData.value.Dns.Overridable ||
!formData.value.DnsSearch.Overridable ||
!formData.value.Mtu.Overridable ||
!formData.value.FirewallMark.Overridable ||
!formData.value.RoutingTable.Overridable ||
!formData.value.PreUp.Overridable ||
!formData.value.PostUp.Overridable ||
!formData.value.PreDown.Overridable ||
!formData.value.PostDown.Overridable) {
formData.value.IgnoreGlobalSettings = true
}
}
}
}
)
watch(() => formData.value.Disabled, async (newValue, oldValue) => {
if (oldValue && !newValue && formData.value.ExpiresAt) {
formData.value.ExpiresAt = "" // reset expiry date
}
}
)
function close() {
formData.value = freshPeer()
emit('close')
}
async function save() {
if (isSaving.value) return
isSaving.value = true
try {
if (props.peerId !== '#NEW#') {
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
} else {
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
}
close()
} catch (e) {
notify({
title: "Failed to save peer!",
text: e.toString(),
type: 'error',
})
} finally {
isSaving.value = false
}
}
async function del() {
if (isDeleting.value) return
isDeleting.value = true
try {
await peers.DeletePeer(selectedPeer.value.Identifier)
close()
} catch (e) {
notify({
title: "Failed to delete peer!",
text: e.toString(),
type: 'error',
})
} finally {
isDeleting.value = false
}
}
</script>
<template>
<Modal :title="title" :visible="visible" @close="close">
<template #default>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')"
v-model="formData.DisplayName">
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
v-model="formData.PrivateKey">
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
v-model="formData.PublicKey">
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
v-model="formData.PresharedKey">
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')"
v-model="formData.PersistentKeepalive.Value">
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
v-model="formData.Mtu.Value">
</div>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
</div>
</fieldset>
<fieldset>
<legend class="mt-4">{{ $t('modals.peer-edit.header-state') }}</legend>
<div class="row">
<div class="form-group col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
<label class="form-check-label">{{ $t('modals.peer-edit.disabled.label') }}</label>
</div>
</div>
<div class="form-group col-md-6">
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01"
v-model="formData.ExpiresAt">
</div>
</div>
</fieldset>
</template>
<template #footer>
<div class="flex-fill text-start">
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.delete') }}
</button>
</div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>
</template>
<style></style>

View File

@@ -88,6 +88,10 @@ function close() {
<td>{{ $t('modals.user-view.department') }}:</td> <td>{{ $t('modals.user-view.department') }}:</td>
<td>{{selectedUser.Department}}</td> <td>{{selectedUser.Department}}</td>
</tr> </tr>
<tr>
<td>{{ $t('modals.user-view.api-enabled') }}:</td>
<td>{{selectedUser.ApiEnabled}}</td>
</tr>
<tr v-if="selectedUser.Disabled"> <tr v-if="selectedUser.Disabled">
<td>{{ $t('modals.user-view.disabled') }}:</td> <td>{{ $t('modals.user-view.disabled') }}:</td>
<td>{{selectedUser.DisabledReason}}</td> <td>{{selectedUser.DisabledReason}}</td>

View File

@@ -5,6 +5,7 @@ export function freshInterface() {
DisplayName: "", DisplayName: "",
Identifier: "", Identifier: "",
Mode: "server", Mode: "server",
Backend: "local",
PublicKey: "", PublicKey: "",
PrivateKey: "", PrivateKey: "",
@@ -42,7 +43,8 @@ export function freshInterface() {
PeerDefPostDown: "", PeerDefPostDown: "",
TotalPeers: 0, TotalPeers: 0,
EnabledPeers: 0 EnabledPeers: 0,
Filename: ""
} }
} }
@@ -51,6 +53,7 @@ export function freshPeer() {
Identifier: "", Identifier: "",
DisplayName: "", DisplayName: "",
UserIdentifier: "", UserIdentifier: "",
UserDisplayName: "",
InterfaceIdentifier: "", InterfaceIdentifier: "",
Disabled: false, Disabled: false,
ExpiresAt: null, ExpiresAt: null,
@@ -120,8 +123,11 @@ export function freshPeer() {
Overridable: true, Overridable: true,
}, },
// Internal value Filename: "",
IgnoreGlobalSettings: false
// Internal values
IgnoreGlobalSettings: false,
IsSelected: false
} }
} }
@@ -146,7 +152,12 @@ export function freshUser() {
Locked: false, Locked: false,
LockedReason: "", LockedReason: "",
PeerCount: 0 ApiEnabled: false,
PeerCount: 0,
// Internal values
IsSelected: false
} }
} }

View File

@@ -0,0 +1,20 @@
import { Address4, Address6 } from "ip-address"
export function ipToBigInt(ip) {
// Check if it's an IPv4 address
if (ip.includes(".")) {
const addr = new Address4(ip)
return addr.bigInteger()
}
// Otherwise, assume it's an IPv6 address
const addr = new Address6(ip)
return addr.bigInteger()
}
export function humanFileSize(size) {
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
if (size === 0) return "0B"
const i = parseInt(Math.floor(Math.log(size) / Math.log(1024)))
return Math.round(size / Math.pow(1024, i), 2) + sizes[i]
}

View File

@@ -1,14 +1,26 @@
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import {isIP} from 'is-ip'; import {isIP} from 'is-ip';
export function validateCIDR(value) { export function validateCIDR() {
return isCidr(value) !== 0 return [{
classes: 'invalid-cidr',
rule: ({ text }) => isCidr(text) === 0,
disableAdd: true,
}]
} }
export function validateIP(value) { export function validateIP() {
return isIP(value) return [{
classes: 'invalid-ip',
rule: ({ text }) => !isIP(text),
disableAdd: true,
}]
} }
export function validateDomain(value) { export function validateDomain() {
return true return [{
classes: 'invalid-domain',
rule: tag => tag.text.length < 3,
disableAdd: true,
}]
} }

View File

@@ -1,27 +1,40 @@
// src/lang/index.js // src/lang/index.js
import de from './translations/de.json'; import de from './translations/de.json';
import en from './translations/en.json'; import en from './translations/en.json';
import {createI18n} from "vue-i18n"; import fr from './translations/fr.json';
import ko from './translations/ko.json';
import pt from './translations/pt.json';
import ru from './translations/ru.json';
import uk from './translations/uk.json';
import vi from './translations/vi.json';
import zh from './translations/zh.json';
import es from './translations/es.json';
function getStoredLanguage() { import {createI18n} from "vue-i18n";
let initialLang = localStorage.getItem('wgLang');
if (!initialLang) {
initialLang = "en"
}
return initialLang
}
// Create i18n instance with options // Create i18n instance with options
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
globalInjection: true, globalInjection: true,
allowComposition: true, allowComposition: true,
locale: getStoredLanguage(), // set locale locale: (
localStorage.getItem('wgLang')
|| (window && window.navigator && (window.navigator.userLanguage || window.navigator.language).split('-')[0])
|| 'en'
), // set locale
fallbackLocale: "en", // set fallback locale fallbackLocale: "en", // set fallback locale
messages: { messages: {
"de": de, "de": de,
"en": en "en": en,
"fr": fr,
"ko": ko,
"pt": pt,
"ru": ru,
"uk": uk,
"vi": vi,
"zh": zh,
"es": es,
} }
}); });
export default i18n export default i18n

View File

@@ -1,4 +1,7 @@
{ {
"languages": {
"de": "Deutsch"
},
"general": { "general": {
"pagination": { "pagination": {
"size": "Anzahl an Elementen", "size": "Anzahl an Elementen",
@@ -23,10 +26,11 @@
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein" "placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
}, },
"password": { "password": {
"label": "Kennwort", "label": "Passwort",
"placeholder": "Bitte geben Sie Ihr Passwort ein" "placeholder": "Bitte geben Sie Ihr Passwort ein"
}, },
"button": "Anmelden" "button": "Anmelden",
"button-webauthn": "Passkey verwenden"
}, },
"menu": { "menu": {
"home": "Home", "home": "Home",
@@ -34,8 +38,11 @@
"users": "Benutzer", "users": "Benutzer",
"lang": "Sprache ändern", "lang": "Sprache ändern",
"profile": "Mein Profil", "profile": "Mein Profil",
"settings": "Einstellungen",
"audit": "Event Protokoll",
"login": "Anmelden", "login": "Anmelden",
"logout": "Abmelden" "logout": "Abmelden",
"keygen": "Schlüsselgenerator"
}, },
"home": { "home": {
"headline": "WireGuard® VPN Portal", "headline": "WireGuard® VPN Portal",
@@ -45,7 +52,7 @@
"box-header": "WireGuard Installation", "box-header": "WireGuard Installation",
"headline": "Installation", "headline": "Installation",
"content": "Die Installationsanweisungen für die Client-Software finden Sie auf der offiziellen WireGuard-Website.", "content": "Die Installationsanweisungen für die Client-Software finden Sie auf der offiziellen WireGuard-Website.",
"btn": "Anleitung öffnen" "button": "Anleitung öffnen"
}, },
"about-wg": { "about-wg": {
"box-header": "Über WireGuard", "box-header": "Über WireGuard",
@@ -75,77 +82,79 @@
}, },
"interfaces": { "interfaces": {
"headline": "Schnittstellenverwaltung", "headline": "Schnittstellenverwaltung",
"headline-peers": "Current VPN Peers", "headline-peers": "Aktuelle VPN-Peers",
"headline-endpoints": "Current Endpoints", "headline-endpoints": "Aktuelle Endpunkte",
"no-interface": { "no-interface": {
"default-selection": "No Interface available", "default-selection": "Keine Schnittstelle verfügbar",
"headline": "No interfaces found...", "headline": "Keine Schnittstellen gefunden...",
"abstract": "Click the plus button above to create a new WireGuard interface." "abstract": "Klicken Sie auf die Plus-Schaltfläche oben, um eine neue WireGuard-Schnittstelle zu erstellen."
}, },
"no-peer": { "no-peer": {
"headline": "No peers available", "headline": "Keine Peers verfügbar",
"abstract": "Currently, there are no peers available for the selected WireGuard interface." "abstract": "Derzeit sind keine Peers für die ausgewählte WireGuard-Schnittstelle verfügbar."
}, },
"table-heading": { "table-heading": {
"name": "Name", "name": "Name",
"user": "User", "user": "Benutzer",
"ip": "IP's", "ip": "IP's",
"endpoint": "Endpoint", "endpoint": "Endpunkt",
"status": "Status" "status": "Status"
}, },
"interface": { "interface": {
"headline": "Interface status for", "headline": "Schnittstellenstatus für",
"mode": "mode", "backend": "Backend",
"key": "Public Key", "unknown-backend": "Unbekannt",
"endpoint": "Public Endpoint", "wrong-backend": "Ungültiges Backend, das lokale WireGuard Backend wird stattdessen verwendet!",
"port": "Listening Port", "key": "Öffentlicher Schlüssel",
"peers": "Enabled Peers", "endpoint": "Öffentlicher Endpunkt",
"total-peers": "Total Peers", "port": "Port",
"endpoints": "Enabled Endpoints", "peers": "Aktive Peers",
"total-endpoints": "Total Endpoints", "total-peers": "Gesamtanzahl Peers",
"ip": "IP Address", "endpoints": "Aktive Endpunkte",
"default-allowed-ip": "Default allowed IPs", "total-endpoints": "Gesamtanzahl Endpunkte",
"dns": "DNS Servers", "ip": "IP-Adresse",
"default-allowed-ip": "Standard Erlaubte-IPs",
"dns": "DNS-Server",
"mtu": "MTU", "mtu": "MTU",
"default-keep-alive": "Default Keepalive Interval", "default-keep-alive": "Standard Keepalive-Intervall",
"button-show-config": "Show configuration", "button-show-config": "Konfiguration anzeigen",
"button-download-config": "Download configuration", "button-download-config": "Konfiguration herunterladen",
"button-store-config": "Store configuration for wg-quick", "button-store-config": "Konfiguration für wg-quick speichern",
"button-edit": "Edit interface" "button-edit": "Schnittstelle bearbeiten"
}, },
"button-add-interface": "Add Interface", "button-add-interface": "Schnittstelle hinzufügen",
"button-add-peer": "Add Peer", "button-add-peer": "Peer hinzufügen",
"button-add-peers": "Add Multiple Peers", "button-add-peers": "Mehrere Peers hinzufügen",
"button-show-peer": "Show Peer", "button-show-peer": "Peer anzeigen",
"button-edit-peer": "Edit Peer", "button-edit-peer": "Peer bearbeiten",
"peer-disabled": "Peer is disabled, reason:", "peer-disabled": "Peer ist deaktiviert, Grund:",
"peer-expiring": "Peer is expiring at", "peer-expiring": "Peer läuft ab am",
"peer-connected": "Connected", "peer-connected": "Verbunden",
"peer-not-connected": "Not Connected", "peer-not-connected": "Nicht verbunden",
"peer-handshake": "Last handshake:" "peer-handshake": "Letzter Handshake:"
}, },
"users": { "users": {
"headline": "Benutzerverwaltung", "headline": "Benutzerverwaltung",
"table-heading": { "table-heading": {
"id": "ID", "id": "ID",
"email": "E-Mail", "email": "E-Mail",
"firstname": "Firstname", "firstname": "Vorname",
"lastname": "Lastname", "lastname": "Nachname",
"source": "Source", "source": "Quelle",
"peers": "Peers", "peers": "Peers",
"admin": "Admin" "admin": "Admin"
}, },
"no-user": { "no-user": {
"headline": "No users available", "headline": "Keine Benutzer verfügbar",
"abstract": "Currently, there are no users registered with WireGuard Portal." "abstract": "Derzeit sind keine Benutzer im WireGuard-Portal registriert."
}, },
"button-add-user": "Add User", "button-add-user": "Benutzer hinzufügen",
"button-show-user": "Show User", "button-show-user": "Benutzer anzeigen",
"button-edit-user": "Edit User", "button-edit-user": "Benutzer bearbeiten",
"user-disabled": "User is disabled, reason:", "user-disabled": "Benutzer ist deaktiviert, Grund:",
"user-locked": "Account is locked, reason:", "user-locked": "Konto ist gesperrt, Grund:",
"admin": "User has administrator privileges", "admin": "Benutzer hat Administratorrechte",
"no-admin": "User has no administrator privileges" "no-admin": "Benutzer hat keine Administratorrechte"
}, },
"profile": { "profile": {
"headline": "Meine VPN-Konfigurationen", "headline": "Meine VPN-Konfigurationen",
@@ -153,337 +162,430 @@
"name": "Name", "name": "Name",
"ip": "IP's", "ip": "IP's",
"stats": "Status", "stats": "Status",
"interface": "Server Interface" "interface": "Server-Schnittstelle"
}, },
"no-peer": { "no-peer": {
"headline": "No peers available", "headline": "Keine Peers verfügbar",
"abstract": "Currently, there are no peers associated with your user profile." "abstract": "Derzeit sind keine Peers mit Ihrem Benutzerprofil verknüpft."
}, },
"peer-connected": "Connected", "peer-connected": "Verbunden",
"button-add-peer": "Add Peer", "button-add-peer": "Peer hinzufügen",
"button-show-peer": "Show Peer", "button-show-peer": "Peer anzeigen",
"button-edit-peer": "Edit Peer" "button-edit-peer": "Peer bearbeiten"
},
"settings": {
"headline": "Einstellungen",
"abstract": "Hier finden Sie persönliche Einstellungen für WireGuard Portal.",
"api": {
"headline": "API Einstellungen",
"abstract": "Hier können Sie die RESTful API verwalten.",
"active-description": "Die API ist derzeit für Ihr Benutzerkonto aktiv. Alle API-Anfragen werden mit Basic Auth authentifiziert. Verwenden Sie zur Authentifizierung die folgenden Anmeldeinformationen.",
"inactive-description": "Die API ist derzeit inaktiv. Klicken Sie auf die Schaltfläche unten, um sie zu aktivieren.",
"user-label": "API Benutzername:",
"user-placeholder": "API Benutzer",
"token-label": "API Passwort:",
"token-placeholder": "API Token",
"token-created-label": "API-Zugriff gewährt seit: ",
"button-disable-title": "Deaktivieren Sie die API. Dadurch wird der aktuelle Token ungültig.",
"button-disable-text": "API deaktivieren",
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
"button-enable-text": "API aktivieren",
"api-link": "API Dokumentation"
},
"webauthn": {
"headline": "Passkey-Einstellungen",
"abstract": "Passkeys sind eine moderne Möglichkeit, Benutzer ohne Passwort zu authentifizieren. Sie werden sicher in Ihrem Browser gespeichert und können verwendet werden, um sich im WireGuard-Portal anzumelden.",
"active-description": "Mindestens ein Passkey ist derzeit für Ihr Benutzerkonto aktiv.",
"inactive-description": "Für Ihr Benutzerkonto sind derzeit keine Passkeys registriert. Drücken Sie die Schaltfläche unten, um einen neuen Passkey zu registrieren.",
"table": {
"name": "Name",
"created": "Erstellt",
"actions": ""
},
"credentials-list": "Derzeit registrierte Passkeys",
"modal-delete": {
"headline": "Passkey löschen",
"abstract": "Sind Sie sicher, dass Sie diesen Passkey löschen möchten? Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
"created": "Erstellt:",
"button-delete": "Löschen",
"button-cancel": "Abbrechen"
},
"button-rename-title": "Umbenennen",
"button-rename-text": "Passkey umbenennen.",
"button-save-title": "Speichern",
"button-save-text": "Neuen Namen des Passkeys speichern.",
"button-cancel-title": "Abbrechen",
"button-cancel-text": "Umbenennung des Passkeys abbrechen.",
"button-delete-title": "Löschen",
"button-delete-text": "Passkey löschen. Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
"button-register-title": "Passkey registrieren",
"button-register-text": "Einen neuen Passkey registrieren, um Ihr Konto zu sichern."
}
},
"audit": {
"headline": "Eventprotokoll",
"abstract": "Hier finden Sie das Eventprotokoll aller im WireGuard-Portal vorgenommenen Aktionen.",
"no-entries": {
"headline": "Keine Protokolleinträge verfügbar",
"abstract": "Derzeit sind keine Eventprotokolle aufgezeichnet."
},
"entries-headline": "Protokolleinträge",
"table-heading": {
"id": "#",
"time": "Zeit",
"user": "Benutzer",
"severity": "Schweregrad",
"origin": "Ursprung",
"message": "Nachricht"
}
},
"keygen": {
"headline": "WireGuard Key Generator",
"abstract": "Hier können Sie WireGuard Schlüsselpaare generieren. Die Schlüssel werden lokal auf Ihrem Computer generiert und niemals an den Server gesendet.",
"headline-keypair": "Neues Schlüsselpaar",
"headline-preshared-key": "Neuer Pre-Shared Key",
"button-generate": "Erzeugen",
"private-key": {
"label": "Privater Schlüssel",
"placeholder": "Der private Schlüssel"
},
"public-key": {
"label": "Öffentlicher Schlüssel",
"placeholder": "Der öffentliche Schlüssel"
},
"preshared-key": {
"label": "Pre-Shared Key",
"placeholder": "Der geteilte Schlüssel"
}
}, },
"modals": { "modals": {
"user-view": { "user-view": {
"headline": "User Account:", "headline": "Benutzerkonto:",
"tab-user": "Information", "tab-user": "Informationen",
"tab-peers": "Peers", "tab-peers": "Peers",
"headline-info": "User Information:", "headline-info": "Benutzerinformationen:",
"headline-notes": "Notes:", "headline-notes": "Notizen:",
"email": "E-Mail", "email": "E-Mail",
"firstname": "Firstname", "firstname": "Vorname",
"lastname": "Lastname", "lastname": "Nachname",
"phone": "Phone number", "phone": "Telefonnummer",
"department": "Department", "department": "Abteilung",
"disabled": "Account Disabled", "api-enabled": "API-Zugriff",
"locked": "Account Locked", "disabled": "Konto deaktiviert",
"no-peers": "User has no associated peers.", "locked": "Konto gesperrt",
"no-peers": "Benutzer hat keine zugeordneten Peers.",
"peers": { "peers": {
"name": "Name", "name": "Name",
"interface": "Interface", "interface": "Schnittstelle",
"ip": "IP's" "ip": "IP's"
} }
}, },
"user-edit": { "user-edit": {
"headline-edit": "Edit user:", "headline-edit": "Benutzer bearbeiten:",
"headline-new": "New user", "headline-new": "Neuer Benutzer",
"header-general": "General", "header-general": "Allgemein",
"header-personal": "User Information", "header-personal": "Benutzerinformationen",
"header-notes": "Notes", "header-notes": "Notizen",
"header-state": "State", "header-state": "Status",
"identifier": { "identifier": {
"label": "Identifier", "label": "Kennung",
"placeholder": "The unique user identifier" "placeholder": "Die eindeutige Benutzerkennung"
}, },
"source": { "source": {
"label": "Source", "label": "Quelle",
"placeholder": "The user source" "placeholder": "Die Benutzerquelle"
}, },
"password": { "password": {
"label": "Password", "label": "Passwort",
"placeholder": "A super secret password", "placeholder": "Ein super geheimes Passwort",
"description": "Leave this field blank to keep current password." "description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten.",
"too-weak": "Das Passwort entspricht nicht den Sicherheitsanforderungen."
}, },
"email": { "email": {
"label": "Email", "label": "E-Mail",
"placeholder": "The email address" "placeholder": "Die E-Mail-Adresse"
}, },
"phone": { "phone": {
"label": "Phone", "label": "Telefon",
"placeholder": "The phone number" "placeholder": "Die Telefonnummer"
}, },
"department": { "department": {
"label": "Department", "label": "Abteilung",
"placeholder": "The department" "placeholder": "Die Abteilung"
}, },
"firstname": { "firstname": {
"label": "Firstname", "label": "Vorname",
"placeholder": "Firstname" "placeholder": "Vorname"
}, },
"lastname": { "lastname": {
"label": "Lastname", "label": "Nachname",
"placeholder": "Lastname" "placeholder": "Nachname"
}, },
"notes": { "notes": {
"label": "Notes", "label": "Notizen",
"placeholder": "" "placeholder": ""
}, },
"disabled": { "disabled": {
"label": "Disabled (no WireGuard connection and no login possible)" "label": "Deaktiviert (keine WireGuard-Verbindung und kein Login möglich)"
}, },
"locked": { "locked": {
"label": "Locked (no login possible, WireGuard connections still work)" "label": "Gesperrt (kein Login möglich, WireGuard-Verbindungen funktionieren weiterhin)"
}, },
"admin": { "admin": {
"label": "Is Admin" "label": "Ist Administrator"
} }
}, },
"interface-view": { "interface-view": {
"headline": "Config for Interface:" "headline": "Konfiguration für Schnittstelle:"
}, },
"interface-edit": { "interface-edit": {
"headline-edit": "Edit Interface:", "headline-edit": "Schnittstelle bearbeiten:",
"headline-new": "New Interface", "headline-new": "Neue Schnittstelle",
"tab-interface": "Interface", "tab-interface": "Schnittstelle",
"tab-peerdef": "Peer Defaults", "tab-peerdef": "Peer-Standardeinstellungen",
"header-general": "General", "header-general": "Allgemein",
"header-network": "Network", "header-network": "Netzwerk",
"header-crypto": "Cryptography", "header-crypto": "Kryptografie",
"header-hooks": "Interface Hooks", "header-hooks": "Schnittstellen-Hooks",
"header-peer-hooks": "Hooks", "header-peer-hooks": "Hooks",
"header-state": "State", "header-state": "Status",
"identifier": { "identifier": {
"label": "Identifier", "label": "Kennung",
"placeholder": "The unique interface identifier" "placeholder": "Die eindeutige Schnittstellenkennung"
}, },
"mode": { "mode": {
"label": "Interface Mode", "label": "Schnittstellenmodus",
"server": "Server Mode", "server": "Server-Modus",
"client": "Client Mode", "client": "Client-Modus",
"any": "Unknown Mode" "any": "Unbekannter Modus"
},
"backend": {
"label": "Schnittstellenbackend",
"invalid-label": "Ursprüngliches Backend ist ungültig, das lokale WireGuard Backend wird stattdessen verwendet!",
"local": "Lokales WireGuard Backend"
}, },
"display-name": { "display-name": {
"label": "Display Name", "label": "Anzeigename",
"placeholder": "The descriptive name for the interface" "placeholder": "Der beschreibende Name für die Schnittstelle"
}, },
"private-key": { "private-key": {
"label": "Private Key", "label": "Privater Schlüssel",
"placeholder": "The private key" "placeholder": "Der private Schlüssel"
}, },
"public-key": { "public-key": {
"label": "Public Key", "label": "Öffentlicher Schlüssel",
"placeholder": "The public key" "placeholder": "Der öffentliche Schlüssel"
}, },
"ip": { "ip": {
"label": "IP Addresses", "label": "IP-Adressen",
"placeholder": "IP Addresses (CIDR format)" "placeholder": "IP-Adressen (CIDR-Format)"
}, },
"listen-port": { "listen-port": {
"label": "Listen Port", "label": "Port",
"placeholder": "The listening port" "placeholder": "Der Port der WireGuard Schnittstelle"
}, },
"dns": { "dns": {
"label": "DNS Server", "label": "DNS-Server",
"placeholder": "The DNS servers that should be used" "placeholder": "Die zu verwendenden DNS-Server"
}, },
"dns-search": { "dns-search": {
"label": "DNS Search Domains", "label": "DNS-Suchdomänen",
"placeholder": "DNS search prefixes" "placeholder": "DNS-Suchpräfixe"
}, },
"mtu": { "mtu": {
"label": "MTU", "label": "MTU",
"placeholder": "The interface MTU (0 = keep default)" "placeholder": "Die Schnittstellen-MTU (0 = Standard beibehalten)"
}, },
"firewall-mark": { "firewall-mark": {
"label": "Firewall Mark", "label": "Firewall-Markierung",
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)" "placeholder": "Firewall-Markierung, die auf ausgehenden Datenverkehr angewendet wird. (0 = automatisch)"
}, },
"routing-table": { "routing-table": {
"label": "Routing Table", "label": "Routing-Tabelle",
"placeholder": "The routing table ID", "placeholder": "Die Routing-Tabellen-ID",
"description": "Special cases: off = do not manage routes, 0 = automatic" "description": "Spezialfälle: off = Routen nicht verwalten, 0 = automatisch"
}, },
"pre-up": { "pre-up": {
"label": "Pre-Up", "label": "Pre-Up",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"post-up": { "post-up": {
"label": "Post-Up", "label": "Post-Up",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"pre-down": { "pre-down": {
"label": "Pre-Down", "label": "Pre-Down",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"post-down": { "post-down": {
"label": "Post-Down", "label": "Post-Down",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"disabled": { "disabled": {
"label": "Interface Disabled" "label": "Schnittstelle deaktiviert"
}, },
"save-config": { "save-config": {
"label": "Automatically save wg-quick config" "label": "wg-quick Konfiguration automatisch speichern"
}, },
"defaults": { "defaults": {
"endpoint": { "endpoint": {
"label": "Endpoint Address", "label": "Endpunktadresse",
"placeholder": "Endpoint Address", "placeholder": "Endpunktadresse",
"description": "The endpoint address that peers will connect to." "description": "Die Endpunktadresse, mit der sich Peers verbinden. (z.B. wg.example.com oder wg.example.com:51820)"
}, },
"networks": { "networks": {
"label": "IP Networks", "label": "IP-Netzwerke",
"placeholder": "Network Addresses", "placeholder": "Netzwerkadressen",
"description": "Peers will get IP addresses from those subnets." "description": "Peers erhalten IP-Adressen aus diesen Subnetzen."
}, },
"allowed-ip": { "allowed-ip": {
"label": "Allowed IP Addresses", "label": "Erlaubte IP-Adressen",
"placeholder": "Default Allowed IP Addresses" "placeholder": "Erlaubte IP-Adressen für Peers"
}, },
"mtu": { "mtu": {
"label": "MTU", "label": "MTU",
"placeholder": "The client MTU (0 = keep default)" "placeholder": "Die Client-MTU (0 = Standard beibehalten)"
}, },
"keep-alive": { "keep-alive": {
"label": "Keep Alive Interval", "label": "Keepalive-Intervall",
"placeholder": "Persistent Keepalive (0 = default)" "placeholder": "Persistentes Keepalive (0 = Standard)"
} }
}, },
"button-apply-defaults": "Peer-Standardeinstellungen anwenden"
"button-apply-defaults": "Apply Peer Defaults"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
"headline-endpoint": "Endpoint:", "headline-endpoint": "Endpunkt:",
"section-info": "Peer Information", "section-info": "Peer-Informationen",
"section-status": "Current Status", "section-status": "Aktueller Status",
"section-config": "Configuration", "section-config": "Konfiguration",
"identifier": "Identifier", "identifier": "Kennung",
"ip": "IP Addresses", "ip": "IP-Adressen",
"user": "Associated User", "user": "Zugeordneter Benutzer",
"notes": "Notes", "notes": "Notizen",
"expiry-status": "Expires At", "expiry-status": "Läuft ab am",
"disabled-status": "Disabled At", "disabled-status": "Deaktiviert am",
"traffic": "Traffic", "traffic": "Datenverkehr",
"connection-status": "Connection Stats", "connection-status": "Verbindungsstatistiken",
"upload": "Uploaded Bytes (from Server to Peer)", "upload": "Hochgeladene Bytes (vom Server zum Peer)",
"download": "Downloaded Bytes (from Peer to Server)", "download": "Heruntergeladene Bytes (vom Peer zum Server)",
"pingable": "Is Pingable", "pingable": "Pingbar",
"handshake": "Last Handshake", "handshake": "Letzter Handshake",
"connected-since": "Connected since", "connected-since": "Verbunden seit",
"endpoint": "Endpoint", "endpoint": "Endpunkt",
"button-download": "Download configuration", "button-download": "Konfiguration herunterladen",
"button-email": "Send configuration via E-Mail" "button-email": "Konfiguration per E-Mail senden",
"style-label": "Konfigurationsformat"
}, },
"peer-edit": { "peer-edit": {
"headline-edit-peer": "Edit peer:", "headline-edit-peer": "Peer bearbeiten:",
"headline-edit-endpoint": "Edit endpoint:", "headline-edit-endpoint": "Endpunkt bearbeiten:",
"headline-new-peer": "Create peer", "headline-new-peer": "Peer erstellen",
"headline-new-endpoint": "Create endpoint", "headline-new-endpoint": "Endpunkt erstellen",
"header-general": "General", "header-general": "Allgemein",
"header-network": "Network", "header-network": "Netzwerk",
"header-crypto": "Cryptography", "header-crypto": "Kryptografie",
"header-hooks": "Hooks (Executed on Peer)", "header-hooks": "Hooks (beim Peer ausgeführt)",
"header-state": "State", "header-state": "Status",
"display-name": { "display-name": {
"label": "Display Name", "label": "Anzeigename",
"placeholder": "The descriptive name for the peer" "placeholder": "Der beschreibende Name für den Peer"
}, },
"linked-user": { "linked-user": {
"label": "Linked User", "label": "Verknüpfter Benutzer",
"placeholder": "The user account which owns this peer" "placeholder": "Das Benutzerkonto, dem dieser Peer gehört"
}, },
"private-key": { "private-key": {
"label": "Private Key", "label": "Privater Schlüssel",
"placeholder": "The private key" "placeholder": "Der private Schlüssel",
"help": "Der private Schlüssel wird sicher auf dem Server gespeichert. Wenn der Benutzer bereits eine Kopie besitzt, kann dieses Feld entfallen. Der Server funktioniert auch ausschließlich mit dem öffentlichen Schlüssel des Peers."
}, },
"public-key": { "public-key": {
"label": "Public Key", "label": "Öffentlicher Schlüssel",
"placeholder": "The public key" "placeholder": "Der öffentliche Schlüssel"
}, },
"preshared-key": { "preshared-key": {
"label": "Preshared Key", "label": "Pre-Shared Key",
"placeholder": "Optional pre-shared key" "placeholder": "Optionaler geteilter Schlüssel"
}, },
"endpoint-public-key": { "endpoint-public-key": {
"label": "Endpoint public Key", "label": "Öffentlicher Endpunktschlüssel",
"placeholder": "The public key of the remote endpoint" "placeholder": "Der öffentliche Schlüssel des entfernten Endpunkts"
}, },
"endpoint": { "endpoint": {
"label": "Endpoint Address", "label": "Endpunktadresse",
"placeholder": "The address of the remote endpoint" "placeholder": "Die Adresse des entfernten Endpunkts"
}, },
"ip": { "ip": {
"label": "IP Addresses", "label": "IP-Adressen",
"placeholder": "IP Addresses (CIDR format)" "placeholder": "IP-Adressen (CIDR-Format)"
}, },
"allowed-ip": { "allowed-ip": {
"label": "Allowed IP Addresses", "label": "Erlaubte IP-Adressen",
"placeholder": "Allowed IP Addresses (CIDR format)" "placeholder": "Erlaubte IP-Adressen (CIDR-Format)"
}, },
"extra-allowed-ip": { "extra-allowed-ip": {
"label": "Extra allowed IP Addresses", "label": "Zusätzliche erlaubte IP-Adressen",
"placeholder": "Extra allowed IP's (Server Sided)", "placeholder": "Zusätzliche erlaubte IP's (Server-seitig)",
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's." "description": "Diese IPs werden an der entfernten WireGuard-Schnittstelle als erlaubte IPs hinzugefügt."
}, },
"dns": { "dns": {
"label": "DNS Server", "label": "DNS-Server",
"placeholder": "The DNS servers that should be used" "placeholder": "Die zu verwendenden DNS-Server"
}, },
"dns-search": { "dns-search": {
"label": "DNS Search Domains", "label": "DNS-Suchdomänen",
"placeholder": "DNS search prefixes" "placeholder": "DNS-Suchpräfixe"
}, },
"keep-alive": { "keep-alive": {
"label": "Keep Alive Interval", "label": "Keepalive-Intervall",
"placeholder": "Persistent Keepalive (0 = default)" "placeholder": "Persistentes Keepalive (0 = Standard)"
}, },
"mtu": { "mtu": {
"label": "MTU", "label": "MTU",
"placeholder": "The client MTU (0 = keep default)" "placeholder": "Die Client-MTU (0 = Standard beibehalten)"
}, },
"pre-up": { "pre-up": {
"label": "Pre-Up", "label": "Pre-Up",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"post-up": { "post-up": {
"label": "Post-Up", "label": "Post-Up",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"pre-down": { "pre-down": {
"label": "Pre-Down", "label": "Pre-Down",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"post-down": { "post-down": {
"label": "Post-Down", "label": "Post-Down",
"placeholder": "One or multiple bash commands separated by ;" "placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
}, },
"disabled": { "disabled": {
"label": "Peer Disabled" "label": "Peer deaktiviert"
}, },
"ignore-global": { "ignore-global": {
"label": "Ignore global settings" "label": "Globale Einstellungen ignorieren"
}, },
"expires-at": { "expires-at": {
"label": "Expiry date" "label": "Ablaufdatum"
} }
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Create multiple peers", "headline-peer": "Mehrere Peers erstellen",
"headline-endpoint": "Create multiple endpoints", "headline-endpoint": "Mehrere Endpunkte erstellen",
"identifiers": { "identifiers": {
"label": "User Identifiers", "label": "Benutzerkennungen",
"placeholder": "User Identifiers", "placeholder": "Benutzerkennungen",
"description": "A user identifier (the username) for which a peer should be created." "description": "Eine Benutzerkennung (der Benutzername), für die ein Peer erstellt werden soll."
}, },
"prefix": { "prefix": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
"headline-endpoint": "Endpoint:", "headline-endpoint": "Endpunkt:",
"label": "Display Name Prefix", "label": "Anzeigename-Präfix",
"placeholder": "The prefix", "placeholder": "Das Präfix",
"description": "A prefix that is added to the peers display name." "description": "Ein Präfix, das dem Anzeigenamen des Peers hinzugefügt wird."
} }
} }
} }
} }

View File

@@ -1,4 +1,7 @@
{ {
"languages": {
"en": "English"
},
"general": { "general": {
"pagination": { "pagination": {
"size": "Number of Elements", "size": "Number of Elements",
@@ -26,7 +29,8 @@
"label": "Password", "label": "Password",
"placeholder": "Please enter your password" "placeholder": "Please enter your password"
}, },
"button": "Sign in" "button": "Sign in",
"button-webauthn": "Use Passkey"
}, },
"menu": { "menu": {
"home": "Home", "home": "Home",
@@ -34,8 +38,11 @@
"users": "Users", "users": "Users",
"lang": "Toggle Language", "lang": "Toggle Language",
"profile": "My Profile", "profile": "My Profile",
"settings": "Settings",
"audit": "Audit Log",
"login": "Login", "login": "Login",
"logout": "Logout" "logout": "Logout",
"keygen": "Key Generator"
}, },
"home": { "home": {
"headline": "WireGuard® VPN Portal", "headline": "WireGuard® VPN Portal",
@@ -45,7 +52,7 @@
"box-header": "WireGuard Installation", "box-header": "WireGuard Installation",
"headline": "Installation", "headline": "Installation",
"content": "Installation instructions for client software can be found on the official WireGuard website.", "content": "Installation instructions for client software can be found on the official WireGuard website.",
"btn": "Open Instructions" "button": "Open Instructions"
}, },
"about-wg": { "about-wg": {
"box-header": "About WireGuard", "box-header": "About WireGuard",
@@ -95,7 +102,9 @@
}, },
"interface": { "interface": {
"headline": "Interface status for", "headline": "Interface status for",
"mode": "mode", "backend": "Backend",
"unknown-backend": "Unknown",
"wrong-backend": "Invalid backend, using local WireGuard backend instead!",
"key": "Public Key", "key": "Public Key",
"endpoint": "Public Endpoint", "endpoint": "Public Endpoint",
"port": "Listening Port", "port": "Listening Port",
@@ -164,6 +173,91 @@
"button-show-peer": "Show Peer", "button-show-peer": "Show Peer",
"button-edit-peer": "Edit Peer" "button-edit-peer": "Edit Peer"
}, },
"settings": {
"headline": "Settings",
"abstract": "Here you can change your personal settings.",
"api": {
"headline": "API Settings",
"abstract": "Here you can configure the RESTful API settings.",
"active-description": "The API is currently active for your user account. All API requests are authenticated with Basic Auth. Use the following credentials for authentication.",
"inactive-description": "The API is currently inactive. Press the button below to activate it.",
"user-label": "API Username:",
"user-placeholder": "The API user",
"token-label": "API Password:",
"token-placeholder": "The API token",
"token-created-label": "API access granted at: ",
"button-disable-title": "Disable API, this will invalidate the current token.",
"button-disable-text": "Disable API",
"button-enable-title": "Enable API, this will generate a new token.",
"button-enable-text": "Enable API",
"api-link": "API Documentation"
},
"webauthn": {
"headline": "Passkey Settings",
"abstract": "Passkeys are a modern way to authenticate users without the need for passwords. They are stored securely in your browser and can be used to log in to the WireGuard Portal.",
"active-description": "At least one passkey is currently active for your user account.",
"inactive-description": "No passkeys are currently registered for your user account. Press the button below to register a new passkey.",
"table": {
"name": "Name",
"created": "Created",
"actions": ""
},
"credentials-list": "Currently registered Passkeys",
"modal-delete": {
"headline": "Delete Passkey",
"abstract": "Are you sure you want to delete this passkey? You will not be able to log in with this passkey anymore.",
"created": "Created:",
"button-delete": "Delete",
"button-cancel": "Cancel"
},
"button-rename-title": "Rename",
"button-rename-text": "Rename the passkey.",
"button-save-title": "Save",
"button-save-text": "Save the new name of the passkey.",
"button-cancel-title": "Cancel",
"button-cancel-text": "Cancel the renaming of the passkey.",
"button-delete-title": "Delete",
"button-delete-text": "Delete the passkey. You will not be able to log in with this passkey anymore.",
"button-register-title": "Register Passkey",
"button-register-text": "Register a new Passkey to secure your account."
}
},
"audit": {
"headline": "Audit Log",
"abstract": "Here you can find the audit log of all actions performed in the WireGuard Portal.",
"no-entries": {
"headline": "No log entries available",
"abstract": "Currently, there are no audit logs recorded."
},
"entries-headline": "Log Entries",
"table-heading": {
"id": "#",
"time": "Time",
"user": "User",
"severity": "Severity",
"origin": "Origin",
"message": "Message"
}
},
"keygen": {
"headline": "WireGuard Key Generator",
"abstract": "Generate a new WireGuard keys. The keys are generated in your local browser and are never sent to the server.",
"headline-keypair": "New Key Pair",
"headline-preshared-key": "New Preshared Key",
"button-generate": "Generate",
"private-key": {
"label": "Private Key",
"placeholder": "The private key"
},
"public-key": {
"label": "Public Key",
"placeholder": "The public key"
},
"preshared-key": {
"label": "Preshared Key",
"placeholder": "The pre-shared key"
}
},
"modals": { "modals": {
"user-view": { "user-view": {
"headline": "User Account:", "headline": "User Account:",
@@ -174,8 +268,9 @@
"email": "E-Mail", "email": "E-Mail",
"firstname": "Firstname", "firstname": "Firstname",
"lastname": "Lastname", "lastname": "Lastname",
"phone": "Phone number", "phone": "Phone Number",
"department": "Department", "department": "Department",
"api-enabled": "API Access",
"disabled": "Account Disabled", "disabled": "Account Disabled",
"locked": "Account Locked", "locked": "Account Locked",
"no-peers": "User has no associated peers.", "no-peers": "User has no associated peers.",
@@ -203,7 +298,8 @@
"password": { "password": {
"label": "Password", "label": "Password",
"placeholder": "A super secret password", "placeholder": "A super secret password",
"description": "Leave this field blank to keep current password." "description": "Leave this field blank to keep current password.",
"too-weak": "The password is too weak. Please use a stronger password."
}, },
"email": { "email": {
"label": "Email", "label": "Email",
@@ -263,6 +359,11 @@
"client": "Client Mode", "client": "Client Mode",
"any": "Unknown Mode" "any": "Unknown Mode"
}, },
"backend": {
"label": "Interface Backend",
"invalid-label": "Original backend is no longer available, using local WireGuard backend instead!",
"local": "Local WireGuard Backend"
},
"display-name": { "display-name": {
"label": "Display Name", "label": "Display Name",
"placeholder": "The descriptive name for the interface" "placeholder": "The descriptive name for the interface"
@@ -330,7 +431,7 @@
"endpoint": { "endpoint": {
"label": "Endpoint Address", "label": "Endpoint Address",
"placeholder": "Endpoint Address", "placeholder": "Endpoint Address",
"description": "The endpoint address that peers will connect to." "description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
}, },
"networks": { "networks": {
"label": "IP Networks", "label": "IP Networks",
@@ -374,7 +475,8 @@
"connected-since": "Connected since", "connected-since": "Connected since",
"endpoint": "Endpoint", "endpoint": "Endpoint",
"button-download": "Download configuration", "button-download": "Download configuration",
"button-email": "Send configuration via E-Mail" "button-email": "Send configuration via E-Mail",
"style-label": "Configuration Style"
}, },
"peer-edit": { "peer-edit": {
"headline-edit-peer": "Edit peer:", "headline-edit-peer": "Edit peer:",
@@ -396,7 +498,8 @@
}, },
"private-key": { "private-key": {
"label": "Private Key", "label": "Private Key",
"placeholder": "The private key" "placeholder": "The private key",
"help": "The private key is stored securely on the server. If the user already holds a copy, you may omit this field. The server still functions exclusively with the peers public key."
}, },
"public-key": { "public-key": {
"label": "Public Key", "label": "Public Key",

View File

@@ -0,0 +1,587 @@
{
"languages": {
"es": "Español"
},
"general": {
"pagination": {
"size": "Numero de elementos",
"all": "Todos (Lento)"
},
"search": {
"placeholder": "Buscar...",
"button": "Buscar"
},
"select-all": "Buscar todos",
"yes": "Sí",
"no": "No",
"cancel": "Cancelar",
"close": "Cerrar",
"save": "Guardar",
"delete": "Eliminar"
},
"login": {
"headline": "Por favor inicie sesión",
"username": {
"label": "Usuario",
"placeholder": "Por favor ingrese su usuario"
},
"password": {
"label": "Contraseña",
"placeholder": "Por favor ingrese su contraseña"
},
"button": "Ingresar",
"button-webauthn": "Usar clave de acceso"
},
"menu": {
"home": "Inicio",
"interfaces": "Interfaces",
"users": "Usuarios",
"lang": "Cambiar idioma",
"profile": "Mi perfil",
"settings": "Configuración",
"audit": "Registro de auditoría",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"keygen": "Generador de claves"
},
"home": {
"headline": "Portal VPN WireGuard®",
"info-headline": "Más información",
"abstract": "WireGuard® es una VPN extremadamente simple pero rápida y moderna que utiliza criptografía de última generación. Su objetivo es ser más rápida, simple, ligera y útil que IPsec, a la vez que evita los enormes problemas que supone. Su objetivo es ofrecer un rendimiento considerablemente superior al de OpenVPN.",
"installation": {
"box-header": "Instalación de WireGuard",
"headline": "Instalación",
"content": "Las instrucciones de instalación del cliente se pueden encontrar en el sitio web oficial de WireGuard.",
"button": "Abrir instrucciones"
},
"about-wg": {
"box-header": "Acerca de WireGuard",
"headline": "Acerca de",
"content": "WireGuard® es una VPN extremadamente simple pero rápida y moderna que utiliza criptografía de última generación.",
"button": "Más"
},
"about-portal": {
"box-header": "Acerca del Portal WireGuard",
"headline": "Portal WireGuard",
"content": "WireGuard Portal es un portal web simple para la configuración de WireGuard.",
"button": "Más"
},
"profiles": {
"headline": "Perfiles VPN",
"abstract": "Puedes acceder y descargar tus configuraciones personales de VPN desde tu perfil de usuario.",
"content": "para ver todos tus perfiles configurados, haz clic en el botón de abajo.",
"button": "Abrir mi perfil"
},
"admin": {
"headline": "Área de administración",
"abstract": "En el área de administración puedes gestionar los peers de WireGuard, la interfaz del servidor, así como los usuarios que tienen acceso al Portal WireGuard.",
"content": "",
"button-admin": "Abrir administración del servidor",
"button-user": "Abrir administración de usuarios"
}
},
"interfaces": {
"headline": "Administración de interfaces",
"headline-peers": "Peers VPN actuales",
"headline-endpoints": "Extremos actuales",
"no-interface": {
"default-selection": "No hay interfaces disponibles",
"headline": "No se encontraron interfaces...",
"abstract": "Haz clic en el botón + para crear una nueva interfaz WireGuard."
},
"no-peer": {
"headline": "No hay peers disponibles",
"abstract": "Actualmente no hay peers disponibles para la interfaz WireGuard seleccionada."
},
"table-heading": {
"name": "Nombre",
"user": "Usuario",
"ip": "IP's",
"endpoint": "Endpoint",
"status": "Estado"
},
"interface": {
"headline": "Estado de la interfaz para",
"backend": "Backend",
"unknown-backend": "Desconocido",
"wrong-backend": "Backend inválido, usando backend local de WireGuard en su lugar.",
"key": "Clave pública",
"endpoint": "Endpoint público",
"port": "Puerto de escucha",
"peers": "Peers habilitados",
"total-peers": "Peers totales",
"endpoints": "Endpoints habilitados",
"total-endpoints": "Endpoints totales",
"ip": "Dirección IP",
"default-allowed-ip": "IPs permitidas por defecto",
"dns": "Servidores DNS",
"mtu": "MTU",
"default-keep-alive": "Intervalo Keepalive por defecto",
"button-show-config": "Mostrar configuración",
"button-download-config": "Descargar configuración",
"button-store-config": "Guardar configuración para wg-quick",
"button-edit": "Editar interfaz"
},
"button-add-interface": "Agregar interfaz",
"button-add-peer": "Agregar peer",
"button-add-peers": "Agregar múltiples peers",
"button-show-peer": "Mostrar peer",
"button-edit-peer": "Editar peer",
"peer-disabled": "Peer deshabilitado, motivo:",
"peer-expiring": "El peer expira en",
"peer-connected": "Conectado",
"peer-not-connected": "No conectado",
"peer-handshake": "Último handshake:"
},
"users": {
"headline": "Administración de usuarios",
"table-heading": {
"id": "ID",
"email": "Correo electrónico",
"firstname": "Nombre",
"lastname": "Apellido",
"source": "Origen",
"peers": "Peers",
"admin": "Administrador"
},
"no-user": {
"headline": "No hay usuarios disponibles",
"abstract": "Actualmente no hay usuarios registrados en el Portal WireGuard."
},
"button-add-user": "Agregar usuario",
"button-show-user": "Mostrar usuario",
"button-edit-user": "Editar usuario",
"user-disabled": "Usuario deshabilitado, motivo:",
"user-locked": "Cuenta bloqueada, motivo:",
"admin": "El usuario tiene privilegios de administrador",
"no-admin": "El usuario no tiene privilegios de administrador"
},
"profile": {
"headline": "Mis peers VPN",
"table-heading": {
"name": "Nombre",
"ip": "IP's",
"stats": "Estado",
"interface": "Interfaz del servidor"
},
"no-peer": {
"headline": "No hay peers disponibles",
"abstract": "Actualmente no hay peers asociados a tu perfil de usuario."
},
"peer-connected": "Conectado",
"button-add-peer": "Agregar peer",
"button-show-peer": "Mostrar peer",
"button-edit-peer": "Editar peer"
},
"settings": {
"headline": "Configuración",
"abstract": "Aquí puedes cambiar tu configuración personal.",
"api": {
"headline": "Configuración de API",
"abstract": "Aquí puedes configurar los ajustes de la API RESTful.",
"active-description": "La API está actualmente activa para tu cuenta. Todas las solicitudes están autenticadas con Basic Auth. Usa las siguientes credenciales.",
"inactive-description": "La API está actualmente inactiva. Presiona el botón de abajo para activarla.",
"user-label": "Usuario de la API:",
"user-placeholder": "Usuario de la API",
"token-label": "Contraseña de la API:",
"token-placeholder": "Token de la API",
"token-created-label": "Acceso API concedido en: ",
"button-disable-title": "Desactivar API, invalidará el token actual.",
"button-disable-text": "Desactivar API",
"button-enable-title": "Activar API, generará un nuevo token.",
"button-enable-text": "Activar API",
"api-link": "Documentación de API"
},
"webauthn": {
"headline": "Configuración de llave de acceso",
"abstract": "Las llaves de acceso son una forma moderna de autenticar usuarios sin necesidad de contraseñas. Se almacenan de forma segura en tu navegador y pueden usarse para iniciar sesión en el Portal WireGuard.",
"active-description": "Al menos una llave de acceso está activa en tu cuenta.",
"inactive-description": "Actualmente no hay llaves de acceso registradas. Presiona el botón de abajo para registrar una.",
"table": {
"name": "Nombre",
"created": "Creada",
"actions": ""
},
"credentials-list": "Llaves de acceso registradas actualmente",
"modal-delete": {
"headline": "Eliminar llaves de acceso",
"abstract": "¿Seguro que deseas eliminar esta llave de acceso? Ya no podrás usarla para iniciar sesión.",
"created": "Creada:",
"button-delete": "Eliminar",
"button-cancel": "Cancelar"
},
"button-rename-title": "Renombrar",
"button-rename-text": "Renombrar la llave de acceso.",
"button-save-title": "Guardar",
"button-save-text": "Guardar el nuevo nombre de la llave de acceso.",
"button-cancel-title": "Cancelar",
"button-cancel-text": "Cancelar el renombrado de la llave de acceso.",
"button-delete-title": "Eliminar",
"button-delete-text": "Eliminar la llave de acceso. Ya no podrás iniciar sesión con ella.",
"button-register-title": "Registrar llave de acceso",
"button-register-text": "Registrar una nueva llave de acceso para proteger tu cuenta."
}
},
"audit": {
"headline": "Registro de Auditoría",
"abstract": "Aquí puedes encontrar el registro de auditoría de todas las acciones realizadas en el Portal WireGuard.",
"no-entries": {
"headline": "No hay entradas en el registro",
"abstract": "Actualmente no se han registrado auditorías."
},
"entries-headline": "Entradas del Registro",
"table-heading": {
"id": "#",
"time": "Hora",
"user": "Usuario",
"severity": "Severidad",
"origin": "Origen",
"message": "Mensaje"
}
},
"keygen": {
"headline": "Generador de claves WireGuard",
"abstract": "Genera nuevas claves de WireGuard. Las claves se generan en tu navegador local y nunca se envían al servidor.",
"headline-keypair": "Nuevo par de claves",
"headline-preshared-key": "Nueva clave pre-compartida",
"button-generate": "Generar",
"private-key": {
"label": "Clave privada",
"placeholder": "La clave privada"
},
"public-key": {
"label": "Clave pública",
"placeholder": "La clave pública"
},
"preshared-key": {
"label": "Clave pre-compartida",
"placeholder": "La clave pre-compartida"
}
},
"modals": {
"user-view": {
"headline": "Cuenta de Usuario:",
"tab-user": "Información",
"tab-peers": "Peers",
"headline-info": "Información del Usuario:",
"headline-notes": "Notas:",
"email": "Correo Electrónico",
"firstname": "Nombre",
"lastname": "Apellido",
"phone": "Número de Teléfono",
"depeertment": "Departamento",
"api-enabled": "Acceso API",
"disabled": "Cuenta Deshabilitada",
"locked": "Cuenta Bloqueada",
"no-peers": "El usuario no tiene peers asociados.",
"peers": {
"name": "Nombre",
"interface": "Interfaz",
"ip": "IP's"
}
},
"user-edit": {
"headline-edit": "Editar usuario:",
"headline-new": "Nuevo usuario",
"header-general": "General",
"header-personal": "Información del Usuario",
"header-notes": "Notas",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "El identificador único del usuario"
},
"source": {
"label": "Origen",
"placeholder": "El origen del usuario"
},
"password": {
"label": "Contraseña",
"placeholder": "Una contraseña súper segura",
"description": "Deja este campo en blanco para mantener la contraseña actual.",
"too-weak": "La contraseña es demasiado débil. Por favor usa una más fuerte."
},
"email": {
"label": "Correo",
"placeholder": "La dirección de correo"
},
"phone": {
"label": "Teléfono",
"placeholder": "El número de teléfono"
},
"depeertment": {
"label": "Departamento",
"placeholder": "El departamento"
},
"firstname": {
"label": "Nombre",
"placeholder": "Nombre"
},
"lastname": {
"label": "Apellido",
"placeholder": "Apellido"
},
"notes": {
"label": "Notas",
"placeholder": ""
},
"disabled": {
"label": "Deshabilitado (sin conexión WireGuard y sin posibilidad de inicio de sesión)"
},
"locked": {
"label": "Bloqueado (no es posible iniciar sesión, las conexiones WireGuard aún funcionan)"
},
"admin": {
"label": "Es administrador"
}
},
"interface-view": {
"headline": "Configuración de la interfaz:"
},
"interface-edit": {
"headline-edit": "Editar interfaz:",
"headline-new": "Nueva interfaz",
"tab-interface": "Interfaz",
"tab-peerdef": "Valores predeterminados del peer",
"header-general": "General",
"header-network": "Red",
"header-crypto": "Criptografía",
"header-hooks": "Hooks de interfaz",
"header-peer-hooks": "Hooks",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "El identificador único de la interfaz"
},
"mode": {
"label": "Modo de Interfaz",
"server": "Modo Servidor",
"client": "Modo Cliente",
"any": "Modo Desconocido"
},
"backend": {
"label": "Backend de la Interfaz",
"invalid-label": "El backend original ya no está disponible, usando el backend local de WireGuard en su lugar.",
"local": "Backend local de WireGuard"
},
"display-name": {
"label": "Nombre para Mostrar",
"placeholder": "El nombre descriptivo de la interfaz"
},
"private-key": {
"label": "La clave Privada",
"placeholder": "La clave privada"
},
"public-key": {
"label": "La clave pública",
"placeholder": "La clave pública"
},
"ip": {
"label": "Direcciones IP",
"placeholder": "Direcciones IP (formato CIDR)"
},
"listen-port": {
"label": "Puerto de Escucha",
"placeholder": "El puerto de escucha"
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Los servidores DNS que deben usarse"
},
"dns-search": {
"label": "Dominios de Búsqueda DNS",
"placeholder": "Prefijos de búsqueda DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "La MTU de la interfaz (0 = mantener por defecto)"
},
"firewall-mark": {
"label": "Marca de Firewall",
"placeholder": "Marca de firewall que se aplica al tráfico saliente. (0 = automático)"
},
"routing-table": {
"label": "Tabla de Enrutamiento",
"placeholder": "El ID de la tabla de enrutamiento",
"description": "Casos especiales: off = no administrar rutas, 0 = automático"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"disabled": {
"label": "Interfaz Deshabilitada"
},
"save-config": {
"label": "Guardar automáticamente la configuración de wg-quick"
},
"defaults": {
"endpoint": {
"label": "Dirección del Endpoint",
"placeholder": "Dirección del Endpoint",
"description": "La dirección del endpoint al que los peers se conectarán. (ej: wg.ejemplo.com o wg.ejemplo.com:51820)"
},
"networks": {
"label": "Redes IP",
"placeholder": "Direcciones de Red",
"description": "Los peers obtendrán direcciones IP de esas subredes."
},
"allowed-ip": {
"label": "Direcciones IP Permitidas",
"placeholder": "Direcciones IP Permitidas por Defecto"
},
"mtu": {
"label": "MTU",
"placeholder": "La MTU del cliente (0 = mantener por defecto)"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive Persistente (0 = por defecto)"
}
},
"button-apply-defaults": "Aplicar Valores Predeterminados de peers"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Información del peer",
"section-status": "Estado Actual",
"section-config": "Configuración",
"identifier": "Identificador",
"ip": "Direcciones IP",
"user": "Usuario Asociado",
"notes": "Notas",
"expiry-status": "Expira en",
"disabled-status": "Deshabilitado en",
"traffic": "Tráfico",
"connection-status": "Estadísticas de Conexión",
"upload": "Bytes Subidos (del Servidor al peer)",
"download": "Bytes Descargados (del peer al Servidor)",
"pingable": "Es Alcanzable (Ping)",
"handshake": "Último Handshake",
"connected-since": "Conectado desde",
"endpoint": "Endpoint",
"button-download": "Descargar configuración",
"button-email": "Enviar configuración por Correo Electrónico",
"style-label": "Estilo de Configuración"
},
"peer-edit": {
"headline-edit-peer": "Editar peer:",
"headline-edit-endpoint": "Editar endpoint:",
"headline-new-peer": "Crear peer",
"headline-new-endpoint": "Crear endpoint",
"header-general": "General",
"header-network": "Red",
"header-crypto": "Criptografía",
"header-hooks": "Hooks (Ejecutados en el peer)",
"header-state": "Estado",
"display-name": {
"label": "Nombre para Mostrar",
"placeholder": "El nombre descriptivo para el peer"
},
"linked-user": {
"label": "Usuario Vinculado",
"placeholder": "La cuenta de usuario que posee este peer"
},
"private-key": {
"label": "Clave Privada",
"placeholder": "Clave privada",
"help": "La clave privada se almacena de forma segura en el servidor. Si el usuario ya posee una copia, puedes omitir este campo. El servidor sigue funcionando exclusivamente con la clave pública del peer."
},
"public-key": {
"label": "Cave Pública",
"placeholder": "La Clave pública"
},
"preshared-key": {
"label": "Clave pre-compartida",
"placeholder": "Clave pre-compartida opcional"
},
"endpoint": {
"label": "Dirección del endpoint",
"placeholder": "La dirección del endpoint remoto"
},
"ip": {
"label": "Direcciones IP",
"placeholder": "Direcciones IP (formato CIDR)"
},
"allowed-ip": {
"label": "Direcciones IP permitidas",
"placeholder": "Direcciones IP permitidas (formato CIDR)"
},
"extra-allowed-ip": {
"label": "Direcciones IP permitidas extra",
"placeholder": "IPs extra permitidas (lado del servidor)",
"description": "Esas IPs serán agregadas en la interfaz remota de WireGuard como direcciones IP permitidas."
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Los servidores DNS que deben usarse"
},
"dns-search": {
"label": "Dominios de búsqueda DNS",
"placeholder": "Prefijos de búsqueda DNS"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive Persistente (0 = por defecto)"
},
"mtu": {
"label": "MTU",
"placeholder": "La MTU del cliente (0 = mantener por defecto)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Uno o varios comandos bash separados por ;"
},
"disabled": {
"label": "Peer Deshabilitado"
},
"ignore-global": {
"label": "Ignorar configuración global"
},
"expires-at": {
"label": "Fecha de expiración"
}
},
"peer-multi-create": {
"headline-peer": "Crear múltiples peers",
"headline-endpoint": "Crear múltiples endpoints",
"identifiers": {
"label": "Identificadores de Usuario",
"placeholder": "Identificadores de Usuario",
"description": "Un identificador de usuario (el nombre de usuario) para el cual debe crearse un peer."
},
"prefix": {
"headline-peer": "peer:",
"headline-endpoint": "Endpoint:",
"label": "Prefijo del Nombre peera Mostrar",
"placeholder": "El prefijo",
"description": "Un prefijo que se agregará al nombre mostrado de los peers."
}
}
}
}

View File

@@ -0,0 +1,515 @@
{
"languages": {
"fr": "Français"
},
"general": {
"pagination": {
"size": "Nombre d'éléments",
"all": "Tous (lent)"
},
"search": {
"placeholder": "Rechercher...",
"button": "Rechercher"
},
"select-all": "Tout sélectionner",
"yes": "Oui",
"no": "Non",
"cancel": "Annuler",
"close": "Fermer",
"save": "Enregistrer",
"delete": "Supprimer"
},
"login": {
"headline": "Veuillez vous connecter",
"username": {
"label": "Nom d'utilisateur",
"placeholder": "Veuillez entrer votre nom d'utilisateur"
},
"password": {
"label": "Mot de passe",
"placeholder": "Veuillez entrer votre mot de passe"
},
"button": "Se connecter"
},
"menu": {
"home": "Accueil",
"interfaces": "Interfaces",
"users": "Utilisateurs",
"lang": "Changer de langue",
"profile": "Mon profil",
"settings": "Paramètres",
"login": "Se connecter",
"logout": "Se déconnecter"
},
"home": {
"headline": "Portail VPN WireGuard®",
"info-headline": "Plus d'informations",
"abstract": "WireGuard® est un VPN extrêmement simple mais rapide et moderne qui utilise une cryptographie de pointe. Il vise à être plus rapide, plus simple, plus léger et plus utile qu'IPsec, tout en évitant le casse-tête massif. Il se veut considérablement plus performant qu'OpenVPN.",
"installation": {
"box-header": "Installation de WireGuard",
"headline": "Installation",
"content": "Les instructions d'installation du logiciel client sont disponibles sur le site Web officiel de WireGuard.",
"button": "Ouvrir les instructions"
},
"about-wg": {
"box-header": "À propos de WireGuard",
"headline": "À propos",
"content": "WireGuard® est un VPN extrêmement simple mais rapide et moderne qui utilise une cryptographie de pointe.",
"button": "Plus d'informations"
},
"about-portal": {
"box-header": "À propos du Portail WireGuard",
"headline": "Portail WireGuard",
"content": "Le Portail WireGuard est un portail de configuration simple basé sur le Web pour WireGuard.",
"button": "Plus d'informations"
},
"profiles": {
"headline": "Profils VPN",
"abstract": "Vous pouvez accéder et télécharger vos configurations VPN personnelles via votre profil utilisateur.",
"content": "Pour trouver tous vos profils configurés, cliquez sur le bouton ci-dessous.",
"button": "Ouvrir mon profil"
},
"admin": {
"headline": "Zone d'administration",
"abstract": "Dans la zone d'administration, vous pouvez gérer les pairs WireGuard et l'interface du serveur, ainsi que les utilisateurs autorisés à se connecter au Portail WireGuard.",
"content": "",
"button-admin": "Ouvrir l'administration du serveur",
"button-user": "Ouvrir l'administration des utilisateurs"
}
},
"interfaces": {
"headline": "Administration des interfaces",
"headline-peers": "Pairs VPN actuels",
"headline-endpoints": "Points de terminaison actuels",
"no-interface": {
"default-selection": "Aucune interface disponible",
"headline": "Aucune interface trouvée...",
"abstract": "Cliquez sur le bouton plus ci-dessus pour créer une nouvelle interface WireGuard."
},
"no-peer": {
"headline": "Aucun pair disponible",
"abstract": "Actuellement, aucun pair n'est disponible pour l'interface WireGuard sélectionnée."
},
"table-heading": {
"name": "Nom",
"user": "Utilisateur",
"ip": "IP",
"endpoint": "Point de terminaison",
"status": "Statut"
},
"interface": {
"headline": "État de l'interface pour",
"backend": "backend",
"key": "Clé publique",
"endpoint": "Point de terminaison public",
"port": "Port d'écoute",
"peers": "Pairs activés",
"total-peers": "Total des pairs",
"endpoints": "Points de terminaison activés",
"total-endpoints": "Total des points de terminaison",
"ip": "Adresse IP",
"default-allowed-ip": "IP autorisées par défaut",
"dns": "Serveurs DNS",
"mtu": "MTU",
"default-keep-alive": "Intervalle Keepalive par défaut",
"button-show-config": "Afficher la configuration",
"button-download-config": "Télécharger la configuration",
"button-store-config": "Enregistrer la configuration pour wg-quick",
"button-edit": "Modifier l'interface"
},
"button-add-interface": "Ajouter une interface",
"button-add-peer": "Ajouter un pair",
"button-add-peers": "Ajouter plusieurs pairs",
"button-show-peer": "Afficher le pair",
"button-edit-peer": "Modifier le pair",
"peer-disabled": "Le pair est désactivé, raison :",
"peer-expiring": "Le pair expire le",
"peer-connected": "Connecté",
"peer-not-connected": "Non connecté",
"peer-handshake": "Dernière négociation :",
"button-show-peer": "Afficher le pair",
"button-edit-peer": "Modifier le pair"
},
"users": {
"headline": "Administration des utilisateurs",
"table-heading": {
"id": "ID",
"email": "E-mail",
"firstname": "Prénom",
"lastname": "Nom",
"source": "Source",
"peers": "Pairs",
"admin": "Admin"
},
"no-user": {
"headline": "Aucun utilisateur disponible",
"abstract": "Actuellement, aucun utilisateur n'est enregistré auprès du Portail WireGuard."
},
"button-add-user": "Ajouter un utilisateur",
"button-show-user": "Afficher l'utilisateur",
"button-edit-user": "Modifier l'utilisateur",
"user-disabled": "L'utilisateur est désactivé, raison :",
"user-locked": "Le compte est verrouillé, raison :",
"admin": "L'utilisateur a des privilèges d'administrateur",
"no-admin": "L'utilisateur n'a pas de privilèges d'administrateur"
},
"profile": {
"headline": "Mes pairs VPN",
"table-heading": {
"name": "Nom",
"ip": "IP",
"stats": "Statut",
"interface": "Interface serveur"
},
"no-peer": {
"headline": "Aucun pair disponible",
"abstract": "Actuellement, aucun pair n'est associé à votre profil utilisateur."
},
"peer-connected": "Connecté",
"button-add-peer": "Ajouter un pair",
"button-show-peer": "Afficher le pair",
"button-edit-peer": "Modifier le pair"
},
"settings": {
"headline": "Paramètres",
"abstract": "Ici, vous pouvez modifier vos paramètres personnels.",
"api": {
"headline": "Paramètres de l'API",
"abstract": "Ici, vous pouvez configurer les paramètres de l'API RESTful.",
"active-description": "L'API est actuellement active pour votre compte utilisateur. Toutes les requêtes API sont authentifiées avec l'authentification de base. Utilisez les informations d'identification suivantes pour l'authentification.",
"inactive-description": "L'API est actuellement inactive. Appuyez sur le bouton ci-dessous pour l'activer.",
"user-label": "Nom d'utilisateur de l'API :",
"user-placeholder": "L'utilisateur de l'API",
"token-label": "Mot de passe de l'API :",
"token-placeholder": "Le jeton de l'API",
"token-created-label": "Accès API accordé le :",
"button-disable-title": "Désactiver l'API, cela invalidera le jeton actuel.",
"button-disable-text": "Désactiver l'API",
"button-enable-title": "Activer l'API, cela générera un nouveau jeton.",
"button-enable-text": "Activer l'API",
"api-link": "Documentation de l'API"
}
},
"modals": {
"user-view": {
"headline": "Compte utilisateur :",
"tab-user": "Informations",
"tab-peers": "Pairs",
"headline-info": "Informations sur l'utilisateur :",
"headline-notes": "Notes :",
"email": "E-mail",
"firstname": "Prénom",
"lastname": "Nom",
"phone": "Numéro de téléphone",
"department": "Département",
"api-enabled": "Accès API",
"disabled": "Compte désactivé",
"locked": "Compte verrouillé",
"no-peers": "L'utilisateur n'a pas de pairs associés.",
"peers": {
"name": "Nom",
"interface": "Interface",
"ip": "IP"
}
},
"user-edit": {
"headline-edit": "Modifier l'utilisateur :",
"headline-new": "Nouvel utilisateur",
"header-general": "Général",
"header-personal": "Informations sur l'utilisateur",
"header-notes": "Notes",
"header-state": "État",
"identifier": {
"label": "Identifiant",
"placeholder": "L'identifiant unique de l'utilisateur"
},
"source": {
"label": "Source",
"placeholder": "La source de l'utilisateur"
},
"password": {
"label": "Mot de passe",
"placeholder": "Un mot de passe super secret",
"description": "Laissez ce champ vide pour conserver le mot de passe actuel."
},
"email": {
"label": "E-mail",
"placeholder": "L'adresse e-mail"
},
"phone": {
"label": "Téléphone",
"placeholder": "Le numéro de téléphone"
},
"department": {
"label": "Département",
"placeholder": "Le département"
},
"firstname": {
"label": "Prénom",
"placeholder": "Prénom"
},
"lastname": {
"label": "Nom",
"placeholder": "Nom"
},
"notes": {
"label": "Notes",
"placeholder": ""
},
"disabled": {
"label": "Désactivé (aucune connexion WireGuard et aucune connexion possible)"
},
"locked": {
"label": "Verrouillé (aucune connexion possible, les connexions WireGuard fonctionnent toujours)"
},
"admin": {
"label": "Est Admin"
}
},
"interface-view": {
"headline": "Configuration pour l'interface :"
},
"interface-edit": {
"headline-edit": "Modifier l'interface :",
"headline-new": "Nouvelle interface",
"tab-interface": "Interface",
"tab-peerdef": "Valeurs par défaut des pairs",
"header-general": "Général",
"header-network": "Réseau",
"header-crypto": "Cryptographie",
"header-hooks": "Hooks d'interface",
"header-peer-hooks": "Hooks",
"header-state": "État",
"identifier": {
"label": "Identifiant",
"placeholder": "L'identifiant unique de l'interface"
},
"mode": {
"label": "Mode de l'interface",
"server": "Mode serveur",
"client": "Mode client",
"any": "Mode inconnu"
},
"display-name": {
"label": "Nom d'affichage",
"placeholder": "Le nom descriptif de l'interface"
},
"private-key": {
"label": "Clé privée",
"placeholder": "La clé privée"
},
"public-key": {
"label": "Clé publique",
"placeholder": "La clé publique"
},
"ip": {
"label": "Adresses IP",
"placeholder": "Adresses IP (format CIDR)"
},
"listen-port": {
"label": "Port d'écoute",
"placeholder": "Le port d'écoute"
},
"dns": {
"label": "Serveur DNS",
"placeholder": "Les serveurs DNS qui doivent être utilisés"
},
"dns-search": {
"label": "Domaines de recherche DNS",
"placeholder": "Préfixes de recherche DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "Le MTU de l'interface (0 = conserver la valeur par défaut)"
},
"firewall-mark": {
"label": "Marque de pare-feu",
"placeholder": "Marque de pare-feu appliquée au trafic sortant. (0 = automatique)"
},
"routing-table": {
"label": "Table de routage",
"placeholder": "L'ID de la table de routage",
"description": "Cas particuliers : off = ne pas gérer les routes, 0 = automatique"
},
"pre-up": {
"label": "Pré-Up",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"pre-down": {
"label": "Pré-Down",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"disabled": {
"label": "Interface désactivée"
},
"save-config": {
"label": "Enregistrer automatiquement la configuration wg-quick"
},
"defaults": {
"endpoint": {
"label": "Adresse du point de terminaison",
"placeholder": "Adresse du point de terminaison",
"description": "L'adresse du point de terminaison auquel les pairs se connecteront. (par exemple, wg.example.com ou wg.example.com:51820)"
},
"networks": {
"label": "Réseaux IP",
"placeholder": "Adresses de réseau",
"description": "Les pairs recevront des adresses IP de ces sous-réseaux."
},
"allowed-ip": {
"label": "Adresses IP autorisées",
"placeholder": "Adresses IP autorisées par défaut"
},
"mtu": {
"label": "MTU",
"placeholder": "Le MTU du client (0 = conserver la valeur par défaut)"
},
"keep-alive": {
"label": "Intervalle Keep Alive",
"placeholder": "Persistent Keepalive (0 = par défaut)"
}
},
"button-apply-defaults": "Appliquer les valeurs par défaut des pairs"
},
"peer-view": {
"headline-peer": "Pair :",
"headline-endpoint": "Point de terminaison :",
"section-info": "Informations sur le pair",
"section-status": "État actuel",
"section-config": "Configuration",
"identifier": "Identifiant",
"ip": "Adresses IP",
"user": "Utilisateur associé",
"notes": "Notes",
"expiry-status": "Expire le",
"disabled-status": "Désactivé le",
"traffic": "Trafic",
"connection-status": "Statistiques de connexion",
"upload": "Octets envoyés (du serveur au pair)",
"download": "Octets téléchargés (du pair au serveur)",
"pingable": "Peut être pingé",
"handshake": "Dernière négociation",
"connected-since": "Connecté depuis",
"endpoint": "Point de terminaison",
"button-download": "Télécharger la configuration",
"button-email": "Envoyer la configuration par e-mail"
},
"peer-edit": {
"headline-edit-peer": "Modifier le pair :",
"headline-edit-endpoint": "Modifier le point de terminaison :",
"headline-new-peer": "Créer un pair",
"headline-new-endpoint": "Créer un point de terminaison",
"header-general": "Général",
"header-network": "Réseau",
"header-crypto": "Cryptographie",
"header-hooks": "Hooks (exécutés sur le pair)",
"header-state": "État",
"display-name": {
"label": "Nom d'affichage",
"placeholder": "Le nom descriptif du pair"
},
"linked-user": {
"label": "Utilisateur lié",
"placeholder": "Le compte utilisateur qui possède ce pair"
},
"private-key": {
"label": "Clé privée",
"placeholder": "La clé privée"
},
"public-key": {
"label": "Clé publique",
"placeholder": "La clé publique"
},
"preshared-key": {
"label": "Clé pré-partagée",
"placeholder": "Clé pré-partagée facultative"
},
"endpoint-public-key": {
"label": "Clé publique du point de terminaison",
"placeholder": "La clé publique du point de terminaison distant"
},
"endpoint": {
"label": "Adresse du point de terminaison",
"placeholder": "L'adresse du point de terminaison distant"
},
"ip": {
"label": "Adresses IP",
"placeholder": "Adresses IP (format CIDR)"
},
"allowed-ip": {
"label": "Adresses IP autorisées",
"placeholder": "Adresses IP autorisées (format CIDR)"
},
"extra-allowed-ip": {
"label": "Adresses IP autorisées supplémentaires",
"placeholder": "IP autorisées supplémentaires (côté serveur)",
"description": "Ces IP seront ajoutées à l'interface WireGuard distante comme IP autorisées."
},
"dns": {
"label": "Serveur DNS",
"placeholder": "Les serveurs DNS qui doivent être utilisés"
},
"dns-search": {
"label": "Domaines de recherche DNS",
"placeholder": "Préfixes de recherche DNS"
},
"keep-alive": {
"label": "Intervalle Keep Alive",
"placeholder": "Persistent Keepalive (0 = par défaut)"
},
"mtu": {
"label": "MTU",
"placeholder": "Le MTU du client (0 = conserver la valeur par défaut)"
},
"pre-up": {
"label": "Pré-Up",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"pre-down": {
"label": "Pré-Down",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
},
"disabled": {
"label": "Pair désactivé"
},
"ignore-global": {
"label": "Ignorer les paramètres globaux"
},
"expires-at": {
"label": "Date d'expiration"
}
},
"peer-multi-create": {
"headline-peer": "Créer plusieurs pairs",
"headline-endpoint": "Créer plusieurs points de terminaison",
"identifiers": {
"label": "Identifiants d'utilisateur",
"placeholder": "Identifiants d'utilisateur",
"description": "Un identifiant d'utilisateur (le nom d'utilisateur) pour lequel un pair doit être créé."
},
"prefix": {
"headline-peer": "Pair :",
"headline-endpoint": "Point de terminaison :",
"label": "Préfixe du nom d'affichage",
"placeholder": "Le préfixe",
"description": "Un préfixe qui est ajouté au nom d'affichage des pairs."
}
}
}
}

View File

@@ -0,0 +1,532 @@
{
"languages": {
"ko": "한국어"
},
"general": {
"pagination": {
"size": "항목 수",
"all": "전체 (느림)"
},
"search": {
"placeholder": "검색...",
"button": "검색"
},
"select-all": "모두 선택",
"yes": "예",
"no": "아니오",
"cancel": "취소",
"close": "닫기",
"save": "저장",
"delete": "삭제"
},
"login": {
"headline": "로그인하세요",
"username": {
"label": "사용자 이름",
"placeholder": "사용자 이름을 입력하세요"
},
"password": {
"label": "비밀번호",
"placeholder": "비밀번호를 입력하세요"
},
"button": "로그인"
},
"menu": {
"home": "홈",
"interfaces": "인터페이스",
"users": "사용자",
"lang": "언어 변경",
"profile": "내 프로필",
"settings": "설정",
"audit": "감사 로그",
"login": "로그인",
"logout": "로그아웃"
},
"home": {
"headline": "WireGuard® VPN 포털",
"info-headline": "추가 정보",
"abstract": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다. IPsec보다 빠르고, 간단하며, 가볍고, 더 유용하면서도 엄청난 골칫거리를 피하는 것을 목표로 합니다. OpenVPN보다 훨씬 더 성능이 뛰어날 것으로 예상됩니다.",
"installation": {
"box-header": "WireGuard 설치",
"headline": "설치",
"content": "클라이언트 소프트웨어 설치 지침은 공식 WireGuard 웹사이트에서 찾을 수 있습니다.",
"button": "지침 열기"
},
"about-wg": {
"box-header": "WireGuard 정보",
"headline": "정보",
"content": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다.",
"button": "더 보기"
},
"about-portal": {
"box-header": "WireGuard 포털 정보",
"headline": "WireGuard 포털",
"content": "WireGuard 포털은 WireGuard를 위한 간단한 웹 기반 구성 포털입니다.",
"button": "더 보기"
},
"profiles": {
"headline": "VPN 프로필",
"abstract": "사용자 프로필을 통해 개인 VPN 구성에 액세스하고 다운로드할 수 있습니다.",
"content": "구성된 모든 프로필을 찾으려면 아래 버튼을 클릭하세요.",
"button": "내 프로필 열기"
},
"admin": {
"headline": "관리 영역",
"abstract": "관리 영역에서는 WireGuard 피어 및 서버 인터페이스뿐만 아니라 WireGuard 포털에 로그인할 수 있는 사용자도 관리할 수 있습니다.",
"content": "",
"button-admin": "서버 관리 열기",
"button-user": "사용자 관리 열기"
}
},
"interfaces": {
"headline": "인터페이스 관리",
"headline-peers": "현재 VPN 피어",
"headline-endpoints": "현재 엔드포인트",
"no-interface": {
"default-selection": "사용 가능한 인터페이스 없음",
"headline": "인터페이스를 찾을 수 없습니다...",
"abstract": "새 WireGuard 인터페이스를 만들려면 위의 플러스 버튼을 클릭하세요."
},
"no-peer": {
"headline": "사용 가능한 피어 없음",
"abstract": "현재 선택한 WireGuard 인터페이스에 사용 가능한 피어가 없습니다."
},
"table-heading": {
"name": "이름",
"user": "사용자",
"ip": "IP 주소",
"endpoint": "엔드포인트",
"status": "상태"
},
"interface": {
"headline": "인터페이스 상태:",
"backend": "백엔드",
"key": "공개 키",
"endpoint": "공개 엔드포인트",
"port": "수신 포트",
"peers": "활성화된 피어",
"total-peers": "총 피어 수",
"endpoints": "활성화된 엔드포인트",
"total-endpoints": "총 엔드포인트 수",
"ip": "IP 주소",
"default-allowed-ip": "기본 허용 IP",
"dns": "DNS 서버",
"mtu": "MTU",
"default-keep-alive": "기본 Keepalive 간격",
"button-show-config": "구성 보기",
"button-download-config": "구성 다운로드",
"button-store-config": "wg-quick용 구성 저장",
"button-edit": "인터페이스 편집"
},
"button-add-interface": "인터페이스 추가",
"button-add-peer": "피어 추가",
"button-add-peers": "여러 피어 추가",
"button-show-peer": "피어 보기",
"button-edit-peer": "피어 편집",
"peer-disabled": "피어가 비활성화됨, 이유:",
"peer-expiring": "피어 만료 예정:",
"peer-connected": "연결됨",
"peer-not-connected": "연결되지 않음",
"peer-handshake": "마지막 핸드셰이크:"
},
"users": {
"headline": "사용자 관리",
"table-heading": {
"id": "ID",
"email": "이메일",
"firstname": "이름",
"lastname": "성",
"source": "소스",
"peers": "피어",
"admin": "관리자"
},
"no-user": {
"headline": "사용 가능한 사용자 없음",
"abstract": "현재 WireGuard 포털에 등록된 사용자가 없습니다."
},
"button-add-user": "사용자 추가",
"button-show-user": "사용자 보기",
"button-edit-user": "사용자 편집",
"user-disabled": "사용자가 비활성화됨, 이유:",
"user-locked": "계정이 잠김, 이유:",
"admin": "사용자에게 관리자 권한이 있습니다",
"no-admin": "사용자에게 관리자 권한이 없습니다"
},
"profile": {
"headline": "내 VPN 피어",
"table-heading": {
"name": "이름",
"ip": "IP 주소",
"stats": "상태",
"interface": "서버 인터페이스"
},
"no-peer": {
"headline": "사용 가능한 피어 없음",
"abstract": "현재 사용자 프로필과 연결된 피어가 없습니다."
},
"peer-connected": "연결됨",
"button-add-peer": "피어 추가",
"button-show-peer": "피어 보기",
"button-edit-peer": "피어 편집"
},
"settings": {
"headline": "설정",
"abstract": "여기에서 개인 설정을 변경할 수 있습니다.",
"api": {
"headline": "API 설정",
"abstract": "여기에서 RESTful API 설정을 구성할 수 있습니다.",
"active-description": "현재 사용자 계정에 대해 API가 활성화되어 있습니다. 모든 API 요청은 기본 인증(Basic Auth)으로 인증됩니다. 인증에 다음 자격 증명을 사용하세요.",
"inactive-description": "현재 API가 비활성화되어 있습니다. 활성화하려면 아래 버튼을 누르세요.",
"user-label": "API 사용자 이름:",
"user-placeholder": "API 사용자",
"token-label": "API 비밀번호:",
"token-placeholder": "API 토큰",
"token-created-label": "API 액세스 권한 부여 시각: ",
"button-disable-title": "API를 비활성화합니다. 현재 토큰이 무효화됩니다.",
"button-disable-text": "API 비활성화",
"button-enable-title": "API를 활성화합니다. 새 토큰이 생성됩니다.",
"button-enable-text": "API 활성화",
"api-link": "API 문서"
}
},
"audit": {
"headline": "감사 로그",
"abstract": "여기에서 WireGuard 포털에서 수행된 모든 작업의 감사 로그를 찾을 수 있습니다.",
"no-entries": {
"headline": "로그 항목 없음",
"abstract": "현재 기록된 감사 로그가 없습니다."
},
"entries-headline": "로그 항목",
"table-heading": {
"id": "#",
"time": "시간",
"user": "사용자",
"severity": "심각도",
"origin": "출처",
"message": "메시지"
}
},
"modals": {
"user-view": {
"headline": "사용자 계정:",
"tab-user": "정보",
"tab-peers": "피어",
"headline-info": "사용자 정보:",
"headline-notes": "메모:",
"email": "이메일",
"firstname": "이름",
"lastname": "성",
"phone": "전화번호",
"department": "부서",
"api-enabled": "API 액세스",
"disabled": "계정 비활성화됨",
"locked": "계정 잠김",
"no-peers": "사용자에게 연결된 피어가 없습니다.",
"peers": {
"name": "이름",
"interface": "인터페이스",
"ip": "IP 주소"
}
},
"user-edit": {
"headline-edit": "사용자 편집:",
"headline-new": "새 사용자",
"header-general": "일반",
"header-personal": "사용자 정보",
"header-notes": "메모",
"header-state": "상태",
"identifier": {
"label": "식별자",
"placeholder": "고유한 사용자 식별자"
},
"source": {
"label": "소스",
"placeholder": "사용자 소스"
},
"password": {
"label": "비밀번호",
"placeholder": "매우 비밀스러운 비밀번호",
"description": "현재 비밀번호를 유지하려면 이 필드를 비워 두세요."
},
"email": {
"label": "이메일",
"placeholder": "이메일 주소"
},
"phone": {
"label": "전화번호",
"placeholder": "전화번호"
},
"department": {
"label": "부서",
"placeholder": "부서"
},
"firstname": {
"label": "이름",
"placeholder": "이름"
},
"lastname": {
"label": "성",
"placeholder": "성"
},
"notes": {
"label": "메모",
"placeholder": ""
},
"disabled": {
"label": "비활성화됨 (WireGuard 연결 및 로그인 불가)"
},
"locked": {
"label": "잠김 (로그인 불가, WireGuard 연결은 계속 작동)"
},
"admin": {
"label": "관리자 여부"
}
},
"interface-view": {
"headline": "인터페이스 구성:"
},
"interface-edit": {
"headline-edit": "인터페이스 편집:",
"headline-new": "새 인터페이스",
"tab-interface": "인터페이스",
"tab-peerdef": "피어 기본값",
"header-general": "일반",
"header-network": "네트워크",
"header-crypto": "암호화",
"header-hooks": "인터페이스 후크",
"header-peer-hooks": "후크",
"header-state": "상태",
"identifier": {
"label": "식별자",
"placeholder": "고유한 인터페이스 식별자"
},
"mode": {
"label": "인터페이스 모드",
"server": "서버 모드",
"client": "클라이언트 모드",
"any": "알 수 없는 모드"
},
"display-name": {
"label": "표시 이름",
"placeholder": "인터페이스에 대한 설명적인 이름"
},
"private-key": {
"label": "개인 키",
"placeholder": "개인 키"
},
"public-key": {
"label": "공개 키",
"placeholder": "공개 키"
},
"ip": {
"label": "IP 주소",
"placeholder": "IP 주소 (CIDR 형식)"
},
"listen-port": {
"label": "수신 포트",
"placeholder": "수신 포트"
},
"dns": {
"label": "DNS 서버",
"placeholder": "사용해야 하는 DNS 서버"
},
"dns-search": {
"label": "DNS 검색 도메인",
"placeholder": "DNS 검색 접두사"
},
"mtu": {
"label": "MTU",
"placeholder": "인터페이스 MTU (0 = 기본값 유지)"
},
"firewall-mark": {
"label": "방화벽 표시",
"placeholder": "나가는 트래픽에 적용되는 방화벽 표시. (0 = 자동)"
},
"routing-table": {
"label": "라우팅 테이블",
"placeholder": "라우팅 테이블 ID",
"description": "특수 사례: off = 경로 관리 안 함, 0 = 자동"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-up": {
"label": "Post-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-down": {
"label": "Post-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"disabled": {
"label": "인터페이스 비활성화됨"
},
"save-config": {
"label": "wg-quick 구성 자동 저장"
},
"defaults": {
"endpoint": {
"label": "엔드포인트 주소",
"placeholder": "엔드포인트 주소",
"description": "피어가 연결할 엔드포인트 주소. (예: wg.example.com 또는 wg.example.com:51820)"
},
"networks": {
"label": "IP 네트워크",
"placeholder": "네트워크 주소",
"description": "피어는 해당 서브넷에서 IP 주소를 받습니다."
},
"allowed-ip": {
"label": "허용된 IP 주소",
"placeholder": "기본 허용 IP 주소"
},
"mtu": {
"label": "MTU",
"placeholder": "클라이언트 MTU (0 = 기본값 유지)"
},
"keep-alive": {
"label": "Keep Alive 간격",
"placeholder": "영구 Keepalive (0 = 기본값)"
}
},
"button-apply-defaults": "피어 기본값 적용"
},
"peer-view": {
"headline-peer": "피어:",
"headline-endpoint": "엔드포인트:",
"section-info": "피어 정보",
"section-status": "현재 상태",
"section-config": "구성",
"identifier": "식별자",
"ip": "IP 주소",
"user": "연결된 사용자",
"notes": "메모",
"expiry-status": "만료 시각",
"disabled-status": "비활성화 시각",
"traffic": "트래픽",
"connection-status": "연결 통계",
"upload": "업로드된 바이트 (서버에서 피어로)",
"download": "다운로드된 바이트 (피어에서 서버로)",
"pingable": "핑 가능 여부",
"handshake": "마지막 핸드셰이크",
"connected-since": "연결 시작 시각",
"endpoint": "엔드포인트",
"button-download": "구성 다운로드",
"button-email": "이메일로 구성 보내기"
},
"peer-edit": {
"headline-edit-peer": "피어 편집:",
"headline-edit-endpoint": "엔드포인트 편집:",
"headline-new-peer": "피어 생성",
"headline-new-endpoint": "엔드포인트 생성",
"header-general": "일반",
"header-network": "네트워크",
"header-crypto": "암호화",
"header-hooks": "후크 (피어에서 실행됨)",
"header-state": "상태",
"display-name": {
"label": "표시 이름",
"placeholder": "피어에 대한 설명적인 이름"
},
"linked-user": {
"label": "연결된 사용자",
"placeholder": "이 피어를 소유한 사용자 계정"
},
"private-key": {
"label": "개인 키",
"placeholder": "개인 키"
},
"public-key": {
"label": "공개 키",
"placeholder": "공개 키"
},
"preshared-key": {
"label": "사전 공유 키",
"placeholder": "선택적 사전 공유 키"
},
"endpoint-public-key": {
"label": "엔드포인트 공개 키",
"placeholder": "원격 엔드포인트의 공개 키"
},
"endpoint": {
"label": "엔드포인트 주소",
"placeholder": "원격 엔드포인트의 주소"
},
"ip": {
"label": "IP 주소",
"placeholder": "IP 주소 (CIDR 형식)"
},
"allowed-ip": {
"label": "허용된 IP 주소",
"placeholder": "허용된 IP 주소 (CIDR 형식)"
},
"extra-allowed-ip": {
"label": "추가 허용 IP 주소",
"placeholder": "추가 허용 IP (서버 측)",
"description": "이 IP 주소는 원격 WireGuard 인터페이스에 허용된 IP로 추가됩니다."
},
"dns": {
"label": "DNS 서버",
"placeholder": "사용해야 하는 DNS 서버"
},
"dns-search": {
"label": "DNS 검색 도메인",
"placeholder": "DNS 검색 접두사"
},
"keep-alive": {
"label": "Keep Alive 간격",
"placeholder": "영구 Keepalive (0 = 기본값)"
},
"mtu": {
"label": "MTU",
"placeholder": "클라이언트 MTU (0 = 기본값 유지)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-up": {
"label": "Post-Up",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"post-down": {
"label": "Post-Down",
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
},
"disabled": {
"label": "피어 비활성화됨"
},
"ignore-global": {
"label": "전역 설정 무시"
},
"expires-at": {
"label": "만료 날짜"
}
},
"peer-multi-create": {
"headline-peer": "여러 피어 생성",
"headline-endpoint": "여러 엔드포인트 생성",
"identifiers": {
"label": "사용자 식별자",
"placeholder": "사용자 식별자",
"description": "피어를 생성할 사용자 식별자 (사용자 이름)."
},
"prefix": {
"headline-peer": "피어:",
"headline-endpoint": "엔드포인트:",
"label": "표시 이름 접두사",
"placeholder": "접두사",
"description": "피어 표시 이름에 추가되는 접두사."
}
}
}
}

View File

@@ -0,0 +1,552 @@
{
"languages": {
"pt": "Português"
},
"general": {
"pagination": {
"size": "Número de Elementos",
"all": "Todos (lento)"
},
"search": {
"placeholder": "Pesquisar...",
"button": "Pesquisar"
},
"select-all": "Selecionar tudo",
"yes": "Sim",
"no": "Não",
"cancel": "Cancelar",
"close": "Fechar",
"save": "Guardar",
"delete": "Eliminar"
},
"login": {
"headline": "Por favor, inicie a sessão",
"username": {
"label": "Nome de utilizador",
"placeholder": "Por favor, insira o seu nome de utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Por favor, insira a sua palavra-passe"
},
"button": "Iniciar sessão"
},
"menu": {
"home": "Início",
"interfaces": "Interfaces",
"users": "Utilizadores",
"lang": "Alterar idioma",
"profile": "O Meu Perfil",
"settings": "Definições",
"audit": "Registo de Auditoria",
"login": "Iniciar Sessão",
"logout": "Terminar Sessão",
"keygen": "Gerador de Chave"
},
"home": {
"headline": "WireGuard® Portal VPN",
"info-headline": "Mais Informações",
"abstract": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração. O seu objetivo é ser mais rápida, simples, leve e útil que o IPsec, enquanto evita grandes dores de cabeça. Pretende ser consideravelmente mais eficiente que o OpenVPN.",
"installation": {
"box-header": "Instalação do WireGuard",
"headline": "Instalação",
"content": "As instruções de instalação para o software cliente podem ser encontradas no site oficial do WireGuard.",
"button": "Abrir Instruções"
},
"about-wg": {
"box-header": "Sobre o WireGuard",
"headline": "Sobre",
"content": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração.",
"button": "Mais"
},
"about-portal": {
"box-header": "Sobre o WireGuard Portal",
"headline": "WireGuard Portal",
"content": "WireGuard Portal é um portal web de configuração simples para o WireGuard.",
"button": "Mais"
},
"profiles": {
"headline": "Perfis VPN",
"abstract": "Pode aceder e baixar as suas configurações pessoais de VPN através do seu Perfil de Utilizador.",
"content": "Para encontrar todos os seus perfis configurados, clique no botão abaixo.",
"button": "Abrir meu perfil"
},
"admin": {
"headline": "Área de Administração",
"abstract": "Na área de administração, pode gerir os peers do WireGuard, a interface do servidor e os utilizadores que têm permissão para aceder ao Portal WireGuard.",
"content": "",
"button-admin": "Abrir Administração do Servidor",
"button-user": "Abrir Administração de Utilizadores"
}
},
"interfaces": {
"headline": "Administração de Interfaces",
"headline-peers": "Peers VPN Atuais",
"headline-endpoints": "Endpoints Atuais",
"no-interface": {
"default-selection": "Nenhuma interface disponível",
"headline": "Nenhuma interface encontrada...",
"abstract": "Clique no botão + acima para criar uma nova interface WireGuard."
},
"no-peer": {
"headline": "Nenhum peer disponível",
"abstract": "Atualmente, não há peers disponíveis para a interface WireGuard selecionada."
},
"table-heading": {
"name": "Nome",
"user": "Utilizador",
"ip": "IPs",
"endpoint": "Endpoint",
"status": "Status"
},
"interface": {
"headline": "Status da interface para",
"mode": "backend",
"key": "Chave Pública",
"endpoint": "Endpoint Público",
"port": "Porta de Escuta",
"peers": "Peers Ativados",
"total-peers": "Total de Peers",
"endpoints": "Endpoints Ativados",
"total-endpoints": "Total de Endpoints",
"ip": "Endereço IP",
"default-allowed-ip": "IPs permitidos por padrão",
"dns": "Servidores DNS",
"mtu": "MTU",
"default-keep-alive": "Intervalo de Keepalive Padrão",
"button-show-config": "Mostrar configuração",
"button-download-config": "Baixar configuração",
"button-store-config": "Armazenar configuração para wg-quick",
"button-edit": "Editar interface"
},
"button-add-interface": "Adicionar Interface",
"button-add-peer": "Adicionar Peer",
"button-add-peers": "Adicionar Vários Peers",
"button-show-peer": "Mostrar Peer",
"button-edit-peer": "Editar Peer",
"peer-disabled": "Peer desativado, razão:",
"peer-expiring": "Peer expira em",
"peer-connected": "Conectado",
"peer-not-connected": "Não Conectado",
"peer-handshake": "Último handshake:"
},
"users": {
"headline": "Administração de Utilizadores",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Primeiro Nome",
"lastname": "Último Nome",
"source": "Fonte",
"peers": "Peers",
"admin": "Administrador"
},
"no-user": {
"headline": "Nenhum utilizador disponível",
"abstract": "Atualmente, não há utilizadores registados no Portal WireGuard."
},
"button-add-user": "Adicionar Utilizador",
"button-show-user": "Mostrar Utilizador",
"button-edit-user": "Editar Utilizador",
"user-disabled": "Utilizador desativado, razão:",
"user-locked": "Conta bloqueada, razão:",
"admin": "O utilizador tem privilégios de administrador",
"no-admin": "O utilizador não tem privilégios de administrador"
},
"profile": {
"headline": "Os Meus Peers VPN",
"table-heading": {
"name": "Nome",
"ip": "IPs",
"stats": "Status",
"interface": "Interface do Servidor"
},
"no-peer": {
"headline": "Nenhum peer disponível",
"abstract": "Atualmente, não há peers associados ao seu perfil de utilizador."
},
"peer-connected": "Conectado",
"button-add-peer": "Adicionar Peer",
"button-show-peer": "Mostrar Peer",
"button-edit-peer": "Editar Peer"
},
"settings": {
"headline": "Definições",
"abstract": "Aqui pode alterar suas Definições pessoais.",
"api": {
"headline": "Definições da API",
"abstract": "Aqui pode configurar as definições da API RESTful.",
"active-description": "A API está atualmente ativa para a sua conta de utilizador. Todos os pedidos para a API são autenticadas com Basic Auth. Use as seguintes credenciais para autenticação.",
"inactive-description": "A API está atualmente inativa. Pressione o botão abaixo para ativá-la.",
"user-label": "Nome de utilizador API:",
"user-placeholder": "O utilizador da API",
"token-label": "Senha da API:",
"token-placeholder": "O token da API",
"token-created-label": "Acesso API concedido em: ",
"button-disable-title": "Desativar API, invalidando o token atual.",
"button-disable-text": "Desativar API",
"button-enable-title": "Ativar API, gerando um novo token.",
"button-enable-text": "Ativar API",
"api-link": "Documentação da API"
}
},
"audit": {
"headline": "Registo de Auditoria",
"abstract": "Aqui pode encontrar o registo de auditoria de todas as ações realizadas no WireGuard Portal.",
"no-entries": {
"headline": "Nenhuma entrada no registo",
"abstract": "Atualmente, não há entradas de registo de auditoria gravadas."
},
"entries-headline": "Entradas do Registo",
"table-heading": {
"id": "#",
"time": "Hora",
"user": "Utilizador",
"severity": "Gravidade",
"origin": "Origem",
"message": "Mensagem"
}
},
"keygen": {
"headline": "Gerador de Chaves WireGuard",
"abstract": "Gere novas chaves WireGuard. As chaves são geradas no seu browser e nunca são enviadas para o servidor.",
"headline-keypair": "Novo Par de Chaves",
"headline-preshared-key": "Nova Chave Pré-Partilhada",
"button-generate": "Gerar",
"private-key": {
"label": "Chave Privada",
"placeholder": "A chave privada"
},
"public-key": {
"label": "Chave Pública",
"placeholder": "A chave pública"
},
"preshared-key": {
"label": "Chave Pré-Partilhada",
"placeholder": "A chave pré-partilhada"
}
},
"modals": {
"user-view": {
"headline": "Conta de Utilizador:",
"tab-user": "Informação",
"tab-peers": "Peers",
"headline-info": "Informação do Utilizador:",
"headline-notes": "Notas:",
"email": "E-Mail",
"firstname": "Primeiro Nome",
"lastname": "Último Nome",
"phone": "Número de Telefone",
"department": "Departamento",
"api-enabled": "Acesso API",
"disabled": "Conta Desativada",
"locked": "Conta Bloqueada",
"no-peers": "O utilizador não tem peers associados.",
"peers": {
"name": "Nome",
"interface": "Interface",
"ip": "IP's"
}
},
"user-edit": {
"headline-edit": "Editar utilizador:",
"headline-new": "Novo utilizador",
"header-general": "Geral",
"header-personal": "Informação do Utilizador",
"header-notes": "Notas",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "O identificador único do utilizador"
},
"source": {
"label": "Fonte",
"placeholder": "A fonte do utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Uma palavra-passe super secreta",
"description": "Deixe este campo em branco para manter a palavra-passe atual."
},
"email": {
"label": "Email",
"placeholder": "O endereço de e-mail"
},
"phone": {
"label": "Telefone",
"placeholder": "O número de telefone"
},
"department": {
"label": "Departamento",
"placeholder": "O departamento"
},
"firstname": {
"label": "Primeiro Nome",
"placeholder": "Primeiro Nome"
},
"lastname": {
"label": "Último Nome",
"placeholder": "Último Nome"
},
"notes": {
"label": "Notas",
"placeholder": ""
},
"disabled": {
"label": "Desativado (sem conexão WireGuard e login possível)"
},
"locked": {
"label": "Bloqueado (sem login possível, as conexões WireGuard ainda funcionam)"
},
"admin": {
"label": "É Administrador"
}
},
"interface-view": {
"headline": "Configuração para a Interface:"
},
"interface-edit": {
"headline-edit": "Editar Interface:",
"headline-new": "Nova Interface",
"tab-interface": "Interface",
"tab-peerdef": "Padrões de Peer",
"header-general": "Geral",
"header-network": "Rede",
"header-crypto": "Criptografia",
"header-hooks": "Hooks da Interface",
"header-peer-hooks": "Hooks",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "O identificador único da interface"
},
"mode": {
"label": "Modo da Interface",
"server": "Modo Servidor",
"client": "Modo Cliente",
"any": "Modo Desconhecido"
},
"display-name": {
"label": "Nome de Exibição",
"placeholder": "O nome descritivo para a interface"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "A chave privada"
},
"public-key": {
"label": "Chave Pública",
"placeholder": "A chave pública"
},
"ip": {
"label": "Endereços IP",
"placeholder": "Endereços IP (formato CIDR)"
},
"listen-port": {
"label": "Porta de Escuta",
"placeholder": "A porta de escuta"
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Os servidores DNS que devem ser usados"
},
"dns-search": {
"label": "Domínios de Pesquisa DNS",
"placeholder": "Prefixos de pesquisa DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "O MTU da interface (0 = manter o valor padrão)"
},
"firewall-mark": {
"label": "Marca de Firewall",
"placeholder": "Marca de firewall aplicada ao tráfego de saída. (0 = automático)"
},
"routing-table": {
"label": "Tabela de Roteamento",
"placeholder": "O ID da tabela de roteamento",
"description": "Casos especiais: off = não gerenciar rotas, 0 = automático"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"disabled": {
"label": "Interface Desativada"
},
"save-config": {
"label": "Guardar configuração wg-quick automaticamente"
},
"defaults": {
"endpoint": {
"label": "Endereço do Endpoint",
"placeholder": "Endereço do Endpoint",
"description": "O endereço do endpoint ao qual os peers se irão conectar. (ex. wg.exemplo.com ou wg.exemplo.com:51820)"
},
"networks": {
"label": "Redes IP",
"placeholder": "Endereços de Rede",
"description": "Os peers irão obter endereços IP a partir dessas sub-redes."
},
"allowed-ip": {
"label": "Endereços IP Permitidos",
"placeholder": "Endereços IP Permitidos por padrão"
},
"mtu": {
"label": "MTU",
"placeholder": "O MTU do cliente (0 = manter o valor padrão)"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive persistente (0 = padrão)"
}
},
"button-apply-defaults": "Aplicar Padrões de Peer"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Informação do Peer",
"section-status": "Estado Atual",
"section-config": "Configuração",
"identifier": "Identificador",
"ip": "Endereços IP",
"user": "Utilizador Associado",
"notes": "Notas",
"expiry-status": "Expira em",
"disabled-status": "Desativado em",
"traffic": "Tráfego",
"connection-status": "Estatísticas de Conexão",
"upload": "Bytes Enviados (do Servidor para o Peer)",
"download": "Bytes Recebidos (do Peer para o Servidor)",
"pingable": "É Pingável",
"handshake": "Último Handshake",
"connected-since": "Conectado desde",
"endpoint": "Endpoint",
"button-download": "Baixar configuração",
"button-email": "Enviar configuração por E-Mail"
},
"peer-edit": {
"headline-edit-peer": "Editar peer:",
"headline-edit-endpoint": "Editar endpoint:",
"headline-new-peer": "Criar peer",
"headline-new-endpoint": "Criar endpoint",
"header-general": "Geral",
"header-network": "Rede",
"header-crypto": "Criptografia",
"header-hooks": "Hooks (Executados no Peer)",
"header-state": "Estado",
"display-name": {
"label": "Nome de Exibição",
"placeholder": "O nome descritivo para o peer"
},
"linked-user": {
"label": "Utilizador Associado",
"placeholder": "A conta de utilizador que possui este peer"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "A chave privada",
"help": "A chave privada é armazenada de forma segura no servidor. Se o utilizador já tiver uma cópia, pode omitir este campo. O servidor ainda funciona exclusivamente com a chave pública do peer."
},
"public-key": {
"label": "Chave Pública",
"placeholder": "A chave pública"
},
"preshared-key": {
"label": "Chave Pré-Partilhada",
"placeholder": "Chave pré-partilhada opcional"
},
"endpoint-public-key": {
"label": "Chave Pública do Endpoint",
"placeholder": "A chave pública do endpoint remoto"
},
"endpoint": {
"label": "Endereço do Endpoint",
"placeholder": "O endereço do endpoint remoto"
},
"ip": {
"label": "Endereços IP",
"placeholder": "Endereços IP (formato CIDR)"
},
"allowed-ip": {
"label": "Endereços IP Permitidos",
"placeholder": "Endereços IP permitidos"
},
"extra-allowed-ip": {
"label": "Endereços IP adicionais permitidos",
"placeholder": "IPs adicionais permitidos (lado do servidor)",
"description": "Esses IPs serão adicionados à interface WireGuard remota como IPs permitidos."
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Os servidores DNS que devem ser utilizados"
},
"dns-search": {
"label": "Domínios de Pesquisa DNS",
"placeholder": "Prefixos de pesquisa DNS"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive persistente (0 = padrão)"
},
"mtu": {
"label": "MTU",
"placeholder": "O MTU do cliente (0 = manter o padrão)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"disabled": {
"label": "Peer Desativado"
},
"ignore-global": {
"label": "Ignorar definições globais"
},
"expires-at": {
"label": "Data de expiração"
}
},
"peer-multi-create": {
"headline-peer": "Criar múltiplos peers",
"headline-endpoint": "Criar múltiplos endpoints",
"identifiers": {
"label": "Identificadores de utilizador",
"placeholder": "Identificadores de utilizador",
"description": "Um identificador de utilizador (nome de utilizador) para o qual um peer deve ser criado."
},
"prefix": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"label": "Prefixo do nome exibido",
"placeholder": "O prefixo",
"description": "Um prefixo que será adicionado ao nome exibido do peer."
}
}
}
}

View File

@@ -0,0 +1,492 @@
{
"languages": {
"ru": "Русский"
},
"general": {
"pagination": {
"size": "Количество элементов",
"all": "Все (медленно)"
},
"search": {
"placeholder": "Поиск...",
"button": "Поиск"
},
"select-all": "Выбрать все",
"yes": "Да",
"no": "Нет",
"cancel": "Отмена",
"close": "Закрыть",
"save": "Сохранить",
"delete": "Удалить"
},
"login": {
"headline": "Пожалуйста, войдите в систему",
"username": {
"label": "Имя пользователя",
"placeholder": "Пожалуйста, введите ваше имя пользователя"
},
"password": {
"label": "Пароль",
"placeholder": "Пожалуйста, введите ваш пароль"
},
"button": "Войти"
},
"menu": {
"home": "Главная",
"interfaces": "Интерфейсы",
"users": "Пользователи",
"lang": "Сменить язык",
"profile": "Мой профиль",
"login": "Вход",
"logout": "Выход"
},
"home": {
"headline": "Портал VPN WireGuard®",
"info-headline": "Дополнительная информация",
"abstract": "WireGuard® - это чрезвычайно простой, но быстрый и современный VPN, использующий передовую криптографию. Он стремится быть быстрее, проще, компактнее и полезнее, чем IPsec, избегая при этом значительных сложностей. Он предназначен для значительного повышения производительности по сравнению с OpenVPN.",
"installation": {
"box-header": "Установка WireGuard",
"headline": "Установка",
"content": "Инструкции по установке клиентского программного обеспечения можно найти на официальном сайте WireGuard.",
"btn": "Открыть инструкции",
"button": "Открыть инструкции"
},
"about-wg": {
"box-header": "О WireGuard",
"headline": "О программе",
"content": "WireGuard® - это чрезвычайно простой, но быстрый и современный VPN, использующий передовую криптографию.",
"button": "Подробнее"
},
"about-portal": {
"box-header": "О портале WireGuard",
"headline": "Портал WireGuard",
"content": "Портал WireGuard - это простой веб-портал для настройки WireGuard.",
"button": "Подробнее"
},
"profiles": {
"headline": "VPN Профили",
"abstract": "Вы можете получить доступ и загрузить свои личные конфигурации VPN через свой пользовательский профиль.",
"content": "Чтобы найти все сконфигурированные профили, нажмите на кнопку ниже.",
"button": "Открыть мой профиль"
},
"admin": {
"headline": "Административная зона",
"abstract": "В административной зоне вы можете управлять узлами и серверным интерфейсом WireGuard, а также пользователями, которым разрешен вход в портал WireGuard.",
"content": "",
"button-admin": "Открыть администрирование сервера",
"button-user": "Открыть администрирование пользователей"
}
},
"interfaces": {
"headline": "Администрирование интерфейсов",
"headline-peers": "Текущие VPN пиры",
"headline-endpoints": "Текущие конечные точки",
"no-interface": {
"default-selection": "Интерфейсы отсутствуют",
"headline": "Интерфейсы не найдены...",
"abstract": "Нажмите на кнопку со знаком плюса выше, чтобы создать новый интерфейс WireGuard."
},
"no-peer": {
"headline": "Пиры отсутствуют",
"abstract": "В настоящее время для выбранного интерфейса WireGuard нет доступных пиров."
},
"table-heading": {
"name": "Имя",
"user": "Пользователь",
"ip": "IP-адреса",
"endpoint": "Конечная точка",
"status": "Статус"
},
"interface": {
"headline": "Статус интерфейса для",
"backend": "бэкэнд",
"key": "Публичный ключ",
"endpoint": "Публичная конечная точка",
"port": "Порт прослушивания",
"peers": "Активные пиры",
"total-peers": "Всего пиров",
"endpoints": "Активные конечные точки",
"total-endpoints": "Всего конечных точек",
"ip": "IP-адрес",
"default-allowed-ip": "Разрешенные IP по умолчанию",
"dns": "DNS-серверы",
"mtu": "MTU",
"default-keep-alive": "Интервал поддержания активности по умолчанию",
"button-show-config": "Показать конфигурацию",
"button-download-config": "Скачать конфигурацию",
"button-store-config": "Сохранить конфигурацию для wg-quick",
"button-edit": "Редактировать интерфейс"
},
"button-add-interface": "Добавить интерфейс",
"button-add-peer": "Добавить пира",
"button-add-peers": "Добавить несколько пиров",
"button-show-peer": "Показать пира",
"button-edit-peer": "Редактировать пира",
"peer-disabled": "Пир отключен, причина:",
"peer-expiring": "Пир истекает в",
"peer-connected": "Подключено",
"peer-not-connected": "Не подключено",
"peer-handshake": "Последнее рукопожатие:"
},
"users": {
"headline": "Администрирование пользователей",
"table-heading": {
"id": "ID",
"email": "Электронная почта",
"firstname": "Имя",
"lastname": "Фамилия",
"source": "Источник",
"peers": "Пиры",
"admin": "Админ"
},
"no-user": {
"headline": "Пользователи отсутствуют",
"abstract": "В настоящее время в портале WireGuard не зарегистрировано ни одного пользователя."
},
"button-add-user": "Добавить пользователя",
"button-show-user": "Показать пользователя",
"button-edit-user": "Редактировать пользователя",
"user-disabled": "Пользователь отключен, причина:",
"user-locked": "Учетная запись заблокирована, причина:",
"admin": "Пользователь имеет права администратора",
"no-admin": "Пользователь не имеет прав администратора"
},
"profile": {
"headline": "Мои VPN пиры",
"table-heading": {
"name": "Имя",
"ip": "IP-адреса",
"stats": "Статус",
"interface": "Интерфейс сервера"
},
"no-peer": {
"headline": "Пиров нет",
"abstract": "В настоящее время у вашего профиля пользователя нет связанных пиров."
},
"peer-connected": "Подключено",
"button-add-peer": "Добавить пира",
"button-show-peer": "Показать пира",
"button-edit-peer": "Редактировать пира"
},
"modals": {
"user-view": {
"headline": "Учетная запись пользователя:",
"tab-user": "Информация",
"tab-peers": "Пиры",
"headline-info": "Информация о пользователе:",
"headline-notes": "Заметки:",
"email": "Электронная почта",
"firstname": "Имя",
"lastname": "Фамилия",
"phone": "Номер телефона",
"department": "Отдел",
"disabled": "Учетная запись отключена",
"locked": "Учетная запись заблокирована",
"no-peers": "У пользователя нет связанных пиров.",
"peers": {
"name": "Имя",
"interface": "Интерфейс",
"ip": "IP-адреса"
}
},
"user-edit": {
"headline-edit": "Редактировать пользователя:",
"headline-new": "Новый пользователь",
"header-general": "Общее",
"header-personal": "Информация о пользователе",
"header-notes": "Заметки",
"header-state": "Состояние",
"identifier": {
"label": "Идентификатор",
"placeholder": "Уникальный идентификатор пользователя"
},
"source": {
"label": "Источник",
"placeholder": "Источник пользователя"
},
"password": {
"label": "Пароль",
"placeholder": "Надежный пароль",
"description": "Оставьте это поле пустым, чтобы сохранить текущий пароль."
},
"email": {
"label": "Электронная почта",
"placeholder": "Адрес электронной почты"
},
"phone": {
"label": "Телефон",
"placeholder": "Номер телефона"
},
"department": {
"label": "Отдел",
"placeholder": "Отдел"
},
"firstname": {
"label": "Имя",
"placeholder": "Имя"
},
"lastname": {
"label": "Фамилия",
"placeholder": "Фамилия"
},
"notes": {
"label": "Заметки",
"placeholder": ""
},
"disabled": {
"label": "Отключен (нет возможности подключения к WireGuard и входа в систему)"
},
"locked": {
"label": "Заблокирован (вход в систему невозможен, подключения WireGuard работают)"
},
"admin": {
"label": "Является администратором"
}
},
"interface-view": {
"headline": "Конфигурация интерфейса:"
},
"interface-edit": {
"headline-edit": "Редактировать интерфейс:",
"headline-new": "Новый интерфейс",
"tab-interface": "Интерфейс",
"tab-peerdef": "Настройки пира по умолчанию",
"header-general": "Общие",
"header-network": "Сеть",
"header-crypto": "Криптография",
"header-hooks": "Хуки интерфейса",
"header-peer-hooks": "Хуки",
"header-state": "Состояние",
"identifier": {
"label": "Идентификатор",
"placeholder": "Уникальный идентификатор интерфейса"
},
"mode": {
"label": "Режим интерфейса",
"server": "Режим сервера",
"client": "Режим клиента",
"any": "Неизвестный режим"
},
"display-name": {
"label": "Отображаемое имя",
"placeholder": "Описательное имя для интерфейса"
},
"private-key": {
"label": "Приватный ключ",
"placeholder": "Приватный ключ"
},
"public-key": {
"label": "Публичный ключ",
"placeholder": "Публичный ключ"
},
"ip": {
"label": "IP-адреса",
"placeholder": "IP-адреса (в формате CIDR)"
},
"listen-port": {
"label": "Порт прослушивания",
"placeholder": "Порт для прослушивания"
},
"dns": {
"label": "DNS-сервер",
"placeholder": "Используемые DNS-серверы"
},
"dns-search": {
"label": "Поисковые домены DNS",
"placeholder": "Префиксы поиска DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU интерфейса (0 = использовать значение по умолчанию)"
},
"firewall-mark": {
"label": "Метка брандмауэра",
"placeholder": "Метка брандмауэра, применяемая к исходящему трафику (0 = автоматически)"
},
"routing-table": {
"label": "Таблица маршрутизации",
"placeholder": "ID таблицы маршрутизации",
"description": "Особые случаи: off = не управлять маршрутами, 0 = автоматически"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"disabled": {
"label": "Интерфейс отключен"
},
"save-config": {
"label": "Автоматически сохранять конфигурацию wg-quick"
},
"defaults": {
"endpoint": {
"label": "Адрес конечной точки",
"placeholder": "Адрес конечной точки",
"description": "Адрес конечной точки, к которой будут подключаться пиры."
},
"networks": {
"label": "IP-сети",
"placeholder": "Сетевые адреса",
"description": "Пиры будут получать IP-адреса из этих подсетей."
},
"allowed-ip": {
"label": "Разрешенные IP-адреса",
"placeholder": "Разрешенные IP-адреса по умолчанию"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU клиента (0 = использовать значение по умолчанию)"
},
"keep-alive": {
"label": "Интервал поддержания активности",
"placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)"
}
},
"button-apply-defaults": "Применить настройки пира по умолчанию"
},
"peer-view": {
"headline-peer": "Пир:",
"headline-endpoint": "Конечная точка:",
"section-info": "Информация о пире",
"section-status": "Текущий статус",
"section-config": "Конфигурация",
"identifier": "Идентификатор",
"ip": "IP-адреса",
"user": "Связанный пользователь",
"notes": "Заметки",
"expiry-status": "Истекает в",
"disabled-status": "Отключено в",
"traffic": "Трафик",
"connection-status": "Статус соединения",
"upload": "Загружено байт (от сервера к пиру)",
"download": "Скачано байт (от пира к серверу)",
"pingable": "Доступность пинга",
"handshake": "Последнее рукопожатие",
"connected-since": "Подключен с",
"endpoint": "Конечная точка",
"button-download": "Скачать конфигурацию",
"button-email": "Отправить конфигурацию по электронной почте"
},
"peer-edit": {
"headline-edit-peer": "Редактировать пира:",
"headline-edit-endpoint": "Редактировать конечную точку:",
"headline-new-peer": "Создать пира",
"headline-new-endpoint": "Создать конечную точку",
"header-general": "Общее",
"header-network": "Сеть",
"header-crypto": "Криптография",
"header-hooks": "Хуки (Выполняются на пире)",
"header-state": "Состояние",
"display-name": {
"label": "Отображаемое имя",
"placeholder": "Описательное имя для пира"
},
"linked-user": {
"label": "Связанный пользователь",
"placeholder": "Учетная запись пользователя, которой принадлежит этот пир"
},
"private-key": {
"label": "Приватный ключ",
"placeholder": "Приватный ключ"
},
"public-key": {
"label": "Публичный ключ",
"placeholder": "Публичный ключ"
},
"preshared-key": {
"label": "Предварительно разделяемый ключ",
"placeholder": "Необязательный предварительно разделяемый ключ"
},
"endpoint-public-key": {
"label": "Публичный ключ конечной точки",
"placeholder": "Публичный ключ удаленной конечной точки"
},
"endpoint": {
"label": "Адрес конечной точки",
"placeholder": "Адрес удаленной конечной точки"
},
"ip": {
"label": "IP-адреса",
"placeholder": "IP-адреса (в формате CIDR)"
},
"allowed-ip": {
"label": "Разрешенные IP-адреса",
"placeholder": "Разрешенные IP-адреса (в формате CIDR)"
},
"extra-allowed-ip": {
"label": "Дополнительно разрешенные IP-адреса",
"placeholder": "Дополнительные разрешенные IP-адреса (на стороне сервера)",
"description": "Эти IP-адреса будут добавлены в удаленный интерфейс WireGuard как разрешенные IP-адреса."
},
"dns": {
"label": "DNS Server",
"placeholder": "The DNS servers that should be used"
},
"dns-search": {
"label": "DNS Search Domains",
"placeholder": "DNS search prefixes"
},
"keep-alive": {
"label": "Keep Alive Interval",
"placeholder": "Persistent Keepalive (0 = default)"
},
"mtu": {
"label": "MTU",
"placeholder": "The client MTU (0 = keep default)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "One or multiple bash commands separated by ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "One or multiple bash commands separated by ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "One or multiple bash commands separated by ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "One or multiple bash commands separated by ;"
},
"disabled": {
"label": "Peer Disabled"
},
"ignore-global": {
"label": "Ignore global settings"
},
"expires-at": {
"label": "Expiry date"
}
},
"peer-multi-create": {
"headline-peer": "Create multiple peers",
"headline-endpoint": "Create multiple endpoints",
"identifiers": {
"label": "User Identifiers",
"placeholder": "User Identifiers",
"description": "A user identifier (the username) for which a peer should be created."
},
"prefix": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"label": "Display Name Prefix",
"placeholder": "The prefix",
"description": "A prefix that is added to the peers display name."
}
}
}
}

View File

@@ -0,0 +1,515 @@
{
"languages": {
"uk": "Українська"
},
"general": {
"pagination": {
"size": "Кількість елементів",
"all": "Всі (повільно)"
},
"search": {
"placeholder": "Пошук...",
"button": "Пошук"
},
"select-all": "Вибрати все",
"yes": "Так",
"no": "Ні",
"cancel": "Скасувати",
"close": "Закрити",
"save": "Зберегти",
"delete": "Видалити"
},
"login": {
"headline": "Будь ласка, увійдіть",
"username": {
"label": "Ім'я користувача",
"placeholder": "Введіть ім'я користувача"
},
"password": {
"label": "Пароль",
"placeholder": "Введіть пароль"
},
"button": "Увійти"
},
"menu": {
"home": "Головна",
"interfaces": "Інтерфейси",
"users": "Користувачі",
"lang": "Змінити мову",
"profile": "Мій профіль",
"settings": "Налаштування",
"login": "Вхід",
"logout": "Вийти"
},
"home": {
"headline": "WireGuard® VPN Портал",
"info-headline": "Додаткова інформація",
"abstract": "WireGuard® — це високоефективний, сучасний і легкий VPN, який використовує передову криптографію. Розроблений для простоти та швидкості, він перевершує IPsec, усуваючи зайву складність. Крім того, він прагне забезпечити значно кращу продуктивність, ніж OpenVPN.",
"installation": {
"box-header": "Встановлення WireGuard",
"headline": "Встановлення",
"content": "Інструкції щодо встановлення клієнтського програмного забезпечення можна знайти на офіційному сайті WireGuard.",
"button": "Відкрити інструкції"
},
"about-wg": {
"box-header": "Про WireGuard",
"headline": "Про програму",
"content": "WireGuard® — це надзвичайно простий, швидкий і сучасний VPN, що використовує передову криптографію.",
"button": "Докладніше"
},
"about-portal": {
"box-header": "Про портал WireGuard",
"headline": "Портал WireGuard",
"content": "Портал WireGuard — це простий веб-інтерфейс для налаштування WireGuard.",
"button": "Докладніше"
},
"profiles": {
"headline": "VPN профілі",
"abstract": "Ви можете отримати доступ та завантажити свої особисті VPN-конфігурації через свій профіль користувача.",
"content": "Щоб переглянути всі налаштовані профілі, натисніть кнопку нижче.",
"button": "Відкрити мій профіль"
},
"admin": {
"headline": "Адміністративна панель",
"abstract": "У адміністративній панелі ви можете керувати клієнтами WireGuard, серверним інтерфейсом і користувачами, які мають доступ до порталу WireGuard.",
"content": "",
"button-admin": "Відкрити адміністрування сервера",
"button-user": "Відкрити адміністрування користувачів"
}
},
"interfaces": {
"headline": "Адміністрування інтерфейсів",
"headline-peers": "Поточні VPN-піри",
"headline-endpoints": "Поточні кінцеві точки",
"no-interface": {
"default-selection": "Немає доступного інтерфейсу",
"headline": "Інтерфейси не знайдено...",
"abstract": "Натисніть кнопку з плюсом вище, щоб створити новий інтерфейс WireGuard."
},
"no-peer": {
"headline": "Немає доступних пірів",
"abstract": "Наразі немає доступних пірів для вибраного інтерфейсу WireGuard."
},
"table-heading": {
"name": "Ім'я",
"user": "Користувач",
"ip": "IP-адреси",
"endpoint": "Кінцева точка",
"status": "Статус"
},
"interface": {
"headline": "Статус інтерфейсу для",
"backend": "бекенд",
"key": "Публічний ключ",
"endpoint": "Публічна кінцева точка",
"port": "Порт прослуховування",
"peers": "Увімкнені піри",
"total-peers": "Загальна кількість пірів",
"endpoints": "Увімкнені кінцеві точки",
"total-endpoints": "Загальна кількість кінцевих точок",
"ip": "IP-адреса",
"default-allowed-ip": "Типові дозволені IP-адреси",
"dns": "DNS-сервери",
"mtu": "MTU",
"default-keep-alive": "Типовий інтервал Keepalive",
"button-show-config": "Показати конфігурацію",
"button-download-config": "Завантажити конфігурацію",
"button-store-config": "Зберегти конфігурацію для wg-quick",
"button-edit": "Редагувати інтерфейс"
},
"button-add-interface": "Додати інтерфейс",
"button-add-peer": "Додати пір",
"button-add-peers": "Додати декілька пірів",
"button-show-peer": "Показати пір",
"button-edit-peer": "Редагувати пір",
"peer-disabled": "Пір вимкнено, причина:",
"peer-expiring": "Пір припиняє дію о",
"peer-connected": "Підключено",
"peer-not-connected": "Не підключено",
"peer-handshake": "Останнє рукостискання:"
},
"users": {
"headline": "Адміністрування користувачів",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Ім'я",
"lastname": "Прізвище",
"source": "Джерело",
"peers": "Піри",
"admin": "Адміністратор"
},
"no-user": {
"headline": "Немає доступних користувачів",
"abstract": "Наразі немає зареєстрованих користувачів у WireGuard Portal."
},
"button-add-user": "Додати користувача",
"button-show-user": "Показати користувача",
"button-edit-user": "Редагувати користувача",
"user-disabled": "Користувача вимкнено, причина:",
"user-locked": "Обліковий запис заблоковано, причина:",
"admin": "Користувач має адміністративні привілеї",
"no-admin": "Користувач не має адміністративних привілеїв"
},
"profile": {
"headline": "Мої VPN-піри",
"table-heading": {
"name": "Ім'я",
"ip": "IP-адреси",
"stats": "Статус",
"interface": "Серверний інтерфейс"
},
"no-peer": {
"headline": "Немає доступних пірів",
"abstract": "Наразі немає пірів, пов'язаних із вашим профілем користувача."
},
"peer-connected": "Підключено",
"button-add-peer": "Додати пір",
"button-show-peer": "Показати пір",
"button-edit-peer": "Редагувати пір"
},
"settings": {
"headline": "Налаштування",
"abstract": "Тут ви можете змінити особисті налаштування.",
"api": {
"headline": "Налаштування API",
"abstract": "Тут ви можете налаштувати RESTful API.",
"active-description": "API наразі активний для вашого облікового запису. Усі API-запити автентифікуються за допомогою Basic Auth. Використовуйте такі облікові дані для автентифікації.",
"inactive-description": "API наразі неактивний. Натисніть кнопку нижче, щоб активувати його.",
"user-label": "Ім'я користувача API:",
"user-placeholder": "Користувач API",
"token-label": "Пароль API:",
"token-placeholder": "Токен API",
"token-created-label": "Доступ до API надано:",
"button-disable-title": "Вимкнути API, це зробить поточний токен недійсним.",
"button-disable-text": "Вимкнути API",
"button-enable-title": "Увімкнути API, це згенерує новий токен.",
"button-enable-text": "Увімкнути API",
"api-link": "Документація API"
}
},
"modals": {
"user-view": {
"headline": "Обліковий запис користувача:",
"tab-user": "Інформація",
"tab-peers": "Піри",
"headline-info": "Інформація про користувача:",
"headline-notes": "Примітки:",
"email": "E-Mail",
"firstname": "Ім'я",
"lastname": "Прізвище",
"phone": "Номер телефону",
"department": "Відділ",
"api-enabled": "Доступ до API",
"disabled": "Обліковий запис вимкнено",
"locked": "Обліковий запис заблоковано",
"no-peers": "У користувача немає пов'язаних пірів.",
"peers": {
"name": "Ім'я",
"interface": "Інтерфейс",
"ip": "IP-адреси"
}
},
"user-edit": {
"headline-edit": "Редагування користувача:",
"headline-new": "Новий користувач",
"header-general": "Загальні",
"header-personal": "Інформація про користувача",
"header-notes": "Примітки",
"header-state": "Стан",
"identifier": {
"label": "Ідентифікатор",
"placeholder": "Унікальний ідентифікатор користувача"
},
"source": {
"label": "Джерело",
"placeholder": "Джерело користувача"
},
"password": {
"label": "Пароль",
"placeholder": "Суперсекретний пароль",
"description": "Залиште це поле порожнім, щоб зберегти поточний пароль."
},
"email": {
"label": "Електронна адреса",
"placeholder": "Електронна адреса"
},
"phone": {
"label": "Телефон",
"placeholder": "Номер телефону"
},
"department": {
"label": "Відділ",
"placeholder": "Відділ"
},
"firstname": {
"label": "Ім'я",
"placeholder": "Ім'я"
},
"lastname": {
"label": "Прізвище",
"placeholder": "Прізвище"
},
"notes": {
"label": "Примітки",
"placeholder": ""
},
"disabled": {
"label": "Вимкнено (неможливо підключитися до WireGuard і увійти в систему)"
},
"locked": {
"label": "Заблоковано (неможливо увійти, але підключення WireGuard працює)"
},
"admin": {
"label": "Адміністратор"
}
},
"interface-view": {
"headline": "Конфігурація для інтерфейсу:"
},
"interface-edit": {
"headline-edit": "Редагувати інтерфейс:",
"headline-new": "Новий інтерфейс",
"tab-interface": "Інтерфейс",
"tab-peerdef": "За замовчуванням для пірів",
"header-general": "Загальне",
"header-network": "Мережа",
"header-crypto": "Криптографія",
"header-hooks": "Хуки інтерфейсу",
"header-peer-hooks": "Хуки",
"header-state": "Стан",
"identifier": {
"label": "Ідентифікатор",
"placeholder": "Унікальний ідентифікатор інтерфейсу"
},
"mode": {
"label": "Режим інтерфейсу",
"server": "Серверний режим",
"client": "Клієнтський режим",
"any": "Невідомий режим"
},
"display-name": {
"label": "Відображуване ім'я",
"placeholder": "Описове ім'я для інтерфейсу"
},
"private-key": {
"label": "Приватний ключ",
"placeholder": "Приватний ключ"
},
"public-key": {
"label": "Публічний ключ",
"placeholder": "Публічний ключ"
},
"ip": {
"label": "IP-адреси",
"placeholder": "IP-адреси (у CIDR форматі)"
},
"listen-port": {
"label": "Порт прослуховування",
"placeholder": "Порт прослуховування"
},
"dns": {
"label": "DNS сервер",
"placeholder": "DNS сервери, які слід використовувати"
},
"dns-search": {
"label": "DNS пошукові домени",
"placeholder": "DNS пошукові префікси"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU інтерфейсу (0 = залишити за замовчуванням)"
},
"firewall-mark": {
"label": "Маркування Firewall",
"placeholder": "Маркування firewall, що застосовується до вихідного трафіку. (0 = автоматично)"
},
"routing-table": {
"label": "Маршрутна таблиця",
"placeholder": "ID маршрутної таблиці",
"description": "Особливі випадки: off = не керувати маршрутами, 0 = автоматично"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"disabled": {
"label": "Інтерфейс відключено"
},
"save-config": {
"label": "Автоматично зберігати конфігурацію wg-quick"
},
"defaults": {
"endpoint": {
"label": "Адреса кінцевої точки",
"placeholder": "Адреса кінцевої точки",
"description": "Адреса кінцевої точки, до якої підключатимуться піри. (наприклад, wg.example.com або wg.example.com:51820)"
},
"networks": {
"label": "IP мережі",
"placeholder": "Адреси мереж",
"description": "Піри отримають IP-адреси з цих підмереж."
},
"allowed-ip": {
"label": "Дозволені IP-адреси",
"placeholder": "За замовчуванням дозволені IP-адреси"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU клієнта (0 = залишити за замовчуванням)"
},
"keep-alive": {
"label": "Інтервал Keep Alive",
"placeholder": "Постійний Keepalive (0 = за замовчуванням)"
}
},
"button-apply-defaults": "Застосувати значення за замовчуванням для пірів"
},
"peer-view": {
"headline-peer": "Пір:",
"headline-endpoint": "Кінцева точка:",
"section-info": "Інформація про пір",
"section-status": "Поточний стан",
"section-config": "Налаштування",
"identifier": "Ідентифікатор",
"ip": "IP-адреси",
"user": "Пов'язаний користувач",
"notes": "Примітки",
"expiry-status": "Закінчується",
"disabled-status": "Відключено",
"traffic": "Трафік",
"connection-status": "Статистика з'єднання",
"upload": "Передано байтів (з серверу до пір)",
"download": "Завантажено байтів (з пір до серверу)",
"pingable": "Відповідає на ping",
"handshake": "Останній handshake",
"connected-since": "Підключено з",
"endpoint": "Кінцева точка",
"button-download": "Завантажити конфігурацію",
"button-email": "Надіслати конфігурацію електронною поштою"
},
"peer-edit": {
"headline-edit-peer": "Редагувати пір:",
"headline-edit-endpoint": "Редагувати кінцеву точку:",
"headline-new-peer": "Створити пір",
"headline-new-endpoint": "Створити кінцеву точку",
"header-general": "Загальне",
"header-network": "Мережа",
"header-crypto": "Криптографія",
"header-hooks": "Хуки (виконуються на пірі)",
"header-state": "Стан",
"display-name": {
"label": "Відображуване ім'я",
"placeholder": "Описове ім'я для пір"
},
"linked-user": {
"label": "Пов'язаний користувач",
"placeholder": "Обліковий запис користувача, що володіє цим піром"
},
"private-key": {
"label": "Приватний ключ",
"placeholder": "Приватний ключ"
},
"public-key": {
"label": "Публічний ключ",
"placeholder": "Публічний ключ"
},
"preshared-key": {
"label": "Попередньо спільний ключ",
"placeholder": "Опціональний попередньо спільний ключ"
},
"endpoint-public-key": {
"label": "Публічний ключ кінцевої точки",
"placeholder": "Публічний ключ віддаленої кінцевої точки"
},
"endpoint": {
"label": "Адреса кінцевої точки",
"placeholder": "Адреса віддаленої кінцевої точки"
},
"ip": {
"label": "IP-адреси",
"placeholder": "IP-адреси (у CIDR форматі)"
},
"allowed-ip": {
"label": "Дозволені IP-адреси",
"placeholder": "Дозволені IP-адреси (у CIDR форматі)"
},
"extra-allowed-ip": {
"label": "Додаткові дозволені IP-адреси",
"placeholder": "Додаткові дозволені IP (на стороні сервера)",
"description": "Ці IP будуть додані на віддаленому інтерфейсі WireGuard як дозволені IP."
},
"dns": {
"label": "DNS сервер",
"placeholder": "DNS сервери, які слід використовувати"
},
"dns-search": {
"label": "DNS пошукові домени",
"placeholder": "DNS пошукові префікси"
},
"keep-alive": {
"label": "Інтервал збереження зв'язку",
"placeholder": "Постійний Keepalive (0 = за замовчуванням)"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU клієнта (0 = залишити за замовчуванням)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Одна або декілька команд bash, розділених ;"
},
"disabled": {
"label": "Пір відключено"
},
"ignore-global": {
"label": "Ігнорувати глобальні налаштування"
},
"expires-at": {
"label": "Дата закінчення терміну дії"
}
},
"peer-multi-create": {
"headline-peer": "Створити декілька пір",
"headline-endpoint": "Створити декілька кінцевих точок",
"identifiers": {
"label": "Ідентифікатори користувача",
"placeholder": "Ідентифікатори користувача",
"description": "Ідентифікатор користувача (ім'я користувача), для якого слід створити пір."
},
"prefix": {
"headline-peer": "Пір:",
"headline-endpoint": "Кінцева точка:",
"label": "Префікс відображуваного імені",
"placeholder": "Префікс",
"description": "Префікс, що додається до відображуваного імені пірів."
}
}
}
}

View File

@@ -0,0 +1,492 @@
{
"languages": {
"vi": "Tiếng Việt"
},
"general": {
"pagination": {
"size": "Số mục",
"all": "Tất (chậm)"
},
"search": {
"placeholder": "Tìm...",
"button": "Tìm kiếm"
},
"select-all": "Chọn tất",
"yes": "Có",
"no": "Không",
"cancel": "Hủy",
"close": "Đóng",
"save": "Lưu",
"delete": "Xóa"
},
"login": {
"headline": "Vui lòng đăng nhập",
"username": {
"label": "Tài khoản",
"placeholder": "Vui lòng nhập tài khoản"
},
"password": {
"label": "Mật khẩu",
"placeholder": "Vui lòng nhập mật khẩu"
},
"button": "Đăng nhập"
},
"menu": {
"home": "Trang chủ",
"interfaces": "Giao diện",
"users": "Người dùng",
"lang": "Chuyển ngữ",
"profile": "Hồ sơ của tôi",
"login": "Đăng nhập",
"logout": "Đăng xuất"
},
"home": {
"headline": "Cổng VPN WireGuard®",
"info-headline": "Thêm thông tin",
"abstract": "WireGuard® là một VPN cực kỳ đơn giản nhưng nhanh chóng và hiện đại, sử dụng mật mã tiên tiến. Nó hướng đến mục tiêu nhanh hơn, đơn giản hơn, gọn nhẹ hơn và hữu ích hơn IPsec, cũng đỡ nhức đầu hơn. Nó có hiệu suất dự kiến là cao hơn đáng kể so với OpenVPN.",
"installation": {
"box-header": "Cài đặt WireGuard",
"headline": "Cài đặt",
"content": "Bạn có thể tìm thấy hướng dẫn cài đặt phần mềm máy khách trên trang web chính thức của WireGuard.",
"button": "Mở hướng dẫn"
},
"about-wg": {
"box-header": "Nói về WireGuard",
"headline": "Về",
"content": "WireGuard® là một VPN cực kỳ đơn giản nhưng nhanh chóng và hiện đại, sử dụng công nghệ mật mã tiên tiến.",
"button": "Thêm"
},
"about-portal": {
"box-header": "Giới thiệu về Cổng thông tin WireGuard",
"headline": "Cổng thông tin WireGuard",
"content": "Cổng thông tin WireGuard là một cổng cấu hình đơn giản, dựa trên web cho WireGuard.",
"button": "Tìm hiểu thêm"
},
"profiles": {
"headline": "Hồ sơ VPN",
"abstract": "Bạn có thể truy cập và tải xuống các cấu hình VPN cá nhân của mình qua hồ sơ người dùng của bạn.",
"content": "Để tìm tất cả các hồ sơ đã cấu hình của bạn, hãy nhấp vào nút dưới đây.",
"button": "Mở hồ sơ của tôi"
},
"admin": {
"headline": "Khu vực Quản trị",
"abstract": "Trong khu vực quản trị, bạn có thể quản lý các peer WireGuard và giao diện máy chủ cũng như người dùng được phép đăng nhập vào Cổng thông tin WireGuard.",
"content": "",
"button-admin": "Mở Quản trị Máy chủ",
"button-user": "Mở Quản trị Người dùng"
}
},
"interfaces": {
"headline": "Quản trị Giao diện",
"headline-peers": "Các Peer VPN Hiện tại",
"headline-endpoints": "Các Điểm cuối Hiện tại",
"no-interface": {
"default-selection": "Không có giao diện nào",
"headline": "Không tìm thấy giao diện...",
"abstract": "Nhấp vào nút cộng trên để tạo một giao diện WireGuard mới."
},
"no-peer": {
"headline": "Không có peer nào",
"abstract": "Hiện tại, không có peer nào khả dụng cho giao diện WireGuard đã chọn."
},
"table-heading": {
"name": "Tên",
"user": "Người dùng",
"ip": "Địa chỉ IP",
"endpoint": "Điểm cuối",
"status": "Trạng thái"
},
"interface": {
"headline": "Trạng thái giao diện cho",
"backend": "phần sau",
"key": "Khóa Công khai",
"endpoint": "Điểm cuối Công khai",
"port": "Cổng Nghe",
"peers": "Các Peer Được Kích hoạt",
"total-peers": "Tổng số Peer",
"endpoints": "Các Điểm cuối Được Kích hoạt",
"total-endpoints": "Tổng số Điểm cuối",
"ip": "Địa chỉ IP",
"default-allowed-ip": "IP được phép mặc định",
"dns": "Máy chủ DNS",
"mtu": "MTU",
"default-keep-alive": "Khoảng thời gian giữ kết nối mặc định",
"button-show-config": "Hiển thị cấu hình",
"button-download-config": "Tải xuống cấu hình",
"button-store-config": "Lưu cấu hình cho wg-quick",
"button-edit": "Chỉnh sửa giao diện"
},
"button-add-interface": "Thêm Giao diện",
"button-add-peer": "Thêm Peer",
"button-add-peers": "Thêm Nhiều Peer",
"button-show-peer": "Hiển thị Peer",
"button-edit-peer": "Chỉnh sửa Peer",
"peer-disabled": "Peer đã bị vô hiệu hóa, lý do:",
"peer-expiring": "Peer sẽ hết hạn vào",
"peer-connected": "Đã kết nối",
"peer-not-connected": "Chưa kết nối",
"peer-handshake": "Lần bắt tay cuối cùng:"
},
"users": {
"headline": "Quản trị Người dùng",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Tên",
"lastname": "Họ",
"source": "Nguồn",
"peers": "Peers",
"admin": "Quản trị viên"
},
"no-user": {
"headline": "Không có người dùng nào",
"abstract": "Hiện tại, không có người dùng nào được đăng ký với WireGuard Portal."
},
"button-add-user": "Thêm Người dùng",
"button-show-user": "Hiển thị Người dùng",
"button-edit-user": "Chỉnh sửa Người dùng",
"user-disabled": "Người dùng đã bị vô hiệu hóa, lý do:",
"user-locked": "Tài khoản bị khóa, lý do:",
"admin": "Người dùng có quyền quản trị",
"no-admin": "Người dùng không có quyền quản trị"
},
"profile": {
"headline": "Các Peer VPN của Tôi",
"table-heading": {
"name": "Tên",
"ip": "Địa chỉ IP",
"stats": "Trạng thái",
"interface": "Giao diện Máy chủ"
},
"no-peer": {
"headline": "Không có peer nào",
"abstract": "Hiện tại, không có peer nào liên kết với hồ sơ người dùng của bạn."
},
"peer-connected": "Đã kết nối",
"button-add-peer": "Thêm Peer",
"button-show-peer": "Hiển thị Peer",
"button-edit-peer": "Chỉnh sửa Peer"
},
"modals": {
"user-view": {
"headline": "Tài khoản Người dùng:",
"tab-user": "Thông tin",
"tab-peers": "Peers",
"headline-info": "Thông tin Người dùng:",
"headline-notes": "Ghi chú:",
"email": "E-Mail",
"firstname": "Tên",
"lastname": "Họ",
"phone": "Số điện thoại",
"department": "Phòng ban",
"disabled": "Tài khoản bị vô hiệu hóa",
"locked": "Tài khoản bị khóa",
"no-peers": "Người dùng không có peers liên kết.",
"peers": {
"name": "Tên",
"interface": "Giao diện",
"ip": "Địa chỉ IP"
}
},
"user-edit": {
"headline-edit": "Chỉnh sửa người dùng:",
"headline-new": "Người dùng mới",
"header-general": "Chung",
"header-personal": "Thông tin Người dùng",
"header-notes": "Ghi chú",
"header-state": "Trạng thái",
"identifier": {
"label": "Mã định danh",
"placeholder": "Mã định danh người dùng duy nhất"
},
"source": {
"label": "Nguồn",
"placeholder": "Nguồn gốc của người dùng"
},
"password": {
"label": "Mật khẩu",
"placeholder": "Mật khẩu siêu bí mật",
"description": "Để trống trường này để giữ nguyên mật khẩu hiện tại."
},
"email": {
"label": "Email",
"placeholder": "Địa chỉ email"
},
"phone": {
"label": "Điện thoại",
"placeholder": "Số điện thoại"
},
"department": {
"label": "Phòng ban",
"placeholder": "Phòng ban"
},
"firstname": {
"label": "Tên",
"placeholder": "Tên"
},
"lastname": {
"label": "Họ",
"placeholder": "Họ"
},
"notes": {
"label": "Ghi chú",
"placeholder": "Chú thích thêm"
},
"disabled": {
"label": "Vô hiệu hóa (không thể kết nối WireGuard và không thể đăng nhập)"
},
"locked": {
"label": "Khóa (không thể đăng nhập, kết nối WireGuard vẫn hoạt động)"
},
"admin": {
"label": "Là Quản trị viên"
}
},
"interface-view": {
"headline": "Cấu hình cho Giao diện:"
},
"interface-edit": {
"headline-edit": "Chỉnh sửa Giao diện:",
"headline-new": "Giao diện Mới",
"tab-interface": "Giao diện",
"tab-peerdef": "Cài đặt Mặc định của Peer",
"header-general": "Chung",
"header-network": "Mạng",
"header-crypto": "Mã hóa",
"header-hooks": "Kẹp Giao diện",
"header-peer-hooks": "Kẹp Peer",
"header-state": "Trạng thái",
"identifier": {
"label": "Mã định danh",
"placeholder": "Mã định danh giao diện duy nhất"
},
"mode": {
"label": "Chế độ Giao diện",
"server": "Chế độ Máy chủ",
"client": "Chế độ Khách hàng",
"any": "Chế độ Không xác định"
},
"display-name": {
"label": "Tên Hiển thị",
"placeholder": "Tên mô tả cho giao diện"
},
"private-key": {
"label": "Khóa Riêng",
"placeholder": "Khóa riêng"
},
"public-key": {
"label": "Khóa Công khai",
"placeholder": "Khóa công khai"
},
"ip": {
"label": "Địa chỉ IP",
"placeholder": "Địa chỉ IP (định dạng CIDR)"
},
"listen-port": {
"label": "Cổng Nghe",
"placeholder": "Cổng nghe"
},
"dns": {
"label": "Máy chủ DNS",
"placeholder": "Các máy chủ DNS sẽ được sử dụng"
},
"dns-search": {
"label": "Tên miền Tìm kiếm DNS",
"placeholder": "Tiền tố tìm kiếm DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU của giao diện (0 = giữ mặc định)"
},
"firewall-mark": {
"label": "Đánh dấu Tường lửa",
"placeholder": "Đánh dấu tường lửa áp dụng cho lưu lượng đi. (0 = tự động)"
},
"routing-table": {
"label": "Bảng Định tuyến",
"placeholder": "ID bảng định tuyến",
"description": "Các trường hợp đặc biệt: off = không quản lý các tuyến đường, 0 = tự động"
},
"pre-up": {
"label": "Trước khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-up": {
"label": "Sau khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"pre-down": {
"label": "Trước khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-down": {
"label": "Sau khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"disabled": {
"label": "Giao diện Bị vô hiệu hóa"
},
"save-config": {
"label": "Tự động lưu cấu hình wg-quick"
},
"defaults": {
"endpoint": {
"label": "Địa chỉ Endpoint",
"placeholder": "Địa chỉ Endpoint",
"description": "Địa chỉ endpoint mà các peer sẽ kết nối tới."
},
"networks": {
"label": "Mạng IP",
"placeholder": "Địa chỉ Mạng",
"description": "Các peer sẽ nhận địa chỉ IP từ những mạng con này."
},
"allowed-ip": {
"label": "Địa chỉ IP Được phép",
"placeholder": "Địa chỉ IP Được phép mặc định"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU của client (0 = giữ mặc định)"
},
"keep-alive": {
"label": "Khoảng thời gian Giữ kết nối",
"placeholder": "Giữ kết nối liên tục (0 = mặc định)"
}
},
"button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Thông tin Peer",
"section-status": "Trạng thái Hiện tại",
"section-config": "Cấu hình",
"identifier": "Mã định danh",
"ip": "Địa chỉ IP",
"user": "Người dùng Liên kết",
"notes": "Ghi chú",
"expiry-status": "Hết hạn vào",
"disabled-status": "Bị Vô hiệu hóa vào",
"traffic": "Lưu lượng",
"connection-status": "Thông tin Kết nối",
"upload": "Số Byte Tải lên (từ Máy chủ đến Peer)",
"download": "Số Byte Tải xuống (từ Peer đến Máy chủ)",
"pingable": "Có thể Ping",
"handshake": "Lần bắt tay cuối cùng",
"connected-since": "Kết nối từ",
"endpoint": "Endpoint",
"button-download": "Tải cấu hình",
"button-email": "Gửi cấu hình qua Email"
},
"peer-edit": {
"headline-edit-peer": "Chỉnh sửa Peer:",
"headline-edit-endpoint": "Chỉnh sửa Endpoint:",
"headline-new-peer": "Tạo Peer mới",
"headline-new-endpoint": "Tạo Endpoint mới",
"header-general": "Chung",
"header-network": "Mạng",
"header-crypto": "Mã hóa",
"header-hooks": "Kẹp (Thực thi trên Peer)",
"header-state": "Trạng thái",
"display-name": {
"label": "Tên Hiển thị",
"placeholder": "Tên mô tả cho peer"
},
"linked-user": {
"label": "Người dùng Liên kết",
"placeholder": "Tài khoản người dùng sở hữu peer này"
},
"private-key": {
"label": "Khóa Riêng",
"placeholder": "Khóa riêng"
},
"public-key": {
"label": "Khóa Công khai",
"placeholder": "Khóa công khai"
},
"preshared-key": {
"label": "Khóa Preshared",
"placeholder": "Khóa chia sẻ trước (tuỳ chọn)"
},
"endpoint-public-key": {
"label": "Khóa Công khai của Endpoint",
"placeholder": "Khóa công khai của endpoint từ xa"
},
"endpoint": {
"label": "Địa chỉ Endpoint",
"placeholder": "Địa chỉ của endpoint từ xa"
},
"ip": {
"label": "Địa chỉ IP",
"placeholder": "Địa chỉ IP (định dạng CIDR)"
},
"allowed-ip": {
"label": "Địa chỉ IP Được phép",
"placeholder": "Địa chỉ IP Được phép (định dạng CIDR)"
},
"extra-allowed-ip": {
"label": "Địa chỉ IP Được phép Thêm",
"placeholder": "Địa chỉ IP Thêm (Phía Máy chủ)",
"description": "Những địa chỉ IP này sẽ được thêm vào giao diện WireGuard từ xa dưới dạng địa chỉ IP được phép."
},
"dns": {
"label": "Máy chủ DNS",
"placeholder": "Các máy chủ DNS sẽ được sử dụng"
},
"dns-search": {
"label": "Tên miền Tìm kiếm DNS",
"placeholder": "Tiền tố tìm kiếm DNS"
},
"keep-alive": {
"label": "Khoảng thời gian Giữ kết nối",
"placeholder": "Giữ kết nối liên tục (0 = mặc định)"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU của client (0 = giữ mặc định)"
},
"pre-up": {
"label": "Trước khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-up": {
"label": "Sau khi Bật",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"pre-down": {
"label": "Trước khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"post-down": {
"label": "Sau khi Tắt",
"placeholder": "Một hoặc nhiều lệnh bash ngăn cách bằng ;"
},
"disabled": {
"label": "Peer Bị Vô hiệu hóa"
},
"ignore-global": {
"label": "Bỏ qua cài đặt toàn cầu"
},
"expires-at": {
"label": "Ngày hết hạn"
}
},
"peer-multi-create": {
"headline-peer": "Tạo nhiều peer",
"headline-endpoint": "Tạo nhiều endpoint",
"identifiers": {
"label": "Mã định danh Người dùng",
"placeholder": "Mã định danh Người dùng",
"description": "Một mã định danh người dùng (tên người dùng) cho mà một peer sẽ được tạo ra."
},
"prefix": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"label": "Tiền tố Tên Hiển thị",
"placeholder": "Tiền tố",
"description": "Một tiền tố được thêm vào tên hiển thị của các peer."
}
}
}
}

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