Compare commits

...

45 Commits

Author SHA1 Message Date
h44z
0cc7ebb83e ensure hooks run after restart (#494) (#497)
(cherry picked from commit 99df4ca3cd)
2025-09-03 22:48:45 +02:00
h44z
eb6a787cfc ensure that LDAP filter values are escaped (#512)
(cherry picked from commit 0cbca61c15)
2025-09-03 22:47:40 +02:00
Christoph Haas
b546eec4ed fix multi-peer generation, fix prefix handling (#491)
(cherry picked from commit c20f17cddf)
2025-08-12 21:25:48 +02:00
h44z
9be2133220 fix migration tool (#495) (#496)
(cherry picked from commit 9884d8c002)
2025-08-12 21:23:30 +02:00
Christoph Haas
b05837b2d9 ensure that v2 (or just 2) tags are only published for stable releases (#493)
(cherry picked from commit b099e8abfa)
2025-08-12 21:23:28 +02:00
Christoph Haas
08c8f8eac0 backport username display bugfix (#456) 2025-06-12 19:11:25 +02:00
Christoph Haas
d864e24145 improve logging of OAuth login issues, decrease auth-code exchange timeout (#451)
(cherry picked from commit e3b65ca337)
2025-06-12 19:07:46 +02:00
Christoph Haas
5b56e58fe9 fix self-provisioned peer-generation (#452)
(cherry picked from commit 61d8aa6589)
2025-06-09 17:41:29 +02:00
Christoph Haas
930ef7b573 Merge branch 'master' into stable 2025-05-16 09:58:14 +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
18296673d7 Merge branch 'master' into stable 2025-05-13 20:25:27 +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
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
4ccc59c109 Merge branch 'master' into stable
# Conflicts:
#	.github/workflows/docker-publish.yml
#	README.md
#	assets/tpl/admin_index.html
#	assets/tpl/user_index.html
#	cmd/wg-portal/main.go
#	docker-compose.yml
#	go.mod
#	go.sum
#	internal/common/util.go
#	internal/server/docs/docs.go
#	internal/server/handlers_common.go
#	internal/server/server.go
#	internal/wireguard/peermanager.go
2025-05-04 20:38:55 +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
onyx-flame
e6b01a9903 Feature (v1): add latest handshake data to API response (#203)
* feature: updated handshake-related fields type

* feature: updated handshake representations in templates

* feature: added handshake field to Swagger schema
2023-12-23 12:56:52 +01:00
Christoph Haas
2f79dd04c0 adopt github actions 2023-10-26 11:29:34 +02:00
Christoph Haas
e5ed9736b3 update docker build settings, move to new docker hub repository, use stable branch and major version tags 2023-10-26 11:22:58 +02:00
Christoph Haas
c8353b85ae Merge branch 'replace_ext_lib' into stable 2023-10-26 10:40:06 +02:00
Christoph Haas
6142031387 update gin 2023-10-26 10:39:01 +02:00
Christoph Haas
dd86d0ff49 replace inaccessible external lib 2023-10-26 10:31:29 +02:00
Christoph Haas
bdd426a679 populate peer device type (#170) 2023-10-26 10:20:08 +02:00
64 changed files with 2060 additions and 598 deletions

View File

@@ -4,7 +4,7 @@ on:
pull_request:
branches: [master]
push:
branches: [master, stable]
branches: [master]
# Publish vX.X.X tags as releases.
tags: ["v*.*.*"]
@@ -64,12 +64,8 @@ jobs:
# major and major.minor tags are not available for alpha or beta releases
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
# add v{{major}} tag, even for beta or release-canidate releases
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# add {{major}} tag, even for beta releases or release-canidate releases
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# set latest tag for default branch
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
- name: Build and push Docker image
uses: docker/build-push-action@v6

View File

@@ -2,7 +2,11 @@ name: github-pages
on:
push:
branches: [master]
tags: ["v*"]
tags:
- 'v*'
- '!v*-alpha*'
- '!v*-beta*'
- '!v*-rc*'
permissions:
contents: write
@@ -23,7 +27,14 @@ jobs:
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
- name: Publish documentation
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
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"

1
.gitignore vendored
View File

@@ -33,6 +33,7 @@ ssh.key
wg_portal.db
sqlite.db
/config.yml
/config.yaml
/config/
venv/
.cache/

View File

@@ -1,4 +1,4 @@
# WireGuard Portal (v2 - testing)
# WireGuard Portal v2
[![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)
@@ -8,14 +8,6 @@
![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/)
> [!CAUTION]
> Version 2 is currently under development and may contain bugs and breaking changes.
> It is not advised to use this version in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
> [!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**.
## Introduction
<!-- 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.
@@ -23,7 +15,7 @@ The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) l
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
connections.
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
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
@@ -44,7 +36,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
* 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
* 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)
@@ -68,3 +60,8 @@ For the complete documentation visit [wgportal.org](https://wgportal.org).
## License
* 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**.

View File

@@ -9,6 +9,7 @@ import (
"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"
@@ -41,6 +42,8 @@ func main() {
cfg.LogStartupValues()
dbEncryptedSerializer := app.NewGormEncryptedStringSerializer(cfg.Database.EncryptionPassphrase)
schema.RegisterSerializer("encstr", dbEncryptedSerializer)
rawDb, err := adapters.NewDatabase(cfg.Database)
internal.AssertNoError(err)

View File

@@ -1,7 +1,7 @@
---
services:
wg-portal:
image: wgportal/wg-portal:latest
image: wgportal/wg-portal:v2
container_name: wg-portal
restart: unless-stopped
logging:
@@ -10,8 +10,10 @@ services:
max-file: "3"
cap_add:
- NET_ADMIN
# Use host network mode for WireGuard and the UI. Ensure that access to the UI is properly secured.
network_mode: "host"
volumes:
# left side is the host path, right side is the container path
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
- ./config:/app/config

View File

@@ -15,7 +15,7 @@ web:
site_title: My WireGuard Server
site_company_name: My Company
listening_address: :8080
external_url: https://my.externa-domain.com
external_url: https://my.external-domain.com
csrf_secret: super-s3cr3t-csrf
session_secret: super-s3cr3t-session
request_logging: true
@@ -31,6 +31,7 @@ database:
debug: true
type: sqlite
dsn: data/sqlite.db
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
```
## LDAP Authentication and Synchronization
@@ -71,7 +72,8 @@ auth:
auth:
oidc:
# a sample Entra ID provider with environment variable substitution
# 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
@@ -79,6 +81,8 @@ auth:
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

View File

@@ -1,10 +1,10 @@
This page provides an overview of **all available configuration options** for WireGuard Portal.
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
The path of the configuration file defaults to **config/config.yml** in the working directory of the executable.
It is possible to override configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
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 config file is supported. Refer to [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
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.
@@ -15,6 +15,7 @@ Configuration examples are available on the [Examples](./examples.md) page.
core:
admin_user: admin@wgportal.local
admin_password: wgportal
admin_api_token: ""
editable_keys: true
create_default_peer: false
create_default_peer_on_creation: false
@@ -35,13 +36,15 @@ advanced:
config_storage_path: ""
expiry_check_interval: 15m
rule_prio_offset: 20000
route_table_offset: 20000
api_admin_only: true
database:
debug: false
slow_query_threshold: 0
slow_query_threshold: "0"
type: sqlite
dsn: data/sqlite.db
encryption_passphrase: ""
statistics:
use_ping_checks: true
@@ -79,6 +82,7 @@ web:
session_secret: very_secret
csrf_secret: extremely_secret
request_logging: false
expose_host_info: false
cert_file: ""
key_File: ""
@@ -214,13 +218,15 @@ Additional or more specialized configuration options for logging and interface c
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 empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
- **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`
@@ -234,6 +240,12 @@ Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
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
@@ -274,7 +286,7 @@ Controls how WireGuard Portal collects and reports usage statistics, including p
### `listening_address`
- **Default:** `:8787`
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787` or `127.0.0.1:8787`).
---
@@ -356,6 +368,10 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- **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.
@@ -425,6 +441,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
- **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.
@@ -568,7 +588,8 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
### `listening_address`
- **Default:** `:8888`
- **Description:** The listening port of the web server.
- **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`
@@ -599,6 +620,10 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
- **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.

View File

@@ -3,23 +3,31 @@ 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
```
```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
```
```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'
```
```shell
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
```
## Install
@@ -28,7 +36,7 @@ sudo mkdir -p /opt/wg-portal
sudo install wg-portal /opt/wg-portal/
```
## Unreleased
## 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).
Unreleased versions could be downloaded from
[GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster) artifacts also.

View File

@@ -10,10 +10,10 @@ The recommended method for deploying WireGuard Portal is via Docker Compose for
A sample docker-compose.yml (managing WireGuard interfaces directly on the host) is provided below:
```yaml
--8<-- "docker-compose.yml::17"
--8<-- "docker-compose.yml::19"
```
By default, the webserver is listening on port **8888**.
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.
@@ -32,6 +32,8 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
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.
@@ -39,12 +41,13 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
```yaml
services:
wg-portal:
image: wgportal/wg-portal:latest
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
@@ -52,6 +55,7 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
# host path : container path
- ./wg/data:/app/data
- ./wg/config:/app/config
```
@@ -63,13 +67,14 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
```yaml
services:
wg-portal:
image: wgportal/wg-portal:latest
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
@@ -81,6 +86,7 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
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:
@@ -104,36 +110,38 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
## 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. 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).
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.
Once these tags show up in this repository, they will never change.
There are different types of these tags:
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. **wgportal/wg-portal:1.0.19**, instead of the latest or canary 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.
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v1` or `1.0`.
#### The `latest` tag
Version **1** is currently **stable**, version **2** is in **development**.
The lastest tag is the latest stable release of WireGuard Portal. For version **2**, this is the same as the `v2` tag.
#### latest
#### The `master` tag
This is the most recent build to master! It changes a lot and is very unstable.
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.
We recommend that you don't use it except for development purposes or to test the latest features.
#### Branch tags
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
## Configuration
You can configure WireGuard Portal using a YAML configuration file.
The filepath of the YAML configuration file defaults to `/app/config/config.yml`.
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`.
@@ -145,7 +153,7 @@ You should mount those directories as a volume:
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 to a location of your choice.
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:

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

@@ -22,4 +22,5 @@ make build
## Install
Compiled binary will be available in `./dist` directory.
For installation instructions, check the [Binaries](./binaries.md) section.

View File

@@ -13,7 +13,7 @@ By default, WG-Portal exposes Prometheus metrics on port `8787` if interface/pee
## Prometheus Config
Add following scrape job to your Prometheus config file:
Add the following scrape job to your Prometheus config file:
```yaml
# prometheus.yaml

View File

@@ -1 +1 @@
--8<-- "README.md:20:47"
--8<-- "README.md:12:41"

View File

@@ -692,6 +692,7 @@ paths:
tags:
- Interfaces
put:
description: This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleUpdatePut
parameters:
- description: The interface identifier.
@@ -739,6 +740,7 @@ paths:
- Interfaces
/interface/new:
post:
description: This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleCreatePost
parameters:
- description: The interface data.
@@ -779,6 +781,34 @@ paths:
summary: Create a new interface record.
tags:
- Interfaces
/interface/prepare:
get:
description: This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
operationId: interfaces_handlePrepareGet
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Interface'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new interface record.
tags:
- Interfaces
/metrics/by-interface/{id}:
get:
operationId: metrics_handleMetricsForInterfaceGet
@@ -967,7 +997,7 @@ paths:
tags:
- Peers
put:
description: Only admins can update existing records.
description: Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
operationId: peers_handleUpdatePut
parameters:
- description: The peer identifier.
@@ -1078,7 +1108,7 @@ paths:
- Peers
/peer/new:
post:
description: Only admins can create new records.
description: Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
operationId: peers_handleCreatePost
parameters:
- description: The peer data.
@@ -1119,6 +1149,48 @@ paths:
summary: Create a new peer record.
tags:
- Peers
/peer/prepare/{id}:
get:
description: This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
operationId: peers_handlePrepareGet
parameters:
- description: The interface identifier.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Peer'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new peer record for the given WireGuard interface.
tags:
- Peers
/provisioning/data/peer-config:
get:
description: Normal users can only access their own record. Admins can access all records.

View File

@@ -1,12 +1,11 @@
For production deployments of WireGuard Portal, we strongly recommend using version 1.
If you want to use version 2, please be aware that it is still a release candidate and not yet fully stable.
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.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
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:
@@ -14,14 +13,16 @@ To upgrade from a previous SQLite database, start wg-portal like:
./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.
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.yml** configuration file.
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:
@@ -29,8 +30,8 @@ If you are using Docker, you can adapt the docker-compose.yml file to start the
```yaml
services:
wg-portal:
image: wgportal/wg-portal:latest
image: wgportal/wg-portal:v2
# ... other settings
restart: no
command: ["-migrateFrom=/app/data/wg_portal.db"]
command: ["-migrateFrom=/app/data/old_wg_portal.db"]
```

View File

@@ -24,7 +24,7 @@
<div id="toasts"></div>
<!-- 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 -->
<div id="modals"></div>

View File

@@ -29,7 +29,7 @@
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"sass-embedded": "^1.86.3",
"vite": "6.3.2"
"vite": "6.3.4"
}
},
"node_modules/@babel/helper-string-parser": {
@@ -2012,13 +2012,13 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@@ -2043,18 +2043,18 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz",
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.12"
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"

View File

@@ -29,6 +29,6 @@
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"sass-embedded": "^1.86.3",
"vite": "6.3.2"
"vite": "6.3.4"
}
}

View File

@@ -61,6 +61,26 @@ const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
const wgVersion = ref(WGPORTAL_VERSION);
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>
<template>
@@ -85,12 +105,15 @@ const currentYear = ref(new Date().getFullYear())
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
</li>
<li class="nav-item">
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
</li>
</ul>
<div class="navbar-nav d-flex justify-content-end">
<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="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
href="#" role="button">{{ userDisplayName }}</a>
<div class="dropdown-menu">
<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')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
@@ -137,6 +160,7 @@ const currentYear = ref(new Date().getFullYear())
</div>
</div>
</div>
</footer></template>
</footer>
</template>
<style></style>

View File

@@ -5,6 +5,10 @@ $web-font-path: false;
@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
/*

View File

@@ -331,11 +331,11 @@ async function del() {
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
<div class="form-group">
<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 class="form-group">
<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>
</fieldset>
<fieldset>

View File

@@ -323,17 +323,18 @@ async function del() {
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
<div class="form-group" v-if="selectedInterface.Mode === 'server'">
<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
<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="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
<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="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
v-model="formData.PresharedKey">
</div>
<div class="form-group" v-if="formData.Mode === 'client'">

View File

@@ -32,7 +32,7 @@ const selectedInterface = computed(() => {
function freshForm() {
return {
Identifiers: [],
Suffix: "",
Prefix: "",
}
}
@@ -102,7 +102,7 @@ async function save() {
</div>
<div class="form-group">
<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>
</div>
</fieldset>

View File

@@ -211,17 +211,18 @@ async function del() {
<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="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
<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="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
<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="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
v-model="formData.PresharedKey">
</div>
</fieldset>

View File

@@ -26,7 +26,7 @@
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
},
"password": {
"label": "Kennwort",
"label": "Passwort",
"placeholder": "Bitte geben Sie Ihr Passwort ein"
},
"button": "Anmelden"
@@ -38,8 +38,10 @@
"lang": "Sprache ändern",
"profile": "Mein Profil",
"settings": "Einstellungen",
"audit": "Event Protokoll",
"login": "Anmelden",
"logout": "Abmelden"
"logout": "Abmelden",
"keygen": "Schlüsselgenerator"
},
"home": {
"headline": "WireGuard® VPN Portal",
@@ -79,77 +81,77 @@
},
"interfaces": {
"headline": "Schnittstellenverwaltung",
"headline-peers": "Current VPN Peers",
"headline-endpoints": "Current Endpoints",
"headline-peers": "Aktuelle VPN-Peers",
"headline-endpoints": "Aktuelle Endpunkte",
"no-interface": {
"default-selection": "No Interface available",
"headline": "No interfaces found...",
"abstract": "Click the plus button above to create a new WireGuard interface."
"default-selection": "Keine Schnittstelle verfügbar",
"headline": "Keine Schnittstellen gefunden...",
"abstract": "Klicken Sie auf die Plus-Schaltfläche oben, um eine neue WireGuard-Schnittstelle zu erstellen."
},
"no-peer": {
"headline": "No peers available",
"abstract": "Currently, there are no peers available for the selected WireGuard interface."
"headline": "Keine Peers verfügbar",
"abstract": "Derzeit sind keine Peers für die ausgewählte WireGuard-Schnittstelle verfügbar."
},
"table-heading": {
"name": "Name",
"user": "User",
"user": "Benutzer",
"ip": "IP's",
"endpoint": "Endpoint",
"endpoint": "Endpunkt",
"status": "Status"
},
"interface": {
"headline": "Interface status for",
"mode": "mode",
"key": "Public Key",
"endpoint": "Public Endpoint",
"port": "Listening Port",
"peers": "Enabled Peers",
"total-peers": "Total Peers",
"endpoints": "Enabled Endpoints",
"total-endpoints": "Total Endpoints",
"ip": "IP Address",
"default-allowed-ip": "Default allowed IPs",
"dns": "DNS Servers",
"headline": "Schnittstellenstatus für",
"mode": "Modus",
"key": "Öffentlicher Schlüssel",
"endpoint": "Öffentlicher Endpunkt",
"port": "Port",
"peers": "Aktive Peers",
"total-peers": "Gesamtanzahl Peers",
"endpoints": "Aktive Endpunkte",
"total-endpoints": "Gesamtanzahl Endpunkte",
"ip": "IP-Adresse",
"default-allowed-ip": "Standard Erlaubte-IPs",
"dns": "DNS-Server",
"mtu": "MTU",
"default-keep-alive": "Default Keepalive Interval",
"button-show-config": "Show configuration",
"button-download-config": "Download configuration",
"button-store-config": "Store configuration for wg-quick",
"button-edit": "Edit interface"
"default-keep-alive": "Standard Keepalive-Intervall",
"button-show-config": "Konfiguration anzeigen",
"button-download-config": "Konfiguration herunterladen",
"button-store-config": "Konfiguration für wg-quick speichern",
"button-edit": "Schnittstelle bearbeiten"
},
"button-add-interface": "Add Interface",
"button-add-peer": "Add Peer",
"button-add-peers": "Add Multiple Peers",
"button-show-peer": "Show Peer",
"button-edit-peer": "Edit Peer",
"peer-disabled": "Peer is disabled, reason:",
"peer-expiring": "Peer is expiring at",
"peer-connected": "Connected",
"peer-not-connected": "Not Connected",
"peer-handshake": "Last handshake:"
"button-add-interface": "Schnittstelle hinzufügen",
"button-add-peer": "Peer hinzufügen",
"button-add-peers": "Mehrere Peers hinzufügen",
"button-show-peer": "Peer anzeigen",
"button-edit-peer": "Peer bearbeiten",
"peer-disabled": "Peer ist deaktiviert, Grund:",
"peer-expiring": "Peer läuft ab am",
"peer-connected": "Verbunden",
"peer-not-connected": "Nicht verbunden",
"peer-handshake": "Letzter Handshake:"
},
"users": {
"headline": "Benutzerverwaltung",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Firstname",
"lastname": "Lastname",
"source": "Source",
"firstname": "Vorname",
"lastname": "Nachname",
"source": "Quelle",
"peers": "Peers",
"admin": "Admin"
},
"no-user": {
"headline": "No users available",
"abstract": "Currently, there are no users registered with WireGuard Portal."
"headline": "Keine Benutzer verfügbar",
"abstract": "Derzeit sind keine Benutzer im WireGuard-Portal registriert."
},
"button-add-user": "Add User",
"button-show-user": "Show User",
"button-edit-user": "Edit User",
"user-disabled": "User is disabled, reason:",
"user-locked": "Account is locked, reason:",
"admin": "User has administrator privileges",
"no-admin": "User has no administrator privileges"
"button-add-user": "Benutzer hinzufügen",
"button-show-user": "Benutzer anzeigen",
"button-edit-user": "Benutzer bearbeiten",
"user-disabled": "Benutzer ist deaktiviert, Grund:",
"user-locked": "Konto ist gesperrt, Grund:",
"admin": "Benutzer hat Administratorrechte",
"no-admin": "Benutzer hat keine Administratorrechte"
},
"profile": {
"headline": "Meine VPN-Konfigurationen",
@@ -157,16 +159,16 @@
"name": "Name",
"ip": "IP's",
"stats": "Status",
"interface": "Server Interface"
"interface": "Server-Schnittstelle"
},
"no-peer": {
"headline": "No peers available",
"abstract": "Currently, there are no peers associated with your user profile."
"headline": "Keine Peers verfügbar",
"abstract": "Derzeit sind keine Peers mit Ihrem Benutzerprofil verknüpft."
},
"peer-connected": "Connected",
"button-add-peer": "Add Peer",
"button-show-peer": "Show Peer",
"button-edit-peer": "Edit Peer"
"peer-connected": "Verbunden",
"button-add-peer": "Peer hinzufügen",
"button-show-peer": "Peer anzeigen",
"button-edit-peer": "Peer bearbeiten"
},
"settings": {
"headline": "Einstellungen",
@@ -188,325 +190,362 @@
"api-link": "API Dokumentation"
}
},
"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": {
"user-view": {
"headline": "User Account:",
"tab-user": "Information",
"headline": "Benutzerkonto:",
"tab-user": "Informationen",
"tab-peers": "Peers",
"headline-info": "User Information:",
"headline-notes": "Notes:",
"headline-info": "Benutzerinformationen:",
"headline-notes": "Notizen:",
"email": "E-Mail",
"firstname": "Firstname",
"lastname": "Lastname",
"phone": "Phone number",
"department": "Department",
"disabled": "Account Disabled",
"locked": "Account Locked",
"no-peers": "User has no associated peers.",
"firstname": "Vorname",
"lastname": "Nachname",
"phone": "Telefonnummer",
"department": "Abteilung",
"api-enabled": "API-Zugriff",
"disabled": "Konto deaktiviert",
"locked": "Konto gesperrt",
"no-peers": "Benutzer hat keine zugeordneten Peers.",
"peers": {
"name": "Name",
"interface": "Interface",
"interface": "Schnittstelle",
"ip": "IP's"
}
},
"user-edit": {
"headline-edit": "Edit user:",
"headline-new": "New user",
"header-general": "General",
"header-personal": "User Information",
"header-notes": "Notes",
"header-state": "State",
"headline-edit": "Benutzer bearbeiten:",
"headline-new": "Neuer Benutzer",
"header-general": "Allgemein",
"header-personal": "Benutzerinformationen",
"header-notes": "Notizen",
"header-state": "Status",
"identifier": {
"label": "Identifier",
"placeholder": "The unique user identifier"
"label": "Kennung",
"placeholder": "Die eindeutige Benutzerkennung"
},
"source": {
"label": "Source",
"placeholder": "The user source"
"label": "Quelle",
"placeholder": "Die Benutzerquelle"
},
"password": {
"label": "Password",
"placeholder": "A super secret password",
"description": "Leave this field blank to keep current password."
"label": "Passwort",
"placeholder": "Ein super geheimes Passwort",
"description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten."
},
"email": {
"label": "Email",
"placeholder": "The email address"
"label": "E-Mail",
"placeholder": "Die E-Mail-Adresse"
},
"phone": {
"label": "Phone",
"placeholder": "The phone number"
"label": "Telefon",
"placeholder": "Die Telefonnummer"
},
"department": {
"label": "Department",
"placeholder": "The department"
"label": "Abteilung",
"placeholder": "Die Abteilung"
},
"firstname": {
"label": "Firstname",
"placeholder": "Firstname"
"label": "Vorname",
"placeholder": "Vorname"
},
"lastname": {
"label": "Lastname",
"placeholder": "Lastname"
"label": "Nachname",
"placeholder": "Nachname"
},
"notes": {
"label": "Notes",
"label": "Notizen",
"placeholder": ""
},
"disabled": {
"label": "Disabled (no WireGuard connection and no login possible)"
"label": "Deaktiviert (keine WireGuard-Verbindung und kein Login möglich)"
},
"locked": {
"label": "Locked (no login possible, WireGuard connections still work)"
"label": "Gesperrt (kein Login möglich, WireGuard-Verbindungen funktionieren weiterhin)"
},
"admin": {
"label": "Is Admin"
"label": "Ist Administrator"
}
},
"interface-view": {
"headline": "Config for Interface:"
"headline": "Konfiguration für Schnittstelle:"
},
"interface-edit": {
"headline-edit": "Edit Interface:",
"headline-new": "New Interface",
"tab-interface": "Interface",
"tab-peerdef": "Peer Defaults",
"header-general": "General",
"header-network": "Network",
"header-crypto": "Cryptography",
"header-hooks": "Interface Hooks",
"headline-edit": "Schnittstelle bearbeiten:",
"headline-new": "Neue Schnittstelle",
"tab-interface": "Schnittstelle",
"tab-peerdef": "Peer-Standardeinstellungen",
"header-general": "Allgemein",
"header-network": "Netzwerk",
"header-crypto": "Kryptografie",
"header-hooks": "Schnittstellen-Hooks",
"header-peer-hooks": "Hooks",
"header-state": "State",
"header-state": "Status",
"identifier": {
"label": "Identifier",
"placeholder": "The unique interface identifier"
"label": "Kennung",
"placeholder": "Die eindeutige Schnittstellenkennung"
},
"mode": {
"label": "Interface Mode",
"server": "Server Mode",
"client": "Client Mode",
"any": "Unknown Mode"
"label": "Schnittstellenmodus",
"server": "Server-Modus",
"client": "Client-Modus",
"any": "Unbekannter Modus"
},
"display-name": {
"label": "Display Name",
"placeholder": "The descriptive name for the interface"
"label": "Anzeigename",
"placeholder": "Der beschreibende Name für die Schnittstelle"
},
"private-key": {
"label": "Private Key",
"placeholder": "The private key"
"label": "Privater Schlüssel",
"placeholder": "Der private Schlüssel"
},
"public-key": {
"label": "Public Key",
"placeholder": "The public key"
"label": "Öffentlicher Schlüssel",
"placeholder": "Der öffentliche Schlüssel"
},
"ip": {
"label": "IP Addresses",
"placeholder": "IP Addresses (CIDR format)"
"label": "IP-Adressen",
"placeholder": "IP-Adressen (CIDR-Format)"
},
"listen-port": {
"label": "Listen Port",
"placeholder": "The listening port"
"label": "Port",
"placeholder": "Der Port der WireGuard Schnittstelle"
},
"dns": {
"label": "DNS Server",
"placeholder": "The DNS servers that should be used"
"label": "DNS-Server",
"placeholder": "Die zu verwendenden DNS-Server"
},
"dns-search": {
"label": "DNS Search Domains",
"placeholder": "DNS search prefixes"
"label": "DNS-Suchdomänen",
"placeholder": "DNS-Suchpräfixe"
},
"mtu": {
"label": "MTU",
"placeholder": "The interface MTU (0 = keep default)"
"placeholder": "Die Schnittstellen-MTU (0 = Standard beibehalten)"
},
"firewall-mark": {
"label": "Firewall Mark",
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
"label": "Firewall-Markierung",
"placeholder": "Firewall-Markierung, die auf ausgehenden Datenverkehr angewendet wird. (0 = automatisch)"
},
"routing-table": {
"label": "Routing Table",
"placeholder": "The routing table ID",
"description": "Special cases: off = do not manage routes, 0 = automatic"
"label": "Routing-Tabelle",
"placeholder": "Die Routing-Tabellen-ID",
"description": "Spezialfälle: off = Routen nicht verwalten, 0 = automatisch"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"disabled": {
"label": "Interface Disabled"
"label": "Schnittstelle deaktiviert"
},
"save-config": {
"label": "Automatically save wg-quick config"
"label": "wg-quick Konfiguration automatisch speichern"
},
"defaults": {
"endpoint": {
"label": "Endpoint Address",
"placeholder": "Endpoint Address",
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
"label": "Endpunktadresse",
"placeholder": "Endpunktadresse",
"description": "Die Endpunktadresse, mit der sich Peers verbinden. (z.B. wg.example.com oder wg.example.com:51820)"
},
"networks": {
"label": "IP Networks",
"placeholder": "Network Addresses",
"description": "Peers will get IP addresses from those subnets."
"label": "IP-Netzwerke",
"placeholder": "Netzwerkadressen",
"description": "Peers erhalten IP-Adressen aus diesen Subnetzen."
},
"allowed-ip": {
"label": "Allowed IP Addresses",
"placeholder": "Default Allowed IP Addresses"
"label": "Erlaubte IP-Adressen",
"placeholder": "Erlaubte IP-Adressen für Peers"
},
"mtu": {
"label": "MTU",
"placeholder": "The client MTU (0 = keep default)"
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
},
"keep-alive": {
"label": "Keep Alive Interval",
"placeholder": "Persistent Keepalive (0 = default)"
"label": "Keepalive-Intervall",
"placeholder": "Persistentes Keepalive (0 = Standard)"
}
},
"button-apply-defaults": "Apply Peer Defaults"
"button-apply-defaults": "Peer-Standardeinstellungen anwenden"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Peer Information",
"section-status": "Current Status",
"section-config": "Configuration",
"identifier": "Identifier",
"ip": "IP Addresses",
"user": "Associated User",
"notes": "Notes",
"expiry-status": "Expires At",
"disabled-status": "Disabled At",
"traffic": "Traffic",
"connection-status": "Connection Stats",
"upload": "Uploaded Bytes (from Server to Peer)",
"download": "Downloaded Bytes (from Peer to Server)",
"pingable": "Is Pingable",
"handshake": "Last Handshake",
"connected-since": "Connected since",
"endpoint": "Endpoint",
"button-download": "Download configuration",
"button-email": "Send configuration via E-Mail"
"headline-endpoint": "Endpunkt:",
"section-info": "Peer-Informationen",
"section-status": "Aktueller Status",
"section-config": "Konfiguration",
"identifier": "Kennung",
"ip": "IP-Adressen",
"user": "Zugeordneter Benutzer",
"notes": "Notizen",
"expiry-status": "Läuft ab am",
"disabled-status": "Deaktiviert am",
"traffic": "Datenverkehr",
"connection-status": "Verbindungsstatistiken",
"upload": "Hochgeladene Bytes (vom Server zum Peer)",
"download": "Heruntergeladene Bytes (vom Peer zum Server)",
"pingable": "Pingbar",
"handshake": "Letzter Handshake",
"connected-since": "Verbunden seit",
"endpoint": "Endpunkt",
"button-download": "Konfiguration herunterladen",
"button-email": "Konfiguration per E-Mail senden"
},
"peer-edit": {
"headline-edit-peer": "Edit peer:",
"headline-edit-endpoint": "Edit endpoint:",
"headline-new-peer": "Create peer",
"headline-new-endpoint": "Create endpoint",
"header-general": "General",
"header-network": "Network",
"header-crypto": "Cryptography",
"header-hooks": "Hooks (Executed on Peer)",
"header-state": "State",
"headline-edit-peer": "Peer bearbeiten:",
"headline-edit-endpoint": "Endpunkt bearbeiten:",
"headline-new-peer": "Peer erstellen",
"headline-new-endpoint": "Endpunkt erstellen",
"header-general": "Allgemein",
"header-network": "Netzwerk",
"header-crypto": "Kryptografie",
"header-hooks": "Hooks (beim Peer ausgeführt)",
"header-state": "Status",
"display-name": {
"label": "Display Name",
"placeholder": "The descriptive name for the peer"
"label": "Anzeigename",
"placeholder": "Der beschreibende Name für den Peer"
},
"linked-user": {
"label": "Linked User",
"placeholder": "The user account which owns this peer"
"label": "Verknüpfter Benutzer",
"placeholder": "Das Benutzerkonto, dem dieser Peer gehört"
},
"private-key": {
"label": "Private Key",
"placeholder": "The private key"
"label": "Privater Schlüssel",
"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": {
"label": "Public Key",
"placeholder": "The public key"
"label": "Öffentlicher Schlüssel",
"placeholder": "Der öffentliche Schlüssel"
},
"preshared-key": {
"label": "Preshared Key",
"placeholder": "Optional pre-shared key"
"label": "Pre-Shared Key",
"placeholder": "Optionaler geteilter Schlüssel"
},
"endpoint-public-key": {
"label": "Endpoint public Key",
"placeholder": "The public key of the remote endpoint"
"label": "Öffentlicher Endpunktschlüssel",
"placeholder": "Der öffentliche Schlüssel des entfernten Endpunkts"
},
"endpoint": {
"label": "Endpoint Address",
"placeholder": "The address of the remote endpoint"
"label": "Endpunktadresse",
"placeholder": "Die Adresse des entfernten Endpunkts"
},
"ip": {
"label": "IP Addresses",
"placeholder": "IP Addresses (CIDR format)"
"label": "IP-Adressen",
"placeholder": "IP-Adressen (CIDR-Format)"
},
"allowed-ip": {
"label": "Allowed IP Addresses",
"placeholder": "Allowed IP Addresses (CIDR format)"
"label": "Erlaubte IP-Adressen",
"placeholder": "Erlaubte IP-Adressen (CIDR-Format)"
},
"extra-allowed-ip": {
"label": "Extra allowed IP Addresses",
"placeholder": "Extra allowed IP's (Server Sided)",
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
"label": "Zusätzliche erlaubte IP-Adressen",
"placeholder": "Zusätzliche erlaubte IP's (Server-seitig)",
"description": "Diese IPs werden an der entfernten WireGuard-Schnittstelle als erlaubte IPs hinzugefügt."
},
"dns": {
"label": "DNS Server",
"placeholder": "The DNS servers that should be used"
"label": "DNS-Server",
"placeholder": "Die zu verwendenden DNS-Server"
},
"dns-search": {
"label": "DNS Search Domains",
"placeholder": "DNS search prefixes"
"label": "DNS-Suchdomänen",
"placeholder": "DNS-Suchpräfixe"
},
"keep-alive": {
"label": "Keep Alive Interval",
"placeholder": "Persistent Keepalive (0 = default)"
"label": "Keepalive-Intervall",
"placeholder": "Persistentes Keepalive (0 = Standard)"
},
"mtu": {
"label": "MTU",
"placeholder": "The client MTU (0 = keep default)"
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "One or multiple bash commands separated by ;"
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
},
"disabled": {
"label": "Peer Disabled"
"label": "Peer deaktiviert"
},
"ignore-global": {
"label": "Ignore global settings"
"label": "Globale Einstellungen ignorieren"
},
"expires-at": {
"label": "Expiry date"
"label": "Ablaufdatum"
}
},
"peer-multi-create": {
"headline-peer": "Create multiple peers",
"headline-endpoint": "Create multiple endpoints",
"headline-peer": "Mehrere Peers erstellen",
"headline-endpoint": "Mehrere Endpunkte erstellen",
"identifiers": {
"label": "User Identifiers",
"placeholder": "User Identifiers",
"description": "A user identifier (the username) for which a peer should be created."
"label": "Benutzerkennungen",
"placeholder": "Benutzerkennungen",
"description": "Eine Benutzerkennung (der Benutzername), für die ein Peer erstellt werden soll."
},
"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."
"headline-endpoint": "Endpunkt:",
"label": "Anzeigename-Präfix",
"placeholder": "Das Präfix",
"description": "Ein Präfix, das dem Anzeigenamen des Peers hinzugefügt wird."
}
}
}

View File

@@ -40,7 +40,8 @@
"settings": "Settings",
"audit": "Audit Log",
"login": "Login",
"logout": "Logout"
"logout": "Logout",
"keygen": "Key Generator"
},
"home": {
"headline": "WireGuard® VPN Portal",
@@ -206,6 +207,25 @@
"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": {
"user-view": {
"headline": "User Account:",
@@ -439,7 +459,8 @@
},
"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": {
"label": "Public Key",

View File

@@ -20,14 +20,14 @@
"delete": "Eliminar"
},
"login": {
"headline": "Por favor, inicie sessão",
"headline": "Por favor, inicie a sessão",
"username": {
"label": "Nome de utilizador",
"placeholder": "Introduza o seu nome de utilizador"
"placeholder": "Por favor, insira o seu nome de utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Introduza a sua palavra-passe"
"placeholder": "Por favor, insira a sua palavra-passe"
},
"button": "Iniciar sessão"
},
@@ -40,143 +40,513 @@
"settings": "Definições",
"audit": "Registo de Auditoria",
"login": "Iniciar Sessão",
"logout": "Terminar Sessão"
"logout": "Terminar Sessão",
"keygen": "Gerador de Chave"
},
"home": {
"title": "Início",
"card": {
"interfaces": "Interfaces",
"users": "Utilizadores"
"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": {
"title": "Interfaces",
"create": "Criar Interface",
"name": "Nome",
"address": "Endereço",
"listen-port": "Porta de Escuta",
"public-key": "Chave Pública",
"private-key": "Chave Privada",
"actions": "Ações",
"delete-dialog": {
"title": "Eliminar Interface",
"text": "Tem a certeza de que deseja eliminar a interface '{{name}}'?"
"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."
},
"form": {
"name": {
"label": "Nome",
"placeholder": "Introduza um nome exclusivo para a interface"
},
"address": {
"label": "Endereço",
"placeholder": "Introduza um endereço válido (ex: 10.0.0.1/24)"
},
"listen-port": {
"label": "Porta de Escuta",
"placeholder": "Introduza a porta onde o WireGuard irá escutar (ex: 51820)"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "Será gerada automaticamente se não for fornecida"
}
}
"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": "modo",
"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": {
"title": "Utilizadores",
"create": "Criar Utilizador",
"name": "Nome",
"email": "Email",
"enabled": "Ativo",
"is-admin": "Administrador",
"actions": "Ações",
"edit": "Editar",
"delete-dialog": {
"title": "Eliminar Utilizador",
"text": "Tem a certeza de que deseja eliminar o utilizador '{{nome}}'?"
"headline": "Administração de Utilizadores",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Primeiro Nome",
"lastname": "Último Nome",
"source": "Fonte",
"peers": "Peers",
"admin": "Administrador"
},
"form": {
"name": {
"label": "Nome",
"placeholder": "Introduza o nome do utilizador"
"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"
},
"email": {
"label": "Email",
"placeholder": "Introduza o email do utilizador"
"source": {
"label": "Fonte",
"placeholder": "A fonte do utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Deixe em branco para manter a atual"
"placeholder": "Uma palavra-passe super secreta",
"description": "Deixe este campo em branco para manter a palavra-passe atual."
},
"is-admin": {
"label": "Administrador"
"email": {
"label": "Email",
"placeholder": "O endereço de e-mail"
},
"enabled": {
"label": "Ativo"
"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"
}
}
},
"peers": {
"title": "Peers",
"create": "Criar Peer",
"public-key": "Chave Pública",
"preshared-key": "Chave Pré-partilhada",
"endpoint": "Endpoint",
"allowed-ips": "IPs Permitidos",
"latest-handshake": "Último Handshake",
"transfer-rx": "Recebido",
"transfer-tx": "Enviado",
"persistent-keepalive": "Keepalive Persistente",
"actions": "Ações",
"edit": "Editar",
"delete-dialog": {
"title": "Eliminar Peer",
"text": "Tem a certeza de que deseja eliminar este peer?"
},
"form": {
"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": "Introduza a chave pública do peer"
"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": "Opcional: Chave partilhada adicional para maior segurança"
"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": "Endpoint",
"placeholder": "Endereço público do peer (ex: 1.2.3.4:51820)"
"label": "Endereço do Endpoint",
"placeholder": "O endereço do endpoint remoto"
},
"allowed-ips": {
"label": "IPs Permitidos",
"placeholder": "Lista de IPs (ex: 10.0.0.2/32, 192.168.1.0/24)"
"ip": {
"label": "Endereços IP",
"placeholder": "Endereços IP (formato CIDR)"
},
"persistent-keepalive": {
"label": "Keepalive Persistente",
"placeholder": "Ex: 25 (em segundos)"
"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."
}
}
},
"settings": {
"title": "Definições",
"password": {
"label": "Nova Palavra-passe",
"placeholder": "Deixe em branco para manter a atual"
},
"save": "Guardar Alterações"
},
"audit": {
"title": "Registo de Auditoria",
"username": "Utilizador",
"ip": "Endereço IP",
"method": "Método",
"path": "Caminho",
"status": "Estado",
"timestamp": "Data/Hora"
},
"errors": {
"required": "Este campo é obrigatório",
"invalid-email": "Endereço de email inválido",
"invalid-address": "Endereço inválido",
"invalid-endpoint": "Endpoint inválido",
"invalid-allowed-ips": "Formato de IPs Permitidos inválido"
}
}

View File

@@ -64,6 +64,14 @@ const router = createRouter({
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AuditView.vue')
},
{
path: '/key-generator',
name: 'key-generator',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/KeyGeneraterView.vue')
}
],
linkActiveClass: "active",
@@ -114,11 +122,11 @@ router.beforeEach(async (to) => {
}
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/', '/login']
const publicPages = ['/', '/login', '/key-generator']
const authRequired = !publicPages.includes(to.path)
if (authRequired && !auth.IsAuthenticated) {
auth.SetReturnUrl(to.fullPath) // store original destination before starting the auth process
auth.SetReturnUrl(to.fullPath) // store the original destination before starting the auth process
return '/login'
}
})

View File

@@ -129,7 +129,7 @@ export const profileStore = defineStore('profile', {
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
.then(this.setUser)
.catch(error => {
this.setPeers([])
this.fetching = false
console.log("Failed to activate API for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",
@@ -143,7 +143,7 @@ export const profileStore = defineStore('profile', {
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
.then(this.setUser)
.catch(error => {
this.setPeers([])
this.fetching = false
console.log("Failed to deactivate API for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",

View File

@@ -81,7 +81,7 @@ onMounted(async () => {
<div class="form-group row">
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
<div class="col-sm-6">
<select v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
<select id="paginationSelector" v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>

View File

@@ -416,7 +416,7 @@ onMounted(async () => {
<div class="form-group row">
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
<div class="col-sm-6">
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
<select id="paginationSelector" v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>

View File

@@ -0,0 +1,147 @@
<script setup>
import {ref} from "vue";
const privateKey = ref("")
const publicKey = ref("")
const presharedKey = ref("")
/**
* Generate an X25519 keypair using the Web Crypto API and return Base64-encoded strings.
* @async
* @function generateKeypair
* @returns {Promise<{ publicKey: string, privateKey: string }>} Resolves with an object containing
* - publicKey: the Base64-encoded public key
* - privateKey: the Base64-encoded private key
*/
async function generateKeypair() {
// 1. Generate an X25519 key pair
const keyPair = await crypto.subtle.generateKey(
{ name: 'X25519', namedCurve: 'X25519' },
true, // extractable
['deriveBits'] // allowed usage for ECDH
);
// 2. Export keys as JWK to access raw key material
const pubJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
const privJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
// 3. Convert Base64URL to standard Base64 with padding
return {
publicKey: b64urlToB64(pubJwk.x),
privateKey: b64urlToB64(privJwk.d)
};
}
/**
* Generate a 32-byte pre-shared key using crypto.getRandomValues.
* @function generatePresharedKey
* @returns {Uint8Array} A Uint8Array of length 32 with random bytes.
*/
function generatePresharedKey() {
let privateKey = new Uint8Array(32);
window.crypto.getRandomValues(privateKey);
return privateKey;
}
/**
* Convert a Base64URL-encoded string to standard Base64 with padding.
* @function b64urlToB64
* @param {string} input - The Base64URL string.
* @returns {string} The padded, standard Base64 string.
*/
function b64urlToB64(input) {
let b64 = input.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) {
b64 += '=';
}
return b64;
}
/**
* Convert an ArrayBuffer or TypedArray buffer to a Base64-encoded string.
* @function arrayBufferToBase64
* @param {ArrayBuffer|Uint8Array} buffer - The buffer to convert.
* @returns {string} Base64-encoded representation of the buffer.
*/
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; ++i) {
binary += String.fromCharCode(bytes[i]);
}
// Window.btoa handles binary → Base64
return btoa(binary);
}
/**
* Generate a new keypair and update the corresponding Vue refs.
* @async
* @function generateNewKeyPair
* @returns {Promise<void>}
*/
async function generateNewKeyPair() {
const keypair = await generateKeypair();
privateKey.value = keypair.privateKey;
publicKey.value = keypair.publicKey;
}
/**
* Generate a new pre-shared key and update the Vue ref.
* @function generateNewPresharedKey
*/
function generateNewPresharedKey() {
const rawPsk = generatePresharedKey();
presharedKey.value = arrayBufferToBase64(rawPsk);
}
</script>
<template>
<div class="page-header">
<h1>{{ $t('keygen.headline') }}</h1>
</div>
<p class="lead">{{ $t('keygen.abstract') }}</p>
<div class="mt-4 row">
<div class="col-12 col-lg-5">
<h1>{{ $t('keygen.headline-keypair') }}</h1>
<fieldset>
<div class="form-group">
<label class="form-label mt-4">{{ $t('keygen.private-key.label') }}</label>
<input class="form-control" v-model="privateKey" :placeholder="$t('keygen.private-key.placeholder')" readonly>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('keygen.public-key.label') }}</label>
<input class="form-control" v-model="publicKey" :placeholder="$t('keygen.private-key.placeholder')" readonly>
</div>
</fieldset>
<fieldset>
<hr class="mt-4">
<button class="btn btn-primary mb-4" type="button" @click.prevent="generateNewKeyPair">{{ $t('keygen.button-generate') }}</button>
</fieldset>
</div>
<div class="col-12 col-lg-2 mt-sm-4">
</div>
<div class="col-12 col-lg-5">
<h1>{{ $t('keygen.headline-preshared-key') }}</h1>
<fieldset>
<div class="form-group">
<label class="form-label mt-4">{{ $t('keygen.preshared-key.label') }}</label>
<input class="form-control" v-model="presharedKey" :placeholder="$t('keygen.preshared-key.placeholder')" readonly>
</div>
</fieldset>
<fieldset>
<hr class="mt-4">
<button class="btn btn-primary mb-4" type="button" @click.prevent="generateNewPresharedKey">{{ $t('keygen.button-generate') }}</button>
</fieldset>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -178,7 +178,7 @@ onMounted(async () => {
{{ $t('general.pagination.size')}}:
</label>
<div class="col-sm-6">
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
<select id="paginationSelector" v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>

View File

@@ -116,7 +116,7 @@ onMounted(() => {
<div class="form-group row">
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
<div class="col-sm-6">
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
<select id="paginationSelector" v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>

15
go.mod
View File

@@ -16,19 +16,19 @@ require (
github.com/stretchr/testify v1.10.0
github.com/swaggo/swag v1.16.4
github.com/vardius/message-bus v1.1.5
github.com/vishvananda/netlink v1.3.0
github.com/vishvananda/netlink v1.3.1
github.com/xhit/go-simple-mail/v2 v2.16.0
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
golang.org/x/crypto v0.37.0
golang.org/x/oauth2 v0.29.0
golang.org/x/sys v0.32.0
golang.org/x/crypto v0.38.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.33.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlserver v1.5.4
gorm.io/gorm v1.25.12
gorm.io/gorm v1.26.1
)
require (
@@ -62,7 +62,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -82,8 +81,8 @@ require (
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/protobuf v1.36.6 // indirect

102
go.sum
View File

@@ -44,24 +44,16 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
@@ -83,8 +75,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
@@ -98,12 +88,10 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -117,8 +105,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo=
github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
@@ -179,16 +165,10 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.6.1 h1:EQukUOma9YFZRPe4DGSscxUf9LH07rpqwisNWjSZrgU=
github.com/prometheus-community/pro-bing v0.6.1/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ=
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
@@ -218,9 +198,8 @@ github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
@@ -237,25 +216,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -268,28 +238,18 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -303,16 +263,11 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -320,13 +275,9 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -337,18 +288,12 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -356,8 +301,6 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -378,35 +321,26 @@ gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=

View File

@@ -57,6 +57,52 @@
}
}
},
"/auth/login/{provider}/callback": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Authentication"
],
"summary": "Handle the OAuth callback.",
"operationId": "auth_handleOauthCallbackGet",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.LoginProviderInfo"
}
}
}
}
}
},
"/auth/login/{provider}/init": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Authentication"
],
"summary": "Initiate the OAuth login flow.",
"operationId": "auth_handleOauthInitiateGet",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.LoginProviderInfo"
}
}
}
}
}
},
"/auth/logout": {
"post": {
"produces": [
@@ -1805,7 +1851,7 @@
"type": "string"
}
},
"Suffix": {
"Prefix": {
"type": "string"
}
}

View File

@@ -206,7 +206,7 @@ definitions:
items:
type: string
type: array
Suffix:
Prefix:
type: string
type: object
model.Peer:
@@ -383,6 +383,8 @@ definitions:
type: boolean
MailLinkOnly:
type: boolean
MinPasswordLength:
type: integer
PersistentConfigSupported:
type: boolean
SelfProvisioning:
@@ -456,7 +458,22 @@ paths:
summary: Get all available audit entries. Ordered by timestamp.
tags:
- Audit
/auth/{provider}/callback:
/auth/login:
post:
operationId: auth_handleLoginPost
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.LoginProviderInfo'
type: array
summary: Get all available external login providers.
tags:
- Authentication
/auth/login/{provider}/callback:
get:
operationId: auth_handleOauthCallbackGet
produces:
@@ -471,7 +488,7 @@ paths:
summary: Handle the OAuth callback.
tags:
- Authentication
/auth/{provider}/init:
/auth/login/{provider}/init:
get:
operationId: auth_handleOauthInitiateGet
produces:
@@ -486,21 +503,6 @@ paths:
summary: Initiate the OAuth login flow.
tags:
- Authentication
/auth/login:
post:
operationId: auth_handleLoginPost
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.LoginProviderInfo'
type: array
summary: Get all available external login providers.
tags:
- Authentication
/auth/logout:
post:
operationId: auth_handleLogoutPost

View File

@@ -118,6 +118,7 @@
"BasicAuth": []
}
],
"description": "This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
"produces": [
"application/json"
],
@@ -250,6 +251,7 @@
"BasicAuth": []
}
],
"description": "This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
"produces": [
"application/json"
],
@@ -309,6 +311,50 @@
}
}
},
"/interface/prepare": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).",
"produces": [
"application/json"
],
"tags": [
"Interfaces"
],
"summary": "Prepare a new interface record.",
"operationId": "interfaces_handlePrepareGet",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Interface"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/metrics/by-interface/{id}": {
"get": {
"security": [
@@ -547,7 +593,7 @@
"BasicAuth": []
}
],
"description": "Only admins can update existing records.",
"description": "Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
"produces": [
"application/json"
],
@@ -779,7 +825,7 @@
"BasicAuth": []
}
],
"description": "Only admins can create new records.",
"description": "Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
"produces": [
"application/json"
],
@@ -839,6 +885,71 @@
}
}
},
"/peer/prepare/{id}": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.",
"produces": [
"application/json"
],
"tags": [
"Peers"
],
"summary": "Prepare a new peer record for the given WireGuard interface.",
"operationId": "peers_handlePrepareGet",
"parameters": [
{
"type": "string",
"description": "The interface identifier.",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Peer"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/provisioning/data/peer-config": {
"get": {
"security": [

View File

@@ -748,6 +748,8 @@ paths:
tags:
- Interfaces
put:
description: This endpoint updates an existing interface with the provided data.
All required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleUpdatePut
parameters:
- description: The interface identifier.
@@ -795,6 +797,8 @@ paths:
- Interfaces
/interface/new:
post:
description: This endpoint creates a new interface with the provided data. All
required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleCreatePost
parameters:
- description: The interface data.
@@ -835,6 +839,35 @@ paths:
summary: Create a new interface record.
tags:
- Interfaces
/interface/prepare:
get:
description: This endpoint returns a new interface with default values (fresh
key pair, valid name, new IP address pool, ...).
operationId: interfaces_handlePrepareGet
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Interface'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new interface record.
tags:
- Interfaces
/metrics/by-interface/{id}:
get:
operationId: metrics_handleMetricsForInterfaceGet
@@ -1024,7 +1057,8 @@ paths:
tags:
- Peers
put:
description: Only admins can update existing records.
description: Only admins can update existing records. The peer record must contain
all required fields (e.g., public key, allowed IPs).
operationId: peers_handleUpdatePut
parameters:
- description: The peer identifier.
@@ -1136,7 +1170,8 @@ paths:
- Peers
/peer/new:
post:
description: Only admins can create new records.
description: Only admins can create new records. The peer record must contain
all required fields (e.g., public key, allowed IPs).
operationId: peers_handleCreatePost
parameters:
- description: The peer data.
@@ -1177,6 +1212,49 @@ paths:
summary: Create a new peer record.
tags:
- Peers
/peer/prepare/{id}:
get:
description: This endpoint is used to prepare a new peer record. The returned
data contains a fresh key pair and valid ip address.
operationId: peers_handlePrepareGet
parameters:
- description: The interface identifier.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Peer'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new peer record for the given WireGuard interface.
tags:
- Peers
/provisioning/data/peer-config:
get:
description: Normal users can only access their own record. Admins can access

View File

@@ -138,7 +138,8 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
s.versions[version].HandleFunc("GET /doc.html", s.rapiDocHandler(version))
groupSetupFn(s.versions[version])
versionGroup := s.versions[version].Group()
groupSetupFn(versionGroup)
}
}
}

View File

@@ -2,6 +2,7 @@ package handlers
import (
"context"
"log/slog"
"net/http"
"net/url"
"strconv"
@@ -132,7 +133,7 @@ func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc {
// @Summary Initiate the OAuth login flow.
// @Produce json
// @Success 200 {object} []model.LoginProviderInfo
// @Router /auth/{provider}/init [get]
// @Router /auth/login/{provider}/init [get]
func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
currentSession := e.session.GetData(r.Context())
@@ -177,6 +178,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
if err != nil {
slog.Debug("failed to create oauth auth code URL",
"provider", provider, "error", err)
if autoRedirect && e.isValidReturnUrl(returnTo) {
redirectToReturn()
} else {
@@ -211,7 +214,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
// @Summary Handle the OAuth callback.
// @Produce json
// @Success 200 {object} []model.LoginProviderInfo
// @Router /auth/{provider}/callback [get]
// @Router /auth/login/{provider}/callback [get]
func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
currentSession := e.session.GetData(r.Context())
@@ -249,6 +252,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
oauthState := request.Query(r, "state")
if provider != currentSession.OauthProvider {
slog.Debug("invalid oauth provider in callback",
"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
redirectToReturn()
} else {
@@ -258,6 +263,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
return
}
if oauthState != currentSession.OauthState {
slog.Debug("invalid oauth state in callback",
"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
redirectToReturn()
} else {
@@ -267,11 +274,13 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
return
}
loginCtx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
oauthCode)
cancel()
if err != nil {
slog.Debug("failed to process oauth code",
"provider", provider, "state", oauthState, "error", err)
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
redirectToReturn()
} else {

View File

@@ -172,13 +172,13 @@ func NewDomainPeer(src *Peer) *domain.Peer {
type MultiPeerRequest struct {
Identifiers []string `json:"Identifiers"`
Suffix string `json:"Suffix"`
Prefix string `json:"Prefix"`
}
func NewDomainPeerCreationRequest(src *MultiPeerRequest) *domain.PeerCreationRequest {
return &domain.PeerCreationRequest{
UserIdentifiers: src.Identifiers,
Suffix: src.Suffix,
Prefix: src.Prefix,
}
}

View File

@@ -11,6 +11,7 @@ import (
type InterfaceServiceInterfaceManagerRepo interface {
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
PrepareInterface(ctx context.Context) (*domain.Interface, error)
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
@@ -60,6 +61,19 @@ func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdenti
return interfaceData, interfacePeers, nil
}
func (s InterfaceService) Prepare(ctx context.Context) (*domain.Interface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
interfaceData, err := s.interfaces.PrepareInterface(ctx)
if err != nil {
return nil, err
}
return interfaceData, nil
}
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err

View File

@@ -13,6 +13,7 @@ type PeerServicePeerManagerRepo interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
@@ -95,6 +96,19 @@ func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*do
return peer, nil
}
func (s PeerService) Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
peer, err := s.peers.PreparePeer(ctx, id)
if err != nil {
return nil, err
}
return peer, nil
}
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err

View File

@@ -15,6 +15,7 @@ import (
type InterfaceEndpointInterfaceService interface {
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
Prepare(context.Context) (*domain.Interface, error)
Create(context.Context, *domain.Interface) (*domain.Interface, error)
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
Delete(context.Context, domain.InterfaceIdentifier) error
@@ -49,6 +50,7 @@ func (e InterfaceEndpoint) RegisterRoutes(g *routegroup.Bundle) {
apiGroup.HandleFunc("GET /all", e.handleAllGet())
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
apiGroup.HandleFunc("GET /prepare", e.handlePrepareGet())
apiGroup.HandleFunc("POST /new", e.handleCreatePost())
apiGroup.HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
apiGroup.HandleFunc("DELETE /by-id/{id}", e.handleDelete())
@@ -112,11 +114,38 @@ func (e InterfaceEndpoint) handleByIdGet() http.HandlerFunc {
}
}
// handlePrepareGet returns a gorm handler function.
//
// @ID interfaces_handlePrepareGet
// @Tags Interfaces
// @Summary Prepare a new interface record.
// @Description This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
// @Produce json
// @Success 200 {object} models.Interface
// @Failure 401 {object} models.Error
// @Failure 403 {object} models.Error
// @Failure 500 {object} models.Error
// @Router /interface/prepare [get]
// @Security BasicAuth
func (e InterfaceEndpoint) handlePrepareGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
iface, err := e.interfaces.Prepare(r.Context())
if err != nil {
status, model := ParseServiceError(err)
respond.JSON(w, status, model)
return
}
respond.JSON(w, http.StatusOK, models.NewInterface(iface, nil))
}
}
// handleCreatePost returns a gorm handler function.
//
// @ID interfaces_handleCreatePost
// @Tags Interfaces
// @Summary Create a new interface record.
// @Description This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
// @Param request body models.Interface true "The interface data."
// @Produce json
// @Success 200 {object} models.Interface
@@ -155,6 +184,7 @@ func (e InterfaceEndpoint) handleCreatePost() http.HandlerFunc {
// @ID interfaces_handleUpdatePut
// @Tags Interfaces
// @Summary Update an interface record.
// @Description This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
// @Param id path string true "The interface identifier."
// @Param request body models.Interface true "The interface data."
// @Produce json

View File

@@ -16,6 +16,7 @@ type PeerService interface {
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
Create(context.Context, *domain.Peer) (*domain.Peer, error)
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
Delete(context.Context, domain.PeerIdentifier) error
@@ -51,6 +52,7 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
apiGroup.HandleFunc("GET /by-user/{id}", e.handleAllForUserGet())
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /prepare/{id}", e.handlePrepareGet())
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /new", e.handleCreatePost())
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id}", e.handleDelete())
@@ -156,12 +158,48 @@ func (e PeerEndpoint) handleByIdGet() http.HandlerFunc {
}
}
// handlePrepareGet returns a gorm handler function.
//
// @ID peers_handlePrepareGet
// @Tags Peers
// @Summary Prepare a new peer record for the given WireGuard interface.
// @Description This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
// @Param id path string true "The interface identifier."
// @Produce json
// @Success 200 {object} models.Peer
// @Failure 400 {object} models.Error
// @Failure 401 {object} models.Error
// @Failure 403 {object} models.Error
// @Failure 404 {object} models.Error
// @Failure 500 {object} models.Error
// @Router /peer/prepare/{id} [get]
// @Security BasicAuth
func (e PeerEndpoint) handlePrepareGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := request.Path(r, "id")
if id == "" {
respond.JSON(w, http.StatusBadRequest,
models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
return
}
peer, err := e.peers.Prepare(r.Context(), domain.InterfaceIdentifier(id))
if err != nil {
status, model := ParseServiceError(err)
respond.JSON(w, status, model)
return
}
respond.JSON(w, http.StatusOK, models.NewPeer(peer))
}
}
// handleCreatePost returns a gorm handler function.
//
// @ID peers_handleCreatePost
// @Tags Peers
// @Summary Create a new peer record.
// @Description Only admins can create new records.
// @Description Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
// @Param request body models.Peer true "The peer data."
// @Produce json
// @Success 200 {object} models.Peer
@@ -200,7 +238,7 @@ func (e PeerEndpoint) handleCreatePost() http.HandlerFunc {
// @ID peers_handleUpdatePut
// @Tags Peers
// @Summary Update a peer record.
// @Description Only admins can update existing records.
// @Description Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
// @Param id path string true "The peer identifier."
// @Param request body models.Peer true "The peer data."
// @Produce json

View File

@@ -63,6 +63,8 @@ type AuthenticatorOauth interface {
ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error)
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
RegistrationEnabled() bool
// GetAllowedDomains returns the list of whitelisted domains
GetAllowedDomains() []string
}
// AuthenticatorLdap is the interface for all LDAP authenticators.
@@ -392,6 +394,23 @@ func (a *Authenticator) randString(nByte int) (string, error) {
return base64.RawURLEncoding.EncodeToString(b), nil
}
func isDomainAllowed(email string, allowedDomains []string) bool {
if len(allowedDomains) == 0 {
return true
}
parts := strings.Split(email, "@")
if len(parts) != 2 {
return false
}
domain := strings.ToLower(parts[1])
for _, allowed := range allowedDomains {
if domain == strings.ToLower(allowed) {
return true
}
}
return false
}
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
// fetching the user information.
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
@@ -431,6 +450,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
return nil, fmt.Errorf("unable to process user information: %w", err)
}
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
return nil, fmt.Errorf("user is not in allowed domains: %w", err)
}
if user.IsLocked() || user.IsDisabled() {
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
Ctx: ctx,

View File

@@ -54,7 +54,7 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
attrs := []string{"dn"}
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", ldap.EscapeFilter(string(userId)), -1)
searchRequest := ldap.NewSearchRequest(
l.cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
@@ -100,7 +100,7 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
attrs := internal.LdapSearchAttributes(&l.cfg.FieldMap)
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", ldap.EscapeFilter(string(userId)), -1)
searchRequest := ldap.NewSearchRequest(
l.cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit

View File

@@ -27,6 +27,7 @@ type PlainOauthAuthenticator struct {
userAdminMapping *config.OauthAdminMapping
registrationEnabled bool
userInfoLogging bool
allowedDomains []string
}
func newPlainOauthAuthenticator(
@@ -56,6 +57,7 @@ func newPlainOauthAuthenticator(
provider.userAdminMapping = &cfg.AdminMapping
provider.registrationEnabled = cfg.RegistrationEnabled
provider.userInfoLogging = cfg.LogUserInfo
provider.allowedDomains = cfg.AllowedDomains
return provider, nil
}
@@ -65,6 +67,10 @@ func (p PlainOauthAuthenticator) GetName() string {
return p.name
}
func (p PlainOauthAuthenticator) GetAllowedDomains() []string {
return p.allowedDomains
}
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
return p.registrationEnabled

View File

@@ -24,6 +24,7 @@ type OidcAuthenticator struct {
userAdminMapping *config.OauthAdminMapping
registrationEnabled bool
userInfoLogging bool
allowedDomains []string
}
func newOidcAuthenticator(
@@ -57,6 +58,7 @@ func newOidcAuthenticator(
provider.userAdminMapping = &cfg.AdminMapping
provider.registrationEnabled = cfg.RegistrationEnabled
provider.userInfoLogging = cfg.LogUserInfo
provider.allowedDomains = cfg.AllowedDomains
return provider, nil
}
@@ -66,6 +68,10 @@ func (o OidcAuthenticator) GetName() string {
return o.name
}
func (o OidcAuthenticator) GetAllowedDomains() []string {
return o.allowedDomains
}
// RegistrationEnabled returns whether registration is enabled for this authenticator.
func (o OidcAuthenticator) RegistrationEnabled() bool {
return o.registrationEnabled

View File

@@ -0,0 +1,203 @@
package app
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"reflect"
"strings"
"gorm.io/gorm/schema"
"github.com/h44z/wg-portal/internal/domain"
)
// GormEncryptedStringSerializer is a GORM serializer that encrypts and decrypts string values using AES256.
// It is used to store sensitive information in the database securely.
// If the serializer encounters a value that is not a string, it will return an error.
type GormEncryptedStringSerializer struct {
useEncryption bool
keyPhrase string
prefix string
}
// NewGormEncryptedStringSerializer creates a new GormEncryptedStringSerializer.
// It needs to be registered with GORM to be used:
// schema.RegisterSerializer("encstr", gormEncryptedStringSerializerInstance)
// You can then use it in your model like this:
//
// EncryptedField string `gorm:"serializer:encstr"`
func NewGormEncryptedStringSerializer(keyPhrase string) GormEncryptedStringSerializer {
return GormEncryptedStringSerializer{
useEncryption: keyPhrase != "",
keyPhrase: keyPhrase,
prefix: "WG_ENC_",
}
}
// Scan implements the GORM serializer interface. It decrypts the value after reading it from the database.
func (s GormEncryptedStringSerializer) Scan(
ctx context.Context,
field *schema.Field,
dst reflect.Value,
dbValue any,
) (err error) {
var dbStringValue string
if dbValue != nil {
switch v := dbValue.(type) {
case []byte:
dbStringValue = string(v)
case string:
dbStringValue = v
default:
return fmt.Errorf("unsupported type %T for encrypted field %s", dbValue, field.Name)
}
}
if !s.useEncryption {
field.ReflectValueOf(ctx, dst).SetString(dbStringValue) // keep the original value
return nil
}
if !strings.HasPrefix(dbStringValue, s.prefix) {
field.ReflectValueOf(ctx, dst).SetString(dbStringValue) // keep the original value
return nil
}
encryptedString := strings.TrimPrefix(dbStringValue, s.prefix)
decryptedString, err := DecryptAES256(encryptedString, s.keyPhrase)
if err != nil {
return fmt.Errorf("failed to decrypt value for field %s: %w", field.Name, err)
}
field.ReflectValueOf(ctx, dst).SetString(decryptedString)
return
}
// Value implements the GORM serializer interface. It encrypts the value before storing it in the database.
func (s GormEncryptedStringSerializer) Value(
_ context.Context,
_ *schema.Field,
_ reflect.Value,
fieldValue any,
) (any, error) {
if fieldValue == nil {
return nil, nil
}
switch v := fieldValue.(type) {
case string:
if v == "" {
return "", nil // empty string, no need to encrypt
}
if !s.useEncryption {
return v, nil // keep the original value
}
encryptedString, err := EncryptAES256(v, s.keyPhrase)
if err != nil {
return nil, err
}
return s.prefix + encryptedString, nil
case domain.PreSharedKey:
if v == "" {
return "", nil // empty string, no need to encrypt
}
if !s.useEncryption {
return string(v), nil // keep the original value
}
encryptedString, err := EncryptAES256(string(v), s.keyPhrase)
if err != nil {
return nil, err
}
return s.prefix + encryptedString, nil
default:
return nil, fmt.Errorf("encryption only supports string values, got %T", fieldValue)
}
}
// EncryptAES256 encrypts the given plaintext with the given key using AES256 in CBC mode with PKCS7 padding
func EncryptAES256(plaintext, key string) (string, error) {
if len(plaintext) == 0 {
return "", fmt.Errorf("plaintext must not be empty")
}
if len(key) == 0 {
return "", fmt.Errorf("key must not be empty")
}
key = trimEncKey(key)
iv := key[:aes.BlockSize]
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
plain := []byte(plaintext)
plain = pkcs7Padding(plain, aes.BlockSize)
ciphertext := make([]byte, len(plain))
mode := cipher.NewCBCEncrypter(block, []byte(iv))
mode.CryptBlocks(ciphertext, plain)
b64String := base64.StdEncoding.EncodeToString(ciphertext)
return b64String, nil
}
// DecryptAES256 decrypts the given ciphertext with the given key using AES256 in CBC mode with PKCS7 padding
func DecryptAES256(encrypted, key string) (string, error) {
if len(encrypted) == 0 {
return "", fmt.Errorf("ciphertext must not be empty")
}
if len(key) == 0 {
return "", fmt.Errorf("key must not be empty")
}
key = trimEncKey(key)
iv := key[:aes.BlockSize]
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return "", err
}
if len(ciphertext)%aes.BlockSize != 0 {
return "", fmt.Errorf("invalid ciphertext length, must be a multiple of %d", aes.BlockSize)
}
block, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}
mode := cipher.NewCBCDecrypter(block, []byte(iv))
mode.CryptBlocks(ciphertext, ciphertext)
ciphertext = pkcs7UnPadding(ciphertext)
return string(ciphertext), nil
}
func pkcs7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func pkcs7UnPadding(src []byte) []byte {
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
func trimEncKey(key string) string {
if len(key) > 32 {
return key[:32]
}
if len(key) < 32 {
key = key + strings.Repeat("0", 32-len(key))
}
return key
}

View File

@@ -47,11 +47,18 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
}
latestVersion := "1.0.9"
if lastVersion.Version != latestVersion {
return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err)
return fmt.Errorf("unsupported old version, update to database version %s first", latestVersion)
}
slog.Info("found valid V1 database", "version", lastVersion.Version)
// validate target database
if err := validateTargetDatabase(db); err != nil {
return fmt.Errorf("target database validation failed: %w", err)
}
slog.Info("found valid target database, starting migration...")
if err := migrateV1Users(oldDb, db); err != nil {
return fmt.Errorf("user migration failed: %w", err)
}
@@ -70,6 +77,36 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
return nil
}
// validateTargetDatabase checks if the target database is empty and ready for migration.
func validateTargetDatabase(db *gorm.DB) error {
var count int64
err := db.Model(&domain.User{}).Count(&count).Error
if err != nil {
return fmt.Errorf("failed to check user table: %w", err)
}
if count > 0 {
return fmt.Errorf("target database contains %d users, please use an empty database for migration", count)
}
err = db.Model(&domain.Interface{}).Count(&count).Error
if err != nil {
return fmt.Errorf("failed to check interface table: %w", err)
}
if count > 0 {
return fmt.Errorf("target database contains %d interfaces, please use an empty database for migration", count)
}
err = db.Model(&domain.Peer{}).Count(&count).Error
if err != nil {
return fmt.Errorf("failed to check peer table: %w", err)
}
if count > 0 {
return fmt.Errorf("target database contains %d peers, please use an empty database for migration", count)
}
return nil
}
func migrateV1Users(oldDb, newDb *gorm.DB) error {
type User struct {
Email string `gorm:"primaryKey"`
@@ -123,7 +160,7 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
LinkedPeerCount: 0,
}
if err := newDb.Save(&newUser).Error; err != nil {
if err := newDb.Create(&newUser).Error; err != nil {
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
}
@@ -217,7 +254,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
PeerDefPostDown: "",
}
if err := newDb.Save(&newInterface).Error; err != nil {
// Create new interface with associations
if err := newDb.Create(&newInterface).Error; err != nil {
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
}
@@ -362,7 +400,7 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
},
}
if err := newDb.Save(&newPeer).Error; err != nil {
if err := newDb.Create(&newPeer).Error; err != nil {
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
}

View File

@@ -3,6 +3,7 @@ package wireguard
import (
"context"
"log/slog"
"sync"
"time"
"github.com/h44z/wg-portal/internal/app"
@@ -76,6 +77,8 @@ type Manager struct {
db InterfaceAndPeerDatabaseRepo
wg InterfaceController
quick WgQuickController
userLockMap *sync.Map
}
func NewWireGuardManager(
@@ -86,11 +89,12 @@ func NewWireGuardManager(
db InterfaceAndPeerDatabaseRepo,
) (*Manager, error) {
m := &Manager{
cfg: cfg,
bus: bus,
wg: wg,
db: db,
quick: quick,
cfg: cfg,
bus: bus,
wg: wg,
db: db,
quick: quick,
userLockMap: &sync.Map{},
}
m.connectToMessageBus()
@@ -117,6 +121,12 @@ func (m Manager) handleUserCreationEvent(user domain.User) {
return
}
_, loaded := m.userLockMap.LoadOrStore(user.Identifier, "create")
if loaded {
return // another goroutine is already handling this user
}
defer m.userLockMap.Delete(user.Identifier)
slog.Debug("handling new user event", "user", user.Identifier)
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
@@ -132,6 +142,12 @@ func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) {
return
}
_, loaded := m.userLockMap.LoadOrStore(userId, "login")
if loaded {
return // another goroutine is already handling this user
}
defer m.userLockMap.Delete(userId)
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
if err != nil {
slog.Error("failed to retrieve existing peers prior to default peer creation",

View File

@@ -217,6 +217,15 @@ func (m Manager) RestoreInterfaceState(
if err != nil && !iface.IsDisabled() {
slog.Debug("creating missing interface", "interface", iface.Identifier)
// temporarily disable interface in database so that the current state is reflected correctly
_ = m.db.SaveInterface(ctx, iface.Identifier,
func(in *domain.Interface) (*domain.Interface, error) {
now := time.Now()
in.Disabled = &now // set
in.DisabledReason = domain.DisabledReasonInterfaceMissing
return in, nil
})
// try to create a new interface
_, err = m.saveInterface(ctx, &iface)
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"slices"
"time"
"github.com/h44z/wg-portal/internal/app"
@@ -23,12 +24,24 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
return fmt.Errorf("failed to fetch all interfaces: %w", err)
}
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
if err != nil {
return fmt.Errorf("failed to retrieve existing peers prior to default peer creation: %w", err)
}
var newPeers []domain.Peer
for _, iface := range existingInterfaces {
if iface.Type != domain.InterfaceTypeServer {
continue // only create default peers for server interfaces
}
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
return peer.InterfaceIdentifier == iface.Identifier
})
if peerAlreadyCreated {
continue // skip creation if a peer already exists for this interface
}
peer, err := m.PreparePeer(ctx, iface.Identifier)
if err != nil {
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
@@ -220,7 +233,7 @@ func (m Manager) CreateMultiplePeers(
return nil, err
}
var newPeers []*domain.Peer
createdPeers := make([]domain.Peer, 0, len(r.UserIdentifiers))
for _, id := range r.UserIdentifiers {
freshPeer, err := m.PreparePeer(ctx, interfaceId)
@@ -229,27 +242,22 @@ func (m Manager) CreateMultiplePeers(
}
freshPeer.UserIdentifier = domain.UserIdentifier(id) // use id as user identifier. peers are allowed to have invalid user identifiers
if r.Suffix != "" {
freshPeer.DisplayName += " " + r.Suffix
if r.Prefix != "" {
freshPeer.DisplayName = r.Prefix + " " + freshPeer.DisplayName
}
if err := m.validatePeerCreation(ctx, nil, freshPeer); err != nil {
return nil, fmt.Errorf("creation not allowed: %w", err)
}
newPeers = append(newPeers, freshPeer)
}
// Save immediately to reserve the assigned IPs so the next prepared peer gets the next free IPs
if err := m.savePeers(ctx, freshPeer); err != nil {
return nil, fmt.Errorf("failed to create new peer %s: %w", freshPeer.Identifier, err)
}
err := m.savePeers(ctx, newPeers...)
if err != nil {
return nil, fmt.Errorf("failed to create new peers: %w", err)
}
createdPeers = append(createdPeers, *freshPeer)
createdPeers := make([]domain.Peer, len(newPeers))
for i := range newPeers {
createdPeers[i] = *newPeers[i]
m.bus.Publish(app.TopicPeerCreated, *newPeers[i])
m.bus.Publish(app.TopicPeerCreated, *freshPeer)
}
return createdPeers, nil

View File

@@ -188,6 +188,9 @@ type OpenIDConnectProvider struct {
// ExtraScopes specifies optional requested permissions.
ExtraScopes []string `yaml:"extra_scopes"`
// AllowedDomains defines the list of allowed domains
AllowedDomains []string `yaml:"allowed_domains"`
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"`
@@ -226,6 +229,9 @@ type OAuthProvider struct {
// Scope specifies optional requested permissions.
Scopes []string `yaml:"scopes"`
// AllowedDomains defines the list of allowed domains
AllowedDomains []string `yaml:"allowed_domains"`
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"`

View File

@@ -174,15 +174,24 @@ func GetConfig() (*Config, error) {
// override config values from YAML file
cfgFileName := "config/config.yml"
cfgFileName := "config/config.yaml"
cfgFileNameFallback := "config/config.yml"
if envCfgFileName := os.Getenv("WG_PORTAL_CONFIG"); envCfgFileName != "" {
cfgFileName = envCfgFileName
cfgFileNameFallback = envCfgFileName
}
// check if the config file exists, otherwise use the fallback file name
if _, err := os.Stat(cfgFileName); os.IsNotExist(err) {
cfgFileName = cfgFileNameFallback
}
if err := loadConfigFile(cfg, cfgFileName); err != nil {
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
}
cfg.Web.Sanitize()
return cfg, nil
}

View File

@@ -18,11 +18,14 @@ type DatabaseConfig struct {
// Debug enables logging of all database statements
Debug bool `yaml:"debug"`
// SlowQueryThreshold enables logging of slow queries which take longer than the specified duration
SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // 0 means no logging of slow queries
SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // "0" means no logging of slow queries
// Type is the database type. Supported: mysql, mssql, postgres, sqlite
Type SupportedDatabase `yaml:"type"`
// DSN is the database connection string.
// For SQLite, it is the path to the database file.
// For other databases, it is the connection string, see: https://gorm.io/docs/connecting_to_the_database.html
DSN string `yaml:"dsn"`
// EncryptionPassphrase is the passphrase used to encrypt sensitive data (WireGuard keys) in the database.
// If no passphrase is provided, no encryption will be used.
EncryptionPassphrase string `yaml:"encryption_passphrase"`
}

View File

@@ -1,5 +1,7 @@
package config
import "strings"
// WebConfig contains the configuration for the web server.
type WebConfig struct {
// RequestLogging enables logging of all HTTP requests.
@@ -26,3 +28,7 @@ type WebConfig struct {
// KeyFile is the path to the TLS certificate key file.
KeyFile string `yaml:"key_file"`
}
func (c *WebConfig) Sanitize() {
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
}

View File

@@ -7,7 +7,7 @@ import (
)
type KeyPair struct {
PrivateKey string
PrivateKey string `gorm:"serializer:encstr"`
PublicKey string
}

View File

@@ -7,9 +7,9 @@ import (
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
)
type PeerIdentifier string
@@ -36,7 +36,7 @@ type Peer struct {
EndpointPublicKey ConfigOption[string] `gorm:"embedded;embeddedPrefix:endpoint_pubkey_"` // the endpoint public key
AllowedIPsStr ConfigOption[string] `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
PresharedKey PreSharedKey // the pre-shared Key of the peer
PresharedKey PreSharedKey `gorm:"serializer:encstr"` // the pre-shared Key of the peer
PersistentKeepalive ConfigOption[int] `gorm:"embedded;embeddedPrefix:persistent_keep_alive_"` // the persistent keep-alive interval
// WG Portal specific
@@ -136,6 +136,7 @@ func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
p.Interface.PublicKey = userPeer.Interface.PublicKey
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
p.PresharedKey = userPeer.PresharedKey
p.Identifier = userPeer.Identifier
}
p.Interface.Mtu = userPeer.Interface.Mtu
p.PersistentKeepalive = userPeer.PersistentKeepalive
@@ -268,5 +269,5 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
type PeerCreationRequest struct {
UserIdentifiers []string
Suffix string
Prefix string
}

View File

@@ -30,6 +30,15 @@ plugins:
- minify:
minify_html: true
- swagger-ui-tag
- mike:
# These fields are all optional; the defaults are as below...
alias_type: symlink
redirect_template: null
deploy_prefix: ''
canonical_version: null
version_selector: true
css_dir: css
javascript_dir: js
extra:
version:
@@ -65,6 +74,7 @@ nav:
- Docker: documentation/getting-started/docker.md
- Helm: documentation/getting-started/helm.md
- Sources: documentation/getting-started/sources.md
- Reverse Proxy (HTTPS): documentation/getting-started/reverse-proxy.md
- Configuration:
- Overview: documentation/configuration/overview.md
- Examples: documentation/configuration/examples.md