Compare commits

...

6 Commits

Author SHA1 Message Date
Christoph Haas
d01d865b4d fix self provisioning feature (#272)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-26 11:35:24 +01:00
Christoph Haas
1b8cdc3417 automatically append listening port to endpoint address (#352) 2025-01-26 09:52:09 +01:00
Christoph Haas
d35889de73 remove external google fonts (#107)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-25 23:06:44 +01:00
Dmytro Bondar
0b18b5efd6 [chart] Fix default configurations (#350)
Some checks failed
Chart / lint-test (push) Has been cancelled
Chart / publish (push) Has been cancelled
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled
2025-01-24 12:48:36 +01:00
Dmytro Bondar
2cf2341e4c [chart] Update helm chart (#349)
Some checks are pending
Chart / lint-test (push) Waiting to run
Chart / publish (push) Waiting to run
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
2025-01-23 13:42:51 +01:00
Dmytro Bondar
043d25a08f [docs] big bang update (#348)
Some checks are pending
Docker / Build and Push (push) Waiting to run
Docker / release (push) Blocked by required conditions
github-pages / deploy (push) Waiting to run
* [docs] big bang update

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

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

* Update sources.md

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

---------

Signed-off-by: Dmytro Bondar <git@bonddim.dev>
Co-authored-by: h44z <christoph.h@sprinternet.at>
2025-01-23 08:06:55 +01:00
38 changed files with 1493 additions and 493 deletions

273
README.md
View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# wg-portal # wg-portal
![Version: 0.5.0](https://img.shields.io/badge/Version-0.5.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square) ![Version: 0.7.0](https://img.shields.io/badge/Version-0.7.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2](https://img.shields.io/badge/AppVersion-v2-informational?style=flat-square)
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
@@ -32,13 +32,13 @@ The [Values](#values) section lists the parameters that can be configured during
| nameOverride | string | `""` | Partially override resource names (adds suffix) | | nameOverride | string | `""` | Partially override resource names (adds suffix) |
| fullnameOverride | string | `""` | Fully override resource names | | fullnameOverride | string | `""` | Fully override resource names |
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release | | extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
| config.advanced | tpl/object | `{}` | Advanced configuration options. | | config.advanced | tpl/object | `{}` | [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options. |
| config.auth | tpl/object | `{}` | Auth configuration options. | | config.auth | tpl/object | `{}` | [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options. |
| config.core | tpl/object | `{}` | Core configuration options.<br> If external admins in `auth` are not defined and there are no `admin_user` and `admin_password` defined here, the default credentials will be generated. | | config.core | tpl/object | `{}` | [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br> If external admins in `auth` are defined and there are no `admin_user` and `admin_password` defined here, the default admin account will be disabled. |
| config.database | tpl/object | `{}` | Database configuration options | | config.database | tpl/object | `{}` | [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options |
| config.mail | tpl/object | `{}` | Mail configuration options | | config.mail | tpl/object | `{}` | [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options |
| config.statistics | tpl/object | `{}` | Statistics configuration options | | config.statistics | tpl/object | `{}` | [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options |
| config.web | tpl/object | `{}` | Web configuration options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. | | config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. | | revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` | | workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet | | strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
@@ -73,6 +73,7 @@ The [Values](#values) section lists the parameters that can be configured during
| service.web.annotations | object | `{}` | Annotations for the web service | | service.web.annotations | object | `{}` | Annotations for the web service |
| service.web.type | string | `"ClusterIP"` | Web service type | | service.web.type | string | `"ClusterIP"` | Web service type |
| service.web.port | int | `8888` | Web service port Used for the web interface listener | | service.web.port | int | `8888` | Web service port Used for the web interface listener |
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service | | service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type | | service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. | | service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
@@ -81,7 +82,7 @@ The [Values](#values) section lists the parameters that can be configured during
| ingress.className | string | `""` | Ingress class name | | ingress.className | string | `""` | Ingress class name |
| ingress.annotations | object | `{}` | Ingress annotations | | ingress.annotations | object | `{}` | Ingress annotations |
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret | | ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created | | certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |
| certificate.issuer.name | string | `""` | Certificate issuer name | | certificate.issuer.name | string | `""` | Certificate issuer name |
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) | | certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group | | certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |

View File

@@ -62,9 +62,9 @@ Create the name of the service account to use
{{- end }} {{- end }}
{{/* {{/*
Define default admin credentials Disables default admin credentials
If external auth is enabled and has admin group mappings, If external auth is enabled and has admin group mappings,
the admin_user and admin_password values are not used. the admin_user will be set to blank (disabled).
*/}} */}}
{{- define "wg-portal.admin" -}} {{- define "wg-portal.admin" -}}
{{- $externalAdmin := false -}} {{- $externalAdmin := false -}}
@@ -80,9 +80,8 @@ the admin_user and admin_password values are not used.
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}
{{- if not $externalAdmin -}} {{- if $externalAdmin -}}
admin_user: admin@wgportal.local admin_user: ""
admin_password: {{ printf "%s/%s" .Release.Name .Release.Namespace | b64enc }}
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}

View File

@@ -51,3 +51,16 @@ spec:
{{- end }} {{- end }}
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }} selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
{{- end -}} {{- end -}}
{{/*
Define the service port template for the web port
*/}}
{{- define "wg-portal.service.webPort" -}}
name: web
port: {{ .Values.service.web.port }}
protocol: TCP
targetPort: web
{{- if semverCompare ">=1.20-0" .Capabilities.KubeVersion.Version }}
appProtocol: {{ ternary "https" .Values.service.web.appProtocol .Values.certificate.enabled }}
{{- end -}}
{{- end -}}

View File

@@ -1,3 +1,11 @@
{{- $advanced := dict "start_listen_port" (.Values.service.wireguard.ports | sortAlpha | first | int) -}}
{{- $statistics := dict "listening_address" (printf ":%v" .Values.service.metrics.port) -}}
{{- $web:= dict "listening_address" (printf ":%v" .Values.service.web.port) -}}
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
{{- $_ := set $web "cert_file" "/app/certs/tls.crt" }}
{{- $_ := set $web "key_file" "/app/certs/tls.key" }}
{{- end }}
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@@ -5,11 +13,9 @@ metadata:
labels: {{- include "wg-portal.labels" . | nindent 4 }} labels: {{- include "wg-portal.labels" . | nindent 4 }}
stringData: stringData:
config.yml: | config.yml: |
advanced: {{- with mustMerge $advanced .Values.config.advanced }}
start_listen_port: {{ .Values.service.wireguard.ports | sortAlpha | first }} advanced: {{- tpl (toYaml .) $ | nindent 6 }}
{{- with .Values.config.advanced }} {{- end }}
{{- tpl (toYaml (omit . "start_listen_port")) $ | nindent 6 }}
{{- end }}
{{- with .Values.config.auth }} {{- with .Values.config.auth }}
auth: {{- tpl (toYaml .) $ | nindent 6 }} auth: {{- tpl (toYaml .) $ | nindent 6 }}
@@ -27,14 +33,10 @@ stringData:
mail: {{- tpl (toYaml .) $ | nindent 6 }} mail: {{- tpl (toYaml .) $ | nindent 6 }}
{{- end }} {{- end }}
statistics: {{- with mustMerge $statistics .Values.config.statistics }}
listening_address: :{{ .Values.service.metrics.port }} statistics: {{- tpl (toYaml .) $ | nindent 6 }}
{{- with .Values.config.statistics }} {{- end }}
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
{{- end }}
web: {{- with mustMerge $web .Values.config.web }}
listening_address: :{{ .Values.service.web.port }} web: {{- tpl (toYaml .) $ | nindent 6 }}
{{- with .Values.config.web }} {{- end }}
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
{{- end }}

View File

@@ -1,4 +1,4 @@
{{- $portsWeb := list (dict "name" "web" "port" .Values.service.web.port "protocol" "TCP" "targetPort" "web") -}} {{- $portsWeb := list (include "wg-portal.service.webPort" . | fromYaml) -}}
{{- $ports := list -}} {{- $ports := list -}}
{{- range $idx, $port := .Values.service.wireguard.ports -}} {{- range $idx, $port := .Values.service.wireguard.ports -}}
{{- $name := printf "wg%d" $idx -}} {{- $name := printf "wg%d" $idx -}}

View File

@@ -3,37 +3,36 @@
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
# -- Partially override resource names (adds suffix) # -- Partially override resource names (adds suffix)
nameOverride: '' nameOverride: ""
# -- Fully override resource names # -- Fully override resource names
fullnameOverride: '' fullnameOverride: ""
# -- Array of extra objects to deploy with the release # -- Array of extra objects to deploy with the release
extraDeploy: [] extraDeploy: []
# https://github.com/h44z/wg-portal/blob/master/README.md#configuration-options
config: config:
# -- (tpl/object) Advanced configuration options. # -- (tpl/object) [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options.
advanced: {} advanced: {}
# -- (tpl/object) Auth configuration options. # -- (tpl/object) [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options.
auth: {} auth: {}
# -- (tpl/object) Core configuration options.<br> # -- (tpl/object) [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br>
# If external admins in `auth` are not defined and # If external admins in `auth` are defined and
# there are no `admin_user` and `admin_password` defined here, # there are no `admin_user` and `admin_password` defined here,
# the default credentials will be generated. # the default admin account will be disabled.
core: {} core: {}
# -- (tpl/object) Database configuration options # -- (tpl/object) [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options
database: {} database: {}
# -- (tpl/object) Mail configuration options # -- (tpl/object) [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options
mail: {} mail: {}
# -- (tpl/object) Statistics configuration options # -- (tpl/object) [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options
statistics: {} statistics: {}
# -- (tpl/object) Web configuration options.<br> # -- (tpl/object) [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br>
# `listening_address` will be set automatically from `service.web.port`. # `listening_address` will be set automatically from `service.web.port`.
# `external_url` is required to enable ingress and certificate resources. # `external_url` is required to enable ingress and certificate resources.
web: {} web: {}
# -- The number of old ReplicaSets to retain to allow rollback. # -- The number of old ReplicaSets to retain to allow rollback.
# @default -- `10` # @default -- `10`
revisionHistoryLimit: '' revisionHistoryLimit: ""
# -- Workload type - `Deployment` or `StatefulSet` # -- Workload type - `Deployment` or `StatefulSet`
workloadType: Deployment workloadType: Deployment
# -- Update strategy for the workload # -- Update strategy for the workload
@@ -49,7 +48,7 @@ image:
# -- Image pull policy # -- Image pull policy
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion # -- Overrides the image tag whose default is the chart appVersion
tag: '' tag: ""
# -- Image pull secrets # -- Image pull secrets
imagePullSecrets: [] imagePullSecrets: []
@@ -73,14 +72,14 @@ sidecarContainers: []
# -- Set DNS policy for the pod. # -- Set DNS policy for the pod.
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`. # Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
# @default -- `"ClusterFirst"` # @default -- `"ClusterFirst"`
dnsPolicy: '' dnsPolicy: ""
# -- Restart policy for all containers within the pod. # -- Restart policy for all containers within the pod.
# Valid values are `Always`, `OnFailure` or `Never`. # Valid values are `Always`, `OnFailure` or `Never`.
# @default -- `"Always"` # @default -- `"Always"`
restartPolicy: '' restartPolicy: ""
# -- Use the host's network namespace. # -- Use the host's network namespace.
# @default -- `false`. # @default -- `false`.
hostNetwork: '' hostNetwork: ""
# -- Resources requests and limits # -- Resources requests and limits
resources: {} resources: {}
# -- Overwrite pod command # -- Overwrite pod command
@@ -123,6 +122,8 @@ service:
# -- Web service port # -- Web service port
# Used for the web interface listener # Used for the web interface listener
port: 8888 port: 8888
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
appProtocol: http
wireguard: wireguard:
# -- Annotations for the WireGuard service # -- Annotations for the WireGuard service
annotations: {} annotations: {}
@@ -141,7 +142,7 @@ ingress:
# -- Specifies whether an ingress resource should be created # -- Specifies whether an ingress resource should be created
enabled: false enabled: false
# -- Ingress class name # -- Ingress class name
className: '' className: ""
# -- Ingress annotations # -- Ingress annotations
annotations: {} annotations: {}
# -- Ingress TLS configuration. # -- Ingress TLS configuration.
@@ -149,21 +150,22 @@ ingress:
tls: false tls: false
certificate: certificate:
# -- Specifies whether a certificate resource should be created # -- Specifies whether a certificate resource should be created.
# If enabled, certificate will be used for the web.
enabled: false enabled: false
issuer: issuer:
# -- Certificate issuer name # -- Certificate issuer name
name: '' name: ""
# -- Certificate issuer kind (ClusterIssuer or Issuer) # -- Certificate issuer kind (ClusterIssuer or Issuer)
kind: '' kind: ""
# -- Certificate issuer group # -- Certificate issuer group
group: cert-manager.io group: cert-manager.io
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) # -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
duration: '' duration: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) # -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
renewBefore: '' renewBefore: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) # -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
commonName: '' commonName: ""
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) # -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
emailAddresses: [] emailAddresses: []
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) # -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
@@ -188,7 +190,7 @@ persistence:
annotations: {} annotations: {}
# -- Persistent Volume storage class. # -- Persistent Volume storage class.
# If undefined (the default) cluster's default provisioner will be used. # If undefined (the default) cluster's default provisioner will be used.
storageClass: '' storageClass: ""
# -- Persistent Volume Access Mode # -- Persistent Volume Access Mode
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
# -- Persistent Volume size # -- Persistent Volume size
@@ -203,7 +205,7 @@ serviceAccount:
automount: false automount: false
# -- The name of the service account to use. # -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template # If not set and create is true, a name is generated using the fullname template
name: '' name: ""
monitoring: monitoring:
# -- Enable Prometheus monitoring. # -- Enable Prometheus monitoring.
@@ -220,15 +222,15 @@ monitoring:
annotations: {} annotations: {}
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used. # -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
# @default -- `1m` # @default -- `1m`
interval: '' interval: ""
# -- Relabelings to samples before ingestion. # -- Relabelings to samples before ingestion.
metricRelabelings: [] metricRelabelings: []
# -- Relabelings to samples before scraping. # -- Relabelings to samples before scraping.
relabelings: [] relabelings: []
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. # -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
scrapeTimeout: '' scrapeTimeout: ""
# -- The label to use to retrieve the job name from. # -- The label to use to retrieve the job name from.
jobLabel: '' jobLabel: ""
# -- Transfers labels on the Kubernetes Pod onto the target. # -- Transfers labels on the Kubernetes Pod onto the target.
podTargetLabels: {} podTargetLabels: {}
@@ -241,4 +243,4 @@ monitoring:
labels: {} labels: {}
# -- Dashboard ConfigMap namespace # -- Dashboard ConfigMap namespace
# Overrides the namespace for the dashboard ConfigMap. # Overrides the namespace for the dashboard ConfigMap.
namespace: '' namespace: ""

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -1,6 +1,7 @@
Below are some sample YAML configurations demonstrating how to override some default values. Below are some sample YAML configurations demonstrating how to override some default values.
## Basic
## Basic Configuration
```yaml ```yaml
core: core:
admin_user: test@example.com admin_user: test@example.com
@@ -8,7 +9,7 @@ core:
import_existing: false import_existing: false
create_default_peer: true create_default_peer: true
self_provisioning_allowed: true self_provisioning_allowed: true
web: web:
site_title: My WireGuard Server site_title: My WireGuard Server
site_company_name: My Company site_company_name: My Company
@@ -31,13 +32,13 @@ database:
dsn: data/sqlite.db dsn: data/sqlite.db
``` ```
## LDAP Authentication and Synchronization Configuration ## LDAP Authentication and Synchronization
```yaml ```yaml
# ... (basic configuration) # ... (basic configuration)
auth: auth:
ldap: ldap:
# a sample LDAP provider with user sync enabled # a sample LDAP provider with user sync enabled
- id: ldap - id: ldap
provider_name: Active Directory provider_name: Active Directory
@@ -63,14 +64,26 @@ auth:
log_user_info: true log_user_info: true
``` ```
## OpenID Connect (OIDC) Authentication Configuration ## OpenID Connect (OIDC) Authentication
```yaml ```yaml
# ... (basic configuration) # ... (basic configuration)
auth: auth:
oidc: oidc:
# a sample Entra ID provider with environment variable substitution
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins - id: azure
provider_name: azure
display_name: Login with</br>Entra ID
registration_enabled: true
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
client_id: "${AZURE_CLIENT_ID}"
client_secret: "${AZURE_CLIENT_SECRET}"
extra_scopes:
- profile
- email
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
- id: oidc-with-admin-attribute - id: oidc-with-admin-attribute
provider_name: google provider_name: google
display_name: Login with</br>Google display_name: Login with</br>Google
@@ -93,7 +106,7 @@ auth:
registration_enabled: true registration_enabled: true
log_user_info: true log_user_info: true
# a sample provider where users in the group `the-admin-group` are considered as admins # a sample provider where users in the group `the-admin-group` are considered as admins
- id: oidc-with-admin-group - id: oidc-with-admin-group
provider_name: google2 provider_name: google2
display_name: Login with</br>Google2 display_name: Login with</br>Google2
@@ -117,15 +130,15 @@ auth:
log_user_info: true log_user_info: true
``` ```
## Plain OAuth2 Authentication Configuration ## Plain OAuth2 Authentication
```yaml ```yaml
# ... (basic configuration) # ... (basic configuration)
auth: auth:
oauth: oauth:
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True` # a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
# are considered as admins # are considered as admins
- id: google_plain_oauth-with-admin-attribute - id: google_plain_oauth-with-admin-attribute
provider_name: google3 provider_name: google3
display_name: Login with</br>Google3 display_name: Login with</br>Google3
@@ -148,7 +161,7 @@ auth:
registration_enabled: true registration_enabled: true
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or # a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
# users in the group `admin-group-name` are considered as admins # users in the group `admin-group-name` are considered as admins
- id: google_plain_oauth_with_groups - id: google_plain_oauth_with_groups
provider_name: google4 provider_name: google4
display_name: Login with</br>Google4 display_name: Login with</br>Google4
@@ -173,4 +186,4 @@ auth:
admin_group_regex: ^admin-group-name$ admin_group_regex: ^admin-group-name$
registration_enabled: true registration_enabled: true
log_user_info: true log_user_info: true
``` ```

View File

@@ -1,17 +1,106 @@
# WireGuard Portal Configuration This page provides an overview of **all available configuration options** for WireGuard Portal.
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. You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
Complete configuration examples are available in the [Configuration Examples](./examples.md) page. 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`.
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).
Below you will find sections like `core`, `advanced`, `statistics`, `mail`, `auth`, `database`, and `web`. Configuration examples are available on the [Examples](./examples.md) page.
<details>
<summary>Default configuration</summary>
```yaml
core:
admin_user: admin@wgportal.local
admin_password: wgportal
editable_keys: true
create_default_peer: false
create_default_peer_on_creation: false
re_enable_peer_after_user_enable: true
delete_peer_after_user_deleted: false
self_provisioning_allowed: false
import_existing: true
restore_state: true
advanced:
log_level: info
log_pretty: false
log_json: false
start_listen_port: 51820
start_cidr_v4: 10.11.12.0/24
start_cidr_v6: fdfd:d3ad:c0de:1234::0/64
use_ip_v6: true
config_storage_path: ""
expiry_check_interval: 15m
rule_prio_offset: 20000
api_admin_only: true
database:
debug: false
slow_query_threshold: 0
type: sqlite
dsn: data/sqlite.db
statistics:
use_ping_checks: true
ping_check_workers: 10
ping_unprivileged: false
ping_check_interval: 1m
data_collection_interval: 1m
collect_interface_data: true
collect_peer_data: true
collect_audit_data: true
listening_address: :8787
mail:
host: 127.0.0.1
port: 25
encryption: none
cert_validation: false
username: ""
password: ""
auth_type: plain
from: Wireguard Portal <noreply@wireguard.local>
link_only: false
auth:
oidc: []
oauth: []
ldap: []
web:
listening_address: :8888
external_url: http://localhost:8888
site_company_name: WireGuard Portal
site_title: WireGuard Portal
session_identifier: wgPortalSession
session_secret: very_secret
csrf_secret: extremely_secret
request_logging: false
cert_file: ""
key_File: ""
```
</details>
Below you will find sections like
[`core`](#core),
[`advanced`](#advanced),
[`database`](#database),
[`statistics`](#statistics),
[`mail`](#mail),
[`auth`](#auth) and
[`web`](#web).
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose. Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
--- ---
## Core ## Core
These are the primary configuration options that control fundamental WireGuard Portal behavior. These are the primary configuration options that control fundamental WireGuard Portal behavior.
More advanced options are found in the subsequent `Advanced` section. More advanced options are found in the subsequent `Advanced` section.
### `admin_user` ### `admin_user`
@@ -108,7 +197,6 @@ Additional or more specialized configuration options for logging and interface c
- **Default:** `true` - **Default:** `true`
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md). - **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
--- ---
## Database ## Database
@@ -224,12 +312,12 @@ Options for configuring email notifications or sending peer configurations via e
## Auth ## Auth
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`). WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
Each can have multiple providers configured. Below are the relevant keys. Each can have multiple providers configured. Below are the relevant keys.
--- ---
### OIDC Provider Properties ### OIDC
The `oidc` array contains a list of OpenID Connect providers. The `oidc` array contains a list of OpenID Connect providers.
Below are the properties for each OIDC provider entry inside `auth.oidc`: Below are the properties for each OIDC provider entry inside `auth.oidc`:
@@ -264,7 +352,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`. - Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical OIDC Claim** | **Explanation** | | **Field** | **Typical OIDC Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. | | `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. | | `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. | | `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
@@ -290,7 +378,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
--- ---
### OAuth Provider Properties ### OAuth
The `oauth` array contains a list of plain OAuth2 providers. The `oauth` array contains a list of plain OAuth2 providers.
Below are the properties for each OAuth provider entry inside `auth.oauth`: Below are the properties for each OAuth provider entry inside `auth.oauth`:
@@ -333,7 +421,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`. - Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical Claim** | **Explanation** | | **Field** | **Typical Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. | | `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. | | `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. | | `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
@@ -359,7 +447,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
--- ---
### LDAP Provider Properties ### LDAP
The `ldap` array contains a list of LDAP authentication providers. The `ldap` array contains a list of LDAP authentication providers.
Below are the properties for each LDAP provider entry inside `auth.ldap`: Below are the properties for each LDAP provider entry inside `auth.ldap`:
@@ -402,7 +490,7 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`. - Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** | | **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|----------------------------|----------------------------|--------------------------------------------------------------| | -------------------------- | -------------------------- | ------------------------------------------------------------ |
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. | | user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
| email | mail / userPrincipalName | Stores the user's primary email address. | | email | mail / userPrincipalName | Stores the user's primary email address. |
| firstname | givenName | Contains the user's first (given) name. | | firstname | givenName | Contains the user's first (given) name. |
@@ -455,3 +543,47 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
#### `log_user_info` #### `log_user_info`
- **Default:** *(empty)* - **Default:** *(empty)*
- **Description:** If `true`, logs LDAP user data at the trace level upon login. - **Description:** If `true`, logs LDAP user data at the trace level upon login.
---
## Web
### `listening_address`
- **Default:** `:8888`
- **Description:** The listening port of the web server.
### `external_url`
- **Default:** `http://localhost:8888`
- **Description:** The URL where a client can access WireGuard Portal.
### `site_company_name`
- **Default:** `WireGuard Portal`
- **Description:** The company name that is shown at the bottom of the web frontend.
### `site_title`
- **Default:** `WireGuard Portal`
- **Description:** The title that is shown in the web frontend.
### `session_identifier`
- **Default:** `wgPortalSession`
- **Description:** The session identifier for the web frontend.
### `session_secret`
- **Default:** `very_secret`
- **Description:** The session secret for the web frontend.
### `csrf_secret`
- **Default:** `extremely_secret`
- **Description:** The CSRF secret.
### `request_logging`
- **Default:** `false`
- **Description:** Log all HTTP requests.
### `cert_file`
- **Default:** *(empty)*
- **Description:** (Optional) Path to the TLS certificate file.
### `key_file`
- **Default:** *(empty)*
- **Description:** (Optional) Path to the TLS certificate key file.

View File

@@ -0,0 +1,34 @@
Starting from v2, each [release](https://github.com/h44z/wg-portal/releases) includes compiled binaries for supported platforms.
These binary versions can be manually downloaded and installed.
## Download
With `curl`:
```shell
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
```
With `wget`:
```shell
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
```
with `gh cli`:
```shell
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
```
## Install
```shell
sudo mkdir -p /opt/wg-portal
sudo install wg-portal /opt/wg-portal/
```
## Unreleased
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,11 +0,0 @@
To build a standalone application, use the Makefile provided in the repository.
Go version **1.23** or higher has to be installed to build WireGuard Portal.
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
```shell
# build the frontend (optional)
make frontend
# build the binary
make build
```

View File

@@ -5,20 +5,7 @@ The preferred way to start WireGuard Portal as Docker container is to use Docker
A sample docker-compose.yml: A sample docker-compose.yml:
```yaml ```yaml
version: '3.6' --8<-- "docker-compose.yml::17"
services:
wg-portal:
image: wgportal/wg-portal:latest
restart: unless-stopped
cap_add:
- NET_ADMIN
network_mode: "host"
ports:
- "8888:8888"
volumes:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
- ./config:/app/config
``` ```
By default, the webserver is listening on port **8888**. By default, the webserver is listening on port **8888**.
@@ -31,6 +18,7 @@ All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-por
There are three types of tags in the repository: There are three types of tags in the repository:
#### Semantic versioned tags #### Semantic versioned tags
For example, `1.0.19`. For example, `1.0.19`.
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. 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).
@@ -44,16 +32,17 @@ If you only want to stay at the same major or major+minor version, use either `v
Version **1** is currently **stable**, version **2** is in **development**. Version **1** is currently **stable**, version **2** is in **development**.
#### latest #### latest
This is the most recent build to master! It changes a lot and is very unstable. This is the most recent build to master! 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.
#### Branch tags #### 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. 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 ## Configuration
You can configure WireGuard Portal using a yaml configuration file. You can configure WireGuard Portal using a yaml configuration file.
The filepath of the yaml configuration file defaults to `/app/config/config.yml`. The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**. It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
@@ -61,6 +50,7 @@ It is possible to override the configuration filepath using the environment vari
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`. By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
You should mount those directories as a volume: You should mount those directories as a volume:
- /app/data - /app/data
- /app/config - /app/config

View File

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

View File

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

View File

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

View File

@@ -1,29 +1 @@
**WireGuard Portal** is a simple, web based configuration portal for [WireGuard](https://wireguard.com). --8<-- "README.md:20:47"
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections.
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
## Features
* Self-hosted - the whole application is a single binary
* Responsive web UI written in Vue.JS
* Automatically select IP from the network pool assigned to client
* QR-Code for convenient mobile client configuration
* Sent email to client with QR-code and client config
* Enable / Disable clients seamlessly
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth or LDAP)
* IPv6 ready
* Docker ready
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* Peer Expiry Feature
* Handle route and DNS settings like wg-quick does
* REST API for management and client deployment
## Quick-Start
The easiest way to get started is to use the provided [Docker image](./getting-started/docker.md).

View File

@@ -1,4 +1,4 @@
For production deployments of WireGuard Portal, we strongly recommend using version 1. 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. If you want to use version 2, please be aware that it is still in beta and not feature complete.
## Upgrade from v1 to v2 ## Upgrade from v1 to v2
@@ -18,7 +18,7 @@ You can also specify the database type using the parameter **-migrateFromType**,
For example: For example:
```shell ```shell
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local ./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.yml** configuration file.
@@ -33,4 +33,4 @@ services:
# ... other settings # ... other settings
restart: no restart: no
command: ["-migrateFrom=/app/data/wg_portal.db"] command: ["-migrateFrom=/app/data/wg_portal.db"]
``` ```

View File

@@ -8,26 +8,28 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1", "@fontsource/nunito-sans": "^5.1.1",
"@kyvg/vue3-notification": "^3.1.3", "@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.3",
"bootswatch": "^5.3.2", "bootswatch": "^5.3.3",
"flag-icons": "^7.1.0", "flag-icons": "^7.3.2",
"ip-address": "^9.0.5", "ip-address": "^10.0.1",
"is-cidr": "^5.0.3", "is-cidr": "^5.1.0",
"is-ip": "^5.0.1", "is-ip": "^5.0.1",
"pinia": "^2.1.7", "pinia": "^2.3.1",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"vue": "^3.3.13", "vue": "^3.5.13",
"vue-i18n": "^9.14.2", "vue-i18n": "^11.0.1",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.5", "vue-router": "^4.5.0",
"vue3-tags-input": "^1.0.12" "vue3-tags-input": "^1.0.12"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^5.2.1",
"vite": "^5.0.10" "sass-embedded": "^1.83.4",
"vite": "^5.4.12"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
@@ -76,6 +78,13 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@bufbuild/protobuf": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz",
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==",
"dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -467,23 +476,29 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@fontsource/nunito-sans": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@fontsource/nunito-sans/-/nunito-sans-5.1.1.tgz",
"integrity": "sha512-84sV7nRYKFlzoY6FeLBAf1FF6+MebDXklVz28Phuh4L52t2juhjRmLXweehNN9pjgdvM0gXCe/kYsgI8WVELUQ==",
"license": "OFL-1.1"
},
"node_modules/@fortawesome/fontawesome-free": { "node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.1", "version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
"integrity": "sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA==", "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@intlify/core-base": { "node_modules/@intlify/core-base": {
"version": "9.14.2", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.0.1.tgz",
"integrity": "sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==", "integrity": "sha512-NAmhw1l/llM0HZRpagR/ChJTNymW4ll6/4EDSJML5c8L5Hl/+k6UyF8EIgE6DeHpfheQujkSRngauViHqq6jJQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/message-compiler": "9.14.2", "@intlify/message-compiler": "11.0.1",
"@intlify/shared": "9.14.2" "@intlify/shared": "11.0.1"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -493,12 +508,12 @@
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
"version": "9.14.2", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.0.1.tgz",
"integrity": "sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==", "integrity": "sha512-5RFH8x+Mn3mbjcHXnb6KCXGiczBdiQkWkv99iiA0JpKrNuTAQeW59Pjq/uObMB0eR0shnKYGTkIJxum+DbL3sw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/shared": "9.14.2", "@intlify/shared": "11.0.1",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"engines": { "engines": {
@@ -509,9 +524,9 @@
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
"version": "9.14.2", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.0.1.tgz",
"integrity": "sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==", "integrity": "sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -805,16 +820,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.6.2", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^18.0.0 || >=20.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^4.0.0 || ^5.0.0", "vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25" "vue": "^3.2.25"
} }
}, },
@@ -949,6 +964,13 @@
"integrity": "sha512-cJLhobnZsVCelU7zdH/L7wpcXAyUoTX4/5l2dWQ0JXgaVK80BdTQNU/ImWwoyIGBeyms4iQDAdNtOfPQZf0Atg==", "integrity": "sha512-cJLhobnZsVCelU7zdH/L7wpcXAyUoTX4/5l2dWQ0JXgaVK80BdTQNU/ImWwoyIGBeyms4iQDAdNtOfPQZf0Atg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/buffer-builder": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true,
"license": "MIT/X11"
},
"node_modules/cidr-regex": { "node_modules/cidr-regex": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.1.1.tgz", "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.1.1.tgz",
@@ -985,6 +1007,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-hrtime": { "node_modules/convert-hrtime": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
@@ -1061,9 +1090,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/flag-icons": { "node_modules/flag-icons": {
"version": "7.2.3", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.3.tgz", "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.3.2.tgz",
"integrity": "sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg==", "integrity": "sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
@@ -1093,15 +1122,28 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/ip-address": { "node_modules/has-flag": {
"version": "9.0.5", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/immutable": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"dev": true,
"license": "MIT"
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT", "license": "MIT",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": { "engines": {
"node": ">= 12" "node": ">= 12"
} }
@@ -1158,12 +1200,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.14", "version": "0.30.14",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
@@ -1198,9 +1234,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/pinia": { "node_modules/pinia": {
"version": "2.2.6", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz", "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==", "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-api": "^6.6.3", "@vue/devtools-api": "^6.6.3",
@@ -1210,14 +1246,10 @@
"url": "https://github.com/sponsors/posva" "url": "https://github.com/sponsors/posva"
}, },
"peerDependencies": { "peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4", "typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.5.11" "vue": "^2.7.0 || ^3.5.11"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": { "typescript": {
"optional": true "optional": true
} }
@@ -1324,6 +1356,401 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/sass-embedded": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.4.tgz",
"integrity": "sha512-Hf2burRA/y5PGxsg6jB9UpoK/xZ6g/pgrkOcdl6j+rRg1Zj8XhGKZ1MTysZGtTPUUmiiErqzkP5+Kzp95yv9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"buffer-builder": "^0.2.0",
"colorjs.io": "^0.5.0",
"immutable": "^5.0.2",
"rxjs": "^7.4.0",
"supports-color": "^8.1.1",
"sync-child-process": "^1.0.2",
"varint": "^6.0.0"
},
"bin": {
"sass": "dist/bin/sass.js"
},
"engines": {
"node": ">=16.0.0"
},
"optionalDependencies": {
"sass-embedded-android-arm": "1.83.4",
"sass-embedded-android-arm64": "1.83.4",
"sass-embedded-android-ia32": "1.83.4",
"sass-embedded-android-riscv64": "1.83.4",
"sass-embedded-android-x64": "1.83.4",
"sass-embedded-darwin-arm64": "1.83.4",
"sass-embedded-darwin-x64": "1.83.4",
"sass-embedded-linux-arm": "1.83.4",
"sass-embedded-linux-arm64": "1.83.4",
"sass-embedded-linux-ia32": "1.83.4",
"sass-embedded-linux-musl-arm": "1.83.4",
"sass-embedded-linux-musl-arm64": "1.83.4",
"sass-embedded-linux-musl-ia32": "1.83.4",
"sass-embedded-linux-musl-riscv64": "1.83.4",
"sass-embedded-linux-musl-x64": "1.83.4",
"sass-embedded-linux-riscv64": "1.83.4",
"sass-embedded-linux-x64": "1.83.4",
"sass-embedded-win32-arm64": "1.83.4",
"sass-embedded-win32-ia32": "1.83.4",
"sass-embedded-win32-x64": "1.83.4"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.83.4.tgz",
"integrity": "sha512-9Z4pJAOgEkXa3VDY/o+U6l5XvV0mZTJcSl0l/mSPHihjAHSpLYnOW6+KOWeM8dxqrsqTYcd6COzhanI/a++5Gw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.83.4.tgz",
"integrity": "sha512-tgX4FzmbVqnQmD67ZxQDvI+qFNABrboOQgwsG05E5bA/US42zGajW9AxpECJYiMXVOHmg+d81ICbjb0fsVHskw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.83.4.tgz",
"integrity": "sha512-RsFOziFqPcfZXdFRULC4Ayzy9aK6R6FwQ411broCjlOBX+b0gurjRadkue3cfUEUR5mmy0KeCbp7zVKPLTK+5Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.83.4.tgz",
"integrity": "sha512-EHwh0nmQarBBrMRU928eTZkFGx19k/XW2YwbPR4gBVdWLkbTgCA5aGe8hTE6/1zStyx++3nDGvTZ78+b/VvvLg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.83.4.tgz",
"integrity": "sha512-0PgQNuPWYy1jEOEPDVsV89KfqOsMLIp9CSbjBY7jRcwRhyVAcigqrUG6bDeNtojHUYKA1kU+Eh/85WxOHUOgBw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.83.4.tgz",
"integrity": "sha512-rp2ywymWc3nymnSnAFG5R/8hvxWCsuhK3wOnD10IDlmNB7o4rzKby1c+2ZfpQGowlYGWsWWTgz8FW2qzmZsQRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.83.4.tgz",
"integrity": "sha512-kLkN2lXz9PCgGfDS8Ev5YVcl/V2173L6379en/CaFuJJi7WiyPgBymW7hOmfCt4uO4R1y7CP2Uc08DRtZsBlAA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.83.4.tgz",
"integrity": "sha512-nL90ryxX2lNmFucr9jYUyHHx21AoAgdCL1O5Ltx2rKg2xTdytAGHYo2MT5S0LIeKLa/yKP/hjuSvrbICYNDvtA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.83.4.tgz",
"integrity": "sha512-E0zjsZX2HgESwyqw31EHtI39DKa7RgK7nvIhIRco1d0QEw227WnoR9pjH3M/ZQy4gQj3GKilOFHM5Krs/omeIA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.83.4.tgz",
"integrity": "sha512-ew5HpchSzgAYbQoriRh8QhlWn5Kw2nQ2jHoV9YLwGKe3fwwOWA0KDedssvDv7FWnY/FCqXyymhLd6Bxae4Xquw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.83.4.tgz",
"integrity": "sha512-0RrJRwMrmm+gG0VOB5b5Cjs7Sd+lhqpQJa6EJNEaZHljJokEfpE5GejZsGMRMIQLxEvVphZnnxl6sonCGFE/QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.83.4.tgz",
"integrity": "sha512-IzMgalf6MZOxgp4AVCgsaWAFDP/IVWOrgVXxkyhw29fyAEoSWBJH4k87wyPhEtxSuzVHLxKNbc8k3UzdWmlBFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.83.4.tgz",
"integrity": "sha512-LLb4lYbcxPzX4UaJymYXC+WwokxUlfTJEFUv5VF0OTuSsHAGNRs/rslPtzVBTvMeG9TtlOQDhku1F7G6iaDotA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.83.4.tgz",
"integrity": "sha512-zoKlPzD5Z13HKin1UGR74QkEy+kZEk2AkGX5RelRG494mi+IWwRuWCppXIovor9+BQb9eDWPYPoMVahwN5F7VA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.83.4.tgz",
"integrity": "sha512-hB8+/PYhfEf2zTIcidO5Bpof9trK6WJjZ4T8g2MrxQh8REVtdPcgIkoxczRynqybf9+fbqbUwzXtiUao2GV+vQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.83.4.tgz",
"integrity": "sha512-83fL4n+oeDJ0Y4KjASmZ9jHS1Vl9ESVQYHMhJE0i4xDi/P3BNarm2rsKljq/QtrwGpbqwn8ujzOu7DsNCMDSHA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.83.4.tgz",
"integrity": "sha512-NlnGdvCmTD5PK+LKXlK3sAuxOgbRIEoZfnHvxd157imCm/s2SYF/R28D0DAAjEViyI8DovIWghgbcqwuertXsA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.83.4.tgz",
"integrity": "sha512-J2BFKrEaeSrVazU2qTjyQdAk+MvbzJeTuCET0uAJEXSKtvQ3AzxvzndS7LqkDPbF32eXAHLw8GVpwcBwKbB3Uw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.83.4.tgz",
"integrity": "sha512-uPAe9T/5sANFhJS5dcfAOhOJy8/l2TRYG4r+UO3Wp4yhqbN7bggPvY9c7zMYS0OC8tU/bCvfYUDFHYMCl91FgA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.83.4.tgz",
"integrity": "sha512-C9fkDY0jKITdJFij4UbfPFswxoXN9O/Dr79v17fJnstVwtUojzVJWKHUXvF0Zg2LIR7TCc4ju3adejKFxj7ueA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1333,12 +1760,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/super-regex": { "node_modules/super-regex": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
@@ -1356,6 +1777,45 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/sync-message-port": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/time-span": { "node_modules/time-span": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
@@ -1371,10 +1831,24 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.11", "version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1453,13 +1927,13 @@
} }
}, },
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "9.14.2", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.2.tgz", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.0.1.tgz",
"integrity": "sha512-JK9Pm80OqssGJU2Y6F7DcM8RFHqVG4WkuCqOZTVsXkEzZME7ABejAUqUdA931zEBedc4thBgSUWxeQh4uocJAQ==", "integrity": "sha512-pWAT8CusK8q9/EpN7V3oxwHwxWm6+Kp2PeTZmRGvdZTkUzMQDpbbmHp0TwQ8xw04XKm23cr6B4GL72y3W8Yekg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/core-base": "9.14.2", "@intlify/core-base": "11.0.1",
"@intlify/shared": "9.14.2", "@intlify/shared": "11.0.1",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
}, },
"engines": { "engines": {

View File

@@ -8,25 +8,27 @@
"preview": "vite preview --port 5050" "preview": "vite preview --port 5050"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1", "@fontsource/nunito-sans": "^5.1.1",
"@kyvg/vue3-notification": "^3.1.3", "@fortawesome/fontawesome-free": "^6.7.2",
"@kyvg/vue3-notification": "^3.4.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.3",
"bootswatch": "^5.3.2", "bootswatch": "^5.3.3",
"flag-icons": "^7.1.0", "flag-icons": "^7.3.2",
"ip-address": "^9.0.5", "ip-address": "^10.0.1",
"is-cidr": "^5.0.3", "is-cidr": "^5.1.0",
"is-ip": "^5.0.1", "is-ip": "^5.0.1",
"pinia": "^2.1.7", "pinia": "^2.3.1",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"vue": "^3.3.13", "vue": "^3.5.13",
"vue-i18n": "^9.14.2", "vue-i18n": "^11.0.1",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.5", "vue-router": "^4.5.0",
"vue3-tags-input": "^1.0.12" "vue3-tags-input": "^1.0.12"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^5.2.1",
"vite": "^5.0.10" "sass-embedded": "^1.83.4",
"vite": "^5.4.12"
} }
} }

View File

@@ -0,0 +1,16 @@
// disable external web fonts
$web-font-path: false;
@import "bootswatch/dist/lux/variables";
@import "bootstrap/scss/bootstrap";
@import "bootswatch/dist/lux/bootswatch";
// for future use, once bootswatch supports @use
/*
@use "bootswatch/dist/lux/_variables.scss" as lux-vars with (
$web-font-path: false
);
@use "bootstrap/scss/bootstrap" as bs;
@use "bootswatch/dist/lux/_bootswatch.scss" as lux-theme;
*/

View File

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

View File

@@ -354,7 +354,7 @@
"endpoint": { "endpoint": {
"label": "Endpoint Address", "label": "Endpoint Address",
"placeholder": "Endpoint Address", "placeholder": "Endpoint Address",
"description": "The endpoint address that peers will connect to." "description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
}, },
"networks": { "networks": {
"label": "IP Networks", "label": "IP Networks",

View File

@@ -355,7 +355,7 @@
"endpoint": { "endpoint": {
"label": "Endpoint Address", "label": "Endpoint Address",
"placeholder": "Endpoint Address", "placeholder": "Endpoint Address",
"description": "The endpoint address that peers will connect to." "description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
}, },
"networks": { "networks": {
"label": "IP Networks", "label": "IP Networks",

View File

@@ -9,13 +9,14 @@ import i18n from "./lang";
import Notifications from '@kyvg/vue3-notification' import Notifications from '@kyvg/vue3-notification'
// Bootstrap (and theme) // Bootstrap (and theme)
//import "bootstrap/dist/css/bootstrap.min.css" import "@/assets/custom.scss";
import "bootswatch/dist/lux/bootstrap.min.css";
import "bootstrap"; import "bootstrap";
import "./assets/base.css"; import "./assets/base.css";
// Fontawesome // Fonts
import "@fortawesome/fontawesome-free/js/all.js" import "@fortawesome/fontawesome-free/js/all.js"
import "@fontsource/nunito-sans/400.css";
import "@fontsource/nunito-sans/600.css";
// Flags // Flags
import "flag-icons/css/flag-icons.min.css" import "flag-icons/css/flag-icons.min.css"

View File

@@ -12,6 +12,8 @@ export const profileStore = defineStore({
id: 'profile', id: 'profile',
state: () => ({ state: () => ({
peers: [], peers: [],
interfaces: [],
selectedInterfaceId: "",
stats: {}, stats: {},
statsEnabled: false, statsEnabled: false,
user: {}, user: {},
@@ -71,6 +73,7 @@ export const profileStore = defineStore({
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats() return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
}, },
hasStatistics: (state) => state.statsEnabled, hasStatistics: (state) => state.statsEnabled,
CountInterfaces: (state) => state.interfaces.length,
}, },
actions: { actions: {
afterPageSizeChange() { afterPageSizeChange() {
@@ -116,6 +119,11 @@ export const profileStore = defineStore({
this.stats = statsResponse.Stats this.stats = statsResponse.Stats
this.statsEnabled = statsResponse.Enabled this.statsEnabled = statsResponse.Enabled
}, },
setInterfaces(interfaces) {
this.interfaces = interfaces
this.selectedInterfaceId = interfaces.length > 0 ? interfaces[0].Identifier : ""
this.fetching = false
},
async enableApi() { async enableApi() {
this.fetching = true this.fetching = true
let currentUser = authStore().user.Identifier let currentUser = authStore().user.Identifier
@@ -186,5 +194,19 @@ export const profileStore = defineStore({
}) })
}) })
}, },
async LoadInterfaces() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/interfaces`)
.then(this.setInterfaces)
.catch(error => {
this.setInterfaces([])
console.log("Failed to load interfaces for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load interfaces!",
})
})
},
} }
}) })

View File

@@ -3,7 +3,7 @@ import PeerViewModal from "../components/PeerViewModal.vue";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { profileStore } from "@/stores/profile"; import { profileStore } from "@/stores/profile";
import PeerEditModal from "@/components/PeerEditModal.vue"; import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
import { settingsStore } from "@/stores/settings"; import { settingsStore } from "@/stores/settings";
import { humanFileSize } from "@/helpers/utils"; import { humanFileSize } from "@/helpers/utils";
@@ -27,10 +27,18 @@ function sortBy(key) {
profile.sortOrder = sortOrder.value; profile.sortOrder = sortOrder.value;
} }
function friendlyInterfaceName(id, name) {
if (name) {
return name
}
return id
}
onMounted(async () => { onMounted(async () => {
await profile.LoadUser() await profile.LoadUser()
await profile.LoadPeers() await profile.LoadPeers()
await profile.LoadStats() await profile.LoadStats()
await profile.LoadInterfaces()
await profile.calculatePages(); // Forces to show initial page number await profile.calculatePages(); // Forces to show initial page number
}) })
@@ -38,7 +46,7 @@ onMounted(async () => {
<template> <template>
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal> <PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
<PeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''"></PeerEditModal> <UserPeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''; profile.LoadPeers()"></UserPeerEditModal>
<!-- Peer list --> <!-- Peer list -->
<div class="mt-4 row"> <div class="mt-4 row">
@@ -56,9 +64,17 @@ onMounted(async () => {
</div> </div>
</div> </div>
<div class="col-12 col-lg-3 text-lg-end"> <div class="col-12 col-lg-3 text-lg-end">
<a v-if="settings.Setting('SelfProvisioning')" class="btn btn-primary ms-2" href="#" <div class="form-group" v-if="settings.Setting('SelfProvisioning')">
:title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"><i <div class="input-group mb-3">
class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a> <button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
</button>
<select v-model="profile.selectedInterfaceId" :disabled="profile.CountInterfaces===0" class="form-select">
<option v-if="profile.CountInterfaces===0" value="nothing">{{ $t('interfaces.no-interface.default-selection') }}</option>
<option v-for="iface in profile.interfaces" :key="iface.Identifier" :value="iface.Identifier">{{ friendlyInterfaceName(iface.Identifier,iface.DisplayName) }}</option>
</select>
</div>
</div>
</div> </div>
</div> </div>
<div class="mt-2 table-responsive"> <div class="mt-2 table-responsive">

View File

@@ -24,8 +24,8 @@ func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet()) apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
apiGroup.GET("/iface/:iface/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet()) apiGroup.GET("/iface/:iface/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet())
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(ScopeAdmin), e.handlePrepareGet()) apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(), e.handlePrepareGet())
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost()) apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(), e.handleCreatePost())
apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost()) apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost())
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet()) apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
apiGroup.POST("/config-mail", e.handleEmailPost()) apiGroup.POST("/config-mail", e.handleEmailPost())

View File

@@ -28,6 +28,7 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost()) apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet()) apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet()) apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
apiGroup.GET("/:id/interfaces", e.authenticator.UserIdMatch("id"), e.handleInterfacesGet())
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost()) apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost()) apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
} }
@@ -170,6 +171,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
// @ID users_handlePeersGet // @ID users_handlePeersGet
// @Tags Users // @Tags Users
// @Summary Get peers for the given user. // @Summary Get peers for the given user.
// @Param id path string true "The user identifier"
// @Produce json // @Produce json
// @Success 200 {object} []model.Peer // @Success 200 {object} []model.Peer
// @Failure 400 {object} model.Error // @Failure 400 {object} model.Error
@@ -179,14 +181,14 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c) ctx := domain.SetUserInfoFromGin(c)
interfaceId := Base64UrlDecode(c.Param("id")) userId := Base64UrlDecode(c.Param("id"))
if interfaceId == "" { if userId == "" {
c.JSON(http.StatusBadRequest, c.JSON(http.StatusBadRequest,
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"}) model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
return return
} }
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId)) peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(userId))
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()}) model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
@@ -202,6 +204,7 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
// @ID users_handleStatsGet // @ID users_handleStatsGet
// @Tags Users // @Tags Users
// @Summary Get peer stats for the given user. // @Summary Get peer stats for the given user.
// @Param id path string true "The user identifier"
// @Produce json // @Produce json
// @Success 200 {object} model.PeerStats // @Success 200 {object} model.PeerStats
// @Failure 400 {object} model.Error // @Failure 400 {object} model.Error
@@ -229,6 +232,39 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
} }
} }
// handleInterfacesGet returns a gorm handler function.
//
// @ID users_handleInterfacesGet
// @Tags Users
// @Summary Get interfaces for the given user. Returns an empty list if self provisioning is disabled.
// @Param id path string true "The user identifier"
// @Produce json
// @Success 200 {object} []model.Interface
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /user/{id}/interfaces [get]
func (e userEndpoint) handleInterfacesGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
userId := Base64UrlDecode(c.Param("id"))
if userId == "" {
c.JSON(http.StatusBadRequest,
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
return
}
peers, err := e.app.GetUserInterfaces(ctx, domain.UserIdentifier(userId))
if err != nil {
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
c.JSON(http.StatusOK, model.NewInterfaces(peers, nil))
}
}
// handleDelete returns a gorm handler function. // handleDelete returns a gorm handler function.
// //
// @ID users_handleDelete // @ID users_handleDelete

View File

@@ -109,7 +109,11 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface { func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
results := make([]Interface, len(src)) results := make([]Interface, len(src))
for i := range src { for i := range src {
results[i] = *NewInterface(&src[i], srcPeers[i]) if srcPeers == nil {
results[i] = *NewInterface(&src[i], nil)
} else {
results[i] = *NewInterface(&src[i], srcPeers[i])
}
} }
return results return results

View File

@@ -39,6 +39,7 @@ type WireGuardManager interface {
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error) GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
PrepareInterface(ctx context.Context) (*domain.Interface, error) PrepareInterface(ctx context.Context) (*domain.Interface, error)
CreateInterface(ctx context.Context, in *domain.Interface) (*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) UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)

View File

@@ -68,6 +68,34 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
return interfaces, allPeers, nil return interfaces, allPeers, nil
} }
// GetUserInterfaces returns all interfaces that are available for users to create new peers.
// If self-provisioning is disabled, this function will return an empty list.
func (m Manager) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
if !m.cfg.Core.SelfProvisioningAllowed {
return nil, nil // self-provisioning is disabled - no interfaces for users
}
interfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return nil, fmt.Errorf("unable to load all interfaces: %w", err)
}
// strip sensitive data, users only need very limited information
userInterfaces := make([]domain.Interface, 0, len(interfaces))
for _, iface := range interfaces {
if iface.IsDisabled() {
continue // skip disabled interfaces
}
if iface.Type != domain.InterfaceTypeServer {
continue // skip client interfaces
}
userInterfaces = append(userInterfaces, iface.PublicInfo())
}
return userInterfaces, nil
}
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) { func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil { if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return 0, err return 0, err
@@ -456,6 +484,10 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
*domain.Interface, *domain.Interface,
error, error,
) { ) {
if err := iface.Validate(); err != nil {
return nil, fmt.Errorf("interface validation failed: %w", err)
}
stateChanged := m.hasInterfaceStateChanged(ctx, iface) stateChanged := m.hasInterfaceStateChanged(ctx, iface)
if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil { if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil {

View File

@@ -62,8 +62,10 @@ func (m Manager) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]
} }
func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) { func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil { if !m.cfg.Core.SelfProvisioningAllowed {
return nil, err // TODO: self provisioning? if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
} }
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
@@ -73,6 +75,10 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
return nil, fmt.Errorf("unable to find interface %s: %w", id, err) return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
} }
if m.cfg.Core.SelfProvisioningAllowed && iface.Type != domain.InterfaceTypeServer {
return nil, fmt.Errorf("self provisioning is only allowed for server interfaces: %w", domain.ErrNoPermission)
}
ips, err := m.getFreshPeerIpConfig(ctx, iface) ips, err := m.getFreshPeerIpConfig(ctx, iface)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err) return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
@@ -149,10 +155,18 @@ func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain
} }
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) { func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil { if !m.cfg.Core.SelfProvisioningAllowed {
return nil, err if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
} else {
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return nil, err
}
} }
sessionUser := domain.GetUserInfo(ctx)
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier) existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil && !errors.Is(err, domain.ErrNotFound) { if err != nil && !errors.Is(err, domain.ErrNotFound) {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err) return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
@@ -161,6 +175,18 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry) return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
} }
// if a peer is self provisioned, ensure that only allowed fields are set from the request
if !sessionUser.IsAdmin {
preparedPeer, err := m.PreparePeer(ctx, peer.InterfaceIdentifier)
if err != nil {
return nil, fmt.Errorf("failed to prepare peer for interface %s: %w", peer.InterfaceIdentifier, err)
}
preparedPeer.OverwriteUserEditableFields(peer)
peer = preparedPeer
}
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil { if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
return nil, fmt.Errorf("creation not allowed: %w", err) return nil, fmt.Errorf("creation not allowed: %w", err)
} }
@@ -229,6 +255,19 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("update not allowed: %w", err) return nil, fmt.Errorf("update not allowed: %w", err)
} }
sessionUser := domain.GetUserInfo(ctx)
// if a peer is self provisioned, ensure that only allowed fields are set from the request
if !sessionUser.IsAdmin {
originalPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
}
originalPeer.OverwriteUserEditableFields(peer)
peer = originalPeer
}
// handle peer identifier change (new public key) // handle peer identifier change (new public key)
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) { if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
@@ -438,7 +477,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error { func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin { if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission return domain.ErrNoPermission
} }
@@ -452,7 +491,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData) return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
} }
if !currentUser.IsAdmin { if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission return domain.ErrNoPermission
} }
@@ -467,7 +506,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error { func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin { if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission return domain.ErrNoPermission
} }

View File

@@ -3,6 +3,7 @@ package domain
import ( import (
"fmt" "fmt"
"math" "math"
"net"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -71,8 +72,35 @@ type Interface struct {
PeerDefPostDown string // default action that is executed after the device is down PeerDefPostDown string // default action that is executed after the device is down
} }
func (i *Interface) IsValid() bool { // PublicInfo returns a copy of the interface with only the public information.
return true // TODO: implement check // Sensible information like keys are not included.
func (i *Interface) PublicInfo() Interface {
return Interface{
Identifier: i.Identifier,
DisplayName: i.DisplayName,
Type: i.Type,
Disabled: i.Disabled,
}
}
// Validate performs checks to ensure that the interface is valid.
func (i *Interface) Validate() error {
// validate peer default endpoint, add port if needed
if i.PeerDefEndpoint != "" {
host, port, err := net.SplitHostPort(i.PeerDefEndpoint)
switch {
case err != nil && !strings.Contains(err.Error(), "missing port in address"):
return fmt.Errorf("invalid default endpoint: %w", err)
case err != nil && strings.Contains(err.Error(), "missing port in address"):
// In this case, the entire string is the host, and there's no port.
host = i.PeerDefEndpoint
port = strconv.Itoa(i.ListenPort)
}
i.PeerDefEndpoint = net.JoinHostPort(host, port)
}
return nil
} }
func (i *Interface) IsDisabled() bool { func (i *Interface) IsDisabled() bool {

View File

@@ -127,6 +127,19 @@ func (p *Peer) GenerateDisplayName(prefix string) {
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8)) p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
} }
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer) {
p.DisplayName = userPeer.DisplayName
p.Interface.PublicKey = userPeer.Interface.PublicKey
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
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 { type PeerInterfaceConfig struct {
KeyPair // private/public Key of the peer KeyPair // private/public Key of the peer

View File

@@ -19,6 +19,7 @@ theme:
favicon: assets/images/favicon-large.png favicon: assets/images/favicon-large.png
language: en language: en
features: features:
- content.code.copy
- navigation.instant - navigation.instant
- navigation.tabs - navigation.tabs
- navigation.expand - navigation.expand
@@ -46,6 +47,7 @@ markdown_extensions:
- admonition - admonition
- meta - meta
- pymdownx.details - pymdownx.details
- pymdownx.snippets
- pymdownx.superfences - pymdownx.superfences
- pymdownx.tabbed: - pymdownx.tabbed:
alternate_style: true alternate_style: true
@@ -59,10 +61,13 @@ nav:
- Documentation: - Documentation:
- Overview: documentation/overview.md - Overview: documentation/overview.md
- Getting Started: - Getting Started:
- Building: documentation/getting-started/building.md - Binaries: documentation/getting-started/binaries.md
- Docker Container: documentation/getting-started/docker.md - Docker: documentation/getting-started/docker.md
- Upgrade from V1: documentation/getting-started/upgrade.md - Helm: documentation/getting-started/helm.md
- Sources: documentation/getting-started/sources.md
- Configuration: - Configuration:
- Overview: documentation/configuration/overview.md - Overview: documentation/configuration/overview.md
- Examples: documentation/configuration/examples.md - Examples: documentation/configuration/examples.md
- Upgrade: documentation/upgrade/v1.md
- Monitoring: documentation/monitoring/prometheus.md
- REST API: documentation/rest-api/api-doc.md - REST API: documentation/rest-api/api-doc.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB