Compare commits

...

50 Commits

Author SHA1 Message Date
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
Christoph Haas
dddf0c475b build v2 tags for release-candidate versions 2025-05-02 10:51:28 +02:00
Christoph Haas
fe60a5ab9b update documentation for Docker usage (#419) 2025-05-02 10:42:33 +02:00
Christoph Haas
e176e07f7d update documentation for Docker usage (#419), include wireguard-tools in Docker image 2025-05-02 10:29:04 +02:00
Christoph Haas
b06c03ef8e fix missing error check (#419) 2025-05-01 19:12:19 +02:00
Christoph Haas
6b0b78d749 docs: add note about running wireguard in Docker (#156) 2025-04-30 22:42:04 +02:00
Vladimir Dombrovski
62f3c8d4a1 Implement EditableKeys parameter (#417)
Signed-off-by: Vladimir DOMBROVSKI <vladimir.dombrovski@bso.co>
2025-04-30 22:05:40 +02:00
acc0mplish
fbcb22198c Added Korean translations (#414) 2025-04-24 14:54:45 +02:00
Rafael Alexandre
2c443a4a9b add portuguese translations (#412)
Signed-off-by: Rafael Alexandre <r.alexandre99@gmail.com>
2025-04-22 22:44:05 +02:00
Christoph
059234d416 never publish pointer payloads on message bus (#411) 2025-04-21 16:42:35 +02:00
Christoph
e2966d32ea fix user creation (#411) 2025-04-21 15:29:53 +02:00
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
65 changed files with 2744 additions and 477 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,12 @@ 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 releases
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') }}
# add {{major}} tag, even for beta releases
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') }}
# 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}}
# 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') }}
- 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

@@ -52,7 +52,7 @@ COPY --from=builder /build/dist/wg-portal /
######
FROM alpine:3.19
# Install OS-level dependencies
RUN apk add --no-cache bash curl iptables nftables openresolv
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
# Setup timezone
ENV TZ=UTC
# Copy binaries

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) artifacs also.

View File

@@ -1,57 +1,161 @@
## Image Usage
The preferred way to start WireGuard Portal as Docker container is to use Docker Compose.
The WireGuard Portal Docker image is available on both [Docker Hub](https://hub.docker.com/r/wgportal/wg-portal) and [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
It is built on the official Alpine Linux base image and comes pre-packaged with all necessary WireGuard dependencies.
A sample docker-compose.yml:
This container allows you to establish WireGuard VPN connections without relying on a host system that supports WireGuard or using the `linuxserver/wireguard` Docker image.
The recommended method for deploying WireGuard Portal is via Docker Compose for ease of configuration and management.
A sample docker-compose.yml (managing WireGuard interfaces directly on the host) is provided below:
```yaml
--8<-- "docker-compose.yml::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.
## WireGuard Interface Handling
WireGuard Portal supports managing WireGuard interfaces through three distinct deployment methods, providing flexibility based on your system architecture and operational preferences:
- **Directly on the host system**:
WireGuard Portal can control WireGuard interfaces natively on the host, without using containers.
This setup is ideal for environments where direct access to system networking is preferred.
To use this method, you need to set the network mode to `host` in your docker-compose.yml file.
```yaml
services:
wg-portal:
...
network_mode: "host"
...
```
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
- **Within the WireGuard Portal Docker container**:
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
```yaml
services:
wg-portal:
image: wgportal/wg-portal:v2
container_name: wg-portal
...
cap_add:
- NET_ADMIN
ports:
# host port : container port
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
- "51820:51820/udp"
# Web UI port
- "8888:8888/tcp"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
# host path : container path
- ./wg/data:/app/data
- ./wg/config:/app/config
```
- **Via a separate Docker container**:
WireGuard Portal can interface with and control WireGuard running in another Docker container, such as the [linuxserver/wireguard](https://docs.linuxserver.io/images/docker-wireguard/) image.
This method is useful in setups that already use `linuxserver/wireguard` or where you want to isolate the VPN backend from the portal frontend.
For this, you need to set the network mode to `service:wireguard` in your docker-compose.yml file, `wireguard` is the service name of your WireGuard container.
```yaml
services:
wg-portal:
image: wgportal/wg-portal:v2
container_name: wg-portal
...
cap_add:
- NET_ADMIN
network_mode: "service:wireguard" # So we ensure to stay on the same network as the wireguard container.
volumes:
# host path : container path
- ./wg/etc:/etc/wireguard
- ./wg/data:/app/data
- ./wg/config:/app/config
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
# host port : container port
- "51820:51820/udp" # WireGuard port, needs to match the port in wg-portal interface config
- "8888:8888/tcp" # Noticed that the port of the web UI is exposed in the wireguard container.
volumes:
- ./wg/etc:/config/wg_confs # We share the configuration (wgx.conf) between wg-portal and wireguard
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
```
As the `linuxserver/wireguard` image uses _wg-quick_ to manage the interfaces, you need to have at least the following configuration set for WireGuard Portal:
```yaml
core:
# The WireGuard container uses wg-quick to manage the WireGuard interfaces - this conflicts with WireGuard Portal during startup.
# To avoid this, we need to set the restore_state option to false so that wg-quick can create the interfaces.
restore_state: false
# Usually, there are no existing interfaces in the WireGuard container, so we can set this to false.
import_existing: false
advanced:
# WireGuard Portal needs to export the WireGuard configuration as wg-quick config files so that the WireGuard container can use them.
config_storage_path: /etc/wireguard/
```
## Image Versioning
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
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, `1.0.19`.
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`.
You can configure WireGuard Portal using a YAML configuration file.
The filepath of the YAML configuration file defaults to `/app/config/config.yaml`.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
By default, WireGuard Portal uses an SQLite database. The database is stored in `/app/data/sqlite.db`.
You should mount those directories as a volume:
- /app/data
- /app/config
- `/app/data`
- `/app/config`
A detailed description of the configuration options can be found [here](../configuration/overview.md).
If you want to access configuration files in wg-quick format, you can mount the `/etc/wireguard` directory inside the container to a location of your choice.
Also enable the `config_storage_path` option in the configuration file:
```yaml
advanced:
config_storage_path: /etc/wireguard
```

View File

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

@@ -21,4 +21,6 @@ make build
## Install
Compiled binary will be available in `./dist` directory.
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 in beta and not feature complete.
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

@@ -48,8 +48,11 @@ const languageFlag = computed(() => {
}
const langMap = {
en: "us",
pt: "pt",
uk: "ua",
zh: "cn",
ko: "kr",
};
return "fi-" + (langMap[lang] || lang);
})
@@ -58,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>
@@ -82,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>
@@ -121,16 +147,20 @@ const currentYear = ref(new Date().getFullYear())
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ko')"><span class="fi fi-kr"></span> 한국어</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('pt')"><span class="fi fi-pt"></span> Português</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
</div>
</div>
</div>
</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

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

@@ -2,10 +2,13 @@
import de from './translations/de.json';
import en from './translations/en.json';
import fr from './translations/fr.json';
import ko from './translations/ko.json';
import pt from './translations/pt.json';
import ru from './translations/ru.json';
import uk from './translations/uk.json';
import vi from './translations/vi.json';
import zh from './translations/zh.json';
import {createI18n} from "vue-i18n";
// Create i18n instance with options
@@ -23,6 +26,8 @@ const i18n = createI18n({
"de": de,
"en": en,
"fr": fr,
"ko": ko,
"pt": pt,
"ru": ru,
"uk": uk,
"vi": vi,

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

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

View File

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

View File

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

@@ -393,8 +393,14 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
},
},
})
if err != nil {
return nil, fmt.Errorf("peer create error for %s: %w", id.ToPublicKey(), err)
}
peer, err = r.getPeer(deviceId, id)
if err != nil {
return nil, fmt.Errorf("peer error after create: %w", err)
}
return peer, nil
}

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": [

View File

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

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

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

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

@@ -82,7 +82,7 @@ func (m Manager) RegisterUser(ctx context.Context, user *domain.User) error {
return err
}
m.bus.Publish(app.TopicUserRegistered, createdUser)
m.bus.Publish(app.TopicUserRegistered, *createdUser)
return nil
}
@@ -294,8 +294,8 @@ func (m Manager) ActivateApi(ctx context.Context, id domain.UserIdentifier) (*do
return nil, fmt.Errorf("update failure: %w", err)
}
m.bus.Publish(app.TopicUserUpdated, user)
m.bus.Publish(app.TopicUserApiEnabled, user)
m.bus.Publish(app.TopicUserUpdated, *user)
m.bus.Publish(app.TopicUserApiEnabled, *user)
return user, nil
}
@@ -322,8 +322,8 @@ func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*
return nil, fmt.Errorf("update failure: %w", err)
}
m.bus.Publish(app.TopicUserUpdated, user)
m.bus.Publish(app.TopicUserApiDisabled, user)
m.bus.Publish(app.TopicUserUpdated, *user)
m.bus.Publish(app.TopicUserApiDisabled, *user)
return user, nil
}
@@ -389,12 +389,14 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
}
if new.Source != domain.UserSourceDatabase {
// Admins are allowed to create users for arbitrary sources.
if new.Source != domain.UserSourceDatabase && !currentUser.IsAdmin {
return fmt.Errorf("invalid user source: %s, only %s is allowed: %w",
new.Source, domain.UserSourceDatabase, domain.ErrInvalidData)
}
if string(new.Password) == "" {
// database users must have a password
if new.Source == domain.UserSourceDatabase && string(new.Password) == "" {
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
}
@@ -430,6 +432,8 @@ func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error
}
func (m Manager) runLdapSynchronizationService(ctx context.Context) {
ctx = domain.SetUserInfo(ctx, domain.LdapSyncContextUserInfo()) // switch to service context for LDAP sync
for _, ldapCfg := range m.cfg.Auth.Ldap { // LDAP Auth providers
go func(cfg config.LdapProvider) {
syncInterval := cfg.SyncInterval

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()
@@ -112,11 +116,17 @@ func (m Manager) connectToMessageBus() {
_ = m.bus.Subscribe(app.TopicUserDeleted, m.handleUserDeletedEvent)
}
func (m Manager) handleUserCreationEvent(user *domain.User) {
func (m Manager) handleUserCreationEvent(user domain.User) {
if !m.cfg.Core.CreateDefaultPeerOnCreation {
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

@@ -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)
@@ -190,7 +203,7 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("failed to prepare peer for interface %s: %w", peer.InterfaceIdentifier, err)
}
preparedPeer.OverwriteUserEditableFields(peer)
preparedPeer.OverwriteUserEditableFields(peer, m.cfg)
peer = preparedPeer
}
@@ -278,7 +291,7 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
if err != nil {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
}
originalPeer.OverwriteUserEditableFields(peer)
originalPeer.OverwriteUserEditableFields(peer, m.cfg)
peer = originalPeer
}

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

@@ -45,6 +45,14 @@ func SystemAdminContextUserInfo() *ContextUserInfo {
}
}
// LdapSyncContextUserInfo returns a context user info for the LDAP syncer.
func LdapSyncContextUserInfo() *ContextUserInfo {
return &ContextUserInfo{
Id: CtxSystemLdapSyncer,
IsAdmin: true,
}
}
// SetUserInfo sets the user info in the context.
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
ctx = context.WithValue(ctx, CtxUserInfo, info)

View File

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

View File

@@ -9,6 +9,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
)
type PeerIdentifier string
@@ -35,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
@@ -129,16 +130,19 @@ func (p *Peer) GenerateDisplayName(prefix string) {
}
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer) {
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
p.DisplayName = userPeer.DisplayName
p.Interface.PublicKey = userPeer.Interface.PublicKey
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
if cfg.Core.EditableKeys {
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
p.ExpiresAt = userPeer.ExpiresAt
p.Disabled = userPeer.Disabled
p.DisabledReason = userPeer.DisabledReason
p.PresharedKey = userPeer.PresharedKey
}
type PeerInterfaceConfig struct {

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/h44z/wg-portal/internal/config"
)
func TestPeer_IsDisabled(t *testing.T) {
@@ -98,7 +99,7 @@ func TestPeer_OverwriteUserEditableFields(t *testing.T) {
DisplayName: "New DisplayName",
}
peer.OverwriteUserEditableFields(userPeer)
peer.OverwriteUserEditableFields(userPeer, &config.Config{})
assert.Equal(t, "New DisplayName", peer.DisplayName)
}

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